Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support for MSC4190: device management for application services #17705

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -728,6 +728,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
Loading