Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Support for MSC4190: device management for application services #17705

Merged
merged 10 commits into from
Dec 4, 2024
1 change: 1 addition & 0 deletions changelog.d/17705.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for MSC4190: device management for Application Services.
2 changes: 2 additions & 0 deletions synapse/appservice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def __init__(
ip_range_whitelist: Optional[IPSet] = None,
supports_ephemeral: bool = False,
msc3202_transaction_extensions: bool = False,
msc4190_device_management: bool = False,
):
self.token = token
self.url = (
Expand All @@ -100,6 +101,7 @@ def __init__(
self.ip_range_whitelist = ip_range_whitelist
self.supports_ephemeral = supports_ephemeral
self.msc3202_transaction_extensions = msc3202_transaction_extensions
self.msc4190_device_management = msc4190_device_management

if "|" in self.id:
raise Exception("application service ID cannot contain '|' character")
Expand Down
13 changes: 13 additions & 0 deletions synapse/config/appservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@ def _load_appservice(
"The `org.matrix.msc3202` option should be true or false if specified."
)

# Opt-in flag for the MSC4190 behaviours.
# When enabled, the following C-S API endpoints change:
# - POST /register does not return an access token
# - PUT /devices/{device_id} creates a new device if one does not exist
# - DELETE /devices/{device_id} no longer requires UIA
# - POST /delete_devices/{device_id} no longer requires UIA
msc4190_enabled = as_info.get("io.element.msc4190", False)
if not isinstance(msc4190_enabled, bool):
raise ValueError(
"The `io.element.msc4190` option should be true or false if specified."
)

return ApplicationService(
token=as_info["as_token"],
url=as_info["url"],
Expand All @@ -195,4 +207,5 @@ def _load_appservice(
ip_range_whitelist=ip_range_whitelist,
supports_ephemeral=supports_ephemeral,
msc3202_transaction_extensions=msc3202_transaction_extensions,
msc4190_device_management=msc4190_enabled,
)
34 changes: 34 additions & 0 deletions synapse/handlers/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,40 @@ async def delete_devices(self, user_id: str, device_ids: List[str]) -> None:

await self.notify_device_update(user_id, device_ids)

async def upsert_device(
self, user_id: str, device_id: str, display_name: Optional[str] = None
) -> bool:
"""Create or update a device

Args:
user_id: The user to update devices of.
device_id: The device to update.
display_name: The new display name for this device.

Returns:
True if the device was created, False if it was updated.

"""

# Reject a new displayname which is too long.
self._check_device_name_length(display_name)

created = await self.store.store_device(
user_id,
device_id,
initial_device_display_name=display_name,
)

if not created:
await self.store.update_device(
user_id,
device_id,
new_display_name=display_name,
)

await self.notify_device_update(user_id, [device_id])
return created

async def update_device(self, user_id: str, device_id: str, content: dict) -> None:
"""Update the given device

Expand Down
6 changes: 4 additions & 2 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,9 @@ async def post_consent_actions(self, user_id: str) -> None:
"""
await self._auto_join_rooms(user_id)

async def appservice_register(self, user_localpart: str, as_token: str) -> str:
async def appservice_register(
self, user_localpart: str, as_token: str
) -> Tuple[str, ApplicationService]:
user = UserID(user_localpart, self.hs.hostname)
user_id = user.to_string()
service = self.store.get_app_service_by_token(as_token)
Expand All @@ -653,7 +655,7 @@ async def appservice_register(self, user_localpart: str, as_token: str) -> str:
appservice_id=service_id,
create_profile_with_displayname=user.localpart,
)
return user_id
return (user_id, service)

def check_user_id_not_appservice_exclusive(
self, user_id: str, allowed_appservice: Optional[ApplicationService] = None
Expand Down
62 changes: 41 additions & 21 deletions synapse/rest/client/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,19 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
else:
raise e

await self.auth_handler.validate_user_via_ui_auth(
requester,
request,
body.dict(exclude_unset=True),
"remove device(s) from your account",
# Users might call this multiple times in a row while cleaning up
# devices, allow a single UI auth session to be re-used.
can_skip_ui_auth=True,
)
if requester.app_service and requester.app_service.msc4190_device_management:
# MSC4190 can skip UIA for this endpoint
pass
else:
await self.auth_handler.validate_user_via_ui_auth(
requester,
request,
body.dict(exclude_unset=True),
"remove device(s) from your account",
# Users might call this multiple times in a row while cleaning up
# devices, allow a single UI auth session to be re-used.
can_skip_ui_auth=True,
)

await self.device_handler.delete_devices(
requester.user.to_string(), body.devices
Expand Down Expand Up @@ -175,9 +179,6 @@ class DeleteBody(RequestBodyModel):
async def on_DELETE(
self, request: SynapseRequest, device_id: str
) -> Tuple[int, JsonDict]:
if self._msc3861_oauth_delegation_enabled:
raise UnrecognizedRequestError(code=404)

requester = await self.auth.get_user_by_req(request)

try:
Expand All @@ -192,15 +193,24 @@ async def on_DELETE(
else:
raise

await self.auth_handler.validate_user_via_ui_auth(
requester,
request,
body.dict(exclude_unset=True),
"remove a device from your account",
# Users might call this multiple times in a row while cleaning up
# devices, allow a single UI auth session to be re-used.
can_skip_ui_auth=True,
)
if requester.app_service and requester.app_service.msc4190_device_management:
# MSC4190 allows appservices to delete devices through this endpoint without UIA
# It's also allowed with MSC3861 enabled
pass

else:
if self._msc3861_oauth_delegation_enabled:
raise UnrecognizedRequestError(code=404)

await self.auth_handler.validate_user_via_ui_auth(
requester,
request,
body.dict(exclude_unset=True),
"remove a device from your account",
# Users might call this multiple times in a row while cleaning up
# devices, allow a single UI auth session to be re-used.
can_skip_ui_auth=True,
)

await self.device_handler.delete_devices(
requester.user.to_string(), [device_id]
Expand All @@ -216,6 +226,16 @@ async def on_PUT(
requester = await self.auth.get_user_by_req(request, allow_guest=True)

body = parse_and_validate_json_object_from_request(request, self.PutBody)

# MSC4190 allows appservices to create devices through this endpoint
if requester.app_service and requester.app_service.msc4190_device_management:
created = await self.device_handler.upsert_device(
user_id=requester.user.to_string(),
device_id=device_id,
display_name=body.display_name,
)
return 201 if created else 200, {}

await self.device_handler.update_device(
requester.user.to_string(), device_id, body.dict()
)
Expand Down
7 changes: 5 additions & 2 deletions synapse/rest/client/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,9 +771,12 @@ async def _do_appservice_registration(
body: JsonDict,
should_issue_refresh_token: bool = False,
) -> JsonDict:
user_id = await self.registration_handler.appservice_register(
user_id, appservice = await self.registration_handler.appservice_register(
username, as_token
)
if appservice.msc4190_device_management:
body["inhibit_login"] = True

return await self._create_registration_details(
user_id,
body,
Expand Down Expand Up @@ -937,7 +940,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:

as_token = self.auth.get_access_token_from_request(request)

user_id = await self.registration_handler.appservice_register(
user_id, _ = await self.registration_handler.appservice_register(
desired_username, as_token
)
return 200, {"user_id": user_id}
Expand Down
13 changes: 11 additions & 2 deletions tests/handlers/test_appservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,12 +1165,21 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.hs.get_datastores().main.services_cache = [self._service]

# Register some appservice users
self._sender_user, self._sender_device = self.register_appservice_user(
user_id, device_id = self.register_appservice_user(
"as.sender", self._service_token
)
self._namespaced_user, self._namespaced_device = self.register_appservice_user(
# With MSC4190 enabled, we may not have a device out of the
# registration, but we don't have it enabled in this test
assert device_id is not None
self._sender_user = user_id
self._sender_device = device_id

user_id, device_id = self.register_appservice_user(
"_as_user1", self._service_token
)
assert device_id is not None
self._namespaced_user = user_id
self._namespaced_device = device_id

# Register a real user as well.
self._real_user = self.register_user("real.user", "meow")
Expand Down
Loading
Loading