This repository was archived by the owner on Apr 26, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add an admin API endpoint to support per-user feature flags #15344
Merged
Merged
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
5a1f946
add an experimental features table and a class to access it
H-Shay ec04542
add an admin endpoint and handler class to access table
H-Shay 0841f79
add some tests of basic functionality
H-Shay 28f835a
newsframent
H-Shay 1b68698
Merge branch 'develop' into shay/experimental_flags
H-Shay daf8a9f
fix schema delta
H-Shay dc6207a
fix bad import (thanks pycharm)
H-Shay 845c07e
drop unnecessary handler class and user enum for verification
H-Shay c999fea
rewrite table
H-Shay 5c9c4b7
allow for bulk setting/getting of features, cache result of getting
H-Shay 6280124
update tests
H-Shay d3f7032
Merge branch 'develop' into shay/experimental_flags
H-Shay aa74c4a
update schema
H-Shay 985eba9
Merge branch 'develop' into shay/experimental_flags
H-Shay 2c1191d
update comments and schema number
H-Shay 4847df7
add new boolean column to `port_db` script and capitalize FALSE
H-Shay 1ca2989
properly handle boolean column in sqlite
H-Shay e86e39c
it's postgres, not postgre
H-Shay fadb5e6
make PUT and GET more symmetrical for ease of use
H-Shay 0496d1d
Update docs/admin_api/experimental_features.md
H-Shay 2044c14
update docs
H-Shay dcc9c41
fix the urls
H-Shay 37e7f30
get rid of sqlite-specific table
H-Shay 401cb10
schema version is now 75 so move file
H-Shay 8927eae
Apply suggestions from code review
H-Shay 17e48be
Merge branch 'develop' into shay/experimental_flags
H-Shay 427d65f
fix schema drift
H-Shay 80d3f69
Merge branch 'shay/experimental_flags' of https://github.com/matrix-o…
H-Shay 6a994dd
update test to accompdate new error message
H-Shay File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add an admin API endpoint to support per-user feature flags. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# Experimental Features API | ||
|
||
This API allows a server administrator to enable or disable some experimental features on a per-user | ||
basis. Currently supported features are [msc3026](https://github.com/matrix-org/matrix-spec-proposals/pull/3026): busy presence state enabled, [msc2654](https://github.com/matrix-org/matrix-spec-proposals/pull/2654): enable unread counts, | ||
[msc3881](https://github.com/matrix-org/matrix-spec-proposals/pull/3881): enable remotely toggling push notifications for another client, and [msc3967](https://github.com/matrix-org/matrix-spec-proposals/pull/3967): do not require | ||
UIA when first uploading cross-signing keys. | ||
|
||
|
||
To use it, you will need to authenticate by providing an `access_token` | ||
for a server admin: see [Admin API](../usage/administration/admin_api/). | ||
|
||
## Enable a Feature | ||
|
||
This API allows a server administrator to enable an experimental feature for a given user, where the | ||
user_id is the user id of the user for whom to enable the feature, and the feature is referred to by | ||
the msc number - i.e. to enable unread counts, the parameter `msc2654` would be added to the url. | ||
|
||
The API is: | ||
|
||
``` | ||
PUT /_synapse/admin/v1/experimental_features/<user_id>/<feature> | ||
``` | ||
|
||
## Disable a Feature | ||
|
||
To disable a currently enabled feature, the API is: | ||
|
||
``` | ||
DELETE /_synapse/admin/v1/experimental_features/<user_id>/<feature> | ||
``` | ||
|
||
## Check if a feature is enabled | ||
|
||
To check if a given feature is enabled for a given user, the API is: | ||
|
||
``` | ||
GET /_synapse/admin/v1/experimental_features/<user_id>/<feature> | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# Copyright 2023 The Matrix.org Foundation C.I.C | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
|
||
from enum import Enum | ||
from http import HTTPStatus | ||
from typing import TYPE_CHECKING, Tuple | ||
|
||
from synapse.api.errors import SynapseError | ||
from synapse.http.servlet import RestServlet | ||
from synapse.http.site import SynapseRequest | ||
from synapse.rest.admin import admin_patterns, assert_requester_is_admin | ||
from synapse.types import JsonDict, UserID | ||
|
||
if TYPE_CHECKING: | ||
from synapse.server import HomeServer | ||
|
||
|
||
class ValidFeatures(str, Enum): | ||
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Currently supported per-user features | ||
""" | ||
|
||
MSC3026 = "msc3026" | ||
MSC2654 = "msc2654" | ||
MSC3881 = "msc3881" | ||
MSC3967 = "msc3967" | ||
|
||
|
||
class ExperimentalFeaturesRestServlet(RestServlet): | ||
""" | ||
Enable or disable an experimental feature or determine whether a given experimental | ||
feature is enabled | ||
""" | ||
|
||
PATTERNS = admin_patterns( | ||
"/experimental_features/(?P<user_id>[^/]*)/(?P<feature>[^/]*)" | ||
) | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
super().__init__() | ||
self.auth = hs.get_auth() | ||
self.store = hs.get_datastores().main | ||
self.is_mine = hs.is_mine | ||
|
||
async def on_GET( | ||
self, | ||
request: SynapseRequest, | ||
user_id: str, | ||
feature: str, | ||
) -> Tuple[int, JsonDict]: | ||
""" | ||
Checks if a given feature is enabled for a given user | ||
""" | ||
await assert_requester_is_admin(self.auth, request) | ||
|
||
target_user = UserID.from_string(user_id) | ||
if not self.is_mine(target_user): | ||
raise SynapseError( | ||
HTTPStatus.BAD_REQUEST, | ||
"User must be local to check what experimental features are enabled.", | ||
) | ||
|
||
# do a basic validation of the given feature | ||
validated = feature in [ | ||
ValidFeatures.MSC3026, | ||
ValidFeatures.MSC2654, | ||
ValidFeatures.MSC3881, | ||
ValidFeatures.MSC3967, | ||
] | ||
|
||
if not validated: | ||
raise SynapseError( | ||
HTTPStatus.BAD_REQUEST, "Please provide a valid experimental feature." | ||
) | ||
|
||
enabled = await self.store.get_feature_enabled(user_id, feature) | ||
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return HTTPStatus.OK, {"user": user_id, "feature": feature, "enabled": enabled} | ||
|
||
async def on_PUT( | ||
self, request: SynapseRequest, user_id: str, feature: str | ||
) -> Tuple[int, JsonDict]: | ||
""" | ||
Enables a given feature for the requester | ||
""" | ||
await assert_requester_is_admin(self.auth, request) | ||
|
||
target_user = UserID.from_string(user_id) | ||
if not self.is_mine(target_user): | ||
raise SynapseError( | ||
HTTPStatus.BAD_REQUEST, | ||
"User must be local to enable experimental features.", | ||
) | ||
|
||
# validate the feature | ||
validated = feature in [ | ||
ValidFeatures.MSC3026, | ||
ValidFeatures.MSC2654, | ||
ValidFeatures.MSC3881, | ||
ValidFeatures.MSC3967, | ||
] | ||
|
||
if not validated: | ||
raise SynapseError( | ||
HTTPStatus.BAD_REQUEST, "Please provide a valid experimental feature." | ||
) | ||
|
||
user, feature, enabled = await self.store.set_feature_for_user( | ||
user_id, feature, True | ||
) | ||
|
||
return HTTPStatus.OK, {"user": user, "feature": feature, "enabled": enabled} | ||
|
||
async def on_DELETE( | ||
self, request: SynapseRequest, user_id: str, feature: str | ||
) -> Tuple[int, JsonDict]: | ||
""" | ||
Disables the requested feature for the given user | ||
""" | ||
await assert_requester_is_admin(self.auth, request) | ||
|
||
target_user = UserID.from_string(user_id) | ||
if not self.is_mine(target_user): | ||
raise SynapseError( | ||
HTTPStatus.BAD_REQUEST, | ||
"User must be local to disable an experimental feature.", | ||
) | ||
|
||
# validate the feature | ||
validated = feature in [ | ||
ValidFeatures.MSC3026, | ||
ValidFeatures.MSC2654, | ||
ValidFeatures.MSC3881, | ||
ValidFeatures.MSC3967, | ||
] | ||
|
||
if not validated: | ||
raise SynapseError( | ||
HTTPStatus.BAD_REQUEST, "Please provide a valid experimental feature." | ||
) | ||
|
||
user, feature, enabled = await self.store.set_feature_for_user( | ||
user_id, feature, False | ||
) | ||
|
||
return HTTPStatus.OK, {"user": user, "feature": feature, "enabled": enabled} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# Copyright 2023 The Matrix.org Foundation C.I.C | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from typing import TYPE_CHECKING, Tuple | ||
|
||
from synapse.api.errors import StoreError | ||
from synapse.storage._base import SQLBaseStore | ||
from synapse.storage.database import DatabasePool, LoggingDatabaseConnection | ||
|
||
if TYPE_CHECKING: | ||
from synapse.server import HomeServer | ||
|
||
|
||
class ExperimentalFeaturesStore(SQLBaseStore): | ||
def __init__( | ||
self, | ||
database: DatabasePool, | ||
db_conn: LoggingDatabaseConnection, | ||
hs: "HomeServer", | ||
) -> None: | ||
super().__init__(database, db_conn, hs) | ||
|
||
async def get_feature_enabled(self, user_id: str, feature: str) -> bool: | ||
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Checks to see if a given feature is enabled for the user | ||
Args: | ||
user: | ||
the user to be queried on | ||
feature: | ||
the feature in question | ||
Returns: | ||
True if the feature is enabled, False if it is not or if the feature was | ||
not found. | ||
""" | ||
enabled = await self.db_pool.simple_select_one( | ||
"per_user_experimental_features", | ||
{"user_id": user_id}, | ||
[feature], | ||
allow_none=True, | ||
) | ||
|
||
if not enabled or not enabled[feature]: | ||
return False | ||
else: | ||
return True | ||
|
||
async def set_feature_for_user( | ||
self, user: str, feature: str, enabled: bool | ||
) -> Tuple[str, str, bool]: | ||
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Enables or disables a given feature for a given user | ||
Args: | ||
user: | ||
the user for whom to enable/disable a feature | ||
feature: | ||
the feature to be enabled/diabled | ||
enabled: | ||
True to enable, False to disable | ||
Returns: | ||
A tuple of user, feature, and a bool indicating that the feature is enabled | ||
or disabled | ||
""" | ||
success = await self.db_pool.simple_upsert( | ||
"per_user_experimental_features", {"user_id": user}, {feature: enabled} | ||
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
if not success: | ||
raise StoreError(500, "There was a problem setting your feature.") | ||
|
||
return user, feature, enabled |
34 changes: 34 additions & 0 deletions
34
synapse/storage/schema/main/delta/75/01_per_user_experimental_features.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* Copyright 2023 The Matrix.org Foundation C.I.C | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
-- Table containing a list of experimental features and whether they are | ||
-- enabled for a given user | ||
CREATE TABLE per_user_experimental_features ( | ||
-- The User ID to check/set the feature for | ||
user_id TEXT NOT NULL PRIMARY KEY, | ||
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
-- busy presence state enabled | ||
msc3026 BOOLEAN, | ||
|
||
-- enable unread counts | ||
msc2654 BOOLEAN, | ||
|
||
-- enable remotely toggling push notifications for another client | ||
msc3881 BOOLEAN, | ||
|
||
-- Do not require UIA when first uploading cross signing keys | ||
msc3967 BOOLEAN | ||
); | ||
H-Shay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.