diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py
index c5cdc36955..00f108de08 100644
--- a/synapse/rest/__init__.py
+++ b/synapse/rest/__init__.py
@@ -2,7 +2,7 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright 2014-2016 OpenMarket Ltd
-# Copyright (C) 2023 New Vector, Ltd
+# Copyright (C) 2023-2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -29,8 +29,9 @@ from synapse.rest.client import (
account_validity,
appservice_ping,
auth,
- auth_issuer,
+ auth_metadata,
capabilities,
+ delayed_events,
devices,
directory,
events,
@@ -81,6 +82,7 @@ CLIENT_SERVLET_FUNCTIONS: Tuple[RegisterServletsFunc, ...] = (
room.register_deprecated_servlets,
events.register_servlets,
room.register_servlets,
+ delayed_events.register_servlets,
login.register_servlets,
profile.register_servlets,
presence.register_servlets,
@@ -119,7 +121,7 @@ CLIENT_SERVLET_FUNCTIONS: Tuple[RegisterServletsFunc, ...] = (
mutual_rooms.register_servlets,
login_token_request.register_servlets,
rendezvous.register_servlets,
- auth_issuer.register_servlets,
+ auth_metadata.register_servlets,
)
SERVLET_GROUPS: Dict[str, Iterable[RegisterServletsFunc]] = {
@@ -185,7 +187,6 @@ class ClientRestResource(JsonResource):
mutual_rooms.register_servlets,
login_token_request.register_servlets,
rendezvous.register_servlets,
- auth_issuer.register_servlets,
]:
continue
diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py
index cdaee17451..b37bf3429b 100644
--- a/synapse/rest/admin/__init__.py
+++ b/synapse/rest/admin/__init__.py
@@ -39,7 +39,7 @@ from typing import TYPE_CHECKING, Optional, Tuple
from synapse.api.errors import Codes, NotFoundError, SynapseError
from synapse.handlers.pagination import PURGE_HISTORY_ACTION_NAME
-from synapse.http.server import HttpServer, JsonResource
+from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.http.site import SynapseRequest
from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin
@@ -51,6 +51,7 @@ from synapse.rest.admin.background_updates import (
from synapse.rest.admin.devices import (
DeleteDevicesRestServlet,
DeviceRestServlet,
+ DevicesGetRestServlet,
DevicesRestServlet,
)
from synapse.rest.admin.event_reports import (
@@ -86,6 +87,7 @@ from synapse.rest.admin.rooms import (
RoomStateRestServlet,
RoomTimestampToEventRestServlet,
)
+from synapse.rest.admin.scheduled_tasks import ScheduledTasksRestServlet
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
from synapse.rest.admin.statistics import (
LargestRoomsStatistics,
@@ -98,13 +100,16 @@ from synapse.rest.admin.users import (
DeactivateAccountRestServlet,
PushersRestServlet,
RateLimitRestServlet,
+ RedactUser,
+ RedactUserStatus,
ResetPasswordRestServlet,
SearchUsersRestServlet,
ShadowBanRestServlet,
SuspendAccountRestServlet,
UserAdminServlet,
UserByExternalId,
- UserByThreePid,
+ UserInvitesCount,
+ UserJoinedRoomCount,
UserMembershipRestServlet,
UserRegisterServlet,
UserReplaceMasterCrossSigningKeyRestServlet,
@@ -201,8 +206,7 @@ class PurgeHistoryRestServlet(RestServlet):
(stream, topo, _event_id) = r
token = "t%d-%d" % (topo, stream)
logger.info(
- "[purge] purging up to token %s (received_ts %i => "
- "stream_ordering %i)",
+ "[purge] purging up to token %s (received_ts %i => stream_ordering %i)",
token,
ts,
stream_ordering,
@@ -259,27 +263,24 @@ class PurgeHistoryStatusRestServlet(RestServlet):
########################################################################################
-class AdminRestResource(JsonResource):
- """The REST resource which gets mounted at /_synapse/admin"""
-
- def __init__(self, hs: "HomeServer"):
- JsonResource.__init__(self, hs, canonical_json=False)
- register_servlets(hs, self)
-
-
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
"""
Register all the admin servlets.
"""
- # Admin servlets aren't registered on workers.
+ RoomRestServlet(hs).register(http_server)
+
+ # Admin servlets below may not work on workers.
if hs.config.worker.worker_app is not None:
+ # Some admin servlets can be mounted on workers when MSC3861 is enabled.
+ if hs.config.experimental.msc3861.enabled:
+ register_servlets_for_msc3861_delegation(hs, http_server)
+
return
register_servlets_for_client_rest_resource(hs, http_server)
BlockRoomRestServlet(hs).register(http_server)
ListRoomRestServlet(hs).register(http_server)
RoomStateRestServlet(hs).register(http_server)
- RoomRestServlet(hs).register(http_server)
RoomRestV2Servlet(hs).register(http_server)
RoomMembersRestServlet(hs).register(http_server)
DeleteRoomStatusByDeleteIdRestServlet(hs).register(http_server)
@@ -318,7 +319,10 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
RoomTimestampToEventRestServlet(hs).register(http_server)
UserReplaceMasterCrossSigningKeyRestServlet(hs).register(http_server)
UserByExternalId(hs).register(http_server)
- UserByThreePid(hs).register(http_server)
+ RedactUser(hs).register(http_server)
+ RedactUserStatus(hs).register(http_server)
+ UserInvitesCount(hs).register(http_server)
+ UserJoinedRoomCount(hs).register(http_server)
DeviceRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
@@ -328,8 +332,8 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
BackgroundUpdateRestServlet(hs).register(http_server)
BackgroundUpdateStartJobRestServlet(hs).register(http_server)
ExperimentalFeaturesRestServlet(hs).register(http_server)
- if hs.config.experimental.msc3823_account_suspension:
- SuspendAccountRestServlet(hs).register(http_server)
+ SuspendAccountRestServlet(hs).register(http_server)
+ ScheduledTasksRestServlet(hs).register(http_server)
def register_servlets_for_client_rest_resource(
@@ -357,4 +361,16 @@ def register_servlets_for_client_rest_resource(
ListMediaInRoom(hs).register(http_server)
# don't add more things here: new servlets should only be exposed on
- # /_synapse/admin so should not go here. Instead register them in AdminRestResource.
+ # /_synapse/admin so should not go here. Instead register them in register_servlets.
+
+
+def register_servlets_for_msc3861_delegation(
+ hs: "HomeServer", http_server: HttpServer
+) -> None:
+ """Register servlets needed by MAS when MSC3861 is enabled"""
+ assert hs.config.experimental.msc3861.enabled
+
+ UserRestServletV2(hs).register(http_server)
+ UsernameAvailableRestServlet(hs).register(http_server)
+ UserReplaceMasterCrossSigningKeyRestServlet(hs).register(http_server)
+ DevicesGetRestServlet(hs).register(http_server)
diff --git a/synapse/rest/admin/devices.py b/synapse/rest/admin/devices.py
index 449b066923..09baf8ce21 100644
--- a/synapse/rest/admin/devices.py
+++ b/synapse/rest/admin/devices.py
@@ -113,18 +113,19 @@ class DeviceRestServlet(RestServlet):
return HTTPStatus.OK, {}
-class DevicesRestServlet(RestServlet):
+class DevicesGetRestServlet(RestServlet):
"""
Retrieve the given user's devices
+
+ This can be mounted on workers as it is read-only, as opposed
+ to `DevicesRestServlet`.
"""
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/devices$", "v2")
def __init__(self, hs: "HomeServer"):
self.auth = hs.get_auth()
- handler = hs.get_device_handler()
- assert isinstance(handler, DeviceHandler)
- self.device_handler = handler
+ self.device_worker_handler = hs.get_device_handler()
self.store = hs.get_datastores().main
self.is_mine = hs.is_mine
@@ -141,9 +142,35 @@ class DevicesRestServlet(RestServlet):
if u is None:
raise NotFoundError("Unknown user")
- devices = await self.device_handler.get_devices_by_user(target_user.to_string())
+ devices = await self.device_worker_handler.get_devices_by_user(
+ target_user.to_string()
+ )
+
+ # mark the dehydrated device by adding a "dehydrated" flag
+ dehydrated_device_info = await self.device_worker_handler.get_dehydrated_device(
+ target_user.to_string()
+ )
+ if dehydrated_device_info:
+ dehydrated_device_id = dehydrated_device_info[0]
+ for device in devices:
+ is_dehydrated = device["device_id"] == dehydrated_device_id
+ device["dehydrated"] = is_dehydrated
+
return HTTPStatus.OK, {"devices": devices, "total": len(devices)}
+
+class DevicesRestServlet(DevicesGetRestServlet):
+ """
+ Retrieve the given user's devices
+ """
+
+ PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/devices$", "v2")
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__(hs)
+ assert isinstance(self.device_worker_handler, DeviceHandler)
+ self.device_handler = self.device_worker_handler
+
async def on_POST(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
diff --git a/synapse/rest/admin/event_reports.py b/synapse/rest/admin/event_reports.py
index 9fb68bfa46..ff1abc0697 100644
--- a/synapse/rest/admin/event_reports.py
+++ b/synapse/rest/admin/event_reports.py
@@ -50,8 +50,10 @@ class EventReportsRestServlet(RestServlet):
The parameters `from` and `limit` are required only for pagination.
By default, a `limit` of 100 is used.
The parameter `dir` can be used to define the order of results.
- The parameter `user_id` can be used to filter by user id.
- The parameter `room_id` can be used to filter by room id.
+ The `user_id` query parameter filters by the user ID of the reporter of the event.
+ The `room_id` query parameter filters by room id.
+ The `event_sender_user_id` query parameter can be used to filter by the user id
+ of the sender of the reported event.
Returns:
A list of reported events and an integer representing the total number of
reported events that exist given this query
@@ -71,6 +73,7 @@ class EventReportsRestServlet(RestServlet):
direction = parse_enum(request, "dir", Direction, Direction.BACKWARDS)
user_id = parse_string(request, "user_id")
room_id = parse_string(request, "room_id")
+ event_sender_user_id = parse_string(request, "event_sender_user_id")
if start < 0:
raise SynapseError(
@@ -87,7 +90,7 @@ class EventReportsRestServlet(RestServlet):
)
event_reports, total = await self._store.get_event_reports_paginate(
- start, limit, direction, user_id, room_id
+ start, limit, direction, user_id, room_id, event_sender_user_id
)
ret = {"event_reports": event_reports, "total": total}
if (start + limit) < total:
diff --git a/synapse/rest/admin/experimental_features.py b/synapse/rest/admin/experimental_features.py
index d7913896d9..afb71f4a0f 100644
--- a/synapse/rest/admin/experimental_features.py
+++ b/synapse/rest/admin/experimental_features.py
@@ -43,12 +43,15 @@ class ExperimentalFeature(str, Enum):
MSC3881 = "msc3881"
MSC3575 = "msc3575"
+ MSC4222 = "msc4222"
def is_globally_enabled(self, config: "HomeServerConfig") -> bool:
if self is ExperimentalFeature.MSC3881:
return config.experimental.msc3881_enabled
if self is ExperimentalFeature.MSC3575:
return config.experimental.msc3575_enabled
+ if self is ExperimentalFeature.MSC4222:
+ return config.experimental.msc4222_enabled
assert_never(self)
diff --git a/synapse/rest/admin/registration_tokens.py b/synapse/rest/admin/registration_tokens.py
index 0867f7a51c..bec2331590 100644
--- a/synapse/rest/admin/registration_tokens.py
+++ b/synapse/rest/admin/registration_tokens.py
@@ -181,8 +181,7 @@ class NewRegistrationTokenRestServlet(RestServlet):
uses_allowed = body.get("uses_allowed", None)
if not (
- uses_allowed is None
- or (type(uses_allowed) is int and uses_allowed >= 0) # noqa: E721
+ uses_allowed is None or (type(uses_allowed) is int and uses_allowed >= 0) # noqa: E721
):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index 01f9de9ffa..adac1f0362 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -23,6 +23,7 @@ from http import HTTPStatus
from typing import TYPE_CHECKING, List, Optional, Tuple, cast
import attr
+from immutabledict import immutabledict
from synapse.api.constants import Direction, EventTypes, JoinRules, Membership
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
@@ -149,6 +150,7 @@ class RoomRestV2Servlet(RestServlet):
def _convert_delete_task_to_response(task: ScheduledTask) -> JsonDict:
return {
"delete_id": task.id,
+ "room_id": task.resource_id,
"status": task.status,
"shutdown_room": task.result,
}
@@ -249,6 +251,10 @@ class ListRoomRestServlet(RestServlet):
direction = parse_enum(request, "dir", Direction, default=Direction.FORWARDS)
reverse_order = True if direction == Direction.BACKWARDS else False
+ emma_include_tombstone = parse_boolean(
+ request, "emma_include_tombstone", default=False
+ )
+
# Return list of rooms according to parameters
rooms, total_rooms = await self.store.get_rooms_paginate(
start,
@@ -258,6 +264,7 @@ class ListRoomRestServlet(RestServlet):
search_term,
public_rooms,
empty_rooms,
+ emma_include_tombstone = emma_include_tombstone
)
response = {
@@ -463,7 +470,18 @@ class RoomStateRestServlet(RestServlet):
if not room:
raise NotFoundError("Room not found")
- event_ids = await self._storage_controllers.state.get_current_state_ids(room_id)
+ state_filter = None
+ type = parse_string(request, "type")
+
+ if type:
+ state_filter = StateFilter(
+ types=immutabledict({type: None}),
+ include_others=False,
+ )
+
+ event_ids = await self._storage_controllers.state.get_current_state_ids(
+ room_id, state_filter
+ )
events = await self.store.get_events(event_ids.values())
now = self.clock.time_msec()
room_state = await self._event_serializer.serialize_events(events.values(), now)
diff --git a/synapse/rest/admin/scheduled_tasks.py b/synapse/rest/admin/scheduled_tasks.py
new file mode 100644
index 0000000000..2ae13021b9
--- /dev/null
+++ b/synapse/rest/admin/scheduled_tasks.py
@@ -0,0 +1,70 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2025 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
+#
+#
+#
+from typing import TYPE_CHECKING, Tuple
+
+from synapse.http.servlet import RestServlet, parse_integer, parse_string
+from synapse.http.site import SynapseRequest
+from synapse.rest.admin import admin_patterns, assert_requester_is_admin
+from synapse.types import JsonDict, TaskStatus
+
+if TYPE_CHECKING:
+ from synapse.server import HomeServer
+
+
+class ScheduledTasksRestServlet(RestServlet):
+ """Get a list of scheduled tasks and their statuses
+ optionally filtered by action name, resource id, status, and max timestamp
+ """
+
+ PATTERNS = admin_patterns("/scheduled_tasks$")
+
+ def __init__(self, hs: "HomeServer"):
+ self._auth = hs.get_auth()
+ self._store = hs.get_datastores().main
+
+ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ await assert_requester_is_admin(self._auth, request)
+
+ # extract query params
+ action_name = parse_string(request, "action_name")
+ resource_id = parse_string(request, "resource_id")
+ status = parse_string(request, "job_status")
+ max_timestamp = parse_integer(request, "max_timestamp")
+
+ actions = [action_name] if action_name else None
+ statuses = [TaskStatus(status)] if status else None
+
+ tasks = await self._store.get_scheduled_tasks(
+ actions=actions,
+ resource_id=resource_id,
+ statuses=statuses,
+ max_timestamp=max_timestamp,
+ )
+
+ json_tasks = []
+ for task in tasks:
+ result_task = {
+ "id": task.id,
+ "action": task.action,
+ "status": task.status,
+ "timestamp_ms": task.timestamp,
+ "resource_id": task.resource_id,
+ "result": task.result,
+ "error": task.error,
+ }
+ json_tasks.append(result_task)
+
+ return 200, {"scheduled_tasks": json_tasks}
diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py
index ad515bd5a3..7671e020e0 100644
--- a/synapse/rest/admin/users.py
+++ b/synapse/rest/admin/users.py
@@ -27,8 +27,8 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
import attr
-from synapse._pydantic_compat import HAS_PYDANTIC_V2
-from synapse.api.constants import Direction, UserTypes
+from synapse._pydantic_compat import StrictBool, StrictInt, StrictStr
+from synapse.api.constants import Direction
from synapse.api.errors import Codes, NotFoundError, SynapseError
from synapse.http.servlet import (
RestServlet,
@@ -50,17 +50,12 @@ from synapse.rest.admin._base import (
from synapse.rest.client._base import client_patterns
from synapse.storage.databases.main.registration import ExternalIDReuseException
from synapse.storage.databases.main.stats import UserSortOrder
-from synapse.types import JsonDict, JsonMapping, UserID
+from synapse.types import JsonDict, JsonMapping, TaskStatus, UserID
from synapse.types.rest import RequestBodyModel
if TYPE_CHECKING:
from synapse.server import HomeServer
-if TYPE_CHECKING or HAS_PYDANTIC_V2:
- from pydantic.v1 import StrictBool
-else:
- from pydantic import StrictBool
-
logger = logging.getLogger(__name__)
@@ -235,6 +230,7 @@ class UserRestServletV2(RestServlet):
self.registration_handler = hs.get_registration_handler()
self.pusher_pool = hs.get_pusherpool()
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
+ self._all_user_types = hs.config.user_types.all_user_types
async def on_GET(
self, request: SynapseRequest, user_id: str
@@ -269,12 +265,6 @@ class UserRestServletV2(RestServlet):
user = await self.admin_handler.get_user(target_user)
user_id = target_user.to_string()
- # check for required parameters for each threepid
- threepids = body.get("threepids")
- if threepids is not None:
- for threepid in threepids:
- assert_params_in_dict(threepid, ["medium", "address"])
-
# check for required parameters for each external_id
external_ids = body.get("external_ids")
if external_ids is not None:
@@ -282,7 +272,7 @@ class UserRestServletV2(RestServlet):
assert_params_in_dict(external_id, ["auth_provider", "external_id"])
user_type = body.get("user_type", None)
- if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
+ if user_type is not None and user_type not in self._all_user_types:
raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid user type")
set_admin_to = body.get("admin", False)
@@ -338,51 +328,12 @@ class UserRestServletV2(RestServlet):
for external_id in external_ids
]
- # convert List[Dict[str, str]] into Set[Tuple[str, str]]
- if threepids is not None:
- new_threepids = {
- (threepid["medium"], threepid["address"]) for threepid in threepids
- }
-
if user: # modify user
if "displayname" in body:
await self.profile_handler.set_displayname(
target_user, requester, body["displayname"], True
)
- if threepids is not None:
- # get changed threepids (added and removed)
- cur_threepids = {
- (threepid.medium, threepid.address)
- for threepid in await self.store.user_get_threepids(user_id)
- }
- add_threepids = new_threepids - cur_threepids
- del_threepids = cur_threepids - new_threepids
-
- # remove old threepids
- for medium, address in del_threepids:
- try:
- # Attempt to remove any known bindings of this third-party ID
- # and user ID from identity servers.
- await self.hs.get_identity_handler().try_unbind_threepid(
- user_id, medium, address, id_server=None
- )
- except Exception:
- logger.exception("Failed to remove threepids")
- raise SynapseError(500, "Failed to remove threepids")
-
- # Delete the local association of this user ID and third-party ID.
- await self.auth_handler.delete_local_threepid(
- user_id, medium, address
- )
-
- # add new threepids
- current_time = self.hs.get_clock().time_msec()
- for medium, address in add_threepids:
- await self.auth_handler.add_threepid(
- user_id, medium, address, current_time
- )
-
if external_ids is not None:
try:
await self.store.replace_user_external_id(
@@ -467,28 +418,6 @@ class UserRestServletV2(RestServlet):
approved=new_user_approved,
)
- if threepids is not None:
- current_time = self.hs.get_clock().time_msec()
- for medium, address in new_threepids:
- await self.auth_handler.add_threepid(
- user_id, medium, address, current_time
- )
- if (
- self.hs.config.email.email_enable_notifs
- and self.hs.config.email.email_notif_for_new_users
- and medium == "email"
- ):
- await self.pusher_pool.add_or_update_pusher(
- user_id=user_id,
- kind="email",
- app_id="m.email",
- app_display_name="Email Notifications",
- device_display_name=address,
- pushkey=address,
- lang=None,
- data={},
- )
-
if external_ids is not None:
try:
for auth_provider, external_id in new_external_ids:
@@ -529,6 +458,7 @@ class UserRegisterServlet(RestServlet):
self.reactor = hs.get_reactor()
self.nonces: Dict[str, int] = {}
self.hs = hs
+ self._all_user_types = hs.config.user_types.all_user_types
def _clear_old_nonces(self) -> None:
"""
@@ -610,7 +540,7 @@ class UserRegisterServlet(RestServlet):
user_type = body.get("user_type", None)
displayname = body.get("displayname", None)
- if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
+ if user_type is not None and user_type not in self._all_user_types:
raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid user type")
if "mac" not in body:
@@ -988,7 +918,7 @@ class UserAdminServlet(RestServlet):
class UserMembershipRestServlet(RestServlet):
"""
- Get room list of an user.
+ Get list of joined room ID's for a user.
"""
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/joined_rooms$")
@@ -1004,8 +934,9 @@ class UserMembershipRestServlet(RestServlet):
await assert_requester_is_admin(self.auth, request)
room_ids = await self.store.get_rooms_for_user(user_id)
- ret = {"joined_rooms": list(room_ids), "total": len(room_ids)}
- return HTTPStatus.OK, ret
+ rooms_response = {"joined_rooms": list(room_ids), "total": len(room_ids)}
+
+ return HTTPStatus.OK, rooms_response
class PushersRestServlet(RestServlet):
@@ -1387,26 +1318,144 @@ class UserByExternalId(RestServlet):
return HTTPStatus.OK, {"user_id": user_id}
-class UserByThreePid(RestServlet):
- """Find a user based on 3PID of a particular medium"""
+class RedactUser(RestServlet):
+ """
+ Redact all the events of a given user in the given rooms or if empty dict is provided
+ then all events in all rooms user is member of. Kicks off a background process and
+ returns an id that can be used to check on the progress of the redaction progress
+ """
- PATTERNS = admin_patterns("/threepid/(?P<medium>[^/]*)/users/(?P<address>[^/]*)")
+ PATTERNS = admin_patterns("/user/(?P<user_id>[^/]*)/redact")
def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastores().main
+ self.admin_handler = hs.get_admin_handler()
+
+ class PostBody(RequestBodyModel):
+ rooms: List[StrictStr]
+ reason: Optional[StrictStr]
+ limit: Optional[StrictInt]
+
+ async def on_POST(
+ self, request: SynapseRequest, user_id: str
+ ) -> Tuple[int, JsonDict]:
+ requester = await self._auth.get_user_by_req(request)
+ await assert_user_is_admin(self._auth, requester)
+
+ # parse provided user id to check that it is valid
+ UserID.from_string(user_id)
+
+ body = parse_and_validate_json_object_from_request(request, self.PostBody)
+
+ limit = body.limit
+ if limit and limit <= 0:
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "If limit is provided it must be a non-negative integer greater than 0.",
+ )
+
+ rooms = body.rooms
+ if not rooms:
+ current_rooms = list(await self._store.get_rooms_for_user(user_id))
+ banned_rooms = list(
+ await self._store.get_rooms_user_currently_banned_from(user_id)
+ )
+ rooms = current_rooms + banned_rooms
+
+ redact_id = await self.admin_handler.start_redact_events(
+ user_id, rooms, requester.serialize(), body.reason, limit
+ )
+
+ return HTTPStatus.OK, {"redact_id": redact_id}
+
+
+class RedactUserStatus(RestServlet):
+ """
+ Check on the progress of the redaction request represented by the provided ID, returning
+ the status of the process and a dict of events that were unable to be redacted, if any
+ """
+
+ PATTERNS = admin_patterns("/user/redact_status/(?P<redact_id>[^/]*)$")
+
+ def __init__(self, hs: "HomeServer"):
+ self._auth = hs.get_auth()
+ self.admin_handler = hs.get_admin_handler()
async def on_GET(
- self,
- request: SynapseRequest,
- medium: str,
- address: str,
+ self, request: SynapseRequest, redact_id: str
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
- user_id = await self._store.get_user_id_by_threepid(medium, address)
+ task = await self.admin_handler.get_redact_task(redact_id)
+
+ if task:
+ if task.status == TaskStatus.ACTIVE:
+ return HTTPStatus.OK, {"status": TaskStatus.ACTIVE}
+ elif task.status == TaskStatus.COMPLETE:
+ assert task.result is not None
+ failed_redactions = task.result.get("failed_redactions")
+ return HTTPStatus.OK, {
+ "status": TaskStatus.COMPLETE,
+ "failed_redactions": failed_redactions if failed_redactions else {},
+ }
+ elif task.status == TaskStatus.SCHEDULED:
+ return HTTPStatus.OK, {"status": TaskStatus.SCHEDULED}
+ else:
+ return HTTPStatus.OK, {
+ "status": TaskStatus.FAILED,
+ "error": (
+ task.error
+ if task.error
+ else "Unknown error, please check the logs for more information."
+ ),
+ }
+ else:
+ raise NotFoundError("redact id '%s' not found" % redact_id)
- if user_id is None:
- raise NotFoundError("User not found")
- return HTTPStatus.OK, {"user_id": user_id}
+class UserInvitesCount(RestServlet):
+ """
+ Return the count of invites that the user has sent after the given timestamp
+ """
+
+ PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/sent_invite_count")
+
+ def __init__(self, hs: "HomeServer"):
+ self._auth = hs.get_auth()
+ self.store = hs.get_datastores().main
+
+ async def on_GET(
+ self, request: SynapseRequest, user_id: str
+ ) -> Tuple[int, JsonDict]:
+ await assert_requester_is_admin(self._auth, request)
+ from_ts = parse_integer(request, "from_ts", required=True)
+
+ sent_invite_count = await self.store.get_sent_invite_count_by_user(
+ user_id, from_ts
+ )
+
+ return HTTPStatus.OK, {"invite_count": sent_invite_count}
+
+
+class UserJoinedRoomCount(RestServlet):
+ """
+ Return the count of rooms that the user has joined at or after the given timestamp, even
+ if they have subsequently left/been banned from those rooms.
+ """
+
+ PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/cumulative_joined_room_count")
+
+ def __init__(self, hs: "HomeServer"):
+ self._auth = hs.get_auth()
+ self.store = hs.get_datastores().main
+
+ async def on_GET(
+ self, request: SynapseRequest, user_id: str
+ ) -> Tuple[int, JsonDict]:
+ await assert_requester_is_admin(self._auth, request)
+ from_ts = parse_integer(request, "from_ts", required=True)
+
+ joined_rooms = await self.store.get_rooms_for_user_by_date(user_id, from_ts)
+
+ return HTTPStatus.OK, {"cumulative_joined_room_count": len(joined_rooms)}
diff --git a/synapse/rest/client/_base.py b/synapse/rest/client/_base.py
index 93dec6375a..6cf37869d8 100644
--- a/synapse/rest/client/_base.py
+++ b/synapse/rest/client/_base.py
@@ -19,8 +19,8 @@
#
#
-"""This module contains base REST classes for constructing client v1 servlets.
-"""
+"""This module contains base REST classes for constructing client v1 servlets."""
+
import logging
import re
from typing import Any, Awaitable, Callable, Iterable, Pattern, Tuple, TypeVar, cast
diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py
index 8daa449f9e..455ddda484 100644
--- a/synapse/rest/client/account.py
+++ b/synapse/rest/client/account.py
@@ -21,28 +21,20 @@
#
import logging
import random
-from typing import TYPE_CHECKING, List, Optional, Tuple
+from typing import TYPE_CHECKING, List, Literal, Optional, Tuple
from urllib.parse import urlparse
-from synapse._pydantic_compat import HAS_PYDANTIC_V2
-
-if TYPE_CHECKING or HAS_PYDANTIC_V2:
- from pydantic.v1 import StrictBool, StrictStr, constr
-else:
- from pydantic import StrictBool, StrictStr, constr
-
import attr
-from typing_extensions import Literal
from twisted.web.server import Request
+from synapse._pydantic_compat import StrictBool, StrictStr, constr
from synapse.api.constants import LoginType
from synapse.api.errors import (
Codes,
InteractiveAuthIncompleteError,
NotFoundError,
SynapseError,
- ThreepidValidationError,
)
from synapse.handlers.ui_auth import UIAuthSessionDataConstants
from synapse.http.server import HttpServer, finish_request, respond_with_html
@@ -54,19 +46,13 @@ from synapse.http.servlet import (
parse_string,
)
from synapse.http.site import SynapseRequest
-from synapse.metrics import threepid_send_requests
-from synapse.push.mailer import Mailer
from synapse.types import JsonDict
from synapse.types.rest import RequestBodyModel
from synapse.types.rest.client import (
AuthenticationData,
- ClientSecretStr,
- EmailRequestTokenBody,
- MsisdnRequestTokenBody,
+ ClientSecretStr
)
-from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.stringutils import assert_valid_client_secret, random_string
-from synapse.util.threepids import check_3pid_allowed, validate_email
from ._base import client_patterns, interactive_auth_handler
@@ -77,80 +63,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
-class EmailPasswordRequestTokenRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/password/email/requestToken$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.datastore = hs.get_datastores().main
- self.config = hs.config
- self.identity_handler = hs.get_identity_handler()
-
- if self.config.email.can_verify_email:
- self.mailer = Mailer(
- hs=self.hs,
- app_name=self.config.email.email_app_name,
- template_html=self.config.email.email_password_reset_template_html,
- template_text=self.config.email.email_password_reset_template_text,
- )
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- if not self.config.email.can_verify_email:
- logger.warning(
- "User password resets have been disabled due to lack of email config"
- )
- raise SynapseError(
- 400, "Email-based password resets have been disabled on this server"
- )
-
- body = parse_and_validate_json_object_from_request(
- request, EmailRequestTokenBody
- )
-
- if body.next_link:
- # Raise if the provided next_link value isn't valid
- assert_valid_next_link(self.hs, body.next_link)
-
- await self.identity_handler.ratelimit_request_token_requests(
- request, "email", body.email
- )
-
- # The email will be sent to the stored address.
- # This avoids a potential account hijack by requesting a password reset to
- # an email address which is controlled by the attacker but which, after
- # canonicalisation, matches the one in our database.
- existing_user_id = await self.hs.get_datastores().main.get_user_id_by_threepid(
- "email", body.email
- )
-
- if existing_user_id is None:
- if self.config.server.request_token_inhibit_3pid_errors:
- # Make the client think the operation succeeded. See the rationale in the
- # comments for request_token_inhibit_3pid_errors.
- # Also wait for some random amount of time between 100ms and 1s to make it
- # look like we did something.
- await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
- return 200, {"sid": random_string(16)}
-
- raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
-
- # Send password reset emails from Synapse
- sid = await self.identity_handler.send_threepid_validation(
- body.email,
- body.client_secret,
- body.send_attempt,
- self.mailer.send_password_reset_mail,
- body.next_link,
- )
- threepid_send_requests.labels(type="email", reason="password_reset").observe(
- body.send_attempt
- )
-
- # Wrap the session id in a JSON object
- return 200, {"sid": sid}
-
-
class PasswordRestServlet(RestServlet):
PATTERNS = client_patterns("/account/password$")
@@ -211,30 +123,8 @@ class PasswordRestServlet(RestServlet):
"modify your account password",
)
- if LoginType.EMAIL_IDENTITY in result:
- threepid = result[LoginType.EMAIL_IDENTITY]
- if "medium" not in threepid or "address" not in threepid:
- raise SynapseError(500, "Malformed threepid")
- if threepid["medium"] == "email":
- # For emails, canonicalise the address.
- # We store all email addresses canonicalised in the DB.
- # (See add_threepid in synapse/handlers/auth.py)
- try:
- threepid["address"] = validate_email(threepid["address"])
- except ValueError as e:
- raise SynapseError(400, str(e))
- # if using email, we must know about the email they're authing with!
- threepid_user_id = await self.datastore.get_user_id_by_threepid(
- threepid["medium"], threepid["address"]
- )
- if not threepid_user_id:
- raise SynapseError(
- 404, "Email address not found", Codes.NOT_FOUND
- )
- user_id = threepid_user_id
- else:
- logger.error("Auth succeeded but no known type! %r", result.keys())
- raise SynapseError(500, "", Codes.UNKNOWN)
+ logger.error("Auth succeeded but no known type (hint: 3PID auth was removed)! %r", result.keys())
+ raise SynapseError(500, "", Codes.UNKNOWN)
except InteractiveAuthIncompleteError as e:
# The user needs to provide more steps to complete auth, but
@@ -326,486 +216,6 @@ class DeactivateAccountRestServlet(RestServlet):
return 200, {"id_server_unbind_result": id_server_unbind_result}
-class EmailThreepidRequestTokenRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/3pid/email/requestToken$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.config = hs.config
- self.identity_handler = hs.get_identity_handler()
- self.store = self.hs.get_datastores().main
-
- if self.config.email.can_verify_email:
- self.mailer = Mailer(
- hs=self.hs,
- app_name=self.config.email.email_app_name,
- template_html=self.config.email.email_add_threepid_template_html,
- template_text=self.config.email.email_add_threepid_template_text,
- )
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- if not self.hs.config.registration.enable_3pid_changes:
- raise SynapseError(
- 400, "3PID changes are disabled on this server", Codes.FORBIDDEN
- )
-
- if not self.config.email.can_verify_email:
- logger.warning(
- "Adding emails have been disabled due to lack of an email config"
- )
- raise SynapseError(
- 400,
- "Adding an email to your account is disabled on this server",
- )
-
- body = parse_and_validate_json_object_from_request(
- request, EmailRequestTokenBody
- )
-
- if not await check_3pid_allowed(self.hs, "email", body.email):
- raise SynapseError(
- 403,
- "Your email domain is not authorized on this server",
- Codes.THREEPID_DENIED,
- )
-
- await self.identity_handler.ratelimit_request_token_requests(
- request, "email", body.email
- )
-
- if body.next_link:
- # Raise if the provided next_link value isn't valid
- assert_valid_next_link(self.hs, body.next_link)
-
- existing_user_id = await self.store.get_user_id_by_threepid("email", body.email)
-
- if existing_user_id is not None:
- if self.config.server.request_token_inhibit_3pid_errors:
- # Make the client think the operation succeeded. See the rationale in the
- # comments for request_token_inhibit_3pid_errors.
- # Also wait for some random amount of time between 100ms and 1s to make it
- # look like we did something.
- await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
- return 200, {"sid": random_string(16)}
-
- raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
-
- # Send threepid validation emails from Synapse
- sid = await self.identity_handler.send_threepid_validation(
- body.email,
- body.client_secret,
- body.send_attempt,
- self.mailer.send_add_threepid_mail,
- body.next_link,
- )
-
- threepid_send_requests.labels(type="email", reason="add_threepid").observe(
- body.send_attempt
- )
-
- # Wrap the session id in a JSON object
- return 200, {"sid": sid}
-
-
-class MsisdnThreepidRequestTokenRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/3pid/msisdn/requestToken$")
-
- def __init__(self, hs: "HomeServer"):
- self.hs = hs
- super().__init__()
- self.store = self.hs.get_datastores().main
- self.identity_handler = hs.get_identity_handler()
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- body = parse_and_validate_json_object_from_request(
- request, MsisdnRequestTokenBody
- )
- msisdn = phone_number_to_msisdn(body.country, body.phone_number)
- logger.info("Request #%s to verify ownership of %s", body.send_attempt, msisdn)
-
- if not await check_3pid_allowed(self.hs, "msisdn", msisdn):
- raise SynapseError(
- 403,
- # TODO: is this error message accurate? Looks like we've only rejected
- # this phone number, not necessarily all phone numbers
- "Account phone numbers are not authorized on this server",
- Codes.THREEPID_DENIED,
- )
-
- await self.identity_handler.ratelimit_request_token_requests(
- request, "msisdn", msisdn
- )
-
- if body.next_link:
- # Raise if the provided next_link value isn't valid
- assert_valid_next_link(self.hs, body.next_link)
-
- existing_user_id = await self.store.get_user_id_by_threepid("msisdn", msisdn)
-
- if existing_user_id is not None:
- if self.hs.config.server.request_token_inhibit_3pid_errors:
- # Make the client think the operation succeeded. See the rationale in the
- # comments for request_token_inhibit_3pid_errors.
- # Also wait for some random amount of time between 100ms and 1s to make it
- # look like we did something.
- await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
- return 200, {"sid": random_string(16)}
-
- logger.info("MSISDN %s is already in use by %s", msisdn, existing_user_id)
- raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)
-
- if not self.hs.config.registration.account_threepid_delegate_msisdn:
- logger.warning(
- "No upstream msisdn account_threepid_delegate configured on the server to "
- "handle this request"
- )
- raise SynapseError(
- 400,
- "Adding phone numbers to user account is not supported by this homeserver",
- )
-
- ret = await self.identity_handler.requestMsisdnToken(
- self.hs.config.registration.account_threepid_delegate_msisdn,
- body.country,
- body.phone_number,
- body.client_secret,
- body.send_attempt,
- body.next_link,
- )
-
- threepid_send_requests.labels(type="msisdn", reason="add_threepid").observe(
- body.send_attempt
- )
- logger.info("MSISDN %s: got response from identity server: %s", msisdn, ret)
-
- return 200, ret
-
-
-class AddThreepidEmailSubmitTokenServlet(RestServlet):
- """Handles 3PID validation token submission for adding an email to a user's account"""
-
- PATTERNS = client_patterns(
- "/add_threepid/email/submit_token$", releases=(), unstable=True
- )
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.config = hs.config
- self.clock = hs.get_clock()
- self.store = hs.get_datastores().main
- if self.config.email.can_verify_email:
- self._failure_email_template = (
- self.config.email.email_add_threepid_template_failure_html
- )
-
- async def on_GET(self, request: Request) -> None:
- if not self.config.email.can_verify_email:
- logger.warning(
- "Adding emails have been disabled due to lack of an email config"
- )
- raise SynapseError(
- 400, "Adding an email to your account is disabled on this server"
- )
-
- sid = parse_string(request, "sid", required=True)
- token = parse_string(request, "token", required=True)
- client_secret = parse_string(request, "client_secret", required=True)
- assert_valid_client_secret(client_secret)
-
- # Attempt to validate a 3PID session
- try:
- # Mark the session as valid
- next_link = await self.store.validate_threepid_session(
- sid, client_secret, token, self.clock.time_msec()
- )
-
- # Perform a 302 redirect if next_link is set
- if next_link:
- request.setResponseCode(302)
- request.setHeader("Location", next_link)
- finish_request(request)
- return None
-
- # Otherwise show the success template
- html = self.config.email.email_add_threepid_template_success_html_content
- status_code = 200
- except ThreepidValidationError as e:
- status_code = e.code
-
- # Show a failure page with a reason
- template_vars = {"failure_reason": e.msg}
- html = self._failure_email_template.render(**template_vars)
-
- respond_with_html(request, status_code, html)
-
-
-class AddThreepidMsisdnSubmitTokenServlet(RestServlet):
- """Handles 3PID validation token submission for adding a phone number to a user's
- account
- """
-
- PATTERNS = client_patterns(
- "/add_threepid/msisdn/submit_token$", releases=(), unstable=True
- )
-
- class PostBody(RequestBodyModel):
- client_secret: ClientSecretStr
- sid: StrictStr
- token: StrictStr
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.config = hs.config
- self.clock = hs.get_clock()
- self.store = hs.get_datastores().main
- self.identity_handler = hs.get_identity_handler()
-
- async def on_POST(self, request: Request) -> Tuple[int, JsonDict]:
- if not self.config.registration.account_threepid_delegate_msisdn:
- raise SynapseError(
- 400,
- "This homeserver is not validating phone numbers. Use an identity server "
- "instead.",
- )
-
- body = parse_and_validate_json_object_from_request(request, self.PostBody)
-
- # Proxy submit_token request to msisdn threepid delegate
- response = await self.identity_handler.proxy_msisdn_submit_token(
- self.config.registration.account_threepid_delegate_msisdn,
- body.client_secret,
- body.sid,
- body.token,
- )
- return 200, response
-
-
-class ThreepidRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/3pid$")
- # This is used as a proxy for all the 3pid endpoints.
-
- CATEGORY = "Client API requests"
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.identity_handler = hs.get_identity_handler()
- self.auth = hs.get_auth()
- self.auth_handler = hs.get_auth_handler()
- self.datastore = self.hs.get_datastores().main
-
- async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- requester = await self.auth.get_user_by_req(request)
-
- threepids = await self.datastore.user_get_threepids(requester.user.to_string())
-
- return 200, {"threepids": [attr.asdict(t) for t in threepids]}
-
- # NOTE(dmr): I have chosen not to use Pydantic to parse this request's body, because
- # the endpoint is deprecated. (If you really want to, you could do this by reusing
- # ThreePidBindRestServelet.PostBody with an `alias_generator` to handle
- # `threePidCreds` versus `three_pid_creds`.
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- if self.hs.config.experimental.msc3861.enabled:
- raise NotFoundError(errcode=Codes.UNRECOGNIZED)
-
- if not self.hs.config.registration.enable_3pid_changes:
- raise SynapseError(
- 400, "3PID changes are disabled on this server", Codes.FORBIDDEN
- )
-
- requester = await self.auth.get_user_by_req(request)
- user_id = requester.user.to_string()
- body = parse_json_object_from_request(request)
-
- threepid_creds = body.get("threePidCreds") or body.get("three_pid_creds")
- if threepid_creds is None:
- raise SynapseError(
- 400, "Missing param three_pid_creds", Codes.MISSING_PARAM
- )
- assert_params_in_dict(threepid_creds, ["client_secret", "sid"])
-
- sid = threepid_creds["sid"]
- client_secret = threepid_creds["client_secret"]
- assert_valid_client_secret(client_secret)
-
- validation_session = await self.identity_handler.validate_threepid_session(
- client_secret, sid
- )
- if validation_session:
- await self.auth_handler.add_threepid(
- user_id,
- validation_session["medium"],
- validation_session["address"],
- validation_session["validated_at"],
- )
- return 200, {}
-
- raise SynapseError(
- 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
- )
-
-
-class ThreepidAddRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/3pid/add$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.identity_handler = hs.get_identity_handler()
- self.auth = hs.get_auth()
- self.auth_handler = hs.get_auth_handler()
-
- class PostBody(RequestBodyModel):
- auth: Optional[AuthenticationData] = None
- client_secret: ClientSecretStr
- sid: StrictStr
-
- @interactive_auth_handler
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- if not self.hs.config.registration.enable_3pid_changes:
- raise SynapseError(
- 400, "3PID changes are disabled on this server", Codes.FORBIDDEN
- )
-
- requester = await self.auth.get_user_by_req(request)
- user_id = requester.user.to_string()
- body = parse_and_validate_json_object_from_request(request, self.PostBody)
-
- await self.auth_handler.validate_user_via_ui_auth(
- requester,
- request,
- body.dict(exclude_unset=True),
- "add a third-party identifier to your account",
- )
-
- validation_session = await self.identity_handler.validate_threepid_session(
- body.client_secret, body.sid
- )
- if validation_session:
- await self.auth_handler.add_threepid(
- user_id,
- validation_session["medium"],
- validation_session["address"],
- validation_session["validated_at"],
- )
- return 200, {}
-
- raise SynapseError(
- 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
- )
-
-
-class ThreepidBindRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/3pid/bind$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.identity_handler = hs.get_identity_handler()
- self.auth = hs.get_auth()
-
- class PostBody(RequestBodyModel):
- client_secret: ClientSecretStr
- id_access_token: StrictStr
- id_server: StrictStr
- sid: StrictStr
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- body = parse_and_validate_json_object_from_request(request, self.PostBody)
-
- requester = await self.auth.get_user_by_req(request)
- user_id = requester.user.to_string()
-
- await self.identity_handler.bind_threepid(
- body.client_secret, body.sid, user_id, body.id_server, body.id_access_token
- )
-
- return 200, {}
-
-
-class ThreepidUnbindRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/3pid/unbind$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.identity_handler = hs.get_identity_handler()
- self.auth = hs.get_auth()
- self.datastore = self.hs.get_datastores().main
-
- class PostBody(RequestBodyModel):
- address: StrictStr
- id_server: Optional[StrictStr] = None
- medium: Literal["email", "msisdn"]
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- """Unbind the given 3pid from a specific identity server, or identity servers that are
- known to have this 3pid bound
- """
- requester = await self.auth.get_user_by_req(request)
- body = parse_and_validate_json_object_from_request(request, self.PostBody)
-
- # Attempt to unbind the threepid from an identity server. If id_server is None, try to
- # unbind from all identity servers this threepid has been added to in the past
- result = await self.identity_handler.try_unbind_threepid(
- requester.user.to_string(), body.medium, body.address, body.id_server
- )
- return 200, {"id_server_unbind_result": "success" if result else "no-support"}
-
-
-class ThreepidDeleteRestServlet(RestServlet):
- PATTERNS = client_patterns("/account/3pid/delete$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.auth = hs.get_auth()
- self.auth_handler = hs.get_auth_handler()
-
- class PostBody(RequestBodyModel):
- address: StrictStr
- id_server: Optional[StrictStr] = None
- medium: Literal["email", "msisdn"]
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- if not self.hs.config.registration.enable_3pid_changes:
- raise SynapseError(
- 400, "3PID changes are disabled on this server", Codes.FORBIDDEN
- )
-
- body = parse_and_validate_json_object_from_request(request, self.PostBody)
-
- requester = await self.auth.get_user_by_req(request)
- user_id = requester.user.to_string()
-
- try:
- # Attempt to remove any known bindings of this third-party ID
- # and user ID from identity servers.
- ret = await self.hs.get_identity_handler().try_unbind_threepid(
- user_id, body.medium, body.address, body.id_server
- )
- except Exception:
- # NB. This endpoint should succeed if there is nothing to
- # delete, so it should only throw if something is wrong
- # that we ought to care about.
- logger.exception("Failed to remove threepid")
- raise SynapseError(500, "Failed to remove threepid")
-
- if ret:
- id_server_unbind_result = "success"
- else:
- id_server_unbind_result = "no-support"
-
- # Delete the local association of this user ID and third-party ID.
- await self.auth_handler.delete_local_threepid(
- user_id, body.medium, body.address
- )
-
- return 200, {"id_server_unbind_result": id_server_unbind_result}
-
-
def assert_valid_next_link(hs: "HomeServer", next_link: str) -> None:
"""
Raises a SynapseError if a given next_link value is invalid
@@ -901,20 +311,8 @@ class AccountStatusRestServlet(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
if hs.config.worker.worker_app is None:
if not hs.config.experimental.msc3861.enabled:
- EmailPasswordRequestTokenRestServlet(hs).register(http_server)
DeactivateAccountRestServlet(hs).register(http_server)
PasswordRestServlet(hs).register(http_server)
- EmailThreepidRequestTokenRestServlet(hs).register(http_server)
- MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
- AddThreepidEmailSubmitTokenServlet(hs).register(http_server)
- AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server)
- ThreepidRestServlet(hs).register(http_server)
- if hs.config.worker.worker_app is None:
- ThreepidBindRestServlet(hs).register(http_server)
- ThreepidUnbindRestServlet(hs).register(http_server)
- if not hs.config.experimental.msc3861.enabled:
- ThreepidAddRestServlet(hs).register(http_server)
- ThreepidDeleteRestServlet(hs).register(http_server)
WhoamiRestServlet(hs).register(http_server)
if hs.config.worker.worker_app is None and hs.config.experimental.msc3720_enabled:
diff --git a/synapse/rest/client/account_data.py b/synapse/rest/client/account_data.py
index 0ee24081fa..734c9e992f 100644
--- a/synapse/rest/client/account_data.py
+++ b/synapse/rest/client/account_data.py
@@ -108,9 +108,9 @@ class AccountDataServlet(RestServlet):
# Push rules are stored in a separate table and must be queried separately.
if account_data_type == AccountDataTypes.PUSH_RULES:
- account_data: Optional[JsonMapping] = (
- await self._push_rules_handler.push_rules_for_user(requester.user)
- )
+ account_data: Optional[
+ JsonMapping
+ ] = await self._push_rules_handler.push_rules_for_user(requester.user)
else:
account_data = await self.store.get_global_account_data_by_type_for_user(
user_id, account_data_type
diff --git a/synapse/rest/client/account_validity.py b/synapse/rest/client/account_validity.py
index 6222a5cc37..ec7836b647 100644
--- a/synapse/rest/client/account_validity.py
+++ b/synapse/rest/client/account_validity.py
@@ -48,9 +48,7 @@ class AccountValidityRenewServlet(RestServlet):
self.account_renewed_template = (
hs.config.account_validity.account_validity_account_renewed_template
)
- self.account_previously_renewed_template = (
- hs.config.account_validity.account_validity_account_previously_renewed_template
- )
+ self.account_previously_renewed_template = hs.config.account_validity.account_validity_account_previously_renewed_template
self.invalid_token_template = (
hs.config.account_validity.account_validity_invalid_token_template
)
diff --git a/synapse/rest/client/appservice_ping.py b/synapse/rest/client/appservice_ping.py
index d6b4e32453..1f9662a95a 100644
--- a/synapse/rest/client/appservice_ping.py
+++ b/synapse/rest/client/appservice_ping.py
@@ -2,7 +2,7 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright 2023 Tulir Asokan
-# Copyright (C) 2023 New Vector, Ltd
+# Copyright (C) 2023, 2025 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -53,6 +53,7 @@ class AppservicePingRestServlet(RestServlet):
def __init__(self, hs: "HomeServer"):
super().__init__()
self.as_api = hs.get_application_service_api()
+ self.scheduler = hs.get_application_service_scheduler()
self.auth = hs.get_auth()
async def on_POST(
@@ -85,6 +86,10 @@ class AppservicePingRestServlet(RestServlet):
start = time.monotonic()
try:
await self.as_api.ping(requester.app_service, txn_id)
+
+ # We got a OK response, so if the AS needs to be recovered then lets recover it now.
+ # This sets off a task in the background and so is safe to execute and forget.
+ self.scheduler.txn_ctrl.force_retry(requester.app_service)
except RequestTimedOutError as e:
raise SynapseError(
HTTPStatus.GATEWAY_TIMEOUT,
diff --git a/synapse/rest/client/auth.py b/synapse/rest/client/auth.py
index 4221f35937..b8dca7c797 100644
--- a/synapse/rest/client/auth.py
+++ b/synapse/rest/client/auth.py
@@ -20,14 +20,14 @@
#
import logging
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, cast
from twisted.web.server import Request
from synapse.api.constants import LoginType
from synapse.api.errors import LoginError, SynapseError
from synapse.api.urls import CLIENT_API_PREFIX
-from synapse.http.server import HttpServer, respond_with_html
+from synapse.http.server import HttpServer, respond_with_html, respond_with_redirect
from synapse.http.servlet import RestServlet, parse_string
from synapse.http.site import SynapseRequest
@@ -66,6 +66,23 @@ class AuthRestServlet(RestServlet):
if not session:
raise SynapseError(400, "No session supplied")
+ if (
+ self.hs.config.experimental.msc3861.enabled
+ and stagetype == "org.matrix.cross_signing_reset"
+ ):
+ # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth
+ # We import lazily here because of the authlib requirement
+ from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth
+
+ auth = cast(MSC3861DelegatedAuth, self.auth)
+
+ url = await auth.account_management_url()
+ if url is not None:
+ url = f"{url}?action=org.matrix.cross_signing_reset"
+ else:
+ url = await auth.issuer()
+ respond_with_redirect(request, str.encode(url))
+
if stagetype == LoginType.RECAPTCHA:
html = self.recaptcha_template.render(
session=session,
diff --git a/synapse/rest/client/auth_issuer.py b/synapse/rest/client/auth_metadata.py
index 77b9720956..5444a89be6 100644
--- a/synapse/rest/client/auth_issuer.py
+++ b/synapse/rest/client/auth_metadata.py
@@ -13,7 +13,7 @@
# limitations under the License.
import logging
import typing
-from typing import Tuple
+from typing import Tuple, cast
from synapse.api.errors import Codes, SynapseError
from synapse.http.server import HttpServer
@@ -32,6 +32,8 @@ logger = logging.getLogger(__name__)
class AuthIssuerServlet(RestServlet):
"""
Advertises what OpenID Connect issuer clients should use to authorise users.
+ This endpoint was defined in a previous iteration of MSC2965, and is still
+ used by some clients.
"""
PATTERNS = client_patterns(
@@ -43,10 +45,16 @@ class AuthIssuerServlet(RestServlet):
def __init__(self, hs: "HomeServer"):
super().__init__()
self._config = hs.config
+ self._auth = hs.get_auth()
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
if self._config.experimental.msc3861.enabled:
- return 200, {"issuer": self._config.experimental.msc3861.issuer}
+ # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth
+ # We import lazily here because of the authlib requirement
+ from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth
+
+ auth = cast(MSC3861DelegatedAuth, self._auth)
+ return 200, {"issuer": await auth.issuer()}
else:
# Wouldn't expect this to be reached: the servelet shouldn't have been
# registered. Still, fail gracefully if we are registered for some reason.
@@ -57,7 +65,42 @@ class AuthIssuerServlet(RestServlet):
)
+class AuthMetadataServlet(RestServlet):
+ """
+ Advertises the OAuth 2.0 server metadata for the homeserver.
+ """
+
+ PATTERNS = client_patterns(
+ "/org.matrix.msc2965/auth_metadata$",
+ unstable=True,
+ releases=(),
+ )
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__()
+ self._config = hs.config
+ self._auth = hs.get_auth()
+
+ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ if self._config.experimental.msc3861.enabled:
+ # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth
+ # We import lazily here because of the authlib requirement
+ from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth
+
+ auth = cast(MSC3861DelegatedAuth, self._auth)
+ return 200, await auth.auth_metadata()
+ else:
+ # Wouldn't expect this to be reached: the servlet shouldn't have been
+ # registered. Still, fail gracefully if we are registered for some reason.
+ raise SynapseError(
+ 404,
+ "OIDC discovery has not been configured on this homeserver",
+ Codes.NOT_FOUND,
+ )
+
+
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
# We use the MSC3861 values as they are used by multiple MSCs
if hs.config.experimental.msc3861.enabled:
AuthIssuerServlet(hs).register(http_server)
+ AuthMetadataServlet(hs).register(http_server)
diff --git a/synapse/rest/client/capabilities.py b/synapse/rest/client/capabilities.py
index 63b8a9364a..caac5826a4 100644
--- a/synapse/rest/client/capabilities.py
+++ b/synapse/rest/client/capabilities.py
@@ -21,7 +21,7 @@ import logging
from http import HTTPStatus
from typing import TYPE_CHECKING, Tuple
-from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, MSC3244_CAPABILITIES
+from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet
from synapse.http.site import SynapseRequest
@@ -69,7 +69,7 @@ class CapabilitiesRestServlet(RestServlet):
"enabled": self.config.registration.enable_set_avatar_url
},
"m.3pid_changes": {
- "enabled": self.config.registration.enable_3pid_changes
+ "enabled": False
},
"m.get_login_token": {
"enabled": self.config.auth.login_via_existing_enabled,
@@ -77,11 +77,6 @@ class CapabilitiesRestServlet(RestServlet):
}
}
- if self.config.experimental.msc3244_enabled:
- response["capabilities"]["m.room_versions"][
- "org.matrix.msc3244.room_capabilities"
- ] = MSC3244_CAPABILITIES
-
if self.config.experimental.msc3720_enabled:
response["capabilities"]["org.matrix.msc3720.account_status"] = {
"enabled": True,
@@ -92,6 +87,23 @@ class CapabilitiesRestServlet(RestServlet):
"enabled": self.config.experimental.msc3664_enabled,
}
+ if self.config.experimental.msc4133_enabled:
+ response["capabilities"]["uk.tcpip.msc4133.profile_fields"] = {
+ "enabled": True,
+ }
+
+ # Ensure this is consistent with the legacy m.set_displayname and
+ # m.set_avatar_url.
+ disallowed = []
+ if not self.config.registration.enable_set_displayname:
+ disallowed.append("displayname")
+ if not self.config.registration.enable_set_avatar_url:
+ disallowed.append("avatar_url")
+ if disallowed:
+ response["capabilities"]["uk.tcpip.msc4133.profile_fields"][
+ "disallowed"
+ ] = disallowed
+
return HTTPStatus.OK, response
diff --git a/synapse/rest/client/delayed_events.py b/synapse/rest/client/delayed_events.py
new file mode 100644
index 0000000000..2dd5a60b2b
--- /dev/null
+++ b/synapse/rest/client/delayed_events.py
@@ -0,0 +1,111 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2024 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
+#
+
+# This module contains REST servlets to do with delayed events: /delayed_events/<paths>
+
+import logging
+from enum import Enum
+from http import HTTPStatus
+from typing import TYPE_CHECKING, Tuple
+
+from synapse.api.errors import Codes, SynapseError
+from synapse.http.server import HttpServer
+from synapse.http.servlet import RestServlet, parse_json_object_from_request
+from synapse.http.site import SynapseRequest
+from synapse.rest.client._base import client_patterns
+from synapse.types import JsonDict
+
+if TYPE_CHECKING:
+ from synapse.server import HomeServer
+
+logger = logging.getLogger(__name__)
+
+
+class _UpdateDelayedEventAction(Enum):
+ CANCEL = "cancel"
+ RESTART = "restart"
+ SEND = "send"
+
+
+class UpdateDelayedEventServlet(RestServlet):
+ PATTERNS = client_patterns(
+ r"/org\.matrix\.msc4140/delayed_events/(?P<delay_id>[^/]+)$",
+ releases=(),
+ )
+ CATEGORY = "Delayed event management requests"
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__()
+ self.auth = hs.get_auth()
+ self.delayed_events_handler = hs.get_delayed_events_handler()
+
+ async def on_POST(
+ self, request: SynapseRequest, delay_id: str
+ ) -> Tuple[int, JsonDict]:
+ requester = await self.auth.get_user_by_req(request)
+
+ body = parse_json_object_from_request(request)
+ try:
+ action = str(body["action"])
+ except KeyError:
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "'action' is missing",
+ Codes.MISSING_PARAM,
+ )
+ try:
+ enum_action = _UpdateDelayedEventAction(action)
+ except ValueError:
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "'action' is not one of "
+ + ", ".join(f"'{m.value}'" for m in _UpdateDelayedEventAction),
+ Codes.INVALID_PARAM,
+ )
+
+ if enum_action == _UpdateDelayedEventAction.CANCEL:
+ await self.delayed_events_handler.cancel(requester, delay_id)
+ elif enum_action == _UpdateDelayedEventAction.RESTART:
+ await self.delayed_events_handler.restart(requester, delay_id)
+ elif enum_action == _UpdateDelayedEventAction.SEND:
+ await self.delayed_events_handler.send(requester, delay_id)
+ return 200, {}
+
+
+class DelayedEventsServlet(RestServlet):
+ PATTERNS = client_patterns(
+ r"/org\.matrix\.msc4140/delayed_events$",
+ releases=(),
+ )
+ CATEGORY = "Delayed event management requests"
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__()
+ self.auth = hs.get_auth()
+ self.delayed_events_handler = hs.get_delayed_events_handler()
+
+ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ requester = await self.auth.get_user_by_req(request)
+ # TODO: Support Pagination stream API ("from" query parameter)
+ delayed_events = await self.delayed_events_handler.get_all_for_user(requester)
+
+ ret = {"delayed_events": delayed_events}
+ return 200, ret
+
+
+def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+ # The following can't currently be instantiated on workers.
+ if hs.config.worker.worker_app is None:
+ UpdateDelayedEventServlet(hs).register(http_server)
+ DelayedEventsServlet(hs).register(http_server)
diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py
index 8313d687b7..0b075cc2f2 100644
--- a/synapse/rest/client/devices.py
+++ b/synapse/rest/client/devices.py
@@ -24,13 +24,7 @@ import logging
from http import HTTPStatus
from typing import TYPE_CHECKING, List, Optional, Tuple
-from synapse._pydantic_compat import HAS_PYDANTIC_V2
-
-if TYPE_CHECKING or HAS_PYDANTIC_V2:
- from pydantic.v1 import Extra, StrictStr
-else:
- from pydantic import Extra, StrictStr
-
+from synapse._pydantic_compat import Extra, StrictStr
from synapse.api import errors
from synapse.api.errors import NotFoundError, SynapseError, UnrecognizedRequestError
from synapse.handlers.device import DeviceHandler
@@ -120,15 +114,19 @@ class DeleteDevicesRestServlet(RestServlet):
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
@@ -145,11 +143,11 @@ class DeviceRestServlet(RestServlet):
self.hs = hs
self.auth = hs.get_auth()
handler = hs.get_device_handler()
- assert isinstance(handler, DeviceHandler)
self.device_handler = handler
self.auth_handler = hs.get_auth_handler()
self._msc3852_enabled = hs.config.experimental.msc3852_enabled
self._msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
+ self._is_main_process = hs.config.worker.worker_app is None
async def on_GET(
self, request: SynapseRequest, device_id: str
@@ -181,8 +179,13 @@ class DeviceRestServlet(RestServlet):
async def on_DELETE(
self, request: SynapseRequest, device_id: str
) -> Tuple[int, JsonDict]:
- if self._msc3861_oauth_delegation_enabled:
- raise UnrecognizedRequestError(code=404)
+ # Can only be run on main process, as changes to device lists must
+ # happen on main.
+ if not self._is_main_process:
+ error_message = "DELETE on /devices/ must be routed to main process"
+ logger.error(error_message)
+ raise SynapseError(500, error_message)
+ assert isinstance(self.device_handler, DeviceHandler)
requester = await self.auth.get_user_by_req(request)
@@ -198,15 +201,24 @@ class DeviceRestServlet(RestServlet):
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]
@@ -219,9 +231,27 @@ class DeviceRestServlet(RestServlet):
async def on_PUT(
self, request: SynapseRequest, device_id: str
) -> Tuple[int, JsonDict]:
+ # Can only be run on main process, as changes to device lists must
+ # happen on main.
+ if not self._is_main_process:
+ error_message = "PUT on /devices/ must be routed to main process"
+ logger.error(error_message)
+ raise SynapseError(500, error_message)
+ assert isinstance(self.device_handler, DeviceHandler)
+
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()
)
@@ -571,9 +601,9 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
):
DeleteDevicesRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
+ DeviceRestServlet(hs).register(http_server)
if hs.config.worker.worker_app is None:
- DeviceRestServlet(hs).register(http_server)
if hs.config.experimental.msc2697_enabled:
DehydratedDeviceServlet(hs).register(http_server)
ClaimDehydratedDeviceServlet(hs).register(http_server)
diff --git a/synapse/rest/client/directory.py b/synapse/rest/client/directory.py
index 11fdd0f7c6..479f489623 100644
--- a/synapse/rest/client/directory.py
+++ b/synapse/rest/client/directory.py
@@ -20,19 +20,11 @@
#
import logging
-from typing import TYPE_CHECKING, List, Optional, Tuple
-
-from synapse._pydantic_compat import HAS_PYDANTIC_V2
-
-if TYPE_CHECKING or HAS_PYDANTIC_V2:
- from pydantic.v1 import StrictStr
-else:
- from pydantic import StrictStr
-
-from typing_extensions import Literal
+from typing import TYPE_CHECKING, List, Literal, Optional, Tuple
from twisted.web.server import Request
+from synapse._pydantic_compat import StrictStr
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
from synapse.http.server import HttpServer
from synapse.http.servlet import (
diff --git a/synapse/rest/client/events.py b/synapse/rest/client/events.py
index 613890061e..ad23cc76ce 100644
--- a/synapse/rest/client/events.py
+++ b/synapse/rest/client/events.py
@@ -20,6 +20,7 @@
#
"""This module contains REST servlets to do with event streaming, /events."""
+
import logging
from typing import TYPE_CHECKING, Dict, List, Tuple, Union
diff --git a/synapse/rest/client/keys.py b/synapse/rest/client/keys.py
index eddad7d5b8..7025662fdc 100644
--- a/synapse/rest/client/keys.py
+++ b/synapse/rest/client/keys.py
@@ -23,10 +23,13 @@
import logging
import re
from collections import Counter
-from http import HTTPStatus
-from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, cast
-from synapse.api.errors import Codes, InvalidAPICallError, SynapseError
+from synapse.api.errors import (
+ InteractiveAuthIncompleteError,
+ InvalidAPICallError,
+ SynapseError,
+)
from synapse.http.server import HttpServer
from synapse.http.servlet import (
RestServlet,
@@ -403,17 +406,36 @@ class SigningKeyUploadServlet(RestServlet):
# explicitly mark the master key as replaceable.
if self.hs.config.experimental.msc3861.enabled:
if not master_key_updatable_without_uia:
- config = self.hs.config.experimental.msc3861
- if config.account_management_url is not None:
- url = f"{config.account_management_url}?action=org.matrix.cross_signing_reset"
+ # If MSC3861 is enabled, we can assume self.auth is an instance of MSC3861DelegatedAuth
+ # We import lazily here because of the authlib requirement
+ from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth
+
+ auth = cast(MSC3861DelegatedAuth, self.auth)
+
+ uri = await auth.account_management_url()
+ if uri is not None:
+ url = f"{uri}?action=org.matrix.cross_signing_reset"
else:
- url = config.issuer
+ url = await auth.issuer()
- raise SynapseError(
- HTTPStatus.NOT_IMPLEMENTED,
- "To reset your end-to-end encryption cross-signing identity, "
- f"you first need to approve it at {url} and then try again.",
- Codes.UNRECOGNIZED,
+ # We use a dummy session ID as this isn't really a UIA flow, but we
+ # reuse the same API shape for better client compatibility.
+ raise InteractiveAuthIncompleteError(
+ "dummy",
+ {
+ "session": "dummy",
+ "flows": [
+ {"stages": ["org.matrix.cross_signing_reset"]},
+ ],
+ "params": {
+ "org.matrix.cross_signing_reset": {
+ "url": url,
+ },
+ },
+ "msg": "To reset your end-to-end encryption cross-signing "
+ f"identity, you first need to approve it at {url} and "
+ "then try again.",
+ },
)
else:
# Without MSC3861, we require UIA.
diff --git a/synapse/rest/client/knock.py b/synapse/rest/client/knock.py
index e31687fc13..d7a17e1b35 100644
--- a/synapse/rest/client/knock.py
+++ b/synapse/rest/client/knock.py
@@ -53,7 +53,6 @@ class KnockRoomAliasServlet(RestServlet):
super().__init__()
self.room_member_handler = hs.get_room_member_handler()
self.auth = hs.get_auth()
- self._support_via = hs.config.experimental.msc4156_enabled
async def on_POST(
self,
@@ -72,15 +71,11 @@ class KnockRoomAliasServlet(RestServlet):
# twisted.web.server.Request.args is incorrectly defined as Optional[Any]
args: Dict[bytes, List[bytes]] = request.args # type: ignore
- remote_room_hosts = parse_strings_from_args(
- args, "server_name", required=False
- )
- if self._support_via:
+ # Prefer via over server_name (deprecated with MSC4156)
+ remote_room_hosts = parse_strings_from_args(args, "via", required=False)
+ if remote_room_hosts is None:
remote_room_hosts = parse_strings_from_args(
- args,
- "org.matrix.msc4156.via",
- default=remote_room_hosts,
- required=False,
+ args, "server_name", required=False
)
elif RoomAlias.is_valid(room_identifier):
handler = self.room_member_handler
diff --git a/synapse/rest/client/login.py b/synapse/rest/client/login.py
index ae691bcdba..cc6863cadc 100644
--- a/synapse/rest/client/login.py
+++ b/synapse/rest/client/login.py
@@ -30,11 +30,10 @@ from typing import (
List,
Optional,
Tuple,
+ TypedDict,
Union,
)
-from typing_extensions import TypedDict
-
from synapse.api.constants import ApprovalNoticeMedium
from synapse.api.errors import (
Codes,
@@ -82,7 +81,6 @@ class LoginRestServlet(RestServlet):
PATTERNS = client_patterns("/login$", v1=True)
CATEGORY = "Registration/login requests"
- CAS_TYPE = "m.login.cas"
SSO_TYPE = "m.login.sso"
TOKEN_TYPE = "m.login.token"
JWT_TYPE = "org.matrix.login.jwt"
@@ -98,8 +96,6 @@ class LoginRestServlet(RestServlet):
self.jwt_enabled = hs.config.jwt.jwt_enabled
# SSO configuration.
- self.saml2_enabled = hs.config.saml2.saml2_enabled
- self.cas_enabled = hs.config.cas.cas_enabled
self.oidc_enabled = hs.config.oidc.oidc_enabled
self._refresh_tokens_enabled = (
hs.config.registration.refreshable_access_token_lifetime is not None
@@ -136,7 +132,7 @@ class LoginRestServlet(RestServlet):
cfg=self.hs.config.ratelimiting.rc_login_account,
)
- # ensure the CAS/SAML/OIDC handlers are loaded on this worker instance.
+ # ensure the OIDC handlers are loaded on this worker instance.
# The reason for this is to ensure that the auth_provider_ids are registered
# with SsoHandler, which in turn ensures that the login/registration prometheus
# counters are initialised for the auth_provider_ids.
@@ -147,15 +143,10 @@ class LoginRestServlet(RestServlet):
if self.jwt_enabled:
flows.append({"type": LoginRestServlet.JWT_TYPE})
- if self.cas_enabled:
- # we advertise CAS for backwards compat, though MSC1721 renamed it
- # to SSO.
- flows.append({"type": LoginRestServlet.CAS_TYPE})
-
# The login token flow requires m.login.token to be advertised.
support_login_token_flow = self._get_login_token_enabled
- if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
+ if self.oidc_enabled:
flows.append(
{
"type": LoginRestServlet.SSO_TYPE,
@@ -268,7 +259,7 @@ class LoginRestServlet(RestServlet):
approval_notice_medium=ApprovalNoticeMedium.NONE,
)
- well_known_data = self._well_known_builder.get_well_known()
+ well_known_data = await self._well_known_builder.get_well_known()
if well_known_data:
result["well_known"] = well_known_data
return 200, result
@@ -325,7 +316,7 @@ class LoginRestServlet(RestServlet):
*,
request_info: RequestInfo,
) -> LoginResponse:
- """Handle non-token/saml/jwt logins
+ """Handle non-token/jwt logins
Args:
login_submission:
@@ -363,6 +354,7 @@ class LoginRestServlet(RestServlet):
login_submission: JsonDict,
callback: Optional[Callable[[LoginResponse], Awaitable[None]]] = None,
create_non_existent_users: bool = False,
+ default_display_name: Optional[str] = None,
ratelimit: bool = True,
auth_provider_id: Optional[str] = None,
should_issue_refresh_token: bool = False,
@@ -410,7 +402,8 @@ class LoginRestServlet(RestServlet):
canonical_uid = await self.auth_handler.check_user_exists(user_id)
if not canonical_uid:
canonical_uid = await self.registration_handler.register_user(
- localpart=UserID.from_string(user_id).localpart
+ localpart=UserID.from_string(user_id).localpart,
+ default_display_name=default_display_name,
)
user_id = canonical_uid
@@ -546,11 +539,14 @@ class LoginRestServlet(RestServlet):
Returns:
The body of the JSON response.
"""
- user_id = self.hs.get_jwt_handler().validate_login(login_submission)
+ user_id, default_display_name = self.hs.get_jwt_handler().validate_login(
+ login_submission
+ )
return await self._complete_login(
user_id,
login_submission,
create_non_existent_users=True,
+ default_display_name=default_display_name,
should_issue_refresh_token=should_issue_refresh_token,
request_info=request_info,
)
@@ -622,7 +618,7 @@ class RefreshTokenServlet(RestServlet):
class SsoRedirectServlet(RestServlet):
- PATTERNS = list(client_patterns("/login/(cas|sso)/redirect$", v1=True)) + [
+ PATTERNS = list(client_patterns("/login/sso/redirect$", v1=True)) + [
re.compile(
"^"
+ CLIENT_API_PREFIX
@@ -679,31 +675,6 @@ class SsoRedirectServlet(RestServlet):
finish_request(request)
-class CasTicketServlet(RestServlet):
- PATTERNS = client_patterns("/login/cas/ticket", v1=True)
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self._cas_handler = hs.get_cas_handler()
-
- async def on_GET(self, request: SynapseRequest) -> None:
- client_redirect_url = parse_string(request, "redirectUrl")
- ticket = parse_string(request, "ticket", required=True)
-
- # Maybe get a session ID (if this ticket is from user interactive
- # authentication).
- session = parse_string(request, "session")
-
- # Either client_redirect_url or session must be provided.
- if not client_redirect_url and not session:
- message = "Missing string query parameter redirectUrl or session"
- raise SynapseError(400, message, errcode=Codes.MISSING_PARAM)
-
- await self._cas_handler.handle_ticket(
- request, ticket, client_redirect_url, session
- )
-
-
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
if hs.config.experimental.msc3861.enabled:
return
@@ -715,26 +686,18 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
):
RefreshTokenServlet(hs).register(http_server)
if (
- hs.config.cas.cas_enabled
- or hs.config.saml2.saml2_enabled
- or hs.config.oidc.oidc_enabled
+ hs.config.oidc.oidc_enabled
):
SsoRedirectServlet(hs).register(http_server)
- if hs.config.cas.cas_enabled:
- CasTicketServlet(hs).register(http_server)
def _load_sso_handlers(hs: "HomeServer") -> None:
"""Ensure that the SSO handlers are loaded, if they are enabled by configuration.
- This is mostly useful to ensure that the CAS/SAML/OIDC handlers register themselves
+ This is mostly useful to ensure that the OIDC handler registers itself
with the main SsoHandler.
It's safe to call this multiple times.
"""
- if hs.config.cas.cas_enabled:
- hs.get_cas_handler()
- if hs.config.saml2.saml2_enabled:
- hs.get_saml_handler()
if hs.config.oidc.oidc_enabled:
hs.get_oidc_handler()
diff --git a/synapse/rest/client/media.py b/synapse/rest/client/media.py
index c30e3022de..4c044ae900 100644
--- a/synapse/rest/client/media.py
+++ b/synapse/rest/client/media.py
@@ -102,10 +102,17 @@ class MediaConfigResource(RestServlet):
self.clock = hs.get_clock()
self.auth = hs.get_auth()
self.limits_dict = {"m.upload.size": config.media.max_upload_size}
+ self.media_repository_callbacks = hs.get_module_api_callbacks().media_repository
async def on_GET(self, request: SynapseRequest) -> None:
- await self.auth.get_user_by_req(request)
- respond_with_json(request, 200, self.limits_dict, send_cors=True)
+ requester = await self.auth.get_user_by_req(request)
+ user_specific_config = (
+ await self.media_repository_callbacks.get_media_config_for_user(
+ requester.user.to_string(),
+ )
+ )
+ response = user_specific_config if user_specific_config else self.limits_dict
+ respond_with_json(request, 200, response, send_cors=True)
class ThumbnailResource(RestServlet):
@@ -138,7 +145,7 @@ class ThumbnailResource(RestServlet):
) -> None:
# Validate the server name, raising if invalid
parse_and_validate_server_name(server_name)
- await self.auth.get_user_by_req(request)
+ await self.auth.get_user_by_req(request, allow_guest=True)
set_cors_headers(request)
set_corp_headers(request)
@@ -229,7 +236,7 @@ class DownloadResource(RestServlet):
# Validate the server name, raising if invalid
parse_and_validate_server_name(server_name)
- await self.auth.get_user_by_req(request)
+ await self.auth.get_user_by_req(request, allow_guest=True)
set_cors_headers(request)
set_corp_headers(request)
diff --git a/synapse/rest/client/presence.py b/synapse/rest/client/presence.py
index 572e92642c..104d54cd89 100644
--- a/synapse/rest/client/presence.py
+++ b/synapse/rest/client/presence.py
@@ -19,12 +19,13 @@
#
#
-""" This module contains REST servlets to do with presence: /presence/<paths>
-"""
+"""This module contains REST servlets to do with presence: /presence/<paths>"""
+
import logging
from typing import TYPE_CHECKING, Tuple
-from synapse.api.errors import AuthError, SynapseError
+from synapse.api.errors import AuthError, Codes, LimitExceededError, SynapseError
+from synapse.api.ratelimiting import Ratelimiter
from synapse.handlers.presence import format_user_presence_state
from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet, parse_json_object_from_request
@@ -48,6 +49,14 @@ class PresenceStatusRestServlet(RestServlet):
self.presence_handler = hs.get_presence_handler()
self.clock = hs.get_clock()
self.auth = hs.get_auth()
+ self.store = hs.get_datastores().main
+
+ # Ratelimiter for presence updates, keyed by requester.
+ self._presence_per_user_limiter = Ratelimiter(
+ store=self.store,
+ clock=self.clock,
+ cfg=hs.config.ratelimiting.rc_presence_per_user,
+ )
async def on_GET(
self, request: SynapseRequest, user_id: str
@@ -82,6 +91,17 @@ class PresenceStatusRestServlet(RestServlet):
if requester.user != user:
raise AuthError(403, "Can only set your own presence state")
+ # ignore the presence update if the ratelimit is exceeded
+ try:
+ await self._presence_per_user_limiter.ratelimit(requester)
+ except LimitExceededError as e:
+ logger.debug("User presence ratelimit exceeded; ignoring it.")
+ return 429, {
+ "errcode": Codes.LIMIT_EXCEEDED,
+ "error": "Too many requests",
+ "retry_after_ms": e.retry_after_ms,
+ }
+
state = {}
content = parse_json_object_from_request(request)
diff --git a/synapse/rest/client/profile.py b/synapse/rest/client/profile.py
index c1a80c5c3d..8326d8017c 100644
--- a/synapse/rest/client/profile.py
+++ b/synapse/rest/client/profile.py
@@ -19,12 +19,15 @@
#
#
-""" This module contains REST servlets to do with profile: /profile/<paths> """
+"""This module contains REST servlets to do with profile: /profile/<paths>"""
+import re
from http import HTTPStatus
from typing import TYPE_CHECKING, Tuple
+from synapse.api.constants import ProfileFields
from synapse.api.errors import Codes, SynapseError
+from synapse.handlers.profile import MAX_CUSTOM_FIELD_LEN
from synapse.http.server import HttpServer
from synapse.http.servlet import (
RestServlet,
@@ -33,7 +36,8 @@ from synapse.http.servlet import (
)
from synapse.http.site import SynapseRequest
from synapse.rest.client._base import client_patterns
-from synapse.types import JsonDict, UserID
+from synapse.types import JsonDict, JsonValue, UserID
+from synapse.util.stringutils import is_namedspaced_grammar
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -91,6 +95,11 @@ class ProfileDisplaynameRestServlet(RestServlet):
async def on_PUT(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
+ if not UserID.is_valid(user_id):
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST, "Invalid user id", Codes.INVALID_PARAM
+ )
+
requester = await self.auth.get_user_by_req(request, allow_guest=True)
user = UserID.from_string(user_id)
is_admin = await self.auth.is_server_admin(requester)
@@ -101,9 +110,7 @@ class ProfileDisplaynameRestServlet(RestServlet):
new_name = content["displayname"]
except Exception:
raise SynapseError(
- code=400,
- msg="Unable to parse name",
- errcode=Codes.BAD_JSON,
+ 400, "Missing key 'displayname'", errcode=Codes.MISSING_PARAM
)
propagate = _read_propagate(self.hs, request)
@@ -166,6 +173,11 @@ class ProfileAvatarURLRestServlet(RestServlet):
async def on_PUT(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
+ if not UserID.is_valid(user_id):
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST, "Invalid user id", Codes.INVALID_PARAM
+ )
+
requester = await self.auth.get_user_by_req(request)
user = UserID.from_string(user_id)
is_admin = await self.auth.is_server_admin(requester)
@@ -227,19 +239,185 @@ class ProfileRestServlet(RestServlet):
user = UserID.from_string(user_id)
await self.profile_handler.check_profile_query_allowed(user, requester_user)
- displayname = await self.profile_handler.get_displayname(user)
- avatar_url = await self.profile_handler.get_avatar_url(user)
-
- ret = {}
- if displayname is not None:
- ret["displayname"] = displayname
- if avatar_url is not None:
- ret["avatar_url"] = avatar_url
+ ret = await self.profile_handler.get_profile(user_id)
return 200, ret
+class UnstableProfileFieldRestServlet(RestServlet):
+ PATTERNS = [
+ re.compile(
+ r"^/_matrix/client/unstable/uk\.tcpip\.msc4133/profile/(?P<user_id>[^/]*)/(?P<field_name>[^/]*)"
+ )
+ ]
+ CATEGORY = "Event sending requests"
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__()
+ self.hs = hs
+ self.profile_handler = hs.get_profile_handler()
+ self.auth = hs.get_auth()
+
+ async def on_GET(
+ self, request: SynapseRequest, user_id: str, field_name: str
+ ) -> Tuple[int, JsonDict]:
+ requester_user = None
+
+ if self.hs.config.server.require_auth_for_profile_requests:
+ requester = await self.auth.get_user_by_req(request)
+ requester_user = requester.user
+
+ if not UserID.is_valid(user_id):
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST, "Invalid user id", Codes.INVALID_PARAM
+ )
+
+ if not field_name:
+ raise SynapseError(400, "Field name too short", errcode=Codes.INVALID_PARAM)
+
+ if len(field_name.encode("utf-8")) > MAX_CUSTOM_FIELD_LEN:
+ raise SynapseError(400, "Field name too long", errcode=Codes.KEY_TOO_LARGE)
+ if not is_namedspaced_grammar(field_name):
+ raise SynapseError(
+ 400,
+ "Field name does not follow Common Namespaced Identifier Grammar",
+ errcode=Codes.INVALID_PARAM,
+ )
+
+ user = UserID.from_string(user_id)
+ await self.profile_handler.check_profile_query_allowed(user, requester_user)
+
+ if field_name == ProfileFields.DISPLAYNAME:
+ field_value: JsonValue = await self.profile_handler.get_displayname(user)
+ elif field_name == ProfileFields.AVATAR_URL:
+ field_value = await self.profile_handler.get_avatar_url(user)
+ else:
+ field_value = await self.profile_handler.get_profile_field(user, field_name)
+
+ return 200, {field_name: field_value}
+
+ async def on_PUT(
+ self, request: SynapseRequest, user_id: str, field_name: str
+ ) -> Tuple[int, JsonDict]:
+ if not UserID.is_valid(user_id):
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST, "Invalid user id", Codes.INVALID_PARAM
+ )
+
+ requester = await self.auth.get_user_by_req(request)
+ user = UserID.from_string(user_id)
+ is_admin = await self.auth.is_server_admin(requester)
+
+ if not field_name:
+ raise SynapseError(400, "Field name too short", errcode=Codes.INVALID_PARAM)
+
+ if len(field_name.encode("utf-8")) > MAX_CUSTOM_FIELD_LEN:
+ raise SynapseError(400, "Field name too long", errcode=Codes.KEY_TOO_LARGE)
+ if not is_namedspaced_grammar(field_name):
+ raise SynapseError(
+ 400,
+ "Field name does not follow Common Namespaced Identifier Grammar",
+ errcode=Codes.INVALID_PARAM,
+ )
+
+ content = parse_json_object_from_request(request)
+ try:
+ new_value = content[field_name]
+ except KeyError:
+ raise SynapseError(
+ 400, f"Missing key '{field_name}'", errcode=Codes.MISSING_PARAM
+ )
+
+ propagate = _read_propagate(self.hs, request)
+
+ requester_suspended = (
+ await self.hs.get_datastores().main.get_user_suspended_status(
+ requester.user.to_string()
+ )
+ )
+
+ if requester_suspended:
+ raise SynapseError(
+ 403,
+ "Updating profile while account is suspended is not allowed.",
+ Codes.USER_ACCOUNT_SUSPENDED,
+ )
+
+ if field_name == ProfileFields.DISPLAYNAME:
+ await self.profile_handler.set_displayname(
+ user, requester, new_value, is_admin, propagate=propagate
+ )
+ elif field_name == ProfileFields.AVATAR_URL:
+ await self.profile_handler.set_avatar_url(
+ user, requester, new_value, is_admin, propagate=propagate
+ )
+ else:
+ await self.profile_handler.set_profile_field(
+ user, requester, field_name, new_value, is_admin
+ )
+
+ return 200, {}
+
+ async def on_DELETE(
+ self, request: SynapseRequest, user_id: str, field_name: str
+ ) -> Tuple[int, JsonDict]:
+ if not UserID.is_valid(user_id):
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST, "Invalid user id", Codes.INVALID_PARAM
+ )
+
+ requester = await self.auth.get_user_by_req(request)
+ user = UserID.from_string(user_id)
+ is_admin = await self.auth.is_server_admin(requester)
+
+ if not field_name:
+ raise SynapseError(400, "Field name too short", errcode=Codes.INVALID_PARAM)
+
+ if len(field_name.encode("utf-8")) > MAX_CUSTOM_FIELD_LEN:
+ raise SynapseError(400, "Field name too long", errcode=Codes.KEY_TOO_LARGE)
+ if not is_namedspaced_grammar(field_name):
+ raise SynapseError(
+ 400,
+ "Field name does not follow Common Namespaced Identifier Grammar",
+ errcode=Codes.INVALID_PARAM,
+ )
+
+ propagate = _read_propagate(self.hs, request)
+
+ requester_suspended = (
+ await self.hs.get_datastores().main.get_user_suspended_status(
+ requester.user.to_string()
+ )
+ )
+
+ if requester_suspended:
+ raise SynapseError(
+ 403,
+ "Updating profile while account is suspended is not allowed.",
+ Codes.USER_ACCOUNT_SUSPENDED,
+ )
+
+ if field_name == ProfileFields.DISPLAYNAME:
+ await self.profile_handler.set_displayname(
+ user, requester, "", is_admin, propagate=propagate
+ )
+ elif field_name == ProfileFields.AVATAR_URL:
+ await self.profile_handler.set_avatar_url(
+ user, requester, "", is_admin, propagate=propagate
+ )
+ else:
+ await self.profile_handler.delete_profile_field(
+ user, requester, field_name, is_admin
+ )
+
+ return 200, {}
+
+
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+ # The specific displayname / avatar URL / custom field endpoints *must* appear
+ # before their corresponding generic profile endpoint.
ProfileDisplaynameRestServlet(hs).register(http_server)
ProfileAvatarURLRestServlet(hs).register(http_server)
ProfileRestServlet(hs).register(http_server)
+ if hs.config.experimental.msc4133_enabled:
+ UnstableProfileFieldRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/pusher.py b/synapse/rest/client/pusher.py
index a455f95a26..2463b3b38c 100644
--- a/synapse/rest/client/pusher.py
+++ b/synapse/rest/client/pusher.py
@@ -34,7 +34,6 @@ from synapse.http.site import SynapseRequest
from synapse.push import PusherConfigException
from synapse.rest.admin.experimental_features import ExperimentalFeature
from synapse.rest.client._base import client_patterns
-from synapse.rest.synapse.client.unsubscribe import UnsubscribeResource
from synapse.types import JsonDict
if TYPE_CHECKING:
@@ -161,21 +160,6 @@ class PushersSetRestServlet(RestServlet):
return 200, {}
-class LegacyPushersRemoveRestServlet(UnsubscribeResource, RestServlet):
- """
- A servlet to handle legacy "email unsubscribe" links, forwarding requests to the ``UnsubscribeResource``
-
- This should be kept for some time, so unsubscribe links in past emails stay valid.
- """
-
- PATTERNS = client_patterns("/pushers/remove$", releases=[], v1=False, unstable=True)
-
- async def on_GET(self, request: SynapseRequest) -> None:
- # Forward the request to the UnsubscribeResource
- await self._async_render(request)
-
-
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
PushersRestServlet(hs).register(http_server)
PushersSetRestServlet(hs).register(http_server)
- LegacyPushersRemoveRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/receipts.py b/synapse/rest/client/receipts.py
index 89203dc45a..4bf93f485c 100644
--- a/synapse/rest/client/receipts.py
+++ b/synapse/rest/client/receipts.py
@@ -39,9 +39,7 @@ logger = logging.getLogger(__name__)
class ReceiptRestServlet(RestServlet):
PATTERNS = client_patterns(
- "/rooms/(?P<room_id>[^/]*)"
- "/receipt/(?P<receipt_type>[^/]*)"
- "/(?P<event_id>[^/]*)$"
+ "/rooms/(?P<room_id>[^/]*)/receipt/(?P<receipt_type>[^/]*)/(?P<event_id>[^/]*)$"
)
CATEGORY = "Receipts requests"
diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py
index 5dddbc69be..9d18d8ba25 100644
--- a/synapse/rest/client/register.py
+++ b/synapse/rest/client/register.py
@@ -38,14 +38,12 @@ from synapse.api.errors import (
InteractiveAuthIncompleteError,
NotApprovedError,
SynapseError,
- ThreepidValidationError,
UnrecognizedRequestError,
)
from synapse.api.ratelimiting import Ratelimiter
from synapse.config import ConfigError
from synapse.config.homeserver import HomeServerConfig
from synapse.config.ratelimiting import FederationRatelimitSettings
-from synapse.config.server import is_threepid_reserved
from synapse.handlers.auth import AuthHandler
from synapse.handlers.ui_auth import UIAuthSessionDataConstants
from synapse.http.server import HttpServer, finish_request, respond_with_html
@@ -56,17 +54,9 @@ from synapse.http.servlet import (
parse_string,
)
from synapse.http.site import SynapseRequest
-from synapse.metrics import threepid_send_requests
-from synapse.push.mailer import Mailer
from synapse.types import JsonDict
-from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.ratelimitutils import FederationRateLimiter
from synapse.util.stringutils import assert_valid_client_secret, random_string
-from synapse.util.threepids import (
- canonicalise_email,
- check_3pid_allowed,
- validate_email,
-)
from ._base import client_patterns, interactive_auth_handler
@@ -76,247 +66,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
-class EmailRegisterRequestTokenRestServlet(RestServlet):
- PATTERNS = client_patterns("/register/email/requestToken$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.identity_handler = hs.get_identity_handler()
- self.config = hs.config
-
- if self.hs.config.email.can_verify_email:
- self.registration_mailer = Mailer(
- hs=self.hs,
- app_name=self.config.email.email_app_name,
- template_html=self.config.email.email_registration_template_html,
- template_text=self.config.email.email_registration_template_text,
- )
- self.already_in_use_mailer = Mailer(
- hs=self.hs,
- app_name=self.config.email.email_app_name,
- template_html=self.config.email.email_already_in_use_template_html,
- template_text=self.config.email.email_already_in_use_template_text,
- )
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- if not self.hs.config.email.can_verify_email:
- logger.warning(
- "Email registration has been disabled due to lack of email config"
- )
- raise SynapseError(
- 400, "Email-based registration has been disabled on this server"
- )
- body = parse_json_object_from_request(request)
-
- assert_params_in_dict(body, ["client_secret", "email", "send_attempt"])
-
- # Extract params from body
- client_secret = body["client_secret"]
- assert_valid_client_secret(client_secret)
-
- # For emails, canonicalise the address.
- # We store all email addresses canonicalised in the DB.
- # (See on_POST in EmailThreepidRequestTokenRestServlet
- # in synapse/rest/client/account.py)
- try:
- email = validate_email(body["email"])
- except ValueError as e:
- raise SynapseError(400, str(e))
- send_attempt = body["send_attempt"]
- next_link = body.get("next_link") # Optional param
-
- if not await check_3pid_allowed(self.hs, "email", email, registration=True):
- raise SynapseError(
- 403,
- "Your email domain is not authorized to register on this server",
- Codes.THREEPID_DENIED,
- )
-
- await self.identity_handler.ratelimit_request_token_requests(
- request, "email", email
- )
-
- existing_user_id = await self.hs.get_datastores().main.get_user_id_by_threepid(
- "email", email
- )
-
- if existing_user_id is not None:
- if self.hs.config.server.request_token_inhibit_3pid_errors:
- # Make the client think the operation succeeded. See the rationale in the
- # comments for request_token_inhibit_3pid_errors.
- # Still send an email to warn the user that an account already exists.
- # Also wait for some random amount of time between 100ms and 1s to make it
- # look like we did something.
- await self.already_in_use_mailer.send_already_in_use_mail(email)
- await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
- return 200, {"sid": random_string(16)}
-
- raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
-
- # Send registration emails from Synapse
- sid = await self.identity_handler.send_threepid_validation(
- email,
- client_secret,
- send_attempt,
- self.registration_mailer.send_registration_mail,
- next_link,
- )
-
- threepid_send_requests.labels(type="email", reason="register").observe(
- send_attempt
- )
-
- # Wrap the session id in a JSON object
- return 200, {"sid": sid}
-
-
-class MsisdnRegisterRequestTokenRestServlet(RestServlet):
- PATTERNS = client_patterns("/register/msisdn/requestToken$")
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.identity_handler = hs.get_identity_handler()
-
- async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
- body = parse_json_object_from_request(request)
-
- assert_params_in_dict(
- body, ["client_secret", "country", "phone_number", "send_attempt"]
- )
- client_secret = body["client_secret"]
- assert_valid_client_secret(client_secret)
- country = body["country"]
- phone_number = body["phone_number"]
- send_attempt = body["send_attempt"]
- next_link = body.get("next_link") # Optional param
-
- msisdn = phone_number_to_msisdn(country, phone_number)
-
- if not await check_3pid_allowed(self.hs, "msisdn", msisdn, registration=True):
- raise SynapseError(
- 403,
- "Phone numbers are not authorized to register on this server",
- Codes.THREEPID_DENIED,
- )
-
- await self.identity_handler.ratelimit_request_token_requests(
- request, "msisdn", msisdn
- )
-
- existing_user_id = await self.hs.get_datastores().main.get_user_id_by_threepid(
- "msisdn", msisdn
- )
-
- if existing_user_id is not None:
- if self.hs.config.server.request_token_inhibit_3pid_errors:
- # Make the client think the operation succeeded. See the rationale in the
- # comments for request_token_inhibit_3pid_errors.
- # Also wait for some random amount of time between 100ms and 1s to make it
- # look like we did something.
- await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
- return 200, {"sid": random_string(16)}
-
- raise SynapseError(
- 400, "Phone number is already in use", Codes.THREEPID_IN_USE
- )
-
- if not self.hs.config.registration.account_threepid_delegate_msisdn:
- logger.warning(
- "No upstream msisdn account_threepid_delegate configured on the server to "
- "handle this request"
- )
- raise SynapseError(
- 400, "Registration by phone number is not supported on this homeserver"
- )
-
- ret = await self.identity_handler.requestMsisdnToken(
- self.hs.config.registration.account_threepid_delegate_msisdn,
- country,
- phone_number,
- client_secret,
- send_attempt,
- next_link,
- )
-
- threepid_send_requests.labels(type="msisdn", reason="register").observe(
- send_attempt
- )
-
- return 200, ret
-
-
-class RegistrationSubmitTokenServlet(RestServlet):
- """Handles registration 3PID validation token submission"""
-
- PATTERNS = client_patterns(
- "/registration/(?P<medium>[^/]*)/submit_token$", releases=(), unstable=True
- )
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.hs = hs
- self.auth = hs.get_auth()
- self.config = hs.config
- self.clock = hs.get_clock()
- self.store = hs.get_datastores().main
-
- if self.config.email.can_verify_email:
- self._failure_email_template = (
- self.config.email.email_registration_template_failure_html
- )
-
- async def on_GET(self, request: Request, medium: str) -> None:
- if medium != "email":
- raise SynapseError(
- 400, "This medium is currently not supported for registration"
- )
- if not self.config.email.can_verify_email:
- logger.warning(
- "User registration via email has been disabled due to lack of email config"
- )
- raise SynapseError(
- 400, "Email-based registration is disabled on this server"
- )
-
- sid = parse_string(request, "sid", required=True)
- client_secret = parse_string(request, "client_secret", required=True)
- assert_valid_client_secret(client_secret)
- token = parse_string(request, "token", required=True)
-
- # Attempt to validate a 3PID session
- try:
- # Mark the session as valid
- next_link = await self.store.validate_threepid_session(
- sid, client_secret, token, self.clock.time_msec()
- )
-
- # Perform a 302 redirect if next_link is set
- if next_link:
- if next_link.startswith("file:///"):
- logger.warning(
- "Not redirecting to next_link as it is a local file: address"
- )
- else:
- request.setResponseCode(302)
- request.setHeader("Location", next_link)
- finish_request(request)
- return None
-
- # Otherwise show the success template
- html = self.config.email.email_registration_template_success_html_content
- status_code = 200
- except ThreepidValidationError as e:
- status_code = e.code
-
- # Show a failure page with a reason
- template_vars = {"failure_reason": e.msg}
- html = self._failure_email_template.render(**template_vars)
-
- respond_with_html(request, status_code, html)
-
-
class UsernameAvailabilityRestServlet(RestServlet):
PATTERNS = client_patterns("/register/available")
@@ -420,7 +169,6 @@ class RegisterRestServlet(RestServlet):
self.store = hs.get_datastores().main
self.auth_handler = hs.get_auth_handler()
self.registration_handler = hs.get_registration_handler()
- self.identity_handler = hs.get_identity_handler()
self.room_member_handler = hs.get_room_member_handler()
self.macaroon_gen = hs.get_macaroon_generator()
self.ratelimiter = hs.get_registration_ratelimiter()
@@ -605,27 +353,6 @@ class RegisterRestServlet(RestServlet):
)
raise
- # Check that we're not trying to register a denied 3pid.
- #
- # the user-facing checks will probably already have happened in
- # /register/email/requestToken when we requested a 3pid, but that's not
- # guaranteed.
- if auth_result:
- for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]:
- if login_type in auth_result:
- medium = auth_result[login_type]["medium"]
- address = auth_result[login_type]["address"]
-
- if not await check_3pid_allowed(
- self.hs, medium, address, registration=True
- ):
- raise SynapseError(
- 403,
- "Third party identifiers (email/phone numbers)"
- + " are not authorized on this server",
- Codes.THREEPID_DENIED,
- )
-
if registered_user_id is not None:
logger.info(
"Already registered user ID %r for this session", registered_user_id
@@ -640,12 +367,10 @@ class RegisterRestServlet(RestServlet):
if not password_hash:
raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM)
- desired_username = (
- await (
- self.password_auth_provider.get_username_for_registration(
- auth_result,
- params,
- )
+ desired_username = await (
+ self.password_auth_provider.get_username_for_registration(
+ auth_result,
+ params,
)
)
@@ -657,50 +382,13 @@ class RegisterRestServlet(RestServlet):
if desired_username is not None:
desired_username = desired_username.lower()
- threepid = None
- if auth_result:
- threepid = auth_result.get(LoginType.EMAIL_IDENTITY)
-
- # Also check that we're not trying to register a 3pid that's already
- # been registered.
- #
- # This has probably happened in /register/email/requestToken as well,
- # but if a user hits this endpoint twice then clicks on each link from
- # the two activation emails, they would register the same 3pid twice.
- for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]:
- if login_type in auth_result:
- medium = auth_result[login_type]["medium"]
- address = auth_result[login_type]["address"]
- # For emails, canonicalise the address.
- # We store all email addresses canonicalised in the DB.
- # (See on_POST in EmailThreepidRequestTokenRestServlet
- # in synapse/rest/client/account.py)
- if medium == "email":
- try:
- address = canonicalise_email(address)
- except ValueError as e:
- raise SynapseError(400, str(e))
-
- existing_user_id = await self.store.get_user_id_by_threepid(
- medium, address
- )
-
- if existing_user_id is not None:
- raise SynapseError(
- 400,
- "%s is already in use" % medium,
- Codes.THREEPID_IN_USE,
- )
-
entries = await self.store.get_user_agents_ips_to_ui_auth_session(
session_id
)
- display_name = (
- await (
- self.password_auth_provider.get_displayname_for_registration(
- auth_result, params
- )
+ display_name = await (
+ self.password_auth_provider.get_displayname_for_registration(
+ auth_result, params
)
)
@@ -708,18 +396,10 @@ class RegisterRestServlet(RestServlet):
localpart=desired_username,
password_hash=password_hash,
guest_access_token=guest_access_token,
- threepid=threepid,
default_display_name=display_name,
address=client_addr,
user_agent_ips=entries,
)
- # Necessary due to auth checks prior to the threepid being
- # written to the db
- if threepid:
- if is_threepid_reserved(
- self.hs.config.server.mau_limits_reserved_threepids, threepid
- ):
- await self.store.upsert_monthly_active_user(registered_user_id)
# Remember that the user account has been registered (and the user
# ID it was registered with, since it might not have been specified).
@@ -775,9 +455,12 @@ class RegisterRestServlet(RestServlet):
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,
@@ -909,6 +592,14 @@ class RegisterAppServiceOnlyRestServlet(RestServlet):
await self.ratelimiter.ratelimit(None, client_addr, update=False)
+ # Allow only ASes to use this API.
+ if body.get("type") != APP_SERVICE_REGISTRATION_TYPE:
+ raise SynapseError(
+ 403,
+ "Registration has been disabled. Only m.login.application_service registrations are allowed.",
+ errcode=Codes.FORBIDDEN,
+ )
+
kind = parse_string(request, "kind", default="user")
if kind == "guest":
@@ -924,10 +615,6 @@ class RegisterAppServiceOnlyRestServlet(RestServlet):
if not isinstance(desired_username, str) or len(desired_username) > 512:
raise SynapseError(400, "Invalid username")
- # Allow only ASes to use this API.
- if body.get("type") != APP_SERVICE_REGISTRATION_TYPE:
- raise SynapseError(403, "Non-application service registration type")
-
if not self.auth.has_access_token(request):
raise SynapseError(
400,
@@ -941,7 +628,7 @@ class RegisterAppServiceOnlyRestServlet(RestServlet):
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}
@@ -958,60 +645,11 @@ def _calculate_registration_flows(
Returns: a list of supported flows
"""
- # FIXME: need a better error than "no auth flow found" for scenarios
- # where we required 3PID for registration but the user didn't give one
- require_email = "email" in config.registration.registrations_require_3pid
- require_msisdn = "msisdn" in config.registration.registrations_require_3pid
-
- show_msisdn = True
- show_email = True
-
- if config.registration.disable_msisdn_registration:
- show_msisdn = False
- require_msisdn = False
-
enabled_auth_types = auth_handler.get_enabled_auth_types()
- if LoginType.EMAIL_IDENTITY not in enabled_auth_types:
- show_email = False
- if require_email:
- raise ConfigError(
- "Configuration requires email address at registration, but email "
- "validation is not configured"
- )
-
- if LoginType.MSISDN not in enabled_auth_types:
- show_msisdn = False
- if require_msisdn:
- raise ConfigError(
- "Configuration requires msisdn at registration, but msisdn "
- "validation is not configured"
- )
-
flows = []
- # only support 3PIDless registration if no 3PIDs are required
- if not require_email and not require_msisdn:
- # Add a dummy step here, otherwise if a client completes
- # recaptcha first we'll assume they were going for this flow
- # and complete the request, when they could have been trying to
- # complete one of the flows with email/msisdn auth.
- flows.append([LoginType.DUMMY])
-
- # only support the email-only flow if we don't require MSISDN 3PIDs
- if show_email and not require_msisdn:
- flows.append([LoginType.EMAIL_IDENTITY])
-
- # only support the MSISDN-only flow if we don't require email 3PIDs
- if show_msisdn and not require_email:
- flows.append([LoginType.MSISDN])
-
- if show_email and show_msisdn:
- # always let users provide both MSISDN & email
- flows.append([LoginType.MSISDN, LoginType.EMAIL_IDENTITY])
-
- # Add a flow that doesn't require any 3pids, if the config requests it.
- if config.registration.enable_registration_token_3pid_bypass:
- flows.append([LoginType.REGISTRATION_TOKEN])
+ # We don't support 3PIDs
+ flows.append([LoginType.DUMMY])
# Prepend m.login.terms to all flows if we're requiring consent
if config.consent.user_consent_at_registration:
@@ -1037,10 +675,6 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
RegisterAppServiceOnlyRestServlet(hs).register(http_server)
return
- if hs.config.worker.worker_app is None:
- EmailRegisterRequestTokenRestServlet(hs).register(http_server)
- MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
- RegistrationSubmitTokenServlet(hs).register(http_server)
UsernameAvailabilityRestServlet(hs).register(http_server)
RegistrationTokenValidityRestServlet(hs).register(http_server)
RegisterRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/rendezvous.py b/synapse/rest/client/rendezvous.py
index 27bf53314a..a1808847f0 100644
--- a/synapse/rest/client/rendezvous.py
+++ b/synapse/rest/client/rendezvous.py
@@ -34,51 +34,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
-# n.b [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) has now been closed.
-# However, we want to keep this implementation around for some time.
-# TODO: define an end-of-life date for this implementation.
-class MSC3886RendezvousServlet(RestServlet):
- """
- This is a placeholder implementation of [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
- simple client rendezvous capability that is used by the "Sign in with QR" functionality.
-
- This implementation only serves as a 307 redirect to a configured server rather than being a full implementation.
-
- A module that implements the full functionality is available at: https://pypi.org/project/matrix-http-rendezvous-synapse/.
-
- Request:
-
- POST /rendezvous HTTP/1.1
- Content-Type: ...
-
- ...
-
- Response:
-
- HTTP/1.1 307
- Location: <configured endpoint>
- """
-
- PATTERNS = client_patterns(
- "/org.matrix.msc3886/rendezvous$", releases=[], v1=False, unstable=True
- )
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- redirection_target: Optional[str] = hs.config.experimental.msc3886_endpoint
- assert (
- redirection_target is not None
- ), "Servlet is only registered if there is a redirection target"
- self.endpoint = redirection_target.encode("utf-8")
-
- async def on_POST(self, request: SynapseRequest) -> None:
- respond_with_redirect(
- request, self.endpoint, statusCode=TEMPORARY_REDIRECT, cors=True
- )
-
- # PUT, GET and DELETE are not implemented as they should be fulfilled by the redirect target.
-
-
class MSC4108DelegationRendezvousServlet(RestServlet):
PATTERNS = client_patterns(
"/org.matrix.msc4108/rendezvous$", releases=[], v1=False, unstable=True
@@ -89,9 +44,9 @@ class MSC4108DelegationRendezvousServlet(RestServlet):
redirection_target: Optional[str] = (
hs.config.experimental.msc4108_delegation_endpoint
)
- assert (
- redirection_target is not None
- ), "Servlet is only registered if there is a delegation target"
+ assert redirection_target is not None, (
+ "Servlet is only registered if there is a delegation target"
+ )
self.endpoint = redirection_target.encode("utf-8")
async def on_POST(self, request: SynapseRequest) -> None:
@@ -114,9 +69,6 @@ class MSC4108RendezvousServlet(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
- if hs.config.experimental.msc3886_endpoint is not None:
- MSC3886RendezvousServlet(hs).register(http_server)
-
if hs.config.experimental.msc4108_enabled:
MSC4108RendezvousServlet(hs).register(http_server)
diff --git a/synapse/rest/client/reporting.py b/synapse/rest/client/reporting.py
index 4eee53e5a8..c5037be8b7 100644
--- a/synapse/rest/client/reporting.py
+++ b/synapse/rest/client/reporting.py
@@ -23,7 +23,7 @@ import logging
from http import HTTPStatus
from typing import TYPE_CHECKING, Tuple
-from synapse._pydantic_compat import HAS_PYDANTIC_V2
+from synapse._pydantic_compat import StrictStr
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
from synapse.http.server import HttpServer
from synapse.http.servlet import (
@@ -40,10 +40,6 @@ from ._base import client_patterns
if TYPE_CHECKING:
from synapse.server import HomeServer
-if TYPE_CHECKING or HAS_PYDANTIC_V2:
- from pydantic.v1 import StrictStr
-else:
- from pydantic import StrictStr
logger = logging.getLogger(__name__)
@@ -109,18 +105,17 @@ class ReportEventRestServlet(RestServlet):
class ReportRoomRestServlet(RestServlet):
"""This endpoint lets clients report a room for abuse.
- Whilst MSC4151 is not yet merged, this unstable endpoint is enabled on matrix.org
- for content moderation purposes, and therefore backwards compatibility should be
- carefully considered when changing anything on this endpoint.
-
- More details on the MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4151
+ Introduced by MSC4151: https://github.com/matrix-org/matrix-spec-proposals/pull/4151
"""
- PATTERNS = client_patterns(
- "/org.matrix.msc4151/rooms/(?P<room_id>[^/]*)/report$",
- releases=[],
- v1=False,
- unstable=True,
+ # Cast the Iterable to a list so that we can `append` below.
+ PATTERNS = list(
+ client_patterns(
+ "/rooms/(?P<room_id>[^/]*)/report$",
+ releases=("v3",),
+ unstable=False,
+ v1=False,
+ )
)
def __init__(self, hs: "HomeServer"):
@@ -157,6 +152,4 @@ class ReportRoomRestServlet(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
ReportEventRestServlet(hs).register(http_server)
-
- if hs.config.experimental.msc4151_enabled:
- ReportRoomRestServlet(hs).register(http_server)
+ ReportRoomRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py
index 903c74f6d8..38230de0de 100644
--- a/synapse/rest/client/room.py
+++ b/synapse/rest/client/room.py
@@ -2,7 +2,7 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright 2014-2016 OpenMarket Ltd
-# Copyright (C) 2023 New Vector, Ltd
+# Copyright (C) 2023-2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -19,7 +19,8 @@
#
#
-""" This module contains REST servlets to do with rooms: /rooms/<paths> """
+"""This module contains REST servlets to do with rooms: /rooms/<paths>"""
+
import logging
import re
from enum import Enum
@@ -67,7 +68,8 @@ from synapse.streams.config import PaginationConfig
from synapse.types import JsonDict, Requester, StreamToken, ThirdPartyInstanceID, UserID
from synapse.types.state import StateFilter
from synapse.util.cancellation import cancellable
-from synapse.util.stringutils import parse_and_validate_server_name, random_string
+from synapse.util.events import generate_fake_event_id
+from synapse.util.stringutils import parse_and_validate_server_name
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -193,7 +195,10 @@ class RoomStateEventRestServlet(RestServlet):
self.event_creation_handler = hs.get_event_creation_handler()
self.room_member_handler = hs.get_room_member_handler()
self.message_handler = hs.get_message_handler()
+ self.delayed_events_handler = hs.get_delayed_events_handler()
self.auth = hs.get_auth()
+ self._max_event_delay_ms = hs.config.server.max_event_delay_ms
+ self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker
def register(self, http_server: HttpServer) -> None:
# /rooms/$roomid/state/$eventtype
@@ -285,10 +290,45 @@ class RoomStateEventRestServlet(RestServlet):
content = parse_json_object_from_request(request)
+ is_requester_admin = await self.auth.is_server_admin(requester)
+ if not is_requester_admin:
+ spam_check = (
+ await self._spam_checker_module_callbacks.user_may_send_state_event(
+ user_id=requester.user.to_string(),
+ room_id=room_id,
+ event_type=event_type,
+ state_key=state_key,
+ content=content,
+ )
+ )
+ if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
+ raise SynapseError(
+ 403,
+ "You are not permitted to send the state event",
+ errcode=spam_check[0],
+ additional_fields=spam_check[1],
+ )
+
origin_server_ts = None
if requester.app_service:
origin_server_ts = parse_integer(request, "ts")
+ delay = _parse_request_delay(request, self._max_event_delay_ms)
+ if delay is not None:
+ delay_id = await self.delayed_events_handler.add(
+ requester,
+ room_id=room_id,
+ event_type=event_type,
+ state_key=state_key,
+ origin_server_ts=origin_server_ts,
+ content=content,
+ delay=delay,
+ )
+
+ set_tag("delay_id", delay_id)
+ ret = {"delay_id": delay_id}
+ return 200, ret
+
try:
if event_type == EventTypes.Member:
membership = content.get("membership", None)
@@ -325,7 +365,7 @@ class RoomStateEventRestServlet(RestServlet):
)
event_id = event.event_id
except ShadowBanError:
- event_id = "$" + random_string(43)
+ event_id = generate_fake_event_id()
set_tag("event_id", event_id)
ret = {"event_id": event_id}
@@ -339,7 +379,9 @@ class RoomSendEventRestServlet(TransactionRestServlet):
def __init__(self, hs: "HomeServer"):
super().__init__(hs)
self.event_creation_handler = hs.get_event_creation_handler()
+ self.delayed_events_handler = hs.get_delayed_events_handler()
self.auth = hs.get_auth()
+ self._max_event_delay_ms = hs.config.server.max_event_delay_ms
def register(self, http_server: HttpServer) -> None:
# /rooms/$roomid/send/$event_type[/$txn_id]
@@ -356,6 +398,26 @@ class RoomSendEventRestServlet(TransactionRestServlet):
) -> Tuple[int, JsonDict]:
content = parse_json_object_from_request(request)
+ origin_server_ts = None
+ if requester.app_service:
+ origin_server_ts = parse_integer(request, "ts")
+
+ delay = _parse_request_delay(request, self._max_event_delay_ms)
+ if delay is not None:
+ delay_id = await self.delayed_events_handler.add(
+ requester,
+ room_id=room_id,
+ event_type=event_type,
+ state_key=None,
+ origin_server_ts=origin_server_ts,
+ content=content,
+ delay=delay,
+ )
+
+ set_tag("delay_id", delay_id)
+ ret = {"delay_id": delay_id}
+ return 200, ret
+
event_dict: JsonDict = {
"type": event_type,
"content": content,
@@ -363,10 +425,8 @@ class RoomSendEventRestServlet(TransactionRestServlet):
"sender": requester.user.to_string(),
}
- if requester.app_service:
- origin_server_ts = parse_integer(request, "ts")
- if origin_server_ts is not None:
- event_dict["origin_server_ts"] = origin_server_ts
+ if origin_server_ts is not None:
+ event_dict["origin_server_ts"] = origin_server_ts
try:
(
@@ -377,7 +437,7 @@ class RoomSendEventRestServlet(TransactionRestServlet):
)
event_id = event.event_id
except ShadowBanError:
- event_id = "$" + random_string(43)
+ event_id = generate_fake_event_id()
set_tag("event_id", event_id)
return 200, {"event_id": event_id}
@@ -409,6 +469,49 @@ class RoomSendEventRestServlet(TransactionRestServlet):
)
+def _parse_request_delay(
+ request: SynapseRequest,
+ max_delay: Optional[int],
+) -> Optional[int]:
+ """Parses from the request string the delay parameter for
+ delayed event requests, and checks it for correctness.
+
+ Args:
+ request: the twisted HTTP request.
+ max_delay: the maximum allowed value of the delay parameter,
+ or None if no delay parameter is allowed.
+ Returns:
+ The value of the requested delay, or None if it was absent.
+
+ Raises:
+ SynapseError: if the delay parameter is present and forbidden,
+ or if it exceeds the maximum allowed value.
+ """
+ delay = parse_integer(request, "org.matrix.msc4140.delay")
+ if delay is None:
+ return None
+ if max_delay is None:
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "Delayed events are not supported on this server",
+ Codes.UNKNOWN,
+ {
+ "org.matrix.msc4140.errcode": "M_MAX_DELAY_UNSUPPORTED",
+ },
+ )
+ if delay > max_delay:
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "The requested delay exceeds the allowed maximum.",
+ Codes.UNKNOWN,
+ {
+ "org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
+ "org.matrix.msc4140.max_delay": max_delay,
+ },
+ )
+ return delay
+
+
# TODO: Needs unit testing for room ID + alias joins
class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
CATEGORY = "Event sending requests"
@@ -417,7 +520,6 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
super().__init__(hs)
super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up
self.auth = hs.get_auth()
- self._support_via = hs.config.experimental.msc4156_enabled
def register(self, http_server: HttpServer) -> None:
# /join/$room_identifier[/$txn_id]
@@ -435,13 +537,11 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
# twisted.web.server.Request.args is incorrectly defined as Optional[Any]
args: Dict[bytes, List[bytes]] = request.args # type: ignore
- remote_room_hosts = parse_strings_from_args(args, "server_name", required=False)
- if self._support_via:
+ # Prefer via over server_name (deprecated with MSC4156)
+ remote_room_hosts = parse_strings_from_args(args, "via", required=False)
+ if remote_room_hosts is None:
remote_room_hosts = parse_strings_from_args(
- args,
- "org.matrix.msc4156.via",
- default=remote_room_hosts,
- required=False,
+ args, "server_name", required=False
)
room_id, remote_room_hosts = await self.resolve_room_id(
room_identifier,
@@ -703,9 +803,9 @@ class RoomMessageListRestServlet(RestServlet):
# decorator on `get_number_joined_users_in_room` doesn't play well with
# the type system. Maybe in the future, it can use some ParamSpec
# wizardry to fix it up.
- room_member_count_deferred = run_in_background( # type: ignore[call-arg]
+ room_member_count_deferred = run_in_background( # type: ignore[call-overload]
self.store.get_number_joined_users_in_room,
- room_id, # type: ignore[arg-type]
+ room_id,
)
requester = await self.auth.get_user_by_req(request, allow_guest=True)
@@ -814,12 +914,10 @@ class RoomEventServlet(RestServlet):
requester = await self.auth.get_user_by_req(request, allow_guest=True)
include_unredacted_content = self.msc2815_enabled and (
- parse_string(
+ parse_boolean(
request,
- "fi.mau.msc2815.include_unredacted_content",
- allowed_values=("true", "false"),
+ "fi.mau.msc2815.include_unredacted_content"
)
- == "true"
)
if include_unredacted_content and not await self.auth.is_server_admin(
requester
@@ -1193,7 +1291,7 @@ class RoomRedactEventRestServlet(TransactionRestServlet):
event_id = event.event_id
except ShadowBanError:
- event_id = "$" + random_string(43)
+ event_id = generate_fake_event_id()
set_tag("event_id", event_id)
return 200, {"event_id": event_id}
diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py
index 8c5db2a513..bac02122d0 100644
--- a/synapse/rest/client/sync.py
+++ b/synapse/rest/client/sync.py
@@ -21,12 +21,13 @@
import itertools
import logging
from collections import defaultdict
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Union
from synapse.api.constants import AccountDataTypes, EduTypes, Membership, PresenceState
from synapse.api.errors import Codes, StoreError, SynapseError
from synapse.api.filtering import FilterCollection
from synapse.api.presence import UserPresenceState
+from synapse.api.ratelimiting import Ratelimiter
from synapse.events.utils import (
SerializeEventConfig,
format_event_for_client_v2_without_room_id,
@@ -126,6 +127,13 @@ class SyncRestServlet(RestServlet):
cache_name="sync_valid_filter",
)
+ # Ratelimiter for presence updates, keyed by requester.
+ self._presence_per_user_limiter = Ratelimiter(
+ store=self.store,
+ clock=self.clock,
+ cfg=hs.config.ratelimiting.rc_presence_per_user,
+ )
+
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
# This will always be set by the time Twisted calls us.
assert request.args is not None
@@ -152,6 +160,14 @@ class SyncRestServlet(RestServlet):
filter_id = parse_string(request, "filter")
full_state = parse_boolean(request, "full_state", default=False)
+ use_state_after = False
+ if await self.store.is_feature_enabled(
+ user.to_string(), ExperimentalFeature.MSC4222
+ ):
+ use_state_after = parse_boolean(
+ request, "org.matrix.msc4222.use_state_after", default=False
+ )
+
logger.debug(
"/sync: user=%r, timeout=%r, since=%r, "
"set_presence=%r, filter_id=%r, device_id=%r",
@@ -184,6 +200,7 @@ class SyncRestServlet(RestServlet):
full_state,
device_id,
last_ignore_accdata_streampos,
+ use_state_after,
)
if filter_id is None:
@@ -220,6 +237,7 @@ class SyncRestServlet(RestServlet):
filter_collection=filter_collection,
is_guest=requester.is_guest,
device_id=device_id,
+ use_state_after=use_state_after,
)
since_token = None
@@ -229,7 +247,13 @@ class SyncRestServlet(RestServlet):
# send any outstanding server notices to the user.
await self._server_notices_sender.on_user_syncing(user.to_string())
- affect_presence = set_presence != PresenceState.OFFLINE
+ # ignore the presence update if the ratelimit is exceeded but do not pause the request
+ allowed, _ = await self._presence_per_user_limiter.can_do_action(requester)
+ if not allowed:
+ affect_presence = False
+ logger.debug("User set_presence ratelimit exceeded; ignoring it.")
+ else:
+ affect_presence = set_presence != PresenceState.OFFLINE
context = await self.presence_handler.user_syncing(
user.to_string(),
@@ -258,7 +282,7 @@ class SyncRestServlet(RestServlet):
# We know that the the requester has an access token since appservices
# cannot use sync.
response_content = await self.encode_response(
- time_now, sync_result, requester, filter_collection
+ time_now, sync_config, sync_result, requester, filter_collection
)
logger.debug("Event formatting complete")
@@ -268,6 +292,7 @@ class SyncRestServlet(RestServlet):
async def encode_response(
self,
time_now: int,
+ sync_config: SyncConfig,
sync_result: SyncResult,
requester: Requester,
filter: FilterCollection,
@@ -292,7 +317,7 @@ class SyncRestServlet(RestServlet):
)
joined = await self.encode_joined(
- sync_result.joined, time_now, serialize_options
+ sync_config, sync_result.joined, time_now, serialize_options
)
invited = await self.encode_invited(
@@ -304,7 +329,7 @@ class SyncRestServlet(RestServlet):
)
archived = await self.encode_archived(
- sync_result.archived, time_now, serialize_options
+ sync_config, sync_result.archived, time_now, serialize_options
)
logger.debug("building sync response dict")
@@ -372,6 +397,7 @@ class SyncRestServlet(RestServlet):
@trace_with_opname("sync.encode_joined")
async def encode_joined(
self,
+ sync_config: SyncConfig,
rooms: List[JoinedSyncResult],
time_now: int,
serialize_options: SerializeEventConfig,
@@ -380,6 +406,7 @@ class SyncRestServlet(RestServlet):
Encode the joined rooms in a sync result
Args:
+ sync_config
rooms: list of sync results for rooms this user is joined to
time_now: current time - used as a baseline for age calculations
serialize_options: Event serializer options
@@ -389,7 +416,11 @@ class SyncRestServlet(RestServlet):
joined = {}
for room in rooms:
joined[room.room_id] = await self.encode_room(
- room, time_now, joined=True, serialize_options=serialize_options
+ sync_config,
+ room,
+ time_now,
+ joined=True,
+ serialize_options=serialize_options,
)
return joined
@@ -419,7 +450,12 @@ class SyncRestServlet(RestServlet):
)
unsigned = dict(invite.get("unsigned", {}))
invite["unsigned"] = unsigned
- invited_state = list(unsigned.pop("invite_room_state", []))
+
+ invited_state = unsigned.pop("invite_room_state", [])
+ if not isinstance(invited_state, list):
+ invited_state = []
+
+ invited_state = list(invited_state)
invited_state.append(invite)
invited[room.room_id] = {"invite_state": {"events": invited_state}}
@@ -459,7 +495,10 @@ class SyncRestServlet(RestServlet):
# Extract the stripped room state from the unsigned dict
# This is for clients to get a little bit of information about
# the room they've knocked on, without revealing any sensitive information
- knocked_state = list(unsigned.pop("knock_room_state", []))
+ knocked_state = unsigned.pop("knock_room_state", [])
+ if not isinstance(knocked_state, list):
+ knocked_state = []
+ knocked_state = list(knocked_state)
# Append the actual knock membership event itself as well. This provides
# the client with:
@@ -477,6 +516,7 @@ class SyncRestServlet(RestServlet):
@trace_with_opname("sync.encode_archived")
async def encode_archived(
self,
+ sync_config: SyncConfig,
rooms: List[ArchivedSyncResult],
time_now: int,
serialize_options: SerializeEventConfig,
@@ -485,6 +525,7 @@ class SyncRestServlet(RestServlet):
Encode the archived rooms in a sync result
Args:
+ sync_config
rooms: list of sync results for rooms this user is joined to
time_now: current time - used as a baseline for age calculations
serialize_options: Event serializer options
@@ -494,13 +535,18 @@ class SyncRestServlet(RestServlet):
joined = {}
for room in rooms:
joined[room.room_id] = await self.encode_room(
- room, time_now, joined=False, serialize_options=serialize_options
+ sync_config,
+ room,
+ time_now,
+ joined=False,
+ serialize_options=serialize_options,
)
return joined
async def encode_room(
self,
+ sync_config: SyncConfig,
room: Union[JoinedSyncResult, ArchivedSyncResult],
time_now: int,
joined: bool,
@@ -508,6 +554,7 @@ class SyncRestServlet(RestServlet):
) -> JsonDict:
"""
Args:
+ sync_config
room: sync result for a single room
time_now: current time - used as a baseline for age calculations
token_id: ID of the user's auth token - used for namespacing
@@ -548,13 +595,20 @@ class SyncRestServlet(RestServlet):
account_data = room.account_data
+ # We either include a `state` or `state_after` field depending on
+ # whether the client has opted in to the newer `state_after` behavior.
+ if sync_config.use_state_after:
+ state_key_name = "org.matrix.msc4222.state_after"
+ else:
+ state_key_name = "state"
+
result: JsonDict = {
"timeline": {
"events": serialized_timeline,
"prev_batch": await room.timeline.prev_batch.to_string(self.store),
"limited": room.timeline.limited,
},
- "state": {"events": serialized_state},
+ state_key_name: {"events": serialized_state},
"account_data": {"events": account_data},
}
@@ -688,6 +742,7 @@ class SlidingSyncE2eeRestServlet(RestServlet):
filter_collection=self.only_member_events_filter_collection,
is_guest=requester.is_guest,
device_id=device_id,
+ use_state_after=False, # We don't return any rooms so this flag is a no-op
)
since_token = None
@@ -975,7 +1030,7 @@ class SlidingSyncRestServlet(RestServlet):
return response
def encode_lists(
- self, lists: Dict[str, SlidingSyncResult.SlidingWindowList]
+ self, lists: Mapping[str, SlidingSyncResult.SlidingWindowList]
) -> JsonDict:
def encode_operation(
operation: SlidingSyncResult.SlidingWindowList.Operation,
@@ -1010,13 +1065,19 @@ class SlidingSyncRestServlet(RestServlet):
serialized_rooms: Dict[str, JsonDict] = {}
for room_id, room_result in rooms.items():
serialized_rooms[room_id] = {
- "bump_stamp": room_result.bump_stamp,
- "joined_count": room_result.joined_count,
- "invited_count": room_result.invited_count,
"notification_count": room_result.notification_count,
"highlight_count": room_result.highlight_count,
}
+ if room_result.bump_stamp is not None:
+ serialized_rooms[room_id]["bump_stamp"] = room_result.bump_stamp
+
+ if room_result.joined_count is not None:
+ serialized_rooms[room_id]["joined_count"] = room_result.joined_count
+
+ if room_result.invited_count is not None:
+ serialized_rooms[room_id]["invited_count"] = room_result.invited_count
+
if room_result.name:
serialized_rooms[room_id]["name"] = room_result.name
@@ -1040,10 +1101,15 @@ class SlidingSyncRestServlet(RestServlet):
serialized_rooms[room_id]["heroes"] = serialized_heroes
# We should only include the `initial` key if it's `True` to save bandwidth.
- # The absense of this flag means `False`.
+ # The absence of this flag means `False`.
if room_result.initial:
serialized_rooms[room_id]["initial"] = room_result.initial
+ if room_result.unstable_expanded_timeline:
+ serialized_rooms[room_id]["unstable_expanded_timeline"] = (
+ room_result.unstable_expanded_timeline
+ )
+
# This will be omitted for invite/knock rooms with `stripped_state`
if (
room_result.required_state is not None
@@ -1077,9 +1143,9 @@ class SlidingSyncRestServlet(RestServlet):
# This will be omitted for invite/knock rooms with `stripped_state`
if room_result.prev_batch is not None:
- serialized_rooms[room_id]["prev_batch"] = (
- await room_result.prev_batch.to_string(self.store)
- )
+ serialized_rooms[room_id][
+ "prev_batch"
+ ] = await room_result.prev_batch.to_string(self.store)
# This will be omitted for invite/knock rooms with `stripped_state`
if room_result.num_live is not None:
diff --git a/synapse/rest/client/tags.py b/synapse/rest/client/tags.py
index 554bcb95dd..b6648f3499 100644
--- a/synapse/rest/client/tags.py
+++ b/synapse/rest/client/tags.py
@@ -78,6 +78,7 @@ class TagServlet(RestServlet):
super().__init__()
self.auth = hs.get_auth()
self.handler = hs.get_account_data_handler()
+ self.room_member_handler = hs.get_room_member_handler()
async def on_PUT(
self, request: SynapseRequest, user_id: str, room_id: str, tag: str
@@ -85,6 +86,12 @@ class TagServlet(RestServlet):
requester = await self.auth.get_user_by_req(request)
if user_id != requester.user.to_string():
raise AuthError(403, "Cannot add tags for other users.")
+ # Check if the user has any membership in the room and raise error if not.
+ # Although it's not harmful for users to tag random rooms, it's just superfluous
+ # data we don't need to track or allow.
+ await self.room_member_handler.check_for_any_membership_in_room(
+ user_id=user_id, room_id=room_id
+ )
body = parse_json_object_from_request(request)
diff --git a/synapse/rest/client/transactions.py b/synapse/rest/client/transactions.py
index 30c1f17fc6..1a57996aec 100644
--- a/synapse/rest/client/transactions.py
+++ b/synapse/rest/client/transactions.py
@@ -21,6 +21,7 @@
"""This module contains logic for storing HTTP PUT transactions. This is used
to ensure idempotency when performing PUTs using the REST API."""
+
import logging
from typing import TYPE_CHECKING, Awaitable, Callable, Dict, Hashable, Tuple
@@ -93,9 +94,9 @@ class HttpTransactionCache:
# (appservice and guest users), but does not cover access tokens minted
# by the admin API. Use the access token ID instead.
else:
- assert (
- requester.access_token_id is not None
- ), "Requester must have an access_token_id"
+ assert requester.access_token_id is not None, (
+ "Requester must have an access_token_id"
+ )
return (path, "user_admin", requester.access_token_id)
def fetch_or_execute_request(
diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py
index 75df684416..f58f11e5cc 100644
--- a/synapse/rest/client/versions.py
+++ b/synapse/rest/client/versions.py
@@ -4,7 +4,7 @@
# Copyright 2019 The Matrix.org Foundation C.I.C.
# Copyright 2017 Vector Creations Ltd
# Copyright 2016 OpenMarket Ltd
-# Copyright (C) 2023 New Vector, Ltd
+# Copyright (C) 2023-2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -64,6 +64,7 @@ class VersionsRestServlet(RestServlet):
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
msc3881_enabled = self.config.experimental.msc3881_enabled
+ msc3575_enabled = self.config.experimental.msc3575_enabled
if self.auth.has_access_token(request):
requester = await self.auth.get_user_by_req(
@@ -77,6 +78,9 @@ class VersionsRestServlet(RestServlet):
msc3881_enabled = await self.store.is_feature_enabled(
user_id, ExperimentalFeature.MSC3881
)
+ msc3575_enabled = await self.store.is_feature_enabled(
+ user_id, ExperimentalFeature.MSC3575
+ )
return (
200,
@@ -145,9 +149,6 @@ class VersionsRestServlet(RestServlet):
"org.matrix.msc3881": msc3881_enabled,
# Adds support for filtering /messages by event relation.
"org.matrix.msc3874": self.config.experimental.msc3874_enabled,
- # Adds support for simple HTTP rendezvous as per MSC3886
- "org.matrix.msc3886": self.config.experimental.msc3886_endpoint
- is not None,
# Adds support for relation-based redactions as per MSC3912.
"org.matrix.msc3912": self.config.experimental.msc3912_enabled,
# Whether recursively provide relations is supported.
@@ -167,8 +168,14 @@ class VersionsRestServlet(RestServlet):
is not None
)
),
- # MSC4151: Report room API (Client-Server API)
- "org.matrix.msc4151": self.config.experimental.msc4151_enabled,
+ # MSC4140: Delayed events
+ "org.matrix.msc4140": bool(self.config.server.max_event_delay_ms),
+ # Simplified sliding sync
+ "org.matrix.simplified_msc3575": msc3575_enabled,
+ # Arbitrary key-value profile fields.
+ "uk.tcpip.msc4133": self.config.experimental.msc4133_enabled,
+ # MSC4155: Invite filtering
+ "org.matrix.msc4155": self.config.experimental.msc4155_enabled,
},
},
)
diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py
index a411ed614e..fea0b9706d 100644
--- a/synapse/rest/key/v2/remote_key_resource.py
+++ b/synapse/rest/key/v2/remote_key_resource.py
@@ -23,17 +23,11 @@ import logging
import re
from typing import TYPE_CHECKING, Dict, Mapping, Optional, Set, Tuple
-from synapse._pydantic_compat import HAS_PYDANTIC_V2
-
-if TYPE_CHECKING or HAS_PYDANTIC_V2:
- from pydantic.v1 import Extra, StrictInt, StrictStr
-else:
- from pydantic import StrictInt, StrictStr, Extra
-
from signedjson.sign import sign_json
from twisted.web.server import Request
+from synapse._pydantic_compat import Extra, StrictInt, StrictStr
from synapse.crypto.keyring import ServerKeyFetcher
from synapse.http.server import HttpServer
from synapse.http.servlet import (
@@ -191,10 +185,10 @@ class RemoteKey(RestServlet):
server_keys: Dict[Tuple[str, str], Optional[FetchKeyResultForRemote]] = {}
for server_name, key_ids in query.items():
if key_ids:
- results: Mapping[str, Optional[FetchKeyResultForRemote]] = (
- await self.store.get_server_keys_json_for_remote(
- server_name, key_ids
- )
+ results: Mapping[
+ str, Optional[FetchKeyResultForRemote]
+ ] = await self.store.get_server_keys_json_for_remote(
+ server_name, key_ids
)
else:
results = await self.store.get_all_server_keys_json_for_remote(
diff --git a/synapse/rest/media/config_resource.py b/synapse/rest/media/config_resource.py
index 80462d65d3..b014e91bdb 100644
--- a/synapse/rest/media/config_resource.py
+++ b/synapse/rest/media/config_resource.py
@@ -40,7 +40,14 @@ class MediaConfigResource(RestServlet):
self.clock = hs.get_clock()
self.auth = hs.get_auth()
self.limits_dict = {"m.upload.size": config.media.max_upload_size}
+ self.media_repository_callbacks = hs.get_module_api_callbacks().media_repository
async def on_GET(self, request: SynapseRequest) -> None:
- await self.auth.get_user_by_req(request)
- respond_with_json(request, 200, self.limits_dict, send_cors=True)
+ requester = await self.auth.get_user_by_req(request)
+ user_specific_config = (
+ await self.media_repository_callbacks.get_media_config_for_user(
+ requester.user.to_string()
+ )
+ )
+ response = user_specific_config if user_specific_config else self.limits_dict
+ respond_with_json(request, 200, response, send_cors=True)
diff --git a/synapse/rest/media/upload_resource.py b/synapse/rest/media/upload_resource.py
index 5ef6bf8836..572f7897fd 100644
--- a/synapse/rest/media/upload_resource.py
+++ b/synapse/rest/media/upload_resource.py
@@ -50,9 +50,12 @@ class BaseUploadServlet(RestServlet):
self.server_name = hs.hostname
self.auth = hs.get_auth()
self.max_upload_size = hs.config.media.max_upload_size
+ self._media_repository_callbacks = (
+ hs.get_module_api_callbacks().media_repository
+ )
- def _get_file_metadata(
- self, request: SynapseRequest
+ async def _get_file_metadata(
+ self, request: SynapseRequest, user_id: str
) -> Tuple[int, Optional[str], str]:
raw_content_length = request.getHeader("Content-Length")
if raw_content_length is None:
@@ -67,7 +70,14 @@ class BaseUploadServlet(RestServlet):
code=413,
errcode=Codes.TOO_LARGE,
)
-
+ if not await self._media_repository_callbacks.is_user_allowed_to_upload_media_of_size(
+ user_id, content_length
+ ):
+ raise SynapseError(
+ msg="Upload request body is too large",
+ code=413,
+ errcode=Codes.TOO_LARGE,
+ )
args: Dict[bytes, List[bytes]] = request.args # type: ignore
upload_name_bytes = parse_bytes_from_args(args, "filename")
if upload_name_bytes:
@@ -94,7 +104,7 @@ class BaseUploadServlet(RestServlet):
# if headers.hasHeader(b"Content-Disposition"):
# disposition = headers.getRawHeaders(b"Content-Disposition")[0]
- # TODO(markjh): parse content-dispostion
+ # TODO(markjh): parse content-disposition
return content_length, upload_name, media_type
@@ -104,7 +114,9 @@ class UploadServlet(BaseUploadServlet):
async def on_POST(self, request: SynapseRequest) -> None:
requester = await self.auth.get_user_by_req(request)
- content_length, upload_name, media_type = self._get_file_metadata(request)
+ content_length, upload_name, media_type = await self._get_file_metadata(
+ request, requester.user.to_string()
+ )
try:
content: IO = request.content # type: ignore
@@ -152,7 +164,9 @@ class AsyncUploadServlet(BaseUploadServlet):
async with lock:
await self.media_repo.verify_can_upload(media_id, requester.user)
- content_length, upload_name, media_type = self._get_file_metadata(request)
+ content_length, upload_name, media_type = await self._get_file_metadata(
+ request, requester.user.to_string()
+ )
try:
content: IO = request.content # type: ignore
diff --git a/synapse/rest/synapse/client/__init__.py b/synapse/rest/synapse/client/__init__.py
index 7b5bfc0421..982b6c0e7e 100644
--- a/synapse/rest/synapse/client/__init__.py
+++ b/synapse/rest/synapse/client/__init__.py
@@ -29,7 +29,6 @@ from synapse.rest.synapse.client.pick_idp import PickIdpResource
from synapse.rest.synapse.client.pick_username import pick_username_resource
from synapse.rest.synapse.client.rendezvous import MSC4108RendezvousSessionResource
from synapse.rest.synapse.client.sso_register import SsoRegisterResource
-from synapse.rest.synapse.client.unsubscribe import UnsubscribeResource
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -50,9 +49,7 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
"/_synapse/client/pick_idp": PickIdpResource(hs),
"/_synapse/client/pick_username": pick_username_resource(hs),
"/_synapse/client/new_user_consent": NewUserConsentResource(hs),
- "/_synapse/client/sso_register": SsoRegisterResource(hs),
- # Unsubscribe to notification emails link
- "/_synapse/client/unsubscribe": UnsubscribeResource(hs),
+ "/_synapse/client/sso_register": SsoRegisterResource(hs)
}
# Expose the JWKS endpoint if OAuth2 delegation is enabled
@@ -68,16 +65,6 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
resources["/_synapse/client/oidc"] = OIDCResource(hs)
- if hs.config.saml2.saml2_enabled:
- from synapse.rest.synapse.client.saml2 import SAML2Resource
-
- res = SAML2Resource(hs)
- resources["/_synapse/client/saml2"] = res
-
- # This is also mounted under '/_matrix' for backwards-compatibility.
- # To be removed in Synapse v1.32.0.
- resources["/_matrix/saml2"] = res
-
if hs.config.federation.federation_whitelist_endpoint_enabled:
resources[FederationWhitelistResource.PATH] = FederationWhitelistResource(hs)
diff --git a/synapse/rest/synapse/client/password_reset.py b/synapse/rest/synapse/client/password_reset.py
deleted file mode 100644
index 29e4b2d07a..0000000000
--- a/synapse/rest/synapse/client/password_reset.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#
-# This file is licensed under the Affero General Public License (AGPL) version 3.
-#
-# Copyright 2020 The Matrix.org Foundation C.I.C.
-# Copyright (C) 2023 New Vector, Ltd
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# See the GNU Affero General Public License for more details:
-# <https://www.gnu.org/licenses/agpl-3.0.html>.
-#
-# Originally licensed under the Apache License, Version 2.0:
-# <http://www.apache.org/licenses/LICENSE-2.0>.
-#
-# [This file includes modifications made by New Vector Limited]
-#
-#
-import logging
-from typing import TYPE_CHECKING, Tuple
-
-from twisted.web.server import Request
-
-from synapse.api.errors import ThreepidValidationError
-from synapse.http.server import DirectServeHtmlResource
-from synapse.http.servlet import parse_string
-from synapse.util.stringutils import assert_valid_client_secret
-
-if TYPE_CHECKING:
- from synapse.server import HomeServer
-
-logger = logging.getLogger(__name__)
-
-
-class PasswordResetSubmitTokenResource(DirectServeHtmlResource):
- """Handles 3PID validation token submission
-
- This resource gets mounted under /_synapse/client/password_reset/email/submit_token
- """
-
- isLeaf = 1
-
- def __init__(self, hs: "HomeServer"):
- """
- Args:
- hs: server
- """
- super().__init__()
-
- self.clock = hs.get_clock()
- self.store = hs.get_datastores().main
-
- self._confirmation_email_template = (
- hs.config.email.email_password_reset_template_confirmation_html
- )
- self._email_password_reset_template_success_html = (
- hs.config.email.email_password_reset_template_success_html_content
- )
- self._failure_email_template = (
- hs.config.email.email_password_reset_template_failure_html
- )
-
- # This resource should only be mounted if email validation is enabled
- assert hs.config.email.can_verify_email
-
- async def _async_render_GET(self, request: Request) -> Tuple[int, bytes]:
- sid = parse_string(request, "sid", required=True)
- token = parse_string(request, "token", required=True)
- client_secret = parse_string(request, "client_secret", required=True)
- assert_valid_client_secret(client_secret)
-
- # Show a confirmation page, just in case someone accidentally clicked this link when
- # they didn't mean to
- template_vars = {
- "sid": sid,
- "token": token,
- "client_secret": client_secret,
- }
- return (
- 200,
- self._confirmation_email_template.render(**template_vars).encode("utf-8"),
- )
-
- async def _async_render_POST(self, request: Request) -> Tuple[int, bytes]:
- sid = parse_string(request, "sid", required=True)
- token = parse_string(request, "token", required=True)
- client_secret = parse_string(request, "client_secret", required=True)
-
- # Attempt to validate a 3PID session
- try:
- # Mark the session as valid
- next_link = await self.store.validate_threepid_session(
- sid, client_secret, token, self.clock.time_msec()
- )
-
- # Perform a 302 redirect if next_link is set
- if next_link:
- if next_link.startswith("file:///"):
- logger.warning(
- "Not redirecting to next_link as it is a local file: address"
- )
- else:
- next_link_bytes = next_link.encode("utf-8")
- request.setHeader("Location", next_link_bytes)
- return (
- 302,
- (
- b'You are being redirected to <a href="%s">%s</a>.'
- % (next_link_bytes, next_link_bytes)
- ),
- )
-
- # Otherwise show the success template
- html_bytes = self._email_password_reset_template_success_html.encode(
- "utf-8"
- )
- status_code = 200
- except ThreepidValidationError as e:
- status_code = e.code
-
- # Show a failure page with a reason
- template_vars = {"failure_reason": e.msg}
- html_bytes = self._failure_email_template.render(**template_vars).encode(
- "utf-8"
- )
-
- return status_code, html_bytes
diff --git a/synapse/rest/synapse/client/pick_idp.py b/synapse/rest/synapse/client/pick_idp.py
index f26929bd60..5e599f85b0 100644
--- a/synapse/rest/synapse/client/pick_idp.py
+++ b/synapse/rest/synapse/client/pick_idp.py
@@ -21,6 +21,7 @@
import logging
from typing import TYPE_CHECKING
+from synapse.api.urls import LoginSSORedirectURIBuilder
from synapse.http.server import (
DirectServeHtmlResource,
finish_request,
@@ -49,6 +50,8 @@ class PickIdpResource(DirectServeHtmlResource):
hs.config.sso.sso_login_idp_picker_template
)
self._server_name = hs.hostname
+ self._public_baseurl = hs.config.server.public_baseurl
+ self._login_sso_redirect_url_builder = LoginSSORedirectURIBuilder(hs.config)
async def _async_render_GET(self, request: SynapseRequest) -> None:
client_redirect_url = parse_string(
@@ -56,25 +59,23 @@ class PickIdpResource(DirectServeHtmlResource):
)
idp = parse_string(request, "idp", required=False)
- # if we need to pick an IdP, do so
+ # If we need to pick an IdP, do so
if not idp:
return await self._serve_id_picker(request, client_redirect_url)
- # otherwise, redirect to the IdP's redirect URI
- providers = self._sso_handler.get_identity_providers()
- auth_provider = providers.get(idp)
- if not auth_provider:
- logger.info("Unknown idp %r", idp)
- self._sso_handler.render_error(
- request, "unknown_idp", "Unknown identity provider ID"
+ # Otherwise, redirect to the login SSO redirect endpoint for the given IdP
+ # (which will in turn take us to the the IdP's redirect URI).
+ #
+ # We could go directly to the IdP's redirect URI, but this way we ensure that
+ # the user goes through the same logic as normal flow. Additionally, if a proxy
+ # needs to intercept the request, it only needs to intercept the one endpoint.
+ sso_login_redirect_url = (
+ self._login_sso_redirect_url_builder.build_login_sso_redirect_uri(
+ idp_id=idp, client_redirect_url=client_redirect_url
)
- return
-
- sso_url = await auth_provider.handle_redirect_request(
- request, client_redirect_url.encode("utf8")
)
- logger.info("Redirecting to %s", sso_url)
- request.redirect(sso_url)
+ logger.info("Redirecting to %s", sso_login_redirect_url)
+ request.redirect(sso_login_redirect_url)
finish_request(request)
async def _serve_id_picker(
diff --git a/synapse/rest/synapse/client/saml2/__init__.py b/synapse/rest/synapse/client/saml2/__init__.py
deleted file mode 100644
index 3658c6a0e3..0000000000
--- a/synapse/rest/synapse/client/saml2/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#
-# This file is licensed under the Affero General Public License (AGPL) version 3.
-#
-# Copyright (C) 2023 New Vector, Ltd
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# See the GNU Affero General Public License for more details:
-# <https://www.gnu.org/licenses/agpl-3.0.html>.
-#
-# Originally licensed under the Apache License, Version 2.0:
-# <http://www.apache.org/licenses/LICENSE-2.0>.
-#
-# [This file includes modifications made by New Vector Limited]
-#
-#
-
-import logging
-from typing import TYPE_CHECKING
-
-from twisted.web.resource import Resource
-
-from synapse.rest.synapse.client.saml2.metadata_resource import SAML2MetadataResource
-from synapse.rest.synapse.client.saml2.response_resource import SAML2ResponseResource
-
-if TYPE_CHECKING:
- from synapse.server import HomeServer
-
-logger = logging.getLogger(__name__)
-
-
-class SAML2Resource(Resource):
- def __init__(self, hs: "HomeServer"):
- Resource.__init__(self)
- self.putChild(b"metadata.xml", SAML2MetadataResource(hs))
- self.putChild(b"authn_response", SAML2ResponseResource(hs))
-
-
-__all__ = ["SAML2Resource"]
diff --git a/synapse/rest/synapse/client/saml2/metadata_resource.py b/synapse/rest/synapse/client/saml2/metadata_resource.py
deleted file mode 100644
index bcd5195108..0000000000
--- a/synapse/rest/synapse/client/saml2/metadata_resource.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#
-# This file is licensed under the Affero General Public License (AGPL) version 3.
-#
-# Copyright (C) 2023 New Vector, Ltd
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# See the GNU Affero General Public License for more details:
-# <https://www.gnu.org/licenses/agpl-3.0.html>.
-#
-# Originally licensed under the Apache License, Version 2.0:
-# <http://www.apache.org/licenses/LICENSE-2.0>.
-#
-# [This file includes modifications made by New Vector Limited]
-#
-#
-
-from typing import TYPE_CHECKING
-
-import saml2.metadata
-
-from twisted.web.resource import Resource
-from twisted.web.server import Request
-
-if TYPE_CHECKING:
- from synapse.server import HomeServer
-
-
-class SAML2MetadataResource(Resource):
- """A Twisted web resource which renders the SAML metadata"""
-
- isLeaf = 1
-
- def __init__(self, hs: "HomeServer"):
- Resource.__init__(self)
- self.sp_config = hs.config.saml2.saml2_sp_config
-
- def render_GET(self, request: Request) -> bytes:
- metadata_xml = saml2.metadata.create_metadata_string(
- configfile=None, config=self.sp_config
- )
- request.setHeader(b"Content-Type", b"text/xml; charset=utf-8")
- return metadata_xml
diff --git a/synapse/rest/synapse/client/saml2/response_resource.py b/synapse/rest/synapse/client/saml2/response_resource.py
deleted file mode 100644
index 7b8667e04c..0000000000
--- a/synapse/rest/synapse/client/saml2/response_resource.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#
-# This file is licensed under the Affero General Public License (AGPL) version 3.
-#
-# Copyright (C) 2023 New Vector, Ltd
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# See the GNU Affero General Public License for more details:
-# <https://www.gnu.org/licenses/agpl-3.0.html>.
-#
-# Originally licensed under the Apache License, Version 2.0:
-# <http://www.apache.org/licenses/LICENSE-2.0>.
-#
-# [This file includes modifications made by New Vector Limited]
-#
-#
-
-from typing import TYPE_CHECKING
-
-from twisted.web.server import Request
-
-from synapse.http.server import DirectServeHtmlResource
-from synapse.http.site import SynapseRequest
-
-if TYPE_CHECKING:
- from synapse.server import HomeServer
-
-
-class SAML2ResponseResource(DirectServeHtmlResource):
- """A Twisted web resource which handles the SAML response"""
-
- isLeaf = 1
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self._saml_handler = hs.get_saml_handler()
- self._sso_handler = hs.get_sso_handler()
-
- async def _async_render_GET(self, request: Request) -> None:
- # We're not expecting any GET request on that resource if everything goes right,
- # but some IdPs sometimes end up responding with a 302 redirect on this endpoint.
- # In this case, just tell the user that something went wrong and they should
- # try to authenticate again.
- self._sso_handler.render_error(
- request, "unexpected_get", "Unexpected GET request on /saml2/authn_response"
- )
-
- async def _async_render_POST(self, request: SynapseRequest) -> None:
- await self._saml_handler.handle_saml_response(request)
diff --git a/synapse/rest/synapse/client/unsubscribe.py b/synapse/rest/synapse/client/unsubscribe.py
deleted file mode 100644
index 6d4bd9f2ed..0000000000
--- a/synapse/rest/synapse/client/unsubscribe.py
+++ /dev/null
@@ -1,88 +0,0 @@
-#
-# This file is licensed under the Affero General Public License (AGPL) version 3.
-#
-# Copyright 2022 The Matrix.org Foundation C.I.C.
-# Copyright (C) 2023 New Vector, Ltd
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# See the GNU Affero General Public License for more details:
-# <https://www.gnu.org/licenses/agpl-3.0.html>.
-#
-# Originally licensed under the Apache License, Version 2.0:
-# <http://www.apache.org/licenses/LICENSE-2.0>.
-#
-# [This file includes modifications made by New Vector Limited]
-#
-#
-
-from typing import TYPE_CHECKING
-
-from synapse.api.errors import StoreError
-from synapse.http.server import DirectServeHtmlResource, respond_with_html_bytes
-from synapse.http.servlet import parse_string
-from synapse.http.site import SynapseRequest
-
-if TYPE_CHECKING:
- from synapse.server import HomeServer
-
-
-class UnsubscribeResource(DirectServeHtmlResource):
- """
- To allow pusher to be delete by clicking a link (ie. GET request)
- """
-
- SUCCESS_HTML = b"<html><body>You have been unsubscribed</body><html>"
-
- def __init__(self, hs: "HomeServer"):
- super().__init__()
- self.notifier = hs.get_notifier()
- self.auth = hs.get_auth()
- self.pusher_pool = hs.get_pusherpool()
- self.macaroon_generator = hs.get_macaroon_generator()
-
- async def _async_render_GET(self, request: SynapseRequest) -> None:
- """
- Handle a user opening an unsubscribe link in the browser, either via an
- HTML/Text email or via the List-Unsubscribe header.
- """
- token = parse_string(request, "access_token", required=True)
- app_id = parse_string(request, "app_id", required=True)
- pushkey = parse_string(request, "pushkey", required=True)
-
- user_id = self.macaroon_generator.verify_delete_pusher_token(
- token, app_id, pushkey
- )
-
- try:
- await self.pusher_pool.remove_pusher(
- app_id=app_id, pushkey=pushkey, user_id=user_id
- )
- except StoreError as se:
- if se.code != 404:
- # This is fine: they're already unsubscribed
- raise
-
- self.notifier.on_new_replication_data()
-
- respond_with_html_bytes(
- request,
- 200,
- UnsubscribeResource.SUCCESS_HTML,
- )
-
- async def _async_render_POST(self, request: SynapseRequest) -> None:
- """
- Handle a mail user agent POSTing to the unsubscribe URL via the
- List-Unsubscribe & List-Unsubscribe-Post headers.
- """
-
- # TODO Assert that the body has a single field
-
- # Assert the body has form encoded key/value pair of
- # List-Unsubscribe=One-Click.
-
- await self._async_render_GET(request)
diff --git a/synapse/rest/well_known.py b/synapse/rest/well_known.py
index d0ca8ca46b..9ce1eb6249 100644
--- a/synapse/rest/well_known.py
+++ b/synapse/rest/well_known.py
@@ -18,12 +18,13 @@
#
#
import logging
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Tuple, cast
from twisted.web.resource import Resource
from twisted.web.server import Request
-from synapse.http.server import set_cors_headers
+from synapse.api.errors import NotFoundError
+from synapse.http.server import DirectServeJsonResource
from synapse.http.site import SynapseRequest
from synapse.types import JsonDict
from synapse.util import json_encoder
@@ -38,27 +39,30 @@ logger = logging.getLogger(__name__)
class WellKnownBuilder:
def __init__(self, hs: "HomeServer"):
self._config = hs.config
+ self._auth = hs.get_auth()
- def get_well_known(self) -> Optional[JsonDict]:
+ async def get_well_known(self) -> Optional[JsonDict]:
if not self._config.server.serve_client_wellknown:
return None
result = {"m.homeserver": {"base_url": self._config.server.public_baseurl}}
- if self._config.registration.default_identity_server:
- result["m.identity_server"] = {
- "base_url": self._config.registration.default_identity_server
- }
-
# We use the MSC3861 values as they are used by multiple MSCs
if self._config.experimental.msc3861.enabled:
+ # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth
+ # We import lazily here because of the authlib requirement
+ from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth
+
+ auth = cast(MSC3861DelegatedAuth, self._auth)
+
result["org.matrix.msc2965.authentication"] = {
- "issuer": self._config.experimental.msc3861.issuer
+ "issuer": await auth.issuer(),
}
- if self._config.experimental.msc3861.account_management_url is not None:
- result["org.matrix.msc2965.authentication"][
- "account"
- ] = self._config.experimental.msc3861.account_management_url
+ account_management_url = await auth.account_management_url()
+ if account_management_url is not None:
+ result["org.matrix.msc2965.authentication"]["account"] = (
+ account_management_url
+ )
if self._config.server.extra_well_known_client_content:
for (
@@ -71,26 +75,22 @@ class WellKnownBuilder:
return result
-class ClientWellKnownResource(Resource):
+class ClientWellKnownResource(DirectServeJsonResource):
"""A Twisted web resource which renders the .well-known/matrix/client file"""
isLeaf = 1
def __init__(self, hs: "HomeServer"):
- Resource.__init__(self)
+ super().__init__()
self._well_known_builder = WellKnownBuilder(hs)
- def render_GET(self, request: SynapseRequest) -> bytes:
- set_cors_headers(request)
- r = self._well_known_builder.get_well_known()
+ async def _async_render_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ r = await self._well_known_builder.get_well_known()
if not r:
- request.setResponseCode(404)
- request.setHeader(b"Content-Type", b"text/plain")
- return b".well-known not available"
+ raise NotFoundError(".well-known not available")
logger.debug("returning: %s", r)
- request.setHeader(b"Content-Type", b"application/json")
- return json_encoder.encode(r).encode("utf-8")
+ return 200, r
class ServerWellKnownResource(Resource):
|