From 642a42eddece60afbbd5e5a6659fa9b939238b4a Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:57:58 +0100 Subject: Flatten the synapse.rest.client package (#10600) --- synapse/rest/client/__init__.py | 2 +- synapse/rest/client/_base.py | 100 ++ synapse/rest/client/account.py | 910 ++++++++++++++++ synapse/rest/client/account_data.py | 122 +++ synapse/rest/client/account_validity.py | 104 ++ synapse/rest/client/auth.py | 143 +++ synapse/rest/client/capabilities.py | 68 ++ synapse/rest/client/devices.py | 300 +++++ synapse/rest/client/directory.py | 185 ++++ synapse/rest/client/events.py | 94 ++ synapse/rest/client/filter.py | 94 ++ synapse/rest/client/groups.py | 957 ++++++++++++++++ synapse/rest/client/initial_sync.py | 47 + synapse/rest/client/keys.py | 344 ++++++ synapse/rest/client/knock.py | 107 ++ synapse/rest/client/login.py | 600 ++++++++++ synapse/rest/client/logout.py | 72 ++ synapse/rest/client/notifications.py | 91 ++ synapse/rest/client/openid.py | 94 ++ synapse/rest/client/password_policy.py | 57 + synapse/rest/client/presence.py | 95 ++ synapse/rest/client/profile.py | 155 +++ synapse/rest/client/push_rule.py | 354 ++++++ synapse/rest/client/pusher.py | 171 +++ synapse/rest/client/read_marker.py | 74 ++ synapse/rest/client/receipts.py | 71 ++ synapse/rest/client/register.py | 879 +++++++++++++++ synapse/rest/client/relations.py | 381 +++++++ synapse/rest/client/report_event.py | 68 ++ synapse/rest/client/room.py | 1152 ++++++++++++++++++++ synapse/rest/client/room_batch.py | 441 ++++++++ synapse/rest/client/room_keys.py | 391 +++++++ synapse/rest/client/room_upgrade_rest_servlet.py | 88 ++ synapse/rest/client/sendtodevice.py | 67 ++ synapse/rest/client/shared_rooms.py | 67 ++ synapse/rest/client/sync.py | 532 +++++++++ synapse/rest/client/tags.py | 85 ++ synapse/rest/client/thirdparty.py | 111 ++ synapse/rest/client/tokenrefresh.py | 37 + synapse/rest/client/user_directory.py | 79 ++ synapse/rest/client/v1/__init__.py | 13 - synapse/rest/client/v1/directory.py | 185 ---- synapse/rest/client/v1/events.py | 94 -- synapse/rest/client/v1/initial_sync.py | 47 - synapse/rest/client/v1/login.py | 600 ---------- synapse/rest/client/v1/logout.py | 72 -- synapse/rest/client/v1/presence.py | 95 -- synapse/rest/client/v1/profile.py | 155 --- synapse/rest/client/v1/push_rule.py | 354 ------ synapse/rest/client/v1/pusher.py | 171 --- synapse/rest/client/v1/room.py | 1152 -------------------- synapse/rest/client/v1/voip.py | 73 -- synapse/rest/client/v2_alpha/__init__.py | 13 - synapse/rest/client/v2_alpha/_base.py | 100 -- synapse/rest/client/v2_alpha/account.py | 910 ---------------- synapse/rest/client/v2_alpha/account_data.py | 122 --- synapse/rest/client/v2_alpha/account_validity.py | 104 -- synapse/rest/client/v2_alpha/auth.py | 143 --- synapse/rest/client/v2_alpha/capabilities.py | 68 -- synapse/rest/client/v2_alpha/devices.py | 300 ----- synapse/rest/client/v2_alpha/filter.py | 94 -- synapse/rest/client/v2_alpha/groups.py | 957 ---------------- synapse/rest/client/v2_alpha/keys.py | 344 ------ synapse/rest/client/v2_alpha/knock.py | 107 -- synapse/rest/client/v2_alpha/notifications.py | 91 -- synapse/rest/client/v2_alpha/openid.py | 94 -- synapse/rest/client/v2_alpha/password_policy.py | 57 - synapse/rest/client/v2_alpha/read_marker.py | 74 -- synapse/rest/client/v2_alpha/receipts.py | 71 -- synapse/rest/client/v2_alpha/register.py | 879 --------------- synapse/rest/client/v2_alpha/relations.py | 381 ------- synapse/rest/client/v2_alpha/report_event.py | 68 -- synapse/rest/client/v2_alpha/room.py | 441 -------- synapse/rest/client/v2_alpha/room_keys.py | 391 ------- .../client/v2_alpha/room_upgrade_rest_servlet.py | 88 -- synapse/rest/client/v2_alpha/sendtodevice.py | 67 -- synapse/rest/client/v2_alpha/shared_rooms.py | 67 -- synapse/rest/client/v2_alpha/sync.py | 532 --------- synapse/rest/client/v2_alpha/tags.py | 85 -- synapse/rest/client/v2_alpha/thirdparty.py | 111 -- synapse/rest/client/v2_alpha/tokenrefresh.py | 37 - synapse/rest/client/v2_alpha/user_directory.py | 79 -- synapse/rest/client/voip.py | 73 ++ 83 files changed, 9861 insertions(+), 9887 deletions(-) create mode 100644 synapse/rest/client/_base.py create mode 100644 synapse/rest/client/account.py create mode 100644 synapse/rest/client/account_data.py create mode 100644 synapse/rest/client/account_validity.py create mode 100644 synapse/rest/client/auth.py create mode 100644 synapse/rest/client/capabilities.py create mode 100644 synapse/rest/client/devices.py create mode 100644 synapse/rest/client/directory.py create mode 100644 synapse/rest/client/events.py create mode 100644 synapse/rest/client/filter.py create mode 100644 synapse/rest/client/groups.py create mode 100644 synapse/rest/client/initial_sync.py create mode 100644 synapse/rest/client/keys.py create mode 100644 synapse/rest/client/knock.py create mode 100644 synapse/rest/client/login.py create mode 100644 synapse/rest/client/logout.py create mode 100644 synapse/rest/client/notifications.py create mode 100644 synapse/rest/client/openid.py create mode 100644 synapse/rest/client/password_policy.py create mode 100644 synapse/rest/client/presence.py create mode 100644 synapse/rest/client/profile.py create mode 100644 synapse/rest/client/push_rule.py create mode 100644 synapse/rest/client/pusher.py create mode 100644 synapse/rest/client/read_marker.py create mode 100644 synapse/rest/client/receipts.py create mode 100644 synapse/rest/client/register.py create mode 100644 synapse/rest/client/relations.py create mode 100644 synapse/rest/client/report_event.py create mode 100644 synapse/rest/client/room.py create mode 100644 synapse/rest/client/room_batch.py create mode 100644 synapse/rest/client/room_keys.py create mode 100644 synapse/rest/client/room_upgrade_rest_servlet.py create mode 100644 synapse/rest/client/sendtodevice.py create mode 100644 synapse/rest/client/shared_rooms.py create mode 100644 synapse/rest/client/sync.py create mode 100644 synapse/rest/client/tags.py create mode 100644 synapse/rest/client/thirdparty.py create mode 100644 synapse/rest/client/tokenrefresh.py create mode 100644 synapse/rest/client/user_directory.py delete mode 100644 synapse/rest/client/v1/__init__.py delete mode 100644 synapse/rest/client/v1/directory.py delete mode 100644 synapse/rest/client/v1/events.py delete mode 100644 synapse/rest/client/v1/initial_sync.py delete mode 100644 synapse/rest/client/v1/login.py delete mode 100644 synapse/rest/client/v1/logout.py delete mode 100644 synapse/rest/client/v1/presence.py delete mode 100644 synapse/rest/client/v1/profile.py delete mode 100644 synapse/rest/client/v1/push_rule.py delete mode 100644 synapse/rest/client/v1/pusher.py delete mode 100644 synapse/rest/client/v1/room.py delete mode 100644 synapse/rest/client/v1/voip.py delete mode 100644 synapse/rest/client/v2_alpha/__init__.py delete mode 100644 synapse/rest/client/v2_alpha/_base.py delete mode 100644 synapse/rest/client/v2_alpha/account.py delete mode 100644 synapse/rest/client/v2_alpha/account_data.py delete mode 100644 synapse/rest/client/v2_alpha/account_validity.py delete mode 100644 synapse/rest/client/v2_alpha/auth.py delete mode 100644 synapse/rest/client/v2_alpha/capabilities.py delete mode 100644 synapse/rest/client/v2_alpha/devices.py delete mode 100644 synapse/rest/client/v2_alpha/filter.py delete mode 100644 synapse/rest/client/v2_alpha/groups.py delete mode 100644 synapse/rest/client/v2_alpha/keys.py delete mode 100644 synapse/rest/client/v2_alpha/knock.py delete mode 100644 synapse/rest/client/v2_alpha/notifications.py delete mode 100644 synapse/rest/client/v2_alpha/openid.py delete mode 100644 synapse/rest/client/v2_alpha/password_policy.py delete mode 100644 synapse/rest/client/v2_alpha/read_marker.py delete mode 100644 synapse/rest/client/v2_alpha/receipts.py delete mode 100644 synapse/rest/client/v2_alpha/register.py delete mode 100644 synapse/rest/client/v2_alpha/relations.py delete mode 100644 synapse/rest/client/v2_alpha/report_event.py delete mode 100644 synapse/rest/client/v2_alpha/room.py delete mode 100644 synapse/rest/client/v2_alpha/room_keys.py delete mode 100644 synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py delete mode 100644 synapse/rest/client/v2_alpha/sendtodevice.py delete mode 100644 synapse/rest/client/v2_alpha/shared_rooms.py delete mode 100644 synapse/rest/client/v2_alpha/sync.py delete mode 100644 synapse/rest/client/v2_alpha/tags.py delete mode 100644 synapse/rest/client/v2_alpha/thirdparty.py delete mode 100644 synapse/rest/client/v2_alpha/tokenrefresh.py delete mode 100644 synapse/rest/client/v2_alpha/user_directory.py create mode 100644 synapse/rest/client/voip.py (limited to 'synapse/rest/client') diff --git a/synapse/rest/client/__init__.py b/synapse/rest/client/__init__.py index 629e2df74a..f9830cc51f 100644 --- a/synapse/rest/client/__init__.py +++ b/synapse/rest/client/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2014-2016 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/client/_base.py b/synapse/rest/client/_base.py new file mode 100644 index 0000000000..0443f4571c --- /dev/null +++ b/synapse/rest/client/_base.py @@ -0,0 +1,100 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains base REST classes for constructing client v1 servlets. +""" +import logging +import re +from typing import Iterable, Pattern + +from synapse.api.errors import InteractiveAuthIncompleteError +from synapse.api.urls import CLIENT_API_PREFIX +from synapse.types import JsonDict + +logger = logging.getLogger(__name__) + + +def client_patterns( + path_regex: str, + releases: Iterable[int] = (0,), + unstable: bool = True, + v1: bool = False, +) -> Iterable[Pattern]: + """Creates a regex compiled client path with the correct client path + prefix. + + Args: + path_regex: The regex string to match. This should NOT have a ^ + as this will be prefixed. + releases: An iterable of releases to include this endpoint under. + unstable: If true, include this endpoint under the "unstable" prefix. + v1: If true, include this endpoint under the "api/v1" prefix. + Returns: + An iterable of patterns. + """ + patterns = [] + + if unstable: + unstable_prefix = CLIENT_API_PREFIX + "/unstable" + patterns.append(re.compile("^" + unstable_prefix + path_regex)) + if v1: + v1_prefix = CLIENT_API_PREFIX + "/api/v1" + patterns.append(re.compile("^" + v1_prefix + path_regex)) + for release in releases: + new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,) + patterns.append(re.compile("^" + new_prefix + path_regex)) + + return patterns + + +def set_timeline_upper_limit(filter_json: JsonDict, filter_timeline_limit: int) -> None: + """ + Enforces a maximum limit of a timeline query. + + Params: + filter_json: The timeline query to modify. + filter_timeline_limit: The maximum limit to allow, passing -1 will + disable enforcing a maximum limit. + """ + if filter_timeline_limit < 0: + return # no upper limits + timeline = filter_json.get("room", {}).get("timeline", {}) + if "limit" in timeline: + filter_json["room"]["timeline"]["limit"] = min( + filter_json["room"]["timeline"]["limit"], filter_timeline_limit + ) + + +def interactive_auth_handler(orig): + """Wraps an on_POST method to handle InteractiveAuthIncompleteErrors + + Takes a on_POST method which returns an Awaitable (errcode, body) response + and adds exception handling to turn a InteractiveAuthIncompleteError into + a 401 response. + + Normal usage is: + + @interactive_auth_handler + async def on_POST(self, request): + # ... + await self.auth_handler.check_auth + """ + + async def wrapped(*args, **kwargs): + try: + return await orig(*args, **kwargs) + except InteractiveAuthIncompleteError as e: + return 401, e.result + + return wrapped diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py new file mode 100644 index 0000000000..fb5ad2906e --- /dev/null +++ b/synapse/rest/client/account.py @@ -0,0 +1,910 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import random +from http import HTTPStatus +from typing import TYPE_CHECKING +from urllib.parse import urlparse + +from synapse.api.constants import LoginType +from synapse.api.errors import ( + Codes, + InteractiveAuthIncompleteError, + SynapseError, + ThreepidValidationError, +) +from synapse.config.emailconfig import ThreepidBehaviour +from synapse.handlers.ui_auth import UIAuthSessionDataConstants +from synapse.http.server import finish_request, respond_with_html +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, + parse_string, +) +from synapse.metrics import threepid_send_requests +from synapse.push.mailer import Mailer +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 + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +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_datastore() + self.config = hs.config + self.identity_handler = hs.get_identity_handler() + + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self.mailer = Mailer( + hs=self.hs, + app_name=self.config.email_app_name, + template_html=self.config.email_password_reset_template_html, + template_text=self.config.email_password_reset_template_text, + ) + + async def on_POST(self, request): + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + 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_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) + + # Canonicalise the email address. The addresses are all stored canonicalised + # in the database. This allows the user to reset his password without having to + # know the exact spelling (eg. upper and lower case) of address in the database. + # Stored in the database "foo@bar.com" + # User requests with "FOO@bar.com" would raise a Not Found error + 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 next_link: + # Raise if the provided next_link value isn't valid + assert_valid_next_link(self.hs, next_link) + + await self.identity_handler.ratelimit_request_token_requests( + request, "email", 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_datastore().get_user_id_by_threepid( + "email", email + ) + + if existing_user_id is None: + if self.config.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) + + if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + assert self.hs.config.account_threepid_delegate_email + + # Have the configured identity server handle the request + ret = await self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate_email, + email, + client_secret, + send_attempt, + next_link, + ) + else: + # Send password reset emails from Synapse + sid = await self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_password_reset_mail, + next_link, + ) + + # Wrap the session id in a JSON object + ret = {"sid": sid} + + threepid_send_requests.labels(type="email", reason="password_reset").observe( + send_attempt + ) + + return 200, ret + + +class PasswordRestServlet(RestServlet): + PATTERNS = client_patterns("/account/password$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + self.datastore = self.hs.get_datastore() + self.password_policy_handler = hs.get_password_policy_handler() + self._set_password_handler = hs.get_set_password_handler() + + @interactive_auth_handler + async def on_POST(self, request): + body = parse_json_object_from_request(request) + + # we do basic sanity checks here because the auth layer will store these + # in sessions. Pull out the new password provided to us. + new_password = body.pop("new_password", None) + if new_password is not None: + if not isinstance(new_password, str) or len(new_password) > 512: + raise SynapseError(400, "Invalid password") + self.password_policy_handler.validate_password(new_password) + + # there are two possibilities here. Either the user does not have an + # access token, and needs to do a password reset; or they have one and + # need to validate their identity. + # + # In the first case, we offer a couple of means of identifying + # themselves (email and msisdn, though it's unclear if msisdn actually + # works). + # + # In the second case, we require a password to confirm their identity. + + if self.auth.has_access_token(request): + requester = await self.auth.get_user_by_req(request) + try: + params, session_id = await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "modify your account password", + ) + except InteractiveAuthIncompleteError as e: + # The user needs to provide more steps to complete auth, but + # they're not required to provide the password again. + # + # If a password is available now, hash the provided password and + # store it for later. + if new_password: + password_hash = await self.auth_handler.hash(new_password) + await self.auth_handler.set_session_data( + e.session_id, + UIAuthSessionDataConstants.PASSWORD_HASH, + password_hash, + ) + raise + user_id = requester.user.to_string() + else: + requester = None + try: + result, params, session_id = await self.auth_handler.check_ui_auth( + [[LoginType.EMAIL_IDENTITY]], + request, + body, + "modify your account password", + ) + except InteractiveAuthIncompleteError as e: + # The user needs to provide more steps to complete auth, but + # they're not required to provide the password again. + # + # If a password is available now, hash the provided password and + # store it for later. + if new_password: + password_hash = await self.auth_handler.hash(new_password) + await self.auth_handler.set_session_data( + e.session_id, + UIAuthSessionDataConstants.PASSWORD_HASH, + password_hash, + ) + raise + + 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) + + # If we have a password in this request, prefer it. Otherwise, use the + # password hash from an earlier request. + if new_password: + password_hash = await self.auth_handler.hash(new_password) + elif session_id is not None: + password_hash = await self.auth_handler.get_session_data( + session_id, UIAuthSessionDataConstants.PASSWORD_HASH, None + ) + else: + # UI validation was skipped, but the request did not include a new + # password. + password_hash = None + if not password_hash: + raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM) + + logout_devices = params.get("logout_devices", True) + + await self._set_password_handler.set_password( + user_id, password_hash, logout_devices, requester + ) + + return 200, {} + + +class DeactivateAccountRestServlet(RestServlet): + PATTERNS = client_patterns("/account/deactivate$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + self._deactivate_account_handler = hs.get_deactivate_account_handler() + + @interactive_auth_handler + async def on_POST(self, request): + body = parse_json_object_from_request(request) + erase = body.get("erase", False) + if not isinstance(erase, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'erase' must be a boolean, if given", + Codes.BAD_JSON, + ) + + requester = await self.auth.get_user_by_req(request) + + # allow ASes to deactivate their own users + if requester.app_service: + await self._deactivate_account_handler.deactivate_account( + requester.user.to_string(), erase, requester + ) + return 200, {} + + await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "deactivate your account", + ) + result = await self._deactivate_account_handler.deactivate_account( + requester.user.to_string(), + erase, + requester, + id_server=body.get("id_server"), + ) + if result: + id_server_unbind_result = "success" + else: + id_server_unbind_result = "no-support" + + return 200, {"id_server_unbind_result": id_server_unbind_result} + + +class EmailThreepidRequestTokenRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/email/requestToken$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.config = hs.config + self.identity_handler = hs.get_identity_handler() + self.store = self.hs.get_datastore() + + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self.mailer = Mailer( + hs=self.hs, + app_name=self.config.email_app_name, + template_html=self.config.email_add_threepid_template_html, + template_text=self.config.email_add_threepid_template_text, + ) + + async def on_POST(self, request): + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + 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_json_object_from_request(request) + assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + # Canonicalise the email address. The addresses are all stored canonicalised + # in the database. + # This ensures that the validation email is sent to the canonicalised address + # as it will later be entered into the database. + # Otherwise the email will be sent to "FOO@bar.com" and stored as + # "foo@bar.com" in database. + 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 check_3pid_allowed(self.hs, "email", 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", email + ) + + if next_link: + # Raise if the provided next_link value isn't valid + assert_valid_next_link(self.hs, next_link) + + existing_user_id = await self.store.get_user_id_by_threepid("email", email) + + if existing_user_id is not None: + if self.config.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) + + if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + assert self.hs.config.account_threepid_delegate_email + + # Have the configured identity server handle the request + ret = await self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate_email, + email, + client_secret, + send_attempt, + next_link, + ) + else: + # Send threepid validation emails from Synapse + sid = await self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_add_threepid_mail, + next_link, + ) + + # Wrap the session id in a JSON object + ret = {"sid": sid} + + threepid_send_requests.labels(type="email", reason="add_threepid").observe( + send_attempt + ) + + return 200, ret + + +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_datastore() + self.identity_handler = hs.get_identity_handler() + + async def on_POST(self, request): + 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 check_3pid_allowed(self.hs, "msisdn", msisdn): + raise SynapseError( + 403, + "Account phone numbers are not authorized on this server", + Codes.THREEPID_DENIED, + ) + + await self.identity_handler.ratelimit_request_token_requests( + request, "msisdn", msisdn + ) + + if next_link: + # Raise if the provided next_link value isn't valid + assert_valid_next_link(self.hs, 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.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, "MSISDN is already in use", Codes.THREEPID_IN_USE) + + if not self.hs.config.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.account_threepid_delegate_msisdn, + country, + phone_number, + client_secret, + send_attempt, + next_link, + ) + + threepid_send_requests.labels(type="msisdn", reason="add_threepid").observe( + send_attempt + ) + + 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): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.config = hs.config + self.clock = hs.get_clock() + self.store = hs.get_datastore() + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self._failure_email_template = ( + self.config.email_add_threepid_template_failure_html + ) + + async def on_GET(self, request): + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + 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" + ) + elif self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + raise SynapseError( + 400, + "This homeserver is not validating threepids. Use an identity server " + "instead.", + ) + + 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_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 + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.config = hs.config + self.clock = hs.get_clock() + self.store = hs.get_datastore() + self.identity_handler = hs.get_identity_handler() + + async def on_POST(self, request): + if not self.config.account_threepid_delegate_msisdn: + raise SynapseError( + 400, + "This homeserver is not validating phone numbers. Use an identity server " + "instead.", + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["client_secret", "sid", "token"]) + assert_valid_client_secret(body["client_secret"]) + + # Proxy submit_token request to msisdn threepid delegate + response = await self.identity_handler.proxy_msisdn_submit_token( + self.config.account_threepid_delegate_msisdn, + body["client_secret"], + body["sid"], + body["token"], + ) + return 200, response + + +class ThreepidRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid$") + + def __init__(self, hs): + 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_datastore() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + + threepids = await self.datastore.user_get_threepids(requester.user.to_string()) + + return 200, {"threepids": threepids} + + async def on_POST(self, request): + if not self.hs.config.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): + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + + @interactive_auth_handler + async def on_POST(self, request): + if not self.hs.config.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) + + assert_params_in_dict(body, ["client_secret", "sid"]) + sid = body["sid"] + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "add a third-party identifier to your account", + ) + + 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 ThreepidBindRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/bind$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.auth = hs.get_auth() + + async def on_POST(self, request): + body = parse_json_object_from_request(request) + + assert_params_in_dict(body, ["id_server", "sid", "client_secret"]) + id_server = body["id_server"] + sid = body["sid"] + id_access_token = body.get("id_access_token") # optional + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + await self.identity_handler.bind_threepid( + client_secret, sid, user_id, id_server, id_access_token + ) + + return 200, {} + + +class ThreepidUnbindRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/unbind$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.auth = hs.get_auth() + self.datastore = self.hs.get_datastore() + + async def on_POST(self, request): + """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_json_object_from_request(request) + assert_params_in_dict(body, ["medium", "address"]) + + medium = body.get("medium") + address = body.get("address") + id_server = body.get("id_server") + + # 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(), + {"address": address, "medium": medium, "id_server": 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): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + + async def on_POST(self, request): + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["medium", "address"]) + + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + try: + ret = await self.auth_handler.delete_threepid( + user_id, body["medium"], body["address"], body.get("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" + + return 200, {"id_server_unbind_result": id_server_unbind_result} + + +def assert_valid_next_link(hs: "HomeServer", next_link: str): + """ + Raises a SynapseError if a given next_link value is invalid + + next_link is valid if the scheme is http(s) and the next_link.domain_whitelist config + option is either empty or contains a domain that matches the one in the given next_link + + Args: + hs: The homeserver object + next_link: The next_link value given by the client + + Raises: + SynapseError: If the next_link is invalid + """ + valid = True + + # Parse the contents of the URL + next_link_parsed = urlparse(next_link) + + # Scheme must not point to the local drive + if next_link_parsed.scheme == "file": + valid = False + + # If the domain whitelist is set, the domain must be in it + if ( + valid + and hs.config.next_link_domain_whitelist is not None + and next_link_parsed.hostname not in hs.config.next_link_domain_whitelist + ): + valid = False + + if not valid: + raise SynapseError( + 400, + "'next_link' domain not included in whitelist, or not http(s)", + errcode=Codes.INVALID_PARAM, + ) + + +class WhoamiRestServlet(RestServlet): + PATTERNS = client_patterns("/account/whoami$") + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + + response = {"user_id": requester.user.to_string()} + + # Appservices and similar accounts do not have device IDs + # that we can report on, so exclude them for compliance. + if requester.device_id is not None: + response["device_id"] = requester.device_id + + return 200, response + + +def register_servlets(hs, http_server): + EmailPasswordRequestTokenRestServlet(hs).register(http_server) + PasswordRestServlet(hs).register(http_server) + DeactivateAccountRestServlet(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) + ThreepidAddRestServlet(hs).register(http_server) + ThreepidBindRestServlet(hs).register(http_server) + ThreepidUnbindRestServlet(hs).register(http_server) + ThreepidDeleteRestServlet(hs).register(http_server) + WhoamiRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/account_data.py b/synapse/rest/client/account_data.py new file mode 100644 index 0000000000..7517e9304e --- /dev/null +++ b/synapse/rest/client/account_data.py @@ -0,0 +1,122 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import AuthError, NotFoundError, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class AccountDataServlet(RestServlet): + """ + PUT /user/{user_id}/account_data/{account_dataType} HTTP/1.1 + GET /user/{user_id}/account_data/{account_dataType} HTTP/1.1 + """ + + PATTERNS = client_patterns( + "/user/(?P[^/]*)/account_data/(?P[^/]*)" + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.handler = hs.get_account_data_handler() + + async def on_PUT(self, request, user_id, account_data_type): + requester = await self.auth.get_user_by_req(request) + if user_id != requester.user.to_string(): + raise AuthError(403, "Cannot add account data for other users.") + + body = parse_json_object_from_request(request) + + await self.handler.add_account_data_for_user(user_id, account_data_type, body) + + return 200, {} + + async def on_GET(self, request, user_id, account_data_type): + requester = await self.auth.get_user_by_req(request) + if user_id != requester.user.to_string(): + raise AuthError(403, "Cannot get account data for other users.") + + event = await self.store.get_global_account_data_by_type_for_user( + account_data_type, user_id + ) + + if event is None: + raise NotFoundError("Account data not found") + + return 200, event + + +class RoomAccountDataServlet(RestServlet): + """ + PUT /user/{user_id}/rooms/{room_id}/account_data/{account_dataType} HTTP/1.1 + GET /user/{user_id}/rooms/{room_id}/account_data/{account_dataType} HTTP/1.1 + """ + + PATTERNS = client_patterns( + "/user/(?P[^/]*)" + "/rooms/(?P[^/]*)" + "/account_data/(?P[^/]*)" + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.handler = hs.get_account_data_handler() + + async def on_PUT(self, request, user_id, room_id, account_data_type): + requester = await self.auth.get_user_by_req(request) + if user_id != requester.user.to_string(): + raise AuthError(403, "Cannot add account data for other users.") + + body = parse_json_object_from_request(request) + + if account_data_type == "m.fully_read": + raise SynapseError( + 405, + "Cannot set m.fully_read through this API." + " Use /rooms/!roomId:server.name/read_markers", + ) + + await self.handler.add_account_data_to_room( + user_id, room_id, account_data_type, body + ) + + return 200, {} + + async def on_GET(self, request, user_id, room_id, account_data_type): + requester = await self.auth.get_user_by_req(request) + if user_id != requester.user.to_string(): + raise AuthError(403, "Cannot get account data for other users.") + + event = await self.store.get_account_data_for_room_and_type( + user_id, room_id, account_data_type + ) + + if event is None: + raise NotFoundError("Room account data not found") + + return 200, event + + +def register_servlets(hs, http_server): + AccountDataServlet(hs).register(http_server) + RoomAccountDataServlet(hs).register(http_server) diff --git a/synapse/rest/client/account_validity.py b/synapse/rest/client/account_validity.py new file mode 100644 index 0000000000..3ebe401861 --- /dev/null +++ b/synapse/rest/client/account_validity.py @@ -0,0 +1,104 @@ +# Copyright 2019 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import SynapseError +from synapse.http.server import respond_with_html +from synapse.http.servlet import RestServlet + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class AccountValidityRenewServlet(RestServlet): + PATTERNS = client_patterns("/account_validity/renew$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + + self.hs = hs + self.account_activity_handler = hs.get_account_validity_handler() + self.auth = hs.get_auth() + 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.invalid_token_template = ( + hs.config.account_validity.account_validity_invalid_token_template + ) + + async def on_GET(self, request): + if b"token" not in request.args: + raise SynapseError(400, "Missing renewal token") + renewal_token = request.args[b"token"][0] + + ( + token_valid, + token_stale, + expiration_ts, + ) = await self.account_activity_handler.renew_account( + renewal_token.decode("utf8") + ) + + if token_valid: + status_code = 200 + response = self.account_renewed_template.render(expiration_ts=expiration_ts) + elif token_stale: + status_code = 200 + response = self.account_previously_renewed_template.render( + expiration_ts=expiration_ts + ) + else: + status_code = 404 + response = self.invalid_token_template.render(expiration_ts=expiration_ts) + + respond_with_html(request, status_code, response) + + +class AccountValiditySendMailServlet(RestServlet): + PATTERNS = client_patterns("/account_validity/send_mail$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + + self.hs = hs + self.account_activity_handler = hs.get_account_validity_handler() + self.auth = hs.get_auth() + self.account_validity_renew_by_email_enabled = ( + hs.config.account_validity.account_validity_renew_by_email_enabled + ) + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request, allow_expired=True) + user_id = requester.user.to_string() + await self.account_activity_handler.send_renewal_email_to_user(user_id) + + return 200, {} + + +def register_servlets(hs, http_server): + AccountValidityRenewServlet(hs).register(http_server) + AccountValiditySendMailServlet(hs).register(http_server) diff --git a/synapse/rest/client/auth.py b/synapse/rest/client/auth.py new file mode 100644 index 0000000000..6ea1b50a62 --- /dev/null +++ b/synapse/rest/client/auth.py @@ -0,0 +1,143 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import TYPE_CHECKING + +from synapse.api.constants import LoginType +from synapse.api.errors import SynapseError +from synapse.api.urls import CLIENT_API_PREFIX +from synapse.http.server import respond_with_html +from synapse.http.servlet import RestServlet, parse_string + +from ._base import client_patterns + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class AuthRestServlet(RestServlet): + """ + Handles Client / Server API authentication in any situations where it + cannot be handled in the normal flow (with requests to the same endpoint). + Current use is for web fallback auth. + """ + + PATTERNS = client_patterns(r"/auth/(?P[\w\.]*)/fallback/web") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + self.registration_handler = hs.get_registration_handler() + self.recaptcha_template = hs.config.recaptcha_template + self.terms_template = hs.config.terms_template + self.success_template = hs.config.fallback_success_template + + async def on_GET(self, request, stagetype): + session = parse_string(request, "session") + if not session: + raise SynapseError(400, "No session supplied") + + if stagetype == LoginType.RECAPTCHA: + html = self.recaptcha_template.render( + session=session, + myurl="%s/r0/auth/%s/fallback/web" + % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), + sitekey=self.hs.config.recaptcha_public_key, + ) + elif stagetype == LoginType.TERMS: + html = self.terms_template.render( + session=session, + terms_url="%s_matrix/consent?v=%s" + % (self.hs.config.public_baseurl, self.hs.config.user_consent_version), + myurl="%s/r0/auth/%s/fallback/web" + % (CLIENT_API_PREFIX, LoginType.TERMS), + ) + + elif stagetype == LoginType.SSO: + # Display a confirmation page which prompts the user to + # re-authenticate with their SSO provider. + html = await self.auth_handler.start_sso_ui_auth(request, session) + + else: + raise SynapseError(404, "Unknown auth stage type") + + # Render the HTML and return. + respond_with_html(request, 200, html) + return None + + async def on_POST(self, request, stagetype): + + session = parse_string(request, "session") + if not session: + raise SynapseError(400, "No session supplied") + + if stagetype == LoginType.RECAPTCHA: + response = parse_string(request, "g-recaptcha-response") + + if not response: + raise SynapseError(400, "No captcha response supplied") + + authdict = {"response": response, "session": session} + + success = await self.auth_handler.add_oob_auth( + LoginType.RECAPTCHA, authdict, request.getClientIP() + ) + + if success: + html = self.success_template.render() + else: + html = self.recaptcha_template.render( + session=session, + myurl="%s/r0/auth/%s/fallback/web" + % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), + sitekey=self.hs.config.recaptcha_public_key, + ) + elif stagetype == LoginType.TERMS: + authdict = {"session": session} + + success = await self.auth_handler.add_oob_auth( + LoginType.TERMS, authdict, request.getClientIP() + ) + + if success: + html = self.success_template.render() + else: + html = self.terms_template.render( + session=session, + terms_url="%s_matrix/consent?v=%s" + % ( + self.hs.config.public_baseurl, + self.hs.config.user_consent_version, + ), + myurl="%s/r0/auth/%s/fallback/web" + % (CLIENT_API_PREFIX, LoginType.TERMS), + ) + elif stagetype == LoginType.SSO: + # The SSO fallback workflow should not post here, + raise SynapseError(404, "Fallback SSO auth does not support POST requests.") + else: + raise SynapseError(404, "Unknown auth stage type") + + # Render the HTML and return. + respond_with_html(request, 200, html) + return None + + +def register_servlets(hs, http_server): + AuthRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/capabilities.py b/synapse/rest/client/capabilities.py new file mode 100644 index 0000000000..88e3aac797 --- /dev/null +++ b/synapse/rest/client/capabilities.py @@ -0,0 +1,68 @@ +# Copyright 2019 New Vector +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from typing import TYPE_CHECKING, Tuple + +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, MSC3244_CAPABILITIES +from synapse.http.servlet import RestServlet +from synapse.http.site import SynapseRequest +from synapse.types import JsonDict + +from ._base import client_patterns + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class CapabilitiesRestServlet(RestServlet): + """End point to expose the capabilities of the server.""" + + PATTERNS = client_patterns("/capabilities$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.hs = hs + self.config = hs.config + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + + async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + await self.auth.get_user_by_req(request, allow_guest=True) + change_password = self.auth_handler.can_change_password() + + response = { + "capabilities": { + "m.room_versions": { + "default": self.config.default_room_version.identifier, + "available": { + v.identifier: v.disposition + for v in KNOWN_ROOM_VERSIONS.values() + }, + }, + "m.change_password": {"enabled": change_password}, + } + } + + if self.config.experimental.msc3244_enabled: + response["capabilities"]["m.room_versions"][ + "org.matrix.msc3244.room_capabilities" + ] = MSC3244_CAPABILITIES + + return 200, response + + +def register_servlets(hs: "HomeServer", http_server): + CapabilitiesRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py new file mode 100644 index 0000000000..8b9674db06 --- /dev/null +++ b/synapse/rest/client/devices.py @@ -0,0 +1,300 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api import errors +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, +) +from synapse.http.site import SynapseRequest + +from ._base import client_patterns, interactive_auth_handler + +logger = logging.getLogger(__name__) + + +class DevicesRestServlet(RestServlet): + PATTERNS = client_patterns("/devices$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.device_handler = hs.get_device_handler() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + devices = await self.device_handler.get_devices_by_user( + requester.user.to_string() + ) + return 200, {"devices": devices} + + +class DeleteDevicesRestServlet(RestServlet): + """ + API for bulk deletion of devices. Accepts a JSON object with a devices + key which lists the device_ids to delete. Requires user interactive auth. + """ + + PATTERNS = client_patterns("/delete_devices") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.device_handler = hs.get_device_handler() + self.auth_handler = hs.get_auth_handler() + + @interactive_auth_handler + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) + + try: + body = parse_json_object_from_request(request) + except errors.SynapseError as e: + if e.errcode == errors.Codes.NOT_JSON: + # DELETE + # deal with older clients which didn't pass a JSON dict + # the same as those that pass an empty dict + body = {} + else: + raise e + + assert_params_in_dict(body, ["devices"]) + + await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "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"] + ) + return 200, {} + + +class DeviceRestServlet(RestServlet): + PATTERNS = client_patterns("/devices/(?P[^/]*)$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.device_handler = hs.get_device_handler() + self.auth_handler = hs.get_auth_handler() + + async def on_GET(self, request, device_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + device = await self.device_handler.get_device( + requester.user.to_string(), device_id + ) + return 200, device + + @interactive_auth_handler + async def on_DELETE(self, request, device_id): + requester = await self.auth.get_user_by_req(request) + + try: + body = parse_json_object_from_request(request) + + except errors.SynapseError as e: + if e.errcode == errors.Codes.NOT_JSON: + # deal with older clients which didn't pass a JSON dict + # the same as those that pass an empty dict + body = {} + else: + raise + + await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "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_device(requester.user.to_string(), device_id) + return 200, {} + + async def on_PUT(self, request, device_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + body = parse_json_object_from_request(request) + await self.device_handler.update_device( + requester.user.to_string(), device_id, body + ) + return 200, {} + + +class DehydratedDeviceServlet(RestServlet): + """Retrieve or store a dehydrated device. + + GET /org.matrix.msc2697.v2/dehydrated_device + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "device_id": "dehydrated_device_id", + "device_data": { + "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", + "account": "dehydrated_device" + } + } + + PUT /org.matrix.msc2697/dehydrated_device + Content-Type: application/json + + { + "device_data": { + "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", + "account": "dehydrated_device" + } + } + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "device_id": "dehydrated_device_id" + } + + """ + + PATTERNS = client_patterns("/org.matrix.msc2697.v2/dehydrated_device", releases=()) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.device_handler = hs.get_device_handler() + + async def on_GET(self, request: SynapseRequest): + requester = await self.auth.get_user_by_req(request) + dehydrated_device = await self.device_handler.get_dehydrated_device( + requester.user.to_string() + ) + if dehydrated_device is not None: + (device_id, device_data) = dehydrated_device + result = {"device_id": device_id, "device_data": device_data} + return (200, result) + else: + raise errors.NotFoundError("No dehydrated device available") + + async def on_PUT(self, request: SynapseRequest): + submission = parse_json_object_from_request(request) + requester = await self.auth.get_user_by_req(request) + + if "device_data" not in submission: + raise errors.SynapseError( + 400, + "device_data missing", + errcode=errors.Codes.MISSING_PARAM, + ) + elif not isinstance(submission["device_data"], dict): + raise errors.SynapseError( + 400, + "device_data must be an object", + errcode=errors.Codes.INVALID_PARAM, + ) + + device_id = await self.device_handler.store_dehydrated_device( + requester.user.to_string(), + submission["device_data"], + submission.get("initial_device_display_name", None), + ) + return 200, {"device_id": device_id} + + +class ClaimDehydratedDeviceServlet(RestServlet): + """Claim a dehydrated device. + + POST /org.matrix.msc2697.v2/dehydrated_device/claim + Content-Type: application/json + + { + "device_id": "dehydrated_device_id" + } + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "success": true, + } + + """ + + PATTERNS = client_patterns( + "/org.matrix.msc2697.v2/dehydrated_device/claim", releases=() + ) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.device_handler = hs.get_device_handler() + + async def on_POST(self, request: SynapseRequest): + requester = await self.auth.get_user_by_req(request) + + submission = parse_json_object_from_request(request) + + if "device_id" not in submission: + raise errors.SynapseError( + 400, + "device_id missing", + errcode=errors.Codes.MISSING_PARAM, + ) + elif not isinstance(submission["device_id"], str): + raise errors.SynapseError( + 400, + "device_id must be a string", + errcode=errors.Codes.INVALID_PARAM, + ) + + result = await self.device_handler.rehydrate_device( + requester.user.to_string(), + self.auth.get_access_token_from_request(request), + submission["device_id"], + ) + + return (200, result) + + +def register_servlets(hs, http_server): + DeleteDevicesRestServlet(hs).register(http_server) + DevicesRestServlet(hs).register(http_server) + DeviceRestServlet(hs).register(http_server) + DehydratedDeviceServlet(hs).register(http_server) + ClaimDehydratedDeviceServlet(hs).register(http_server) diff --git a/synapse/rest/client/directory.py b/synapse/rest/client/directory.py new file mode 100644 index 0000000000..ffa075c8e5 --- /dev/null +++ b/synapse/rest/client/directory.py @@ -0,0 +1,185 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging + +from synapse.api.errors import ( + AuthError, + Codes, + InvalidClientCredentialsError, + NotFoundError, + SynapseError, +) +from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.rest.client._base import client_patterns +from synapse.types import RoomAlias + +logger = logging.getLogger(__name__) + + +def register_servlets(hs, http_server): + ClientDirectoryServer(hs).register(http_server) + ClientDirectoryListServer(hs).register(http_server) + ClientAppserviceDirectoryListServer(hs).register(http_server) + + +class ClientDirectoryServer(RestServlet): + PATTERNS = client_patterns("/directory/room/(?P[^/]*)$", v1=True) + + def __init__(self, hs): + super().__init__() + self.store = hs.get_datastore() + self.directory_handler = hs.get_directory_handler() + self.auth = hs.get_auth() + + async def on_GET(self, request, room_alias): + room_alias = RoomAlias.from_string(room_alias) + + res = await self.directory_handler.get_association(room_alias) + + return 200, res + + async def on_PUT(self, request, room_alias): + room_alias = RoomAlias.from_string(room_alias) + + content = parse_json_object_from_request(request) + if "room_id" not in content: + raise SynapseError( + 400, 'Missing params: ["room_id"]', errcode=Codes.BAD_JSON + ) + + logger.debug("Got content: %s", content) + logger.debug("Got room name: %s", room_alias.to_string()) + + room_id = content["room_id"] + servers = content["servers"] if "servers" in content else None + + logger.debug("Got room_id: %s", room_id) + logger.debug("Got servers: %s", servers) + + # TODO(erikj): Check types. + + room = await self.store.get_room(room_id) + if room is None: + raise SynapseError(400, "Room does not exist") + + requester = await self.auth.get_user_by_req(request) + + await self.directory_handler.create_association( + requester, room_alias, room_id, servers + ) + + return 200, {} + + async def on_DELETE(self, request, room_alias): + try: + service = self.auth.get_appservice_by_req(request) + room_alias = RoomAlias.from_string(room_alias) + await self.directory_handler.delete_appservice_association( + service, room_alias + ) + logger.info( + "Application service at %s deleted alias %s", + service.url, + room_alias.to_string(), + ) + return 200, {} + except InvalidClientCredentialsError: + # fallback to default user behaviour if they aren't an AS + pass + + requester = await self.auth.get_user_by_req(request) + user = requester.user + + room_alias = RoomAlias.from_string(room_alias) + + await self.directory_handler.delete_association(requester, room_alias) + + logger.info( + "User %s deleted alias %s", user.to_string(), room_alias.to_string() + ) + + return 200, {} + + +class ClientDirectoryListServer(RestServlet): + PATTERNS = client_patterns("/directory/list/room/(?P[^/]*)$", v1=True) + + def __init__(self, hs): + super().__init__() + self.store = hs.get_datastore() + self.directory_handler = hs.get_directory_handler() + self.auth = hs.get_auth() + + async def on_GET(self, request, room_id): + room = await self.store.get_room(room_id) + if room is None: + raise NotFoundError("Unknown room") + + return 200, {"visibility": "public" if room["is_public"] else "private"} + + async def on_PUT(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + + content = parse_json_object_from_request(request) + visibility = content.get("visibility", "public") + + await self.directory_handler.edit_published_room_list( + requester, room_id, visibility + ) + + return 200, {} + + async def on_DELETE(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + + await self.directory_handler.edit_published_room_list( + requester, room_id, "private" + ) + + return 200, {} + + +class ClientAppserviceDirectoryListServer(RestServlet): + PATTERNS = client_patterns( + "/directory/list/appservice/(?P[^/]*)/(?P[^/]*)$", v1=True + ) + + def __init__(self, hs): + super().__init__() + self.store = hs.get_datastore() + self.directory_handler = hs.get_directory_handler() + self.auth = hs.get_auth() + + def on_PUT(self, request, network_id, room_id): + content = parse_json_object_from_request(request) + visibility = content.get("visibility", "public") + return self._edit(request, network_id, room_id, visibility) + + def on_DELETE(self, request, network_id, room_id): + return self._edit(request, network_id, room_id, "private") + + async def _edit(self, request, network_id, room_id, visibility): + requester = await self.auth.get_user_by_req(request) + if not requester.app_service: + raise AuthError( + 403, "Only appservices can edit the appservice published room list" + ) + + await self.directory_handler.edit_published_appservice_room_list( + requester.app_service.id, network_id, room_id, visibility + ) + + return 200, {} diff --git a/synapse/rest/client/events.py b/synapse/rest/client/events.py new file mode 100644 index 0000000000..52bb579cfd --- /dev/null +++ b/synapse/rest/client/events.py @@ -0,0 +1,94 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains REST servlets to do with event streaming, /events.""" +import logging + +from synapse.api.errors import SynapseError +from synapse.http.servlet import RestServlet +from synapse.rest.client._base import client_patterns +from synapse.streams.config import PaginationConfig + +logger = logging.getLogger(__name__) + + +class EventStreamRestServlet(RestServlet): + PATTERNS = client_patterns("/events$", v1=True) + + DEFAULT_LONGPOLL_TIME_MS = 30000 + + def __init__(self, hs): + super().__init__() + self.event_stream_handler = hs.get_event_stream_handler() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + is_guest = requester.is_guest + room_id = None + if is_guest: + if b"room_id" not in request.args: + raise SynapseError(400, "Guest users must specify room_id param") + if b"room_id" in request.args: + room_id = request.args[b"room_id"][0].decode("ascii") + + pagin_config = await PaginationConfig.from_request(self.store, request) + timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS + if b"timeout" in request.args: + try: + timeout = int(request.args[b"timeout"][0]) + except ValueError: + raise SynapseError(400, "timeout must be in milliseconds.") + + as_client_event = b"raw" not in request.args + + chunk = await self.event_stream_handler.get_stream( + requester.user.to_string(), + pagin_config, + timeout=timeout, + as_client_event=as_client_event, + affect_presence=(not is_guest), + room_id=room_id, + is_guest=is_guest, + ) + + return 200, chunk + + +class EventRestServlet(RestServlet): + PATTERNS = client_patterns("/events/(?P[^/]*)$", v1=True) + + def __init__(self, hs): + super().__init__() + self.clock = hs.get_clock() + self.event_handler = hs.get_event_handler() + self.auth = hs.get_auth() + self._event_serializer = hs.get_event_client_serializer() + + async def on_GET(self, request, event_id): + requester = await self.auth.get_user_by_req(request) + event = await self.event_handler.get_event(requester.user, None, event_id) + + time_now = self.clock.time_msec() + if event: + event = await self._event_serializer.serialize_event(event, time_now) + return 200, event + else: + return 404, "Event not found." + + +def register_servlets(hs, http_server): + EventStreamRestServlet(hs).register(http_server) + EventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/filter.py b/synapse/rest/client/filter.py new file mode 100644 index 0000000000..411667a9c8 --- /dev/null +++ b/synapse/rest/client/filter.py @@ -0,0 +1,94 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import AuthError, NotFoundError, StoreError, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.types import UserID + +from ._base import client_patterns, set_timeline_upper_limit + +logger = logging.getLogger(__name__) + + +class GetFilterRestServlet(RestServlet): + PATTERNS = client_patterns("/user/(?P[^/]*)/filter/(?P[^/]*)") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.filtering = hs.get_filtering() + + async def on_GET(self, request, user_id, filter_id): + target_user = UserID.from_string(user_id) + requester = await self.auth.get_user_by_req(request) + + if target_user != requester.user: + raise AuthError(403, "Cannot get filters for other users") + + if not self.hs.is_mine(target_user): + raise AuthError(403, "Can only get filters for local users") + + try: + filter_id = int(filter_id) + except Exception: + raise SynapseError(400, "Invalid filter_id") + + try: + filter_collection = await self.filtering.get_user_filter( + user_localpart=target_user.localpart, filter_id=filter_id + ) + except StoreError as e: + if e.code != 404: + raise + raise NotFoundError("No such filter") + + return 200, filter_collection.get_filter_json() + + +class CreateFilterRestServlet(RestServlet): + PATTERNS = client_patterns("/user/(?P[^/]*)/filter") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.filtering = hs.get_filtering() + + async def on_POST(self, request, user_id): + + target_user = UserID.from_string(user_id) + requester = await self.auth.get_user_by_req(request) + + if target_user != requester.user: + raise AuthError(403, "Cannot create filters for other users") + + if not self.hs.is_mine(target_user): + raise AuthError(403, "Can only create filters for local users") + + content = parse_json_object_from_request(request) + set_timeline_upper_limit(content, self.hs.config.filter_timeline_limit) + + filter_id = await self.filtering.add_user_filter( + user_localpart=target_user.localpart, user_filter=content + ) + + return 200, {"filter_id": str(filter_id)} + + +def register_servlets(hs, http_server): + GetFilterRestServlet(hs).register(http_server) + CreateFilterRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/groups.py b/synapse/rest/client/groups.py new file mode 100644 index 0000000000..6285680c00 --- /dev/null +++ b/synapse/rest/client/groups.py @@ -0,0 +1,957 @@ +# Copyright 2017 Vector Creations Ltd +# Copyright 2018 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from functools import wraps +from typing import TYPE_CHECKING, Optional, Tuple + +from twisted.web.server import Request + +from synapse.api.constants import ( + MAX_GROUP_CATEGORYID_LENGTH, + MAX_GROUP_ROLEID_LENGTH, + MAX_GROUPID_LENGTH, +) +from synapse.api.errors import Codes, SynapseError +from synapse.handlers.groups_local import GroupsLocalHandler +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, +) +from synapse.http.site import SynapseRequest +from synapse.types import GroupID, JsonDict + +from ._base import client_patterns + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +def _validate_group_id(f): + """Wrapper to validate the form of the group ID. + + Can be applied to any on_FOO methods that accepts a group ID as a URL parameter. + """ + + @wraps(f) + def wrapper(self, request: Request, group_id: str, *args, **kwargs): + if not GroupID.is_valid(group_id): + raise SynapseError(400, "%s is not a legal group ID" % (group_id,)) + + return f(self, request, group_id, *args, **kwargs) + + return wrapper + + +class GroupServlet(RestServlet): + """Get the group profile""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/profile$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + group_description = await self.groups_handler.get_group_profile( + group_id, requester_user_id + ) + + return 200, group_description + + @_validate_group_id + async def on_POST( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + assert_params_in_dict( + content, ("name", "avatar_url", "short_description", "long_description") + ) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot create group profiles." + await self.groups_handler.update_group_profile( + group_id, requester_user_id, content + ) + + return 200, {} + + +class GroupSummaryServlet(RestServlet): + """Get the full group summary""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/summary$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + get_group_summary = await self.groups_handler.get_group_summary( + group_id, requester_user_id + ) + + return 200, get_group_summary + + +class GroupSummaryRoomsCatServlet(RestServlet): + """Update/delete a rooms entry in the summary. + + Matches both: + - /groups/:group/summary/rooms/:room_id + - /groups/:group/summary/categories/:category/rooms/:room_id + """ + + PATTERNS = client_patterns( + "/groups/(?P[^/]*)/summary" + "(/categories/(?P[^/]+))?" + "/rooms/(?P[^/]*)$" + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, + request: SynapseRequest, + group_id: str, + category_id: Optional[str], + room_id: str, + ): + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + if category_id == "": + raise SynapseError(400, "category_id cannot be empty", Codes.INVALID_PARAM) + + if category_id and len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group summaries." + resp = await self.groups_handler.update_group_summary_room( + group_id, + requester_user_id, + room_id=room_id, + category_id=category_id, + content=content, + ) + + return 200, resp + + @_validate_group_id + async def on_DELETE( + self, request: SynapseRequest, group_id: str, category_id: str, room_id: str + ): + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group profiles." + resp = await self.groups_handler.delete_group_summary_room( + group_id, requester_user_id, room_id=room_id, category_id=category_id + ) + + return 200, resp + + +class GroupCategoryServlet(RestServlet): + """Get/add/update/delete a group category""" + + PATTERNS = client_patterns( + "/groups/(?P[^/]*)/categories/(?P[^/]+)$" + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str, category_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + category = await self.groups_handler.get_group_category( + group_id, requester_user_id, category_id=category_id + ) + + return 200, category + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str, category_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + if not category_id: + raise SynapseError(400, "category_id cannot be empty", Codes.INVALID_PARAM) + + if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." + resp = await self.groups_handler.update_group_category( + group_id, requester_user_id, category_id=category_id, content=content + ) + + return 200, resp + + @_validate_group_id + async def on_DELETE( + self, request: SynapseRequest, group_id: str, category_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." + resp = await self.groups_handler.delete_group_category( + group_id, requester_user_id, category_id=category_id + ) + + return 200, resp + + +class GroupCategoriesServlet(RestServlet): + """Get all group categories""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/categories/$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + category = await self.groups_handler.get_group_categories( + group_id, requester_user_id + ) + + return 200, category + + +class GroupRoleServlet(RestServlet): + """Get/add/update/delete a group role""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/roles/(?P[^/]+)$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str, role_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + category = await self.groups_handler.get_group_role( + group_id, requester_user_id, role_id=role_id + ) + + return 200, category + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str, role_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + if not role_id: + raise SynapseError(400, "role_id cannot be empty", Codes.INVALID_PARAM) + + if len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group roles." + resp = await self.groups_handler.update_group_role( + group_id, requester_user_id, role_id=role_id, content=content + ) + + return 200, resp + + @_validate_group_id + async def on_DELETE( + self, request: SynapseRequest, group_id: str, role_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group roles." + resp = await self.groups_handler.delete_group_role( + group_id, requester_user_id, role_id=role_id + ) + + return 200, resp + + +class GroupRolesServlet(RestServlet): + """Get all group roles""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/roles/$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + category = await self.groups_handler.get_group_roles( + group_id, requester_user_id + ) + + return 200, category + + +class GroupSummaryUsersRoleServlet(RestServlet): + """Update/delete a user's entry in the summary. + + Matches both: + - /groups/:group/summary/users/:room_id + - /groups/:group/summary/roles/:role/users/:user_id + """ + + PATTERNS = client_patterns( + "/groups/(?P[^/]*)/summary" + "(/roles/(?P[^/]+))?" + "/users/(?P[^/]*)$" + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, + request: SynapseRequest, + group_id: str, + role_id: Optional[str], + user_id: str, + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + if role_id == "": + raise SynapseError(400, "role_id cannot be empty", Codes.INVALID_PARAM) + + if role_id and len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group summaries." + resp = await self.groups_handler.update_group_summary_user( + group_id, + requester_user_id, + user_id=user_id, + role_id=role_id, + content=content, + ) + + return 200, resp + + @_validate_group_id + async def on_DELETE( + self, request: SynapseRequest, group_id: str, role_id: str, user_id: str + ): + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group summaries." + resp = await self.groups_handler.delete_group_summary_user( + group_id, requester_user_id, user_id=user_id, role_id=role_id + ) + + return 200, resp + + +class GroupRoomServlet(RestServlet): + """Get all rooms in a group""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/rooms$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + result = await self.groups_handler.get_rooms_in_group( + group_id, requester_user_id + ) + + return 200, result + + +class GroupUsersServlet(RestServlet): + """Get all users in a group""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/users$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + result = await self.groups_handler.get_users_in_group( + group_id, requester_user_id + ) + + return 200, result + + +class GroupInvitedUsersServlet(RestServlet): + """Get users invited to a group""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/invited_users$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_GET( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + result = await self.groups_handler.get_invited_users_in_group( + group_id, requester_user_id + ) + + return 200, result + + +class GroupSettingJoinPolicyServlet(RestServlet): + """Set group join policy""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/settings/m.join_policy$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group join policy." + result = await self.groups_handler.set_group_join_policy( + group_id, requester_user_id, content + ) + + return 200, result + + +class GroupCreateServlet(RestServlet): + """Create a group""" + + PATTERNS = client_patterns("/create_group$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + self.server_name = hs.hostname + + async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + # TODO: Create group on remote server + content = parse_json_object_from_request(request) + localpart = content.pop("localpart") + group_id = GroupID(localpart, self.server_name).to_string() + + if not localpart: + raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM) + + if len(group_id) > MAX_GROUPID_LENGTH: + raise SynapseError( + 400, + "Group ID may not be longer than %s characters" % (MAX_GROUPID_LENGTH,), + Codes.INVALID_PARAM, + ) + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot create groups." + result = await self.groups_handler.create_group( + group_id, requester_user_id, content + ) + + return 200, result + + +class GroupAdminRoomsServlet(RestServlet): + """Add a room to the group""" + + PATTERNS = client_patterns( + "/groups/(?P[^/]*)/admin/rooms/(?P[^/]*)$" + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str, room_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify rooms in a group." + result = await self.groups_handler.add_room_to_group( + group_id, requester_user_id, room_id, content + ) + + return 200, result + + @_validate_group_id + async def on_DELETE( + self, request: SynapseRequest, group_id: str, room_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." + result = await self.groups_handler.remove_room_from_group( + group_id, requester_user_id, room_id + ) + + return 200, result + + +class GroupAdminRoomsConfigServlet(RestServlet): + """Update the config of a room in a group""" + + PATTERNS = client_patterns( + "/groups/(?P[^/]*)/admin/rooms/(?P[^/]*)" + "/config/(?P[^/]*)$" + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str, room_id: str, config_key: str + ): + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." + result = await self.groups_handler.update_room_in_group( + group_id, requester_user_id, room_id, config_key, content + ) + + return 200, result + + +class GroupAdminUsersInviteServlet(RestServlet): + """Invite a user to the group""" + + PATTERNS = client_patterns( + "/groups/(?P[^/]*)/admin/users/invite/(?P[^/]*)$" + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + self.store = hs.get_datastore() + self.is_mine_id = hs.is_mine_id + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id, user_id + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + config = content.get("config", {}) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot invite users to a group." + result = await self.groups_handler.invite( + group_id, user_id, requester_user_id, config + ) + + return 200, result + + +class GroupAdminUsersKickServlet(RestServlet): + """Kick a user from the group""" + + PATTERNS = client_patterns( + "/groups/(?P[^/]*)/admin/users/remove/(?P[^/]*)$" + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id, user_id + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot kick users from a group." + result = await self.groups_handler.remove_user_from_group( + group_id, user_id, requester_user_id, content + ) + + return 200, result + + +class GroupSelfLeaveServlet(RestServlet): + """Leave a joined group""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/self/leave$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot leave a group for a users." + result = await self.groups_handler.remove_user_from_group( + group_id, requester_user_id, requester_user_id, content + ) + + return 200, result + + +class GroupSelfJoinServlet(RestServlet): + """Attempt to join a group, or knock""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/self/join$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot join a user to a group." + result = await self.groups_handler.join_group( + group_id, requester_user_id, content + ) + + return 200, result + + +class GroupSelfAcceptInviteServlet(RestServlet): + """Accept a group invite""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/self/accept_invite$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot accept an invite to a group." + result = await self.groups_handler.accept_invite( + group_id, requester_user_id, content + ) + + return 200, result + + +class GroupSelfUpdatePublicityServlet(RestServlet): + """Update whether we publicise a users membership of a group""" + + PATTERNS = client_patterns("/groups/(?P[^/]*)/self/update_publicity$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.store = hs.get_datastore() + + @_validate_group_id + async def on_PUT( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + requester_user_id = requester.user.to_string() + + content = parse_json_object_from_request(request) + publicise = content["publicise"] + await self.store.update_group_publicity(group_id, requester_user_id, publicise) + + return 200, {} + + +class PublicisedGroupsForUserServlet(RestServlet): + """Get the list of groups a user is advertising""" + + PATTERNS = client_patterns("/publicised_groups/(?P[^/]*)$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.store = hs.get_datastore() + self.groups_handler = hs.get_groups_local_handler() + + async def on_GET( + self, request: SynapseRequest, user_id: str + ) -> Tuple[int, JsonDict]: + await self.auth.get_user_by_req(request, allow_guest=True) + + result = await self.groups_handler.get_publicised_groups_for_user(user_id) + + return 200, result + + +class PublicisedGroupsForUsersServlet(RestServlet): + """Get the list of groups a user is advertising""" + + PATTERNS = client_patterns("/publicised_groups$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.store = hs.get_datastore() + self.groups_handler = hs.get_groups_local_handler() + + async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + await self.auth.get_user_by_req(request, allow_guest=True) + + content = parse_json_object_from_request(request) + user_ids = content["user_ids"] + + result = await self.groups_handler.bulk_get_publicised_groups(user_ids) + + return 200, result + + +class GroupsForUserServlet(RestServlet): + """Get all groups the logged in user is joined to""" + + PATTERNS = client_patterns("/joined_groups$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.groups_handler = hs.get_groups_local_handler() + + async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + requester_user_id = requester.user.to_string() + + result = await self.groups_handler.get_joined_groups(requester_user_id) + + return 200, result + + +def register_servlets(hs: "HomeServer", http_server): + GroupServlet(hs).register(http_server) + GroupSummaryServlet(hs).register(http_server) + GroupInvitedUsersServlet(hs).register(http_server) + GroupUsersServlet(hs).register(http_server) + GroupRoomServlet(hs).register(http_server) + GroupSettingJoinPolicyServlet(hs).register(http_server) + GroupCreateServlet(hs).register(http_server) + GroupAdminRoomsServlet(hs).register(http_server) + GroupAdminRoomsConfigServlet(hs).register(http_server) + GroupAdminUsersInviteServlet(hs).register(http_server) + GroupAdminUsersKickServlet(hs).register(http_server) + GroupSelfLeaveServlet(hs).register(http_server) + GroupSelfJoinServlet(hs).register(http_server) + GroupSelfAcceptInviteServlet(hs).register(http_server) + GroupsForUserServlet(hs).register(http_server) + GroupCategoryServlet(hs).register(http_server) + GroupCategoriesServlet(hs).register(http_server) + GroupSummaryRoomsCatServlet(hs).register(http_server) + GroupRoleServlet(hs).register(http_server) + GroupRolesServlet(hs).register(http_server) + GroupSelfUpdatePublicityServlet(hs).register(http_server) + GroupSummaryUsersRoleServlet(hs).register(http_server) + PublicisedGroupsForUserServlet(hs).register(http_server) + PublicisedGroupsForUsersServlet(hs).register(http_server) diff --git a/synapse/rest/client/initial_sync.py b/synapse/rest/client/initial_sync.py new file mode 100644 index 0000000000..12ba0e91db --- /dev/null +++ b/synapse/rest/client/initial_sync.py @@ -0,0 +1,47 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from synapse.http.servlet import RestServlet, parse_boolean +from synapse.rest.client._base import client_patterns +from synapse.streams.config import PaginationConfig + + +# TODO: Needs unit testing +class InitialSyncRestServlet(RestServlet): + PATTERNS = client_patterns("/initialSync$", v1=True) + + def __init__(self, hs): + super().__init__() + self.initial_sync_handler = hs.get_initial_sync_handler() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + as_client_event = b"raw" not in request.args + pagination_config = await PaginationConfig.from_request(self.store, request) + include_archived = parse_boolean(request, "archived", default=False) + content = await self.initial_sync_handler.snapshot_all_rooms( + user_id=requester.user.to_string(), + pagin_config=pagination_config, + as_client_event=as_client_event, + include_archived=include_archived, + ) + + return 200, content + + +def register_servlets(hs, http_server): + InitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/keys.py b/synapse/rest/client/keys.py new file mode 100644 index 0000000000..d0d9d30d40 --- /dev/null +++ b/synapse/rest/client/keys.py @@ -0,0 +1,344 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2019 New Vector Ltd +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import SynapseError +from synapse.http.servlet import ( + RestServlet, + parse_integer, + parse_json_object_from_request, + parse_string, +) +from synapse.logging.opentracing import log_kv, set_tag, trace +from synapse.types import StreamToken + +from ._base import client_patterns, interactive_auth_handler + +logger = logging.getLogger(__name__) + + +class KeyUploadServlet(RestServlet): + """ + POST /keys/upload HTTP/1.1 + Content-Type: application/json + + { + "device_keys": { + "user_id": "", + "device_id": "", + "valid_until_ts": , + "algorithms": [ + "m.olm.curve25519-aes-sha2", + ] + "keys": { + ":": "", + }, + "signatures:" { + "" { + ":": "" + } } }, + "one_time_keys": { + ":": "" + }, + } + """ + + PATTERNS = client_patterns("/keys/upload(/(?P[^/]+))?$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.auth = hs.get_auth() + self.e2e_keys_handler = hs.get_e2e_keys_handler() + self.device_handler = hs.get_device_handler() + + @trace(opname="upload_keys") + async def on_POST(self, request, device_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + if device_id is not None: + # Providing the device_id should only be done for setting keys + # for dehydrated devices; however, we allow it for any device for + # compatibility with older clients. + if requester.device_id is not None and device_id != requester.device_id: + dehydrated_device = await self.device_handler.get_dehydrated_device( + user_id + ) + if dehydrated_device is not None and device_id != dehydrated_device[0]: + set_tag("error", True) + log_kv( + { + "message": "Client uploading keys for a different device", + "logged_in_id": requester.device_id, + "key_being_uploaded": device_id, + } + ) + logger.warning( + "Client uploading keys for a different device " + "(logged in as %s, uploading for %s)", + requester.device_id, + device_id, + ) + else: + device_id = requester.device_id + + if device_id is None: + raise SynapseError( + 400, "To upload keys, you must pass device_id when authenticating" + ) + + result = await self.e2e_keys_handler.upload_keys_for_user( + user_id, device_id, body + ) + return 200, result + + +class KeyQueryServlet(RestServlet): + """ + POST /keys/query HTTP/1.1 + Content-Type: application/json + { + "device_keys": { + "": [""] + } } + + HTTP/1.1 200 OK + { + "device_keys": { + "": { + "": { + "user_id": "", // Duplicated to be signed + "device_id": "", // Duplicated to be signed + "valid_until_ts": , + "algorithms": [ // List of supported algorithms + "m.olm.curve25519-aes-sha2", + ], + "keys": { // Must include a ed25519 signing key + ":": "", + }, + "signatures:" { + // Must be signed with device's ed25519 key + "/": { + ":": "" + } + // Must be signed by this server. + "": { + ":": "" + } } } } } } + """ + + PATTERNS = client_patterns("/keys/query$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): + """ + super().__init__() + self.auth = hs.get_auth() + self.e2e_keys_handler = hs.get_e2e_keys_handler() + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + user_id = requester.user.to_string() + device_id = requester.device_id + timeout = parse_integer(request, "timeout", 10 * 1000) + body = parse_json_object_from_request(request) + result = await self.e2e_keys_handler.query_devices( + body, timeout, user_id, device_id + ) + return 200, result + + +class KeyChangesServlet(RestServlet): + """Returns the list of changes of keys between two stream tokens (may return + spurious extra results, since we currently ignore the `to` param). + + GET /keys/changes?from=...&to=... + + 200 OK + { "changed": ["@foo:example.com"] } + """ + + PATTERNS = client_patterns("/keys/changes$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): + """ + super().__init__() + self.auth = hs.get_auth() + self.device_handler = hs.get_device_handler() + self.store = hs.get_datastore() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + from_token_string = parse_string(request, "from", required=True) + set_tag("from", from_token_string) + + # We want to enforce they do pass us one, but we ignore it and return + # changes after the "to" as well as before. + set_tag("to", parse_string(request, "to")) + + from_token = await StreamToken.from_string(self.store, from_token_string) + + user_id = requester.user.to_string() + + results = await self.device_handler.get_user_ids_changed(user_id, from_token) + + return 200, results + + +class OneTimeKeyServlet(RestServlet): + """ + POST /keys/claim HTTP/1.1 + { + "one_time_keys": { + "": { + "": "" + } } } + + HTTP/1.1 200 OK + { + "one_time_keys": { + "": { + "": { + ":": "" + } } } } + + """ + + PATTERNS = client_patterns("/keys/claim$") + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.e2e_keys_handler = hs.get_e2e_keys_handler() + + async def on_POST(self, request): + await self.auth.get_user_by_req(request, allow_guest=True) + timeout = parse_integer(request, "timeout", 10 * 1000) + body = parse_json_object_from_request(request) + result = await self.e2e_keys_handler.claim_one_time_keys(body, timeout) + return 200, result + + +class SigningKeyUploadServlet(RestServlet): + """ + POST /keys/device_signing/upload HTTP/1.1 + Content-Type: application/json + + { + } + """ + + PATTERNS = client_patterns("/keys/device_signing/upload$", releases=()) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.e2e_keys_handler = hs.get_e2e_keys_handler() + self.auth_handler = hs.get_auth_handler() + + @interactive_auth_handler + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "add a device signing key to your account", + # Allow skipping of UI auth since this is frequently called directly + # after login and it is silly to ask users to re-auth immediately. + can_skip_ui_auth=True, + ) + + result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) + return 200, result + + +class SignaturesUploadServlet(RestServlet): + """ + POST /keys/signatures/upload HTTP/1.1 + Content-Type: application/json + + { + "@alice:example.com": { + "": { + "user_id": "", + "device_id": "", + "algorithms": [ + "m.olm.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "keys": { + ":": "", + }, + "signatures": { + "": { + ":": ">" + } + } + } + } + } + """ + + PATTERNS = client_patterns("/keys/signatures/upload$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.auth = hs.get_auth() + self.e2e_keys_handler = hs.get_e2e_keys_handler() + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + result = await self.e2e_keys_handler.upload_signatures_for_device_keys( + user_id, body + ) + return 200, result + + +def register_servlets(hs, http_server): + KeyUploadServlet(hs).register(http_server) + KeyQueryServlet(hs).register(http_server) + KeyChangesServlet(hs).register(http_server) + OneTimeKeyServlet(hs).register(http_server) + SigningKeyUploadServlet(hs).register(http_server) + SignaturesUploadServlet(hs).register(http_server) diff --git a/synapse/rest/client/knock.py b/synapse/rest/client/knock.py new file mode 100644 index 0000000000..7d1bc40658 --- /dev/null +++ b/synapse/rest/client/knock.py @@ -0,0 +1,107 @@ +# Copyright 2020 Sorunome +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple + +from twisted.web.server import Request + +from synapse.api.constants import Membership +from synapse.api.errors import SynapseError +from synapse.http.servlet import ( + RestServlet, + parse_json_object_from_request, + parse_strings_from_args, +) +from synapse.http.site import SynapseRequest +from synapse.logging.opentracing import set_tag +from synapse.rest.client.transactions import HttpTransactionCache +from synapse.types import JsonDict, RoomAlias, RoomID + +if TYPE_CHECKING: + from synapse.app.homeserver import HomeServer + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class KnockRoomAliasServlet(RestServlet): + """ + POST /knock/{roomIdOrAlias} + """ + + PATTERNS = client_patterns("/knock/(?P[^/]*)") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.txns = HttpTransactionCache(hs) + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + + async def on_POST( + self, + request: SynapseRequest, + room_identifier: str, + txn_id: Optional[str] = None, + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + + content = parse_json_object_from_request(request) + event_content = None + if "reason" in content: + event_content = {"reason": content["reason"]} + + if RoomID.is_valid(room_identifier): + room_id = room_identifier + + # 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 + ) + elif RoomAlias.is_valid(room_identifier): + handler = self.room_member_handler + room_alias = RoomAlias.from_string(room_identifier) + room_id_obj, remote_room_hosts = await handler.lookup_room_alias(room_alias) + room_id = room_id_obj.to_string() + else: + raise SynapseError( + 400, "%s was not legal room ID or room alias" % (room_identifier,) + ) + + await self.room_member_handler.update_membership( + requester=requester, + target=requester.user, + room_id=room_id, + action=Membership.KNOCK, + txn_id=txn_id, + third_party_signed=None, + remote_room_hosts=remote_room_hosts, + content=event_content, + ) + + return 200, {"room_id": room_id} + + def on_PUT(self, request: Request, room_identifier: str, txn_id: str): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_identifier, txn_id + ) + + +def register_servlets(hs, http_server): + KnockRoomAliasServlet(hs).register(http_server) diff --git a/synapse/rest/client/login.py b/synapse/rest/client/login.py new file mode 100644 index 0000000000..0c8d8967b7 --- /dev/null +++ b/synapse/rest/client/login.py @@ -0,0 +1,600 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional + +from typing_extensions import TypedDict + +from synapse.api.errors import Codes, LoginError, SynapseError +from synapse.api.ratelimiting import Ratelimiter +from synapse.api.urls import CLIENT_API_PREFIX +from synapse.appservice import ApplicationService +from synapse.handlers.sso import SsoIdentityProvider +from synapse.http import get_request_uri +from synapse.http.server import HttpServer, finish_request +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_boolean, + parse_bytes_from_args, + parse_json_object_from_request, + parse_string, +) +from synapse.http.site import SynapseRequest +from synapse.rest.client._base import client_patterns +from synapse.rest.well_known import WellKnownBuilder +from synapse.types import JsonDict, UserID + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class LoginResponse(TypedDict, total=False): + user_id: str + access_token: str + home_server: str + expires_in_ms: Optional[int] + refresh_token: Optional[str] + device_id: str + well_known: Optional[Dict[str, Any]] + + +class LoginRestServlet(RestServlet): + PATTERNS = client_patterns("/login$", v1=True) + CAS_TYPE = "m.login.cas" + SSO_TYPE = "m.login.sso" + TOKEN_TYPE = "m.login.token" + JWT_TYPE = "org.matrix.login.jwt" + JWT_TYPE_DEPRECATED = "m.login.jwt" + APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service" + REFRESH_TOKEN_PARAM = "org.matrix.msc2918.refresh_token" + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.hs = hs + + # JWT configuration variables. + self.jwt_enabled = hs.config.jwt_enabled + self.jwt_secret = hs.config.jwt_secret + self.jwt_algorithm = hs.config.jwt_algorithm + self.jwt_issuer = hs.config.jwt_issuer + self.jwt_audiences = hs.config.jwt_audiences + + # SSO configuration. + self.saml2_enabled = hs.config.saml2_enabled + self.cas_enabled = hs.config.cas_enabled + self.oidc_enabled = hs.config.oidc_enabled + self._msc2858_enabled = hs.config.experimental.msc2858_enabled + self._msc2918_enabled = hs.config.access_token_lifetime is not None + + self.auth = hs.get_auth() + + self.clock = hs.get_clock() + + self.auth_handler = self.hs.get_auth_handler() + self.registration_handler = hs.get_registration_handler() + self._sso_handler = hs.get_sso_handler() + + self._well_known_builder = WellKnownBuilder(hs) + self._address_ratelimiter = Ratelimiter( + store=hs.get_datastore(), + clock=hs.get_clock(), + rate_hz=self.hs.config.rc_login_address.per_second, + burst_count=self.hs.config.rc_login_address.burst_count, + ) + self._account_ratelimiter = Ratelimiter( + store=hs.get_datastore(), + clock=hs.get_clock(), + rate_hz=self.hs.config.rc_login_account.per_second, + burst_count=self.hs.config.rc_login_account.burst_count, + ) + + def on_GET(self, request: SynapseRequest): + flows = [] + if self.jwt_enabled: + flows.append({"type": LoginRestServlet.JWT_TYPE}) + flows.append({"type": LoginRestServlet.JWT_TYPE_DEPRECATED}) + + if self.cas_enabled: + # we advertise CAS for backwards compat, though MSC1721 renamed it + # to SSO. + flows.append({"type": LoginRestServlet.CAS_TYPE}) + + if self.cas_enabled or self.saml2_enabled or self.oidc_enabled: + sso_flow: JsonDict = { + "type": LoginRestServlet.SSO_TYPE, + "identity_providers": [ + _get_auth_flow_dict_for_idp( + idp, + ) + for idp in self._sso_handler.get_identity_providers().values() + ], + } + + if self._msc2858_enabled: + # backwards-compatibility support for clients which don't + # support the stable API yet + sso_flow["org.matrix.msc2858.identity_providers"] = [ + _get_auth_flow_dict_for_idp(idp, use_unstable_brands=True) + for idp in self._sso_handler.get_identity_providers().values() + ] + + flows.append(sso_flow) + + # While it's valid for us to advertise this login type generally, + # synapse currently only gives out these tokens as part of the + # SSO login flow. + # Generally we don't want to advertise login flows that clients + # don't know how to implement, since they (currently) will always + # fall back to the fallback API if they don't understand one of the + # login flow types returned. + flows.append({"type": LoginRestServlet.TOKEN_TYPE}) + + flows.extend({"type": t} for t in self.auth_handler.get_supported_login_types()) + + flows.append({"type": LoginRestServlet.APPSERVICE_TYPE}) + + return 200, {"flows": flows} + + async def on_POST(self, request: SynapseRequest): + login_submission = parse_json_object_from_request(request) + + if self._msc2918_enabled: + # Check if this login should also issue a refresh token, as per + # MSC2918 + should_issue_refresh_token = parse_boolean( + request, name=LoginRestServlet.REFRESH_TOKEN_PARAM, default=False + ) + else: + should_issue_refresh_token = False + + try: + if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE: + appservice = self.auth.get_appservice_by_req(request) + + if appservice.is_rate_limited(): + await self._address_ratelimiter.ratelimit( + None, request.getClientIP() + ) + + result = await self._do_appservice_login( + login_submission, + appservice, + should_issue_refresh_token=should_issue_refresh_token, + ) + elif self.jwt_enabled and ( + login_submission["type"] == LoginRestServlet.JWT_TYPE + or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED + ): + await self._address_ratelimiter.ratelimit(None, request.getClientIP()) + result = await self._do_jwt_login( + login_submission, + should_issue_refresh_token=should_issue_refresh_token, + ) + elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE: + await self._address_ratelimiter.ratelimit(None, request.getClientIP()) + result = await self._do_token_login( + login_submission, + should_issue_refresh_token=should_issue_refresh_token, + ) + else: + await self._address_ratelimiter.ratelimit(None, request.getClientIP()) + result = await self._do_other_login( + login_submission, + should_issue_refresh_token=should_issue_refresh_token, + ) + except KeyError: + raise SynapseError(400, "Missing JSON keys.") + + well_known_data = self._well_known_builder.get_well_known() + if well_known_data: + result["well_known"] = well_known_data + return 200, result + + async def _do_appservice_login( + self, + login_submission: JsonDict, + appservice: ApplicationService, + should_issue_refresh_token: bool = False, + ): + identifier = login_submission.get("identifier") + logger.info("Got appservice login request with identifier: %r", identifier) + + if not isinstance(identifier, dict): + raise SynapseError( + 400, "Invalid identifier in login submission", Codes.INVALID_PARAM + ) + + # this login flow only supports identifiers of type "m.id.user". + if identifier.get("type") != "m.id.user": + raise SynapseError( + 400, "Unknown login identifier type", Codes.INVALID_PARAM + ) + + user = identifier.get("user") + if not isinstance(user, str): + raise SynapseError(400, "Invalid user in identifier", Codes.INVALID_PARAM) + + if user.startswith("@"): + qualified_user_id = user + else: + qualified_user_id = UserID(user, self.hs.hostname).to_string() + + if not appservice.is_interested_in_user(qualified_user_id): + raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN) + + return await self._complete_login( + qualified_user_id, + login_submission, + ratelimit=appservice.is_rate_limited(), + should_issue_refresh_token=should_issue_refresh_token, + ) + + async def _do_other_login( + self, login_submission: JsonDict, should_issue_refresh_token: bool = False + ) -> LoginResponse: + """Handle non-token/saml/jwt logins + + Args: + login_submission: + should_issue_refresh_token: True if this login should issue + a refresh token alongside the access token. + + Returns: + HTTP response + """ + # Log the request we got, but only certain fields to minimise the chance of + # logging someone's password (even if they accidentally put it in the wrong + # field) + logger.info( + "Got login request with identifier: %r, medium: %r, address: %r, user: %r", + login_submission.get("identifier"), + login_submission.get("medium"), + login_submission.get("address"), + login_submission.get("user"), + ) + canonical_user_id, callback = await self.auth_handler.validate_login( + login_submission, ratelimit=True + ) + result = await self._complete_login( + canonical_user_id, + login_submission, + callback, + should_issue_refresh_token=should_issue_refresh_token, + ) + return result + + async def _complete_login( + self, + user_id: str, + login_submission: JsonDict, + callback: Optional[Callable[[LoginResponse], Awaitable[None]]] = None, + create_non_existent_users: bool = False, + ratelimit: bool = True, + auth_provider_id: Optional[str] = None, + should_issue_refresh_token: bool = False, + ) -> LoginResponse: + """Called when we've successfully authed the user and now need to + actually login them in (e.g. create devices). This gets called on + all successful logins. + + Applies the ratelimiting for successful login attempts against an + account. + + Args: + user_id: ID of the user to register. + login_submission: Dictionary of login information. + callback: Callback function to run after login. + create_non_existent_users: Whether to create the user if they don't + exist. Defaults to False. + ratelimit: Whether to ratelimit the login request. + auth_provider_id: The SSO IdP the user used, if any (just used for the + prometheus metrics). + should_issue_refresh_token: True if this login should issue + a refresh token alongside the access token. + + Returns: + result: Dictionary of account information after successful login. + """ + + # Before we actually log them in we check if they've already logged in + # too often. This happens here rather than before as we don't + # necessarily know the user before now. + if ratelimit: + await self._account_ratelimiter.ratelimit(None, user_id.lower()) + + if create_non_existent_users: + 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 + ) + user_id = canonical_uid + + device_id = login_submission.get("device_id") + initial_display_name = login_submission.get("initial_device_display_name") + ( + device_id, + access_token, + valid_until_ms, + refresh_token, + ) = await self.registration_handler.register_device( + user_id, + device_id, + initial_display_name, + auth_provider_id=auth_provider_id, + should_issue_refresh_token=should_issue_refresh_token, + ) + + result = LoginResponse( + user_id=user_id, + access_token=access_token, + home_server=self.hs.hostname, + device_id=device_id, + ) + + if valid_until_ms is not None: + expires_in_ms = valid_until_ms - self.clock.time_msec() + result["expires_in_ms"] = expires_in_ms + + if refresh_token is not None: + result["refresh_token"] = refresh_token + + if callback is not None: + await callback(result) + + return result + + async def _do_token_login( + self, login_submission: JsonDict, should_issue_refresh_token: bool = False + ) -> LoginResponse: + """ + Handle the final stage of SSO login. + + Args: + login_submission: The JSON request body. + should_issue_refresh_token: True if this login should issue + a refresh token alongside the access token. + + Returns: + The body of the JSON response. + """ + token = login_submission["token"] + auth_handler = self.auth_handler + res = await auth_handler.validate_short_term_login_token(token) + + return await self._complete_login( + res.user_id, + login_submission, + self.auth_handler._sso_login_callback, + auth_provider_id=res.auth_provider_id, + should_issue_refresh_token=should_issue_refresh_token, + ) + + async def _do_jwt_login( + self, login_submission: JsonDict, should_issue_refresh_token: bool = False + ) -> LoginResponse: + token = login_submission.get("token", None) + if token is None: + raise LoginError( + 403, "Token field for JWT is missing", errcode=Codes.FORBIDDEN + ) + + import jwt + + try: + payload = jwt.decode( + token, + self.jwt_secret, + algorithms=[self.jwt_algorithm], + issuer=self.jwt_issuer, + audience=self.jwt_audiences, + ) + except jwt.PyJWTError as e: + # A JWT error occurred, return some info back to the client. + raise LoginError( + 403, + "JWT validation failed: %s" % (str(e),), + errcode=Codes.FORBIDDEN, + ) + + user = payload.get("sub", None) + if user is None: + raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN) + + user_id = UserID(user, self.hs.hostname).to_string() + result = await self._complete_login( + user_id, + login_submission, + create_non_existent_users=True, + should_issue_refresh_token=should_issue_refresh_token, + ) + return result + + +def _get_auth_flow_dict_for_idp( + idp: SsoIdentityProvider, use_unstable_brands: bool = False +) -> JsonDict: + """Return an entry for the login flow dict + + Returns an entry suitable for inclusion in "identity_providers" in the + response to GET /_matrix/client/r0/login + + Args: + idp: the identity provider to describe + use_unstable_brands: whether we should use brand identifiers suitable + for the unstable API + """ + e: JsonDict = {"id": idp.idp_id, "name": idp.idp_name} + if idp.idp_icon: + e["icon"] = idp.idp_icon + if idp.idp_brand: + e["brand"] = idp.idp_brand + # use the stable brand identifier if the unstable identifier isn't defined. + if use_unstable_brands and idp.unstable_idp_brand: + e["brand"] = idp.unstable_idp_brand + return e + + +class RefreshTokenServlet(RestServlet): + PATTERNS = client_patterns( + "/org.matrix.msc2918.refresh_token/refresh$", releases=(), unstable=True + ) + + def __init__(self, hs: "HomeServer"): + self._auth_handler = hs.get_auth_handler() + self._clock = hs.get_clock() + self.access_token_lifetime = hs.config.access_token_lifetime + + async def on_POST( + self, + request: SynapseRequest, + ): + refresh_submission = parse_json_object_from_request(request) + + assert_params_in_dict(refresh_submission, ["refresh_token"]) + token = refresh_submission["refresh_token"] + if not isinstance(token, str): + raise SynapseError(400, "Invalid param: refresh_token", Codes.INVALID_PARAM) + + valid_until_ms = self._clock.time_msec() + self.access_token_lifetime + access_token, refresh_token = await self._auth_handler.refresh_token( + token, valid_until_ms + ) + expires_in_ms = valid_until_ms - self._clock.time_msec() + return ( + 200, + { + "access_token": access_token, + "refresh_token": refresh_token, + "expires_in_ms": expires_in_ms, + }, + ) + + +class SsoRedirectServlet(RestServlet): + PATTERNS = list(client_patterns("/login/(cas|sso)/redirect$", v1=True)) + [ + re.compile( + "^" + + CLIENT_API_PREFIX + + "/r0/login/sso/redirect/(?P[A-Za-z0-9_.~-]+)$" + ) + ] + + def __init__(self, hs: "HomeServer"): + # make sure that the relevant handlers are instantiated, so that they + # register themselves with the main SSOHandler. + if hs.config.cas_enabled: + hs.get_cas_handler() + if hs.config.saml2_enabled: + hs.get_saml_handler() + if hs.config.oidc_enabled: + hs.get_oidc_handler() + self._sso_handler = hs.get_sso_handler() + self._msc2858_enabled = hs.config.experimental.msc2858_enabled + self._public_baseurl = hs.config.public_baseurl + + def register(self, http_server: HttpServer) -> None: + super().register(http_server) + if self._msc2858_enabled: + # expose additional endpoint for MSC2858 support: backwards-compat support + # for clients which don't yet support the stable endpoints. + http_server.register_paths( + "GET", + client_patterns( + "/org.matrix.msc2858/login/sso/redirect/(?P[A-Za-z0-9_.~-]+)$", + releases=(), + unstable=True, + ), + self.on_GET, + self.__class__.__name__, + ) + + async def on_GET( + self, request: SynapseRequest, idp_id: Optional[str] = None + ) -> None: + if not self._public_baseurl: + raise SynapseError(400, "SSO requires a valid public_baseurl") + + # if this isn't the expected hostname, redirect to the right one, so that we + # get our cookies back. + requested_uri = get_request_uri(request) + baseurl_bytes = self._public_baseurl.encode("utf-8") + if not requested_uri.startswith(baseurl_bytes): + # swap out the incorrect base URL for the right one. + # + # The idea here is to redirect from + # https://foo.bar/whatever/_matrix/... + # to + # https://public.baseurl/_matrix/... + # + i = requested_uri.index(b"/_matrix") + new_uri = baseurl_bytes[:-1] + requested_uri[i:] + logger.info( + "Requested URI %s is not canonical: redirecting to %s", + requested_uri.decode("utf-8", errors="replace"), + new_uri.decode("utf-8", errors="replace"), + ) + request.redirect(new_uri) + finish_request(request) + return + + args: Dict[bytes, List[bytes]] = request.args # type: ignore + client_redirect_url = parse_bytes_from_args(args, "redirectUrl", required=True) + sso_url = await self._sso_handler.handle_redirect_request( + request, + client_redirect_url, + idp_id, + ) + logger.info("Redirecting to %s", sso_url) + request.redirect(sso_url) + finish_request(request) + + +class CasTicketServlet(RestServlet): + PATTERNS = client_patterns("/login/cas/ticket", v1=True) + + def __init__(self, hs): + 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, http_server): + LoginRestServlet(hs).register(http_server) + if hs.config.access_token_lifetime is not None: + RefreshTokenServlet(hs).register(http_server) + SsoRedirectServlet(hs).register(http_server) + if hs.config.cas_enabled: + CasTicketServlet(hs).register(http_server) diff --git a/synapse/rest/client/logout.py b/synapse/rest/client/logout.py new file mode 100644 index 0000000000..6055cac2bd --- /dev/null +++ b/synapse/rest/client/logout.py @@ -0,0 +1,72 @@ +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.http.servlet import RestServlet +from synapse.rest.client._base import client_patterns + +logger = logging.getLogger(__name__) + + +class LogoutRestServlet(RestServlet): + PATTERNS = client_patterns("/logout$", v1=True) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self._auth_handler = hs.get_auth_handler() + self._device_handler = hs.get_device_handler() + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request, allow_expired=True) + + if requester.device_id is None: + # The access token wasn't associated with a device. + # Just delete the access token + access_token = self.auth.get_access_token_from_request(request) + await self._auth_handler.delete_access_token(access_token) + else: + await self._device_handler.delete_device( + requester.user.to_string(), requester.device_id + ) + + return 200, {} + + +class LogoutAllRestServlet(RestServlet): + PATTERNS = client_patterns("/logout/all$", v1=True) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self._auth_handler = hs.get_auth_handler() + self._device_handler = hs.get_device_handler() + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request, allow_expired=True) + user_id = requester.user.to_string() + + # first delete all of the user's devices + await self._device_handler.delete_all_devices_for_user(user_id) + + # .. and then delete any access tokens which weren't associated with + # devices. + await self._auth_handler.delete_access_tokens_for_user(user_id) + return 200, {} + + +def register_servlets(hs, http_server): + LogoutRestServlet(hs).register(http_server) + LogoutAllRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/notifications.py b/synapse/rest/client/notifications.py new file mode 100644 index 0000000000..0ede643c2d --- /dev/null +++ b/synapse/rest/client/notifications.py @@ -0,0 +1,91 @@ +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.events.utils import format_event_for_client_v2_without_room_id +from synapse.http.servlet import RestServlet, parse_integer, parse_string + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class NotificationsServlet(RestServlet): + PATTERNS = client_patterns("/notifications$") + + def __init__(self, hs): + super().__init__() + self.store = hs.get_datastore() + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self._event_serializer = hs.get_event_client_serializer() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + from_token = parse_string(request, "from", required=False) + limit = parse_integer(request, "limit", default=50) + only = parse_string(request, "only", required=False) + + limit = min(limit, 500) + + push_actions = await self.store.get_push_actions_for_user( + user_id, from_token, limit, only_highlight=(only == "highlight") + ) + + receipts_by_room = await self.store.get_receipts_for_user_with_orderings( + user_id, "m.read" + ) + + notif_event_ids = [pa["event_id"] for pa in push_actions] + notif_events = await self.store.get_events(notif_event_ids) + + returned_push_actions = [] + + next_token = None + + for pa in push_actions: + returned_pa = { + "room_id": pa["room_id"], + "profile_tag": pa["profile_tag"], + "actions": pa["actions"], + "ts": pa["received_ts"], + "event": ( + await self._event_serializer.serialize_event( + notif_events[pa["event_id"]], + self.clock.time_msec(), + event_format=format_event_for_client_v2_without_room_id, + ) + ), + } + + if pa["room_id"] not in receipts_by_room: + returned_pa["read"] = False + else: + receipt = receipts_by_room[pa["room_id"]] + + returned_pa["read"] = ( + receipt["topological_ordering"], + receipt["stream_ordering"], + ) >= (pa["topological_ordering"], pa["stream_ordering"]) + returned_push_actions.append(returned_pa) + next_token = str(pa["stream_ordering"]) + + return 200, {"notifications": returned_push_actions, "next_token": next_token} + + +def register_servlets(hs, http_server): + NotificationsServlet(hs).register(http_server) diff --git a/synapse/rest/client/openid.py b/synapse/rest/client/openid.py new file mode 100644 index 0000000000..e8d2673819 --- /dev/null +++ b/synapse/rest/client/openid.py @@ -0,0 +1,94 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging + +from synapse.api.errors import AuthError +from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.util.stringutils import random_string + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class IdTokenServlet(RestServlet): + """ + Get a bearer token that may be passed to a third party to confirm ownership + of a matrix user id. + + The format of the response could be made compatible with the format given + in http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse + + But instead of returning a signed "id_token" the response contains the + name of the issuing matrix homeserver. This means that for now the third + party will need to check the validity of the "id_token" against the + federation /openid/userinfo endpoint of the homeserver. + + Request: + + POST /user/{user_id}/openid/request_token?access_token=... HTTP/1.1 + + {} + + Response: + + HTTP/1.1 200 OK + { + "access_token": "ABDEFGH", + "token_type": "Bearer", + "matrix_server_name": "example.com", + "expires_in": 3600, + } + """ + + PATTERNS = client_patterns("/user/(?P[^/]*)/openid/request_token") + + EXPIRES_MS = 3600 * 1000 + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.clock = hs.get_clock() + self.server_name = hs.config.server_name + + async def on_POST(self, request, user_id): + requester = await self.auth.get_user_by_req(request) + if user_id != requester.user.to_string(): + raise AuthError(403, "Cannot request tokens for other users.") + + # Parse the request body to make sure it's JSON, but ignore the contents + # for now. + parse_json_object_from_request(request) + + token = random_string(24) + ts_valid_until_ms = self.clock.time_msec() + self.EXPIRES_MS + + await self.store.insert_open_id_token(token, ts_valid_until_ms, user_id) + + return ( + 200, + { + "access_token": token, + "token_type": "Bearer", + "matrix_server_name": self.server_name, + "expires_in": self.EXPIRES_MS // 1000, + }, + ) + + +def register_servlets(hs, http_server): + IdTokenServlet(hs).register(http_server) diff --git a/synapse/rest/client/password_policy.py b/synapse/rest/client/password_policy.py new file mode 100644 index 0000000000..a83927aee6 --- /dev/null +++ b/synapse/rest/client/password_policy.py @@ -0,0 +1,57 @@ +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.http.servlet import RestServlet + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class PasswordPolicyServlet(RestServlet): + PATTERNS = client_patterns("/password_policy$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + + self.policy = hs.config.password_policy + self.enabled = hs.config.password_policy_enabled + + def on_GET(self, request): + if not self.enabled or not self.policy: + return (200, {}) + + policy = {} + + for param in [ + "minimum_length", + "require_digit", + "require_symbol", + "require_lowercase", + "require_uppercase", + ]: + if param in self.policy: + policy["m.%s" % param] = self.policy[param] + + return (200, policy) + + +def register_servlets(hs, http_server): + PasswordPolicyServlet(hs).register(http_server) diff --git a/synapse/rest/client/presence.py b/synapse/rest/client/presence.py new file mode 100644 index 0000000000..6c27e5faf9 --- /dev/null +++ b/synapse/rest/client/presence.py @@ -0,0 +1,95 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with presence: /presence/ +""" +import logging + +from synapse.api.errors import AuthError, SynapseError +from synapse.handlers.presence import format_user_presence_state +from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.rest.client._base import client_patterns +from synapse.types import UserID + +logger = logging.getLogger(__name__) + + +class PresenceStatusRestServlet(RestServlet): + PATTERNS = client_patterns("/presence/(?P[^/]*)/status", v1=True) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.presence_handler = hs.get_presence_handler() + self.clock = hs.get_clock() + self.auth = hs.get_auth() + + self._use_presence = hs.config.server.use_presence + + async def on_GET(self, request, user_id): + requester = await self.auth.get_user_by_req(request) + user = UserID.from_string(user_id) + + if not self._use_presence: + return 200, {"presence": "offline"} + + if requester.user != user: + allowed = await self.presence_handler.is_visible( + observed_user=user, observer_user=requester.user + ) + + if not allowed: + raise AuthError(403, "You are not allowed to see their presence.") + + state = await self.presence_handler.get_state(target_user=user) + state = format_user_presence_state( + state, self.clock.time_msec(), include_user_id=False + ) + + return 200, state + + async def on_PUT(self, request, user_id): + requester = await self.auth.get_user_by_req(request) + user = UserID.from_string(user_id) + + if requester.user != user: + raise AuthError(403, "Can only set your own presence state") + + state = {} + + content = parse_json_object_from_request(request) + + try: + state["presence"] = content.pop("presence") + + if "status_msg" in content: + state["status_msg"] = content.pop("status_msg") + if not isinstance(state["status_msg"], str): + raise SynapseError(400, "status_msg must be a string.") + + if content: + raise KeyError() + except SynapseError as e: + raise e + except Exception: + raise SynapseError(400, "Unable to parse state") + + if self._use_presence: + await self.presence_handler.set_state(user, state) + + return 200, {} + + +def register_servlets(hs, http_server): + PresenceStatusRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/profile.py b/synapse/rest/client/profile.py new file mode 100644 index 0000000000..5463ed2c4f --- /dev/null +++ b/synapse/rest/client/profile.py @@ -0,0 +1,155 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with profile: /profile/ """ + +from synapse.api.errors import Codes, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.rest.client._base import client_patterns +from synapse.types import UserID + + +class ProfileDisplaynameRestServlet(RestServlet): + PATTERNS = client_patterns("/profile/(?P[^/]*)/displayname", v1=True) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.profile_handler = hs.get_profile_handler() + self.auth = hs.get_auth() + + async def on_GET(self, request, user_id): + requester_user = None + + if self.hs.config.require_auth_for_profile_requests: + requester = await self.auth.get_user_by_req(request) + requester_user = requester.user + + 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) + + ret = {} + if displayname is not None: + ret["displayname"] = displayname + + return 200, ret + + async def on_PUT(self, request, user_id): + 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.user) + + content = parse_json_object_from_request(request) + + try: + new_name = content["displayname"] + except Exception: + raise SynapseError( + code=400, + msg="Unable to parse name", + errcode=Codes.BAD_JSON, + ) + + await self.profile_handler.set_displayname(user, requester, new_name, is_admin) + + return 200, {} + + +class ProfileAvatarURLRestServlet(RestServlet): + PATTERNS = client_patterns("/profile/(?P[^/]*)/avatar_url", v1=True) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.profile_handler = hs.get_profile_handler() + self.auth = hs.get_auth() + + async def on_GET(self, request, user_id): + requester_user = None + + if self.hs.config.require_auth_for_profile_requests: + requester = await self.auth.get_user_by_req(request) + requester_user = requester.user + + user = UserID.from_string(user_id) + + await self.profile_handler.check_profile_query_allowed(user, requester_user) + + avatar_url = await self.profile_handler.get_avatar_url(user) + + ret = {} + if avatar_url is not None: + ret["avatar_url"] = avatar_url + + return 200, ret + + async def on_PUT(self, request, user_id): + requester = await self.auth.get_user_by_req(request) + user = UserID.from_string(user_id) + is_admin = await self.auth.is_server_admin(requester.user) + + content = parse_json_object_from_request(request) + try: + new_avatar_url = content["avatar_url"] + except KeyError: + raise SynapseError( + 400, "Missing key 'avatar_url'", errcode=Codes.MISSING_PARAM + ) + + await self.profile_handler.set_avatar_url( + user, requester, new_avatar_url, is_admin + ) + + return 200, {} + + +class ProfileRestServlet(RestServlet): + PATTERNS = client_patterns("/profile/(?P[^/]*)", v1=True) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.profile_handler = hs.get_profile_handler() + self.auth = hs.get_auth() + + async def on_GET(self, request, user_id): + requester_user = None + + if self.hs.config.require_auth_for_profile_requests: + requester = await self.auth.get_user_by_req(request) + requester_user = requester.user + + 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 + + return 200, ret + + +def register_servlets(hs, http_server): + ProfileDisplaynameRestServlet(hs).register(http_server) + ProfileAvatarURLRestServlet(hs).register(http_server) + ProfileRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/push_rule.py b/synapse/rest/client/push_rule.py new file mode 100644 index 0000000000..702b351d18 --- /dev/null +++ b/synapse/rest/client/push_rule.py @@ -0,0 +1,354 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.api.errors import ( + NotFoundError, + StoreError, + SynapseError, + UnrecognizedRequestError, +) +from synapse.http.servlet import ( + RestServlet, + parse_json_value_from_request, + parse_string, +) +from synapse.push.baserules import BASE_RULE_IDS, NEW_RULE_IDS +from synapse.push.clientformat import format_push_rules_for_user +from synapse.push.rulekinds import PRIORITY_CLASS_MAP +from synapse.rest.client._base import client_patterns +from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException + + +class PushRuleRestServlet(RestServlet): + PATTERNS = client_patterns("/(?Ppushrules/.*)$", v1=True) + SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = ( + "Unrecognised request: You probably wanted a trailing slash" + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.notifier = hs.get_notifier() + self._is_worker = hs.config.worker_app is not None + + self._users_new_default_push_rules = hs.config.users_new_default_push_rules + + async def on_PUT(self, request, path): + if self._is_worker: + raise Exception("Cannot handle PUT /push_rules on worker") + + spec = _rule_spec_from_path(path.split("/")) + try: + priority_class = _priority_class_from_spec(spec) + except InvalidRuleException as e: + raise SynapseError(400, str(e)) + + requester = await self.auth.get_user_by_req(request) + + if "/" in spec["rule_id"] or "\\" in spec["rule_id"]: + raise SynapseError(400, "rule_id may not contain slashes") + + content = parse_json_value_from_request(request) + + user_id = requester.user.to_string() + + if "attr" in spec: + await self.set_rule_attr(user_id, spec, content) + self.notify_user(user_id) + return 200, {} + + if spec["rule_id"].startswith("."): + # Rule ids starting with '.' are reserved for server default rules. + raise SynapseError(400, "cannot add new rule_ids that start with '.'") + + try: + (conditions, actions) = _rule_tuple_from_request_object( + spec["template"], spec["rule_id"], content + ) + except InvalidRuleException as e: + raise SynapseError(400, str(e)) + + before = parse_string(request, "before") + if before: + before = _namespaced_rule_id(spec, before) + + after = parse_string(request, "after") + if after: + after = _namespaced_rule_id(spec, after) + + try: + await self.store.add_push_rule( + user_id=user_id, + rule_id=_namespaced_rule_id_from_spec(spec), + priority_class=priority_class, + conditions=conditions, + actions=actions, + before=before, + after=after, + ) + self.notify_user(user_id) + except InconsistentRuleException as e: + raise SynapseError(400, str(e)) + except RuleNotFoundException as e: + raise SynapseError(400, str(e)) + + return 200, {} + + async def on_DELETE(self, request, path): + if self._is_worker: + raise Exception("Cannot handle DELETE /push_rules on worker") + + spec = _rule_spec_from_path(path.split("/")) + + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + namespaced_rule_id = _namespaced_rule_id_from_spec(spec) + + try: + await self.store.delete_push_rule(user_id, namespaced_rule_id) + self.notify_user(user_id) + return 200, {} + except StoreError as e: + if e.code == 404: + raise NotFoundError() + else: + raise + + async def on_GET(self, request, path): + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + # we build up the full structure and then decide which bits of it + # to send which means doing unnecessary work sometimes but is + # is probably not going to make a whole lot of difference + rules = await self.store.get_push_rules_for_user(user_id) + + rules = format_push_rules_for_user(requester.user, rules) + + path = path.split("/")[1:] + + if path == []: + # we're a reference impl: pedantry is our job. + raise UnrecognizedRequestError( + PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR + ) + + if path[0] == "": + return 200, rules + elif path[0] == "global": + result = _filter_ruleset_with_path(rules["global"], path[1:]) + return 200, result + else: + raise UnrecognizedRequestError() + + def notify_user(self, user_id): + stream_id = self.store.get_max_push_rules_stream_id() + self.notifier.on_new_event("push_rules_key", stream_id, users=[user_id]) + + async def set_rule_attr(self, user_id, spec, val): + if spec["attr"] not in ("enabled", "actions"): + # for the sake of potential future expansion, shouldn't report + # 404 in the case of an unknown request so check it corresponds to + # a known attribute first. + raise UnrecognizedRequestError() + + namespaced_rule_id = _namespaced_rule_id_from_spec(spec) + rule_id = spec["rule_id"] + is_default_rule = rule_id.startswith(".") + if is_default_rule: + if namespaced_rule_id not in BASE_RULE_IDS: + raise NotFoundError("Unknown rule %s" % (namespaced_rule_id,)) + if spec["attr"] == "enabled": + if isinstance(val, dict) and "enabled" in val: + val = val["enabled"] + if not isinstance(val, bool): + # Legacy fallback + # This should *actually* take a dict, but many clients pass + # bools directly, so let's not break them. + raise SynapseError(400, "Value for 'enabled' must be boolean") + return await self.store.set_push_rule_enabled( + user_id, namespaced_rule_id, val, is_default_rule + ) + elif spec["attr"] == "actions": + actions = val.get("actions") + _check_actions(actions) + namespaced_rule_id = _namespaced_rule_id_from_spec(spec) + rule_id = spec["rule_id"] + is_default_rule = rule_id.startswith(".") + if is_default_rule: + if user_id in self._users_new_default_push_rules: + rule_ids = NEW_RULE_IDS + else: + rule_ids = BASE_RULE_IDS + + if namespaced_rule_id not in rule_ids: + raise SynapseError(404, "Unknown rule %r" % (namespaced_rule_id,)) + return await self.store.set_push_rule_actions( + user_id, namespaced_rule_id, actions, is_default_rule + ) + else: + raise UnrecognizedRequestError() + + +def _rule_spec_from_path(path): + """Turn a sequence of path components into a rule spec + + Args: + path (sequence[unicode]): the URL path components. + + Returns: + dict: rule spec dict, containing scope/template/rule_id entries, + and possibly attr. + + Raises: + UnrecognizedRequestError if the path components cannot be parsed. + """ + if len(path) < 2: + raise UnrecognizedRequestError() + if path[0] != "pushrules": + raise UnrecognizedRequestError() + + scope = path[1] + path = path[2:] + if scope != "global": + raise UnrecognizedRequestError() + + if len(path) == 0: + raise UnrecognizedRequestError() + + template = path[0] + path = path[1:] + + if len(path) == 0 or len(path[0]) == 0: + raise UnrecognizedRequestError() + + rule_id = path[0] + + spec = {"scope": scope, "template": template, "rule_id": rule_id} + + path = path[1:] + + if len(path) > 0 and len(path[0]) > 0: + spec["attr"] = path[0] + + return spec + + +def _rule_tuple_from_request_object(rule_template, rule_id, req_obj): + if rule_template in ["override", "underride"]: + if "conditions" not in req_obj: + raise InvalidRuleException("Missing 'conditions'") + conditions = req_obj["conditions"] + for c in conditions: + if "kind" not in c: + raise InvalidRuleException("Condition without 'kind'") + elif rule_template == "room": + conditions = [{"kind": "event_match", "key": "room_id", "pattern": rule_id}] + elif rule_template == "sender": + conditions = [{"kind": "event_match", "key": "user_id", "pattern": rule_id}] + elif rule_template == "content": + if "pattern" not in req_obj: + raise InvalidRuleException("Content rule missing 'pattern'") + pat = req_obj["pattern"] + + conditions = [{"kind": "event_match", "key": "content.body", "pattern": pat}] + else: + raise InvalidRuleException("Unknown rule template: %s" % (rule_template,)) + + if "actions" not in req_obj: + raise InvalidRuleException("No actions found") + actions = req_obj["actions"] + + _check_actions(actions) + + return conditions, actions + + +def _check_actions(actions): + if not isinstance(actions, list): + raise InvalidRuleException("No actions found") + + for a in actions: + if a in ["notify", "dont_notify", "coalesce"]: + pass + elif isinstance(a, dict) and "set_tweak" in a: + pass + else: + raise InvalidRuleException("Unrecognised action") + + +def _filter_ruleset_with_path(ruleset, path): + if path == []: + raise UnrecognizedRequestError( + PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR + ) + + if path[0] == "": + return ruleset + template_kind = path[0] + if template_kind not in ruleset: + raise UnrecognizedRequestError() + path = path[1:] + if path == []: + raise UnrecognizedRequestError( + PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR + ) + if path[0] == "": + return ruleset[template_kind] + rule_id = path[0] + + the_rule = None + for r in ruleset[template_kind]: + if r["rule_id"] == rule_id: + the_rule = r + if the_rule is None: + raise NotFoundError + + path = path[1:] + if len(path) == 0: + return the_rule + + attr = path[0] + if attr in the_rule: + # Make sure we return a JSON object as the attribute may be a + # JSON value. + return {attr: the_rule[attr]} + else: + raise UnrecognizedRequestError() + + +def _priority_class_from_spec(spec): + if spec["template"] not in PRIORITY_CLASS_MAP.keys(): + raise InvalidRuleException("Unknown template: %s" % (spec["template"])) + pc = PRIORITY_CLASS_MAP[spec["template"]] + + return pc + + +def _namespaced_rule_id_from_spec(spec): + return _namespaced_rule_id(spec, spec["rule_id"]) + + +def _namespaced_rule_id(spec, rule_id): + return "global/%s/%s" % (spec["template"], rule_id) + + +class InvalidRuleException(Exception): + pass + + +def register_servlets(hs, http_server): + PushRuleRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/pusher.py b/synapse/rest/client/pusher.py new file mode 100644 index 0000000000..84619c5e41 --- /dev/null +++ b/synapse/rest/client/pusher.py @@ -0,0 +1,171 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import Codes, StoreError, SynapseError +from synapse.http.server import respond_with_html_bytes +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, + parse_string, +) +from synapse.push import PusherConfigException +from synapse.rest.client._base import client_patterns + +logger = logging.getLogger(__name__) + + +class PushersRestServlet(RestServlet): + PATTERNS = client_patterns("/pushers$", v1=True) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + user = requester.user + + pushers = await self.hs.get_datastore().get_pushers_by_user_id(user.to_string()) + + filtered_pushers = [p.as_dict() for p in pushers] + + return 200, {"pushers": filtered_pushers} + + +class PushersSetRestServlet(RestServlet): + PATTERNS = client_patterns("/pushers/set$", v1=True) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.notifier = hs.get_notifier() + self.pusher_pool = self.hs.get_pusherpool() + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) + user = requester.user + + content = parse_json_object_from_request(request) + + if ( + "pushkey" in content + and "app_id" in content + and "kind" in content + and content["kind"] is None + ): + await self.pusher_pool.remove_pusher( + content["app_id"], content["pushkey"], user_id=user.to_string() + ) + return 200, {} + + assert_params_in_dict( + content, + [ + "kind", + "app_id", + "app_display_name", + "device_display_name", + "pushkey", + "lang", + "data", + ], + ) + + logger.debug("set pushkey %s to kind %s", content["pushkey"], content["kind"]) + logger.debug("Got pushers request with body: %r", content) + + append = False + if "append" in content: + append = content["append"] + + if not append: + await self.pusher_pool.remove_pushers_by_app_id_and_pushkey_not_user( + app_id=content["app_id"], + pushkey=content["pushkey"], + not_user_id=user.to_string(), + ) + + try: + await self.pusher_pool.add_pusher( + user_id=user.to_string(), + access_token=requester.access_token_id, + kind=content["kind"], + app_id=content["app_id"], + app_display_name=content["app_display_name"], + device_display_name=content["device_display_name"], + pushkey=content["pushkey"], + lang=content["lang"], + data=content["data"], + profile_tag=content.get("profile_tag", ""), + ) + except PusherConfigException as pce: + raise SynapseError( + 400, "Config Error: " + str(pce), errcode=Codes.MISSING_PARAM + ) + + self.notifier.on_new_replication_data() + + return 200, {} + + +class PushersRemoveRestServlet(RestServlet): + """ + To allow pusher to be delete by clicking a link (ie. GET request) + """ + + PATTERNS = client_patterns("/pushers/remove$", v1=True) + SUCCESS_HTML = b"You have been unsubscribed" + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.notifier = hs.get_notifier() + self.auth = hs.get_auth() + self.pusher_pool = self.hs.get_pusherpool() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, rights="delete_pusher") + user = requester.user + + app_id = parse_string(request, "app_id", required=True) + pushkey = parse_string(request, "pushkey", required=True) + + try: + await self.pusher_pool.remove_pusher( + app_id=app_id, pushkey=pushkey, user_id=user.to_string() + ) + 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, + PushersRemoveRestServlet.SUCCESS_HTML, + ) + return None + + +def register_servlets(hs, http_server): + PushersRestServlet(hs).register(http_server) + PushersSetRestServlet(hs).register(http_server) + PushersRemoveRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/read_marker.py b/synapse/rest/client/read_marker.py new file mode 100644 index 0000000000..027f8b81fa --- /dev/null +++ b/synapse/rest/client/read_marker.py @@ -0,0 +1,74 @@ +# Copyright 2017 Vector Creations Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.constants import ReadReceiptEventFields +from synapse.api.errors import Codes, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class ReadMarkerRestServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/read_markers$") + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.receipts_handler = hs.get_receipts_handler() + self.read_marker_handler = hs.get_read_marker_handler() + self.presence_handler = hs.get_presence_handler() + + async def on_POST(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + + await self.presence_handler.bump_presence_active_time(requester.user) + + body = parse_json_object_from_request(request) + read_event_id = body.get("m.read", None) + hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) + + if not isinstance(hidden, bool): + raise SynapseError( + 400, + "Param %s must be a boolean, if given" + % ReadReceiptEventFields.MSC2285_HIDDEN, + Codes.BAD_JSON, + ) + + if read_event_id: + await self.receipts_handler.received_client_receipt( + room_id, + "m.read", + user_id=requester.user.to_string(), + event_id=read_event_id, + hidden=hidden, + ) + + read_marker_event_id = body.get("m.fully_read", None) + if read_marker_event_id: + await self.read_marker_handler.received_client_read_marker( + room_id, + user_id=requester.user.to_string(), + event_id=read_marker_event_id, + ) + + return 200, {} + + +def register_servlets(hs, http_server): + ReadMarkerRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/receipts.py b/synapse/rest/client/receipts.py new file mode 100644 index 0000000000..d9ab836cd8 --- /dev/null +++ b/synapse/rest/client/receipts.py @@ -0,0 +1,71 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.constants import ReadReceiptEventFields +from synapse.api.errors import Codes, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class ReceiptRestServlet(RestServlet): + PATTERNS = client_patterns( + "/rooms/(?P[^/]*)" + "/receipt/(?P[^/]*)" + "/(?P[^/]*)$" + ) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.receipts_handler = hs.get_receipts_handler() + self.presence_handler = hs.get_presence_handler() + + async def on_POST(self, request, room_id, receipt_type, event_id): + requester = await self.auth.get_user_by_req(request) + + if receipt_type != "m.read": + raise SynapseError(400, "Receipt type must be 'm.read'") + + body = parse_json_object_from_request(request, allow_empty_body=True) + hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) + + if not isinstance(hidden, bool): + raise SynapseError( + 400, + "Param %s must be a boolean, if given" + % ReadReceiptEventFields.MSC2285_HIDDEN, + Codes.BAD_JSON, + ) + + await self.presence_handler.bump_presence_active_time(requester.user) + + await self.receipts_handler.received_client_receipt( + room_id, + receipt_type, + user_id=requester.user.to_string(), + event_id=event_id, + hidden=hidden, + ) + + return 200, {} + + +def register_servlets(hs, http_server): + ReceiptRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py new file mode 100644 index 0000000000..58b8e8f261 --- /dev/null +++ b/synapse/rest/client/register.py @@ -0,0 +1,879 @@ +# Copyright 2015 - 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import hmac +import logging +import random +from typing import List, Union + +import synapse +import synapse.api.auth +import synapse.types +from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType +from synapse.api.errors import ( + Codes, + InteractiveAuthIncompleteError, + SynapseError, + ThreepidValidationError, + UnrecognizedRequestError, +) +from synapse.config import ConfigError +from synapse.config.captcha import CaptchaConfig +from synapse.config.consent import ConsentConfig +from synapse.config.emailconfig import ThreepidBehaviour +from synapse.config.ratelimiting import FederationRateLimitConfig +from synapse.config.registration import RegistrationConfig +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 finish_request, respond_with_html +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_boolean, + parse_json_object_from_request, + parse_string, +) +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 + +# We ought to be using hmac.compare_digest() but on older pythons it doesn't +# exist. It's a _really minor_ security flaw to use plain string comparison +# because the timing attack is so obscured by all the other code here it's +# unlikely to make much difference +if hasattr(hmac, "compare_digest"): + compare_digest = hmac.compare_digest +else: + + def compare_digest(a, b): + return a == b + + +logger = logging.getLogger(__name__) + + +class EmailRegisterRequestTokenRestServlet(RestServlet): + PATTERNS = client_patterns("/register/email/requestToken$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.config = hs.config + + if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self.mailer = Mailer( + hs=self.hs, + app_name=self.config.email_app_name, + template_html=self.config.email_registration_template_html, + template_text=self.config.email_registration_template_text, + ) + + async def on_POST(self, request): + if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.hs.config.local_threepid_handling_disabled_due_to_email_config: + 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 check_3pid_allowed(self.hs, "email", email): + 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_datastore().get_user_id_by_threepid( + "email", email + ) + + if existing_user_id is not None: + if self.hs.config.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) + + if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + assert self.hs.config.account_threepid_delegate_email + + # Have the configured identity server handle the request + ret = await self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate_email, + email, + client_secret, + send_attempt, + next_link, + ) + else: + # Send registration emails from Synapse + sid = await self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_registration_mail, + next_link, + ) + + # Wrap the session id in a JSON object + ret = {"sid": sid} + + threepid_send_requests.labels(type="email", reason="register").observe( + send_attempt + ) + + return 200, ret + + +class MsisdnRegisterRequestTokenRestServlet(RestServlet): + PATTERNS = client_patterns("/register/msisdn/requestToken$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + + async def on_POST(self, request): + 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 check_3pid_allowed(self.hs, "msisdn", msisdn): + 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_datastore().get_user_id_by_threepid( + "msisdn", msisdn + ) + + if existing_user_id is not None: + if self.hs.config.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.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.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[^/]*)/submit_token$", releases=(), unstable=True + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.config = hs.config + self.clock = hs.get_clock() + self.store = hs.get_datastore() + + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self._failure_email_template = ( + self.config.email_registration_template_failure_html + ) + + async def on_GET(self, request, medium): + if medium != "email": + raise SynapseError( + 400, "This medium is currently not supported for registration" + ) + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + 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_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") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.registration_handler = hs.get_registration_handler() + self.ratelimiter = FederationRateLimiter( + hs.get_clock(), + FederationRateLimitConfig( + # Time window of 2s + window_size=2000, + # Artificially delay requests if rate > sleep_limit/window_size + sleep_limit=1, + # Amount of artificial delay to apply + sleep_msec=1000, + # Error with 429 if more than reject_limit requests are queued + reject_limit=1, + # Allow 1 request at a time + concurrent_requests=1, + ), + ) + + async def on_GET(self, request): + if not self.hs.config.enable_registration: + raise SynapseError( + 403, "Registration has been disabled", errcode=Codes.FORBIDDEN + ) + + ip = request.getClientIP() + with self.ratelimiter.ratelimit(ip) as wait_deferred: + await wait_deferred + + username = parse_string(request, "username", required=True) + + await self.registration_handler.check_username(username) + + return 200, {"available": True} + + +class RegisterRestServlet(RestServlet): + PATTERNS = client_patterns("/register$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + + self.hs = hs + self.auth = hs.get_auth() + self.store = hs.get_datastore() + 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() + self.password_policy_handler = hs.get_password_policy_handler() + self.clock = hs.get_clock() + self._registration_enabled = self.hs.config.enable_registration + self._msc2918_enabled = hs.config.access_token_lifetime is not None + + self._registration_flows = _calculate_registration_flows( + hs.config, self.auth_handler + ) + + @interactive_auth_handler + async def on_POST(self, request): + body = parse_json_object_from_request(request) + + client_addr = request.getClientIP() + + await self.ratelimiter.ratelimit(None, client_addr, update=False) + + kind = b"user" + if b"kind" in request.args: + kind = request.args[b"kind"][0] + + if kind == b"guest": + ret = await self._do_guest_registration(body, address=client_addr) + return ret + elif kind != b"user": + raise UnrecognizedRequestError( + "Do not understand membership kind: %s" % (kind.decode("utf8"),) + ) + + if self._msc2918_enabled: + # Check if this registration should also issue a refresh token, as + # per MSC2918 + should_issue_refresh_token = parse_boolean( + request, name="org.matrix.msc2918.refresh_token", default=False + ) + else: + should_issue_refresh_token = False + + # Pull out the provided username and do basic sanity checks early since + # the auth layer will store these in sessions. + desired_username = None + if "username" in body: + if not isinstance(body["username"], str) or len(body["username"]) > 512: + raise SynapseError(400, "Invalid username") + desired_username = body["username"] + + # fork off as soon as possible for ASes which have completely + # different registration flows to normal users + + # == Application Service Registration == + if body.get("type") == APP_SERVICE_REGISTRATION_TYPE: + if not self.auth.has_access_token(request): + raise SynapseError( + 400, + "Appservice token must be provided when using a type of m.login.application_service", + ) + + # Verify the AS + self.auth.get_appservice_by_req(request) + + # Set the desired user according to the AS API (which uses the + # 'user' key not 'username'). Since this is a new addition, we'll + # fallback to 'username' if they gave one. + desired_username = body.get("user", desired_username) + + # XXX we should check that desired_username is valid. Currently + # we give appservices carte blanche for any insanity in mxids, + # because the IRC bridges rely on being able to register stupid + # IDs. + + access_token = self.auth.get_access_token_from_request(request) + + if not isinstance(desired_username, str): + raise SynapseError(400, "Desired Username is missing or not a string") + + result = await self._do_appservice_registration( + desired_username, + access_token, + body, + should_issue_refresh_token=should_issue_refresh_token, + ) + + return 200, result + elif self.auth.has_access_token(request): + raise SynapseError( + 400, + "An access token should not be provided on requests to /register (except if type is m.login.application_service)", + ) + + # == Normal User Registration == (everyone else) + if not self._registration_enabled: + raise SynapseError(403, "Registration has been disabled", Codes.FORBIDDEN) + + # For regular registration, convert the provided username to lowercase + # before attempting to register it. This should mean that people who try + # to register with upper-case in their usernames don't get a nasty surprise. + # + # Note that we treat usernames case-insensitively in login, so they are + # free to carry on imagining that their username is CrAzYh4cKeR if that + # keeps them happy. + if desired_username is not None: + desired_username = desired_username.lower() + + # Check if this account is upgrading from a guest account. + guest_access_token = body.get("guest_access_token", None) + + # Pull out the provided password and do basic sanity checks early. + # + # Note that we remove the password from the body since the auth layer + # will store the body in the session and we don't want a plaintext + # password store there. + password = body.pop("password", None) + if password is not None: + if not isinstance(password, str) or len(password) > 512: + raise SynapseError(400, "Invalid password") + self.password_policy_handler.validate_password(password) + + if "initial_device_display_name" in body and password is None: + # ignore 'initial_device_display_name' if sent without + # a password to work around a client bug where it sent + # the 'initial_device_display_name' param alone, wiping out + # the original registration params + logger.warning("Ignoring initial_device_display_name without password") + del body["initial_device_display_name"] + + session_id = self.auth_handler.get_session_id(body) + registered_user_id = None + password_hash = None + if session_id: + # if we get a registered user id out of here, it means we previously + # registered a user for this session, so we could just return the + # user here. We carry on and go through the auth checks though, + # for paranoia. + registered_user_id = await self.auth_handler.get_session_data( + session_id, UIAuthSessionDataConstants.REGISTERED_USER_ID, None + ) + # Extract the previously-hashed password from the session. + password_hash = await self.auth_handler.get_session_data( + session_id, UIAuthSessionDataConstants.PASSWORD_HASH, None + ) + + # Ensure that the username is valid. + if desired_username is not None: + await self.registration_handler.check_username( + desired_username, + guest_access_token=guest_access_token, + assigned_user_id=registered_user_id, + ) + + # Check if the user-interactive authentication flows are complete, if + # not this will raise a user-interactive auth error. + try: + auth_result, params, session_id = await self.auth_handler.check_ui_auth( + self._registration_flows, + request, + body, + "register a new account", + ) + except InteractiveAuthIncompleteError as e: + # The user needs to provide more steps to complete auth. + # + # Hash the password and store it with the session since the client + # is not required to provide the password again. + # + # If a password hash was previously stored we will not attempt to + # re-hash and store it for efficiency. This assumes the password + # does not change throughout the authentication flow, but this + # should be fine since the data is meant to be consistent. + if not password_hash and password: + password_hash = await self.auth_handler.hash(password) + await self.auth_handler.set_session_data( + e.session_id, + UIAuthSessionDataConstants.PASSWORD_HASH, + password_hash, + ) + 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 check_3pid_allowed(self.hs, medium, address): + 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 + ) + # don't re-register the threepids + registered = False + else: + # If we have a password in this request, prefer it. Otherwise, there + # might be a password hash from an earlier request. + if password: + password_hash = await self.auth_handler.hash(password) + if not password_hash: + raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM) + + desired_username = params.get("username", None) + guest_access_token = params.get("guest_access_token", None) + + 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 + ) + + registered_user_id = await self.registration_handler.register_user( + localpart=desired_username, + password_hash=password_hash, + guest_access_token=guest_access_token, + threepid=threepid, + 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.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). + await self.auth_handler.set_session_data( + session_id, + UIAuthSessionDataConstants.REGISTERED_USER_ID, + registered_user_id, + ) + + registered = True + + return_dict = await self._create_registration_details( + registered_user_id, + params, + should_issue_refresh_token=should_issue_refresh_token, + ) + + if registered: + await self.registration_handler.post_registration_actions( + user_id=registered_user_id, + auth_result=auth_result, + access_token=return_dict.get("access_token"), + ) + + return 200, return_dict + + async def _do_appservice_registration( + self, username, as_token, body, should_issue_refresh_token: bool = False + ): + user_id = await self.registration_handler.appservice_register( + username, as_token + ) + return await self._create_registration_details( + user_id, + body, + is_appservice_ghost=True, + should_issue_refresh_token=should_issue_refresh_token, + ) + + async def _create_registration_details( + self, + user_id: str, + params: JsonDict, + is_appservice_ghost: bool = False, + should_issue_refresh_token: bool = False, + ): + """Complete registration of newly-registered user + + Allocates device_id if one was not given; also creates access_token. + + Args: + user_id: full canonical @user:id + params: registration parameters, from which we pull device_id, + initial_device_name and inhibit_login + is_appservice_ghost + should_issue_refresh_token: True if this registration should issue + a refresh token alongside the access token. + Returns: + dictionary for response from /register + """ + result = {"user_id": user_id, "home_server": self.hs.hostname} + if not params.get("inhibit_login", False): + device_id = params.get("device_id") + initial_display_name = params.get("initial_device_display_name") + ( + device_id, + access_token, + valid_until_ms, + refresh_token, + ) = await self.registration_handler.register_device( + user_id, + device_id, + initial_display_name, + is_guest=False, + is_appservice_ghost=is_appservice_ghost, + should_issue_refresh_token=should_issue_refresh_token, + ) + + result.update({"access_token": access_token, "device_id": device_id}) + + if valid_until_ms is not None: + expires_in_ms = valid_until_ms - self.clock.time_msec() + result["expires_in_ms"] = expires_in_ms + + if refresh_token is not None: + result["refresh_token"] = refresh_token + + return result + + async def _do_guest_registration(self, params, address=None): + if not self.hs.config.allow_guest_access: + raise SynapseError(403, "Guest access is disabled") + user_id = await self.registration_handler.register_user( + make_guest=True, address=address + ) + + # we don't allow guests to specify their own device_id, because + # we have nowhere to store it. + device_id = synapse.api.auth.GUEST_DEVICE_ID + initial_display_name = params.get("initial_device_display_name") + ( + device_id, + access_token, + valid_until_ms, + refresh_token, + ) = await self.registration_handler.register_device( + user_id, device_id, initial_display_name, is_guest=True + ) + + result = { + "user_id": user_id, + "device_id": device_id, + "access_token": access_token, + "home_server": self.hs.hostname, + } + + if valid_until_ms is not None: + expires_in_ms = valid_until_ms - self.clock.time_msec() + result["expires_in_ms"] = expires_in_ms + + if refresh_token is not None: + result["refresh_token"] = refresh_token + + return 200, result + + +def _calculate_registration_flows( + # technically `config` has to provide *all* of these interfaces, not just one + config: Union[RegistrationConfig, ConsentConfig, CaptchaConfig], + auth_handler: AuthHandler, +) -> List[List[str]]: + """Get a suitable flows list for registration + + Args: + config: server configuration + auth_handler: authorization handler + + 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.registrations_require_3pid + require_msisdn = "msisdn" in config.registrations_require_3pid + + show_msisdn = True + show_email = True + + if config.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]) + + # Prepend m.login.terms to all flows if we're requiring consent + if config.user_consent_at_registration: + for flow in flows: + flow.insert(0, LoginType.TERMS) + + # Prepend recaptcha to all flows if we're requiring captcha + if config.enable_registration_captcha: + for flow in flows: + flow.insert(0, LoginType.RECAPTCHA) + + return flows + + +def register_servlets(hs, http_server): + EmailRegisterRequestTokenRestServlet(hs).register(http_server) + MsisdnRegisterRequestTokenRestServlet(hs).register(http_server) + UsernameAvailabilityRestServlet(hs).register(http_server) + RegistrationSubmitTokenServlet(hs).register(http_server) + RegisterRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/relations.py b/synapse/rest/client/relations.py new file mode 100644 index 0000000000..0821cd285f --- /dev/null +++ b/synapse/rest/client/relations.py @@ -0,0 +1,381 @@ +# Copyright 2019 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This class implements the proposed relation APIs from MSC 1849. + +Since the MSC has not been approved all APIs here are unstable and may change at +any time to reflect changes in the MSC. +""" + +import logging + +from synapse.api.constants import EventTypes, RelationTypes +from synapse.api.errors import ShadowBanError, SynapseError +from synapse.http.servlet import ( + RestServlet, + parse_integer, + parse_json_object_from_request, + parse_string, +) +from synapse.rest.client.transactions import HttpTransactionCache +from synapse.storage.relations import ( + AggregationPaginationToken, + PaginationChunk, + RelationPaginationToken, +) +from synapse.util.stringutils import random_string + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class RelationSendServlet(RestServlet): + """Helper API for sending events that have relation data. + + Example API shape to send a 👍 reaction to a room: + + POST /rooms/!foo/send_relation/$bar/m.annotation/m.reaction?key=%F0%9F%91%8D + {} + + { + "event_id": "$foobar" + } + """ + + PATTERN = ( + "/rooms/(?P[^/]*)/send_relation" + "/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)" + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.event_creation_handler = hs.get_event_creation_handler() + self.txns = HttpTransactionCache(hs) + + def register(self, http_server): + http_server.register_paths( + "POST", + client_patterns(self.PATTERN + "$", releases=()), + self.on_PUT_or_POST, + self.__class__.__name__, + ) + http_server.register_paths( + "PUT", + client_patterns(self.PATTERN + "/(?P[^/]*)$", releases=()), + self.on_PUT, + self.__class__.__name__, + ) + + def on_PUT(self, request, *args, **kwargs): + return self.txns.fetch_or_execute_request( + request, self.on_PUT_or_POST, request, *args, **kwargs + ) + + async def on_PUT_or_POST( + self, request, room_id, parent_id, relation_type, event_type, txn_id=None + ): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + if event_type == EventTypes.Member: + # Add relations to a membership is meaningless, so we just deny it + # at the CS API rather than trying to handle it correctly. + raise SynapseError(400, "Cannot send member events with relations") + + content = parse_json_object_from_request(request) + + aggregation_key = parse_string(request, "key", encoding="utf-8") + + content["m.relates_to"] = { + "event_id": parent_id, + "key": aggregation_key, + "rel_type": relation_type, + } + + event_dict = { + "type": event_type, + "content": content, + "room_id": room_id, + "sender": requester.user.to_string(), + } + + try: + ( + event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + requester, event_dict=event_dict, txn_id=txn_id + ) + event_id = event.event_id + except ShadowBanError: + event_id = "$" + random_string(43) + + return 200, {"event_id": event_id} + + +class RelationPaginationServlet(RestServlet): + """API to paginate relations on an event by topological ordering, optionally + filtered by relation type and event type. + """ + + PATTERNS = client_patterns( + "/rooms/(?P[^/]*)/relations/(?P[^/]*)" + "(/(?P[^/]*)(/(?P[^/]*))?)?$", + releases=(), + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.clock = hs.get_clock() + self._event_serializer = hs.get_event_client_serializer() + self.event_handler = hs.get_event_handler() + + async def on_GET( + self, request, room_id, parent_id, relation_type=None, event_type=None + ): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + await self.auth.check_user_in_room_or_world_readable( + room_id, requester.user.to_string(), allow_departed_users=True + ) + + # This gets the original event and checks that a) the event exists and + # b) the user is allowed to view it. + event = await self.event_handler.get_event(requester.user, room_id, parent_id) + + limit = parse_integer(request, "limit", default=5) + from_token_str = parse_string(request, "from") + to_token_str = parse_string(request, "to") + + if event.internal_metadata.is_redacted(): + # If the event is redacted, return an empty list of relations + pagination_chunk = PaginationChunk(chunk=[]) + else: + # Return the relations + from_token = None + if from_token_str: + from_token = RelationPaginationToken.from_string(from_token_str) + + to_token = None + if to_token_str: + to_token = RelationPaginationToken.from_string(to_token_str) + + pagination_chunk = await self.store.get_relations_for_event( + event_id=parent_id, + relation_type=relation_type, + event_type=event_type, + limit=limit, + from_token=from_token, + to_token=to_token, + ) + + events = await self.store.get_events_as_list( + [c["event_id"] for c in pagination_chunk.chunk] + ) + + now = self.clock.time_msec() + # We set bundle_aggregations to False when retrieving the original + # event because we want the content before relations were applied to + # it. + original_event = await self._event_serializer.serialize_event( + event, now, bundle_aggregations=False + ) + # Similarly, we don't allow relations to be applied to relations, so we + # return the original relations without any aggregations on top of them + # here. + events = await self._event_serializer.serialize_events( + events, now, bundle_aggregations=False + ) + + return_value = pagination_chunk.to_dict() + return_value["chunk"] = events + return_value["original_event"] = original_event + + return 200, return_value + + +class RelationAggregationPaginationServlet(RestServlet): + """API to paginate aggregation groups of relations, e.g. paginate the + types and counts of the reactions on the events. + + Example request and response: + + GET /rooms/{room_id}/aggregations/{parent_id} + + { + chunk: [ + { + "type": "m.reaction", + "key": "👍", + "count": 3 + } + ] + } + """ + + PATTERNS = client_patterns( + "/rooms/(?P[^/]*)/aggregations/(?P[^/]*)" + "(/(?P[^/]*)(/(?P[^/]*))?)?$", + releases=(), + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.event_handler = hs.get_event_handler() + + async def on_GET( + self, request, room_id, parent_id, relation_type=None, event_type=None + ): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + await self.auth.check_user_in_room_or_world_readable( + room_id, + requester.user.to_string(), + allow_departed_users=True, + ) + + # This checks that a) the event exists and b) the user is allowed to + # view it. + event = await self.event_handler.get_event(requester.user, room_id, parent_id) + + if relation_type not in (RelationTypes.ANNOTATION, None): + raise SynapseError(400, "Relation type must be 'annotation'") + + limit = parse_integer(request, "limit", default=5) + from_token_str = parse_string(request, "from") + to_token_str = parse_string(request, "to") + + if event.internal_metadata.is_redacted(): + # If the event is redacted, return an empty list of relations + pagination_chunk = PaginationChunk(chunk=[]) + else: + # Return the relations + from_token = None + if from_token_str: + from_token = AggregationPaginationToken.from_string(from_token_str) + + to_token = None + if to_token_str: + to_token = AggregationPaginationToken.from_string(to_token_str) + + pagination_chunk = await self.store.get_aggregation_groups_for_event( + event_id=parent_id, + event_type=event_type, + limit=limit, + from_token=from_token, + to_token=to_token, + ) + + return 200, pagination_chunk.to_dict() + + +class RelationAggregationGroupPaginationServlet(RestServlet): + """API to paginate within an aggregation group of relations, e.g. paginate + all the 👍 reactions on an event. + + Example request and response: + + GET /rooms/{room_id}/aggregations/{parent_id}/m.annotation/m.reaction/👍 + + { + chunk: [ + { + "type": "m.reaction", + "content": { + "m.relates_to": { + "rel_type": "m.annotation", + "key": "👍" + } + } + }, + ... + ] + } + """ + + PATTERNS = client_patterns( + "/rooms/(?P[^/]*)/aggregations/(?P[^/]*)" + "/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)$", + releases=(), + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.clock = hs.get_clock() + self._event_serializer = hs.get_event_client_serializer() + self.event_handler = hs.get_event_handler() + + async def on_GET(self, request, room_id, parent_id, relation_type, event_type, key): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + await self.auth.check_user_in_room_or_world_readable( + room_id, + requester.user.to_string(), + allow_departed_users=True, + ) + + # This checks that a) the event exists and b) the user is allowed to + # view it. + await self.event_handler.get_event(requester.user, room_id, parent_id) + + if relation_type != RelationTypes.ANNOTATION: + raise SynapseError(400, "Relation type must be 'annotation'") + + limit = parse_integer(request, "limit", default=5) + from_token_str = parse_string(request, "from") + to_token_str = parse_string(request, "to") + + from_token = None + if from_token_str: + from_token = RelationPaginationToken.from_string(from_token_str) + + to_token = None + if to_token_str: + to_token = RelationPaginationToken.from_string(to_token_str) + + result = await self.store.get_relations_for_event( + event_id=parent_id, + relation_type=relation_type, + event_type=event_type, + aggregation_key=key, + limit=limit, + from_token=from_token, + to_token=to_token, + ) + + events = await self.store.get_events_as_list( + [c["event_id"] for c in result.chunk] + ) + + now = self.clock.time_msec() + events = await self._event_serializer.serialize_events(events, now) + + return_value = result.to_dict() + return_value["chunk"] = events + + return 200, return_value + + +def register_servlets(hs, http_server): + RelationSendServlet(hs).register(http_server) + RelationPaginationServlet(hs).register(http_server) + RelationAggregationPaginationServlet(hs).register(http_server) + RelationAggregationGroupPaginationServlet(hs).register(http_server) diff --git a/synapse/rest/client/report_event.py b/synapse/rest/client/report_event.py new file mode 100644 index 0000000000..07ea39a8a3 --- /dev/null +++ b/synapse/rest/client/report_event.py @@ -0,0 +1,68 @@ +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from http import HTTPStatus + +from synapse.api.errors import Codes, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class ReportEventRestServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/report/(?P[^/]*)$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.store = hs.get_datastore() + + async def on_POST(self, request, room_id, event_id): + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + body = parse_json_object_from_request(request) + + if not isinstance(body.get("reason", ""), str): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'reason' must be a string", + Codes.BAD_JSON, + ) + if not isinstance(body.get("score", 0), int): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'score' must be an integer", + Codes.BAD_JSON, + ) + + await self.store.add_event_report( + room_id=room_id, + event_id=event_id, + user_id=user_id, + reason=body.get("reason"), + content=body, + received_ts=self.clock.time_msec(), + ) + + return 200, {} + + +def register_servlets(hs, http_server): + ReportEventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py new file mode 100644 index 0000000000..ed238b2141 --- /dev/null +++ b/synapse/rest/client/room.py @@ -0,0 +1,1152 @@ +# Copyright 2014-2016 OpenMarket Ltd +# Copyright 2018 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with rooms: /rooms/ """ +import logging +import re +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple +from urllib import parse as urlparse + +from synapse.api.constants import EventTypes, Membership +from synapse.api.errors import ( + AuthError, + Codes, + InvalidClientCredentialsError, + MissingClientTokenError, + ShadowBanError, + SynapseError, +) +from synapse.api.filtering import Filter +from synapse.events.utils import format_event_for_client_v2 +from synapse.http.servlet import ( + ResolveRoomIdMixin, + RestServlet, + assert_params_in_dict, + parse_boolean, + parse_integer, + parse_json_object_from_request, + parse_string, + parse_strings_from_args, +) +from synapse.http.site import SynapseRequest +from synapse.logging.opentracing import set_tag +from synapse.rest.client._base import client_patterns +from synapse.rest.client.transactions import HttpTransactionCache +from synapse.storage.state import StateFilter +from synapse.streams.config import PaginationConfig +from synapse.types import JsonDict, StreamToken, ThirdPartyInstanceID, UserID +from synapse.util import json_decoder +from synapse.util.stringutils import parse_and_validate_server_name, random_string + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class TransactionRestServlet(RestServlet): + def __init__(self, hs): + super().__init__() + self.txns = HttpTransactionCache(hs) + + +class RoomCreateRestServlet(TransactionRestServlet): + # No PATTERN; we have custom dispatch rules here + + def __init__(self, hs): + super().__init__(hs) + self._room_creation_handler = hs.get_room_creation_handler() + self.auth = hs.get_auth() + + def register(self, http_server): + PATTERNS = "/createRoom" + register_txn_path(self, PATTERNS, http_server) + + def on_PUT(self, request, txn_id): + set_tag("txn_id", txn_id) + return self.txns.fetch_or_execute_request(request, self.on_POST, request) + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) + + info, _ = await self._room_creation_handler.create_room( + requester, self.get_room_config(request) + ) + + return 200, info + + def get_room_config(self, request): + user_supplied_config = parse_json_object_from_request(request) + return user_supplied_config + + +# TODO: Needs unit testing for generic events +class RoomStateEventRestServlet(TransactionRestServlet): + def __init__(self, hs): + super().__init__(hs) + 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.auth = hs.get_auth() + + def register(self, http_server): + # /room/$roomid/state/$eventtype + no_state_key = "/rooms/(?P[^/]*)/state/(?P[^/]*)$" + + # /room/$roomid/state/$eventtype/$statekey + state_key = ( + "/rooms/(?P[^/]*)/state/" + "(?P[^/]*)/(?P[^/]*)$" + ) + + http_server.register_paths( + "GET", + client_patterns(state_key, v1=True), + self.on_GET, + self.__class__.__name__, + ) + http_server.register_paths( + "PUT", + client_patterns(state_key, v1=True), + self.on_PUT, + self.__class__.__name__, + ) + http_server.register_paths( + "GET", + client_patterns(no_state_key, v1=True), + self.on_GET_no_state_key, + self.__class__.__name__, + ) + http_server.register_paths( + "PUT", + client_patterns(no_state_key, v1=True), + self.on_PUT_no_state_key, + self.__class__.__name__, + ) + + def on_GET_no_state_key(self, request, room_id, event_type): + return self.on_GET(request, room_id, event_type, "") + + def on_PUT_no_state_key(self, request, room_id, event_type): + return self.on_PUT(request, room_id, event_type, "") + + async def on_GET(self, request, room_id, event_type, state_key): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + format = parse_string( + request, "format", default="content", allowed_values=["content", "event"] + ) + + msg_handler = self.message_handler + data = await msg_handler.get_room_data( + user_id=requester.user.to_string(), + room_id=room_id, + event_type=event_type, + state_key=state_key, + ) + + if not data: + raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) + + if format == "event": + event = format_event_for_client_v2(data.get_dict()) + return 200, event + elif format == "content": + return 200, data.get_dict()["content"] + + async def on_PUT(self, request, room_id, event_type, state_key, txn_id=None): + requester = await self.auth.get_user_by_req(request) + + if txn_id: + set_tag("txn_id", txn_id) + + content = parse_json_object_from_request(request) + + event_dict = { + "type": event_type, + "content": content, + "room_id": room_id, + "sender": requester.user.to_string(), + } + + if state_key is not None: + event_dict["state_key"] = state_key + + try: + if event_type == EventTypes.Member: + membership = content.get("membership", None) + event_id, _ = await self.room_member_handler.update_membership( + requester, + target=UserID.from_string(state_key), + room_id=room_id, + action=membership, + content=content, + ) + else: + ( + event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + requester, event_dict, txn_id=txn_id + ) + event_id = event.event_id + except ShadowBanError: + event_id = "$" + random_string(43) + + set_tag("event_id", event_id) + ret = {"event_id": event_id} + return 200, ret + + +# TODO: Needs unit testing for generic events + feedback +class RoomSendEventRestServlet(TransactionRestServlet): + def __init__(self, hs): + super().__init__(hs) + self.event_creation_handler = hs.get_event_creation_handler() + self.auth = hs.get_auth() + + def register(self, http_server): + # /rooms/$roomid/send/$event_type[/$txn_id] + PATTERNS = "/rooms/(?P[^/]*)/send/(?P[^/]*)" + register_txn_path(self, PATTERNS, http_server, with_get=True) + + async def on_POST(self, request, room_id, event_type, txn_id=None): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + content = parse_json_object_from_request(request) + + event_dict = { + "type": event_type, + "content": content, + "room_id": room_id, + "sender": requester.user.to_string(), + } + + if b"ts" in request.args and requester.app_service: + event_dict["origin_server_ts"] = parse_integer(request, "ts", 0) + + try: + ( + event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + requester, event_dict, txn_id=txn_id + ) + event_id = event.event_id + except ShadowBanError: + event_id = "$" + random_string(43) + + set_tag("event_id", event_id) + return 200, {"event_id": event_id} + + def on_GET(self, request, room_id, event_type, txn_id): + return 200, "Not implemented" + + def on_PUT(self, request, room_id, event_type, txn_id): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id, event_type, txn_id + ) + + +# TODO: Needs unit testing for room ID + alias joins +class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet): + def __init__(self, hs): + super().__init__(hs) + super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up + self.auth = hs.get_auth() + + def register(self, http_server): + # /join/$room_identifier[/$txn_id] + PATTERNS = "/join/(?P[^/]*)" + register_txn_path(self, PATTERNS, http_server) + + async def on_POST( + self, + request: SynapseRequest, + room_identifier: str, + txn_id: Optional[str] = None, + ): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + try: + content = parse_json_object_from_request(request) + except Exception: + # Turns out we used to ignore the body entirely, and some clients + # cheekily send invalid bodies. + content = {} + + # 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) + room_id, remote_room_hosts = await self.resolve_room_id( + room_identifier, + remote_room_hosts, + ) + + await self.room_member_handler.update_membership( + requester=requester, + target=requester.user, + room_id=room_id, + action="join", + txn_id=txn_id, + remote_room_hosts=remote_room_hosts, + content=content, + third_party_signed=content.get("third_party_signed", None), + ) + + return 200, {"room_id": room_id} + + def on_PUT(self, request, room_identifier, txn_id): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_identifier, txn_id + ) + + +# TODO: Needs unit testing +class PublicRoomListRestServlet(TransactionRestServlet): + PATTERNS = client_patterns("/publicRooms$", v1=True) + + def __init__(self, hs): + super().__init__(hs) + self.hs = hs + self.auth = hs.get_auth() + + async def on_GET(self, request): + server = parse_string(request, "server") + + try: + await self.auth.get_user_by_req(request, allow_guest=True) + except InvalidClientCredentialsError as e: + # Option to allow servers to require auth when accessing + # /publicRooms via CS API. This is especially helpful in private + # federations. + if not self.hs.config.allow_public_rooms_without_auth: + raise + + # We allow people to not be authed if they're just looking at our + # room list, but require auth when we proxy the request. + # In both cases we call the auth function, as that has the side + # effect of logging who issued this request if an access token was + # provided. + if server: + raise e + + limit: Optional[int] = parse_integer(request, "limit", 0) + since_token = parse_string(request, "since") + + if limit == 0: + # zero is a special value which corresponds to no limit. + limit = None + + handler = self.hs.get_room_list_handler() + if server and server != self.hs.config.server_name: + # Ensure the server is valid. + try: + parse_and_validate_server_name(server) + except ValueError: + raise SynapseError( + 400, + "Invalid server name: %s" % (server,), + Codes.INVALID_PARAM, + ) + + data = await handler.get_remote_public_room_list( + server, limit=limit, since_token=since_token + ) + else: + data = await handler.get_local_public_room_list( + limit=limit, since_token=since_token + ) + + return 200, data + + async def on_POST(self, request): + await self.auth.get_user_by_req(request, allow_guest=True) + + server = parse_string(request, "server") + content = parse_json_object_from_request(request) + + limit: Optional[int] = int(content.get("limit", 100)) + since_token = content.get("since", None) + search_filter = content.get("filter", None) + + include_all_networks = content.get("include_all_networks", False) + third_party_instance_id = content.get("third_party_instance_id", None) + + if include_all_networks: + network_tuple = None + if third_party_instance_id is not None: + raise SynapseError( + 400, "Can't use include_all_networks with an explicit network" + ) + elif third_party_instance_id is None: + network_tuple = ThirdPartyInstanceID(None, None) + else: + network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) + + if limit == 0: + # zero is a special value which corresponds to no limit. + limit = None + + handler = self.hs.get_room_list_handler() + if server and server != self.hs.config.server_name: + # Ensure the server is valid. + try: + parse_and_validate_server_name(server) + except ValueError: + raise SynapseError( + 400, + "Invalid server name: %s" % (server,), + Codes.INVALID_PARAM, + ) + + data = await handler.get_remote_public_room_list( + server, + limit=limit, + since_token=since_token, + search_filter=search_filter, + include_all_networks=include_all_networks, + third_party_instance_id=third_party_instance_id, + ) + + else: + data = await handler.get_local_public_room_list( + limit=limit, + since_token=since_token, + search_filter=search_filter, + network_tuple=network_tuple, + ) + + return 200, data + + +# TODO: Needs unit testing +class RoomMemberListRestServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/members$", v1=True) + + def __init__(self, hs): + super().__init__() + self.message_handler = hs.get_message_handler() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request, room_id): + # TODO support Pagination stream API (limit/tokens) + requester = await self.auth.get_user_by_req(request, allow_guest=True) + handler = self.message_handler + + # request the state as of a given event, as identified by a stream token, + # for consistency with /messages etc. + # useful for getting the membership in retrospect as of a given /sync + # response. + at_token_string = parse_string(request, "at") + if at_token_string is None: + at_token = None + else: + at_token = await StreamToken.from_string(self.store, at_token_string) + + # let you filter down on particular memberships. + # XXX: this may not be the best shape for this API - we could pass in a filter + # instead, except filters aren't currently aware of memberships. + # See https://github.com/matrix-org/matrix-doc/issues/1337 for more details. + membership = parse_string(request, "membership") + not_membership = parse_string(request, "not_membership") + + events = await handler.get_state_events( + room_id=room_id, + user_id=requester.user.to_string(), + at_token=at_token, + state_filter=StateFilter.from_types([(EventTypes.Member, None)]), + ) + + chunk = [] + + for event in events: + if (membership and event["content"].get("membership") != membership) or ( + not_membership and event["content"].get("membership") == not_membership + ): + continue + chunk.append(event) + + return 200, {"chunk": chunk} + + +# deprecated in favour of /members?membership=join? +# except it does custom AS logic and has a simpler return format +class JoinedRoomMemberListRestServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/joined_members$", v1=True) + + def __init__(self, hs): + super().__init__() + self.message_handler = hs.get_message_handler() + self.auth = hs.get_auth() + + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + + users_with_profile = await self.message_handler.get_joined_members( + requester, room_id + ) + + return 200, {"joined": users_with_profile} + + +# TODO: Needs better unit testing +class RoomMessageListRestServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/messages$", v1=True) + + def __init__(self, hs): + super().__init__() + self.pagination_handler = hs.get_pagination_handler() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + pagination_config = await PaginationConfig.from_request( + self.store, request, default_limit=10 + ) + as_client_event = b"raw" not in request.args + filter_str = parse_string(request, "filter", encoding="utf-8") + if filter_str: + filter_json = urlparse.unquote(filter_str) + event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) + if ( + event_filter + and event_filter.filter_json.get("event_format", "client") + == "federation" + ): + as_client_event = False + else: + event_filter = None + + msgs = await self.pagination_handler.get_messages( + room_id=room_id, + requester=requester, + pagin_config=pagination_config, + as_client_event=as_client_event, + event_filter=event_filter, + ) + + return 200, msgs + + +# TODO: Needs unit testing +class RoomStateRestServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/state$", v1=True) + + def __init__(self, hs): + super().__init__() + self.message_handler = hs.get_message_handler() + self.auth = hs.get_auth() + + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + # Get all the current state for this room + events = await self.message_handler.get_state_events( + room_id=room_id, + user_id=requester.user.to_string(), + is_guest=requester.is_guest, + ) + return 200, events + + +# TODO: Needs unit testing +class RoomInitialSyncRestServlet(RestServlet): + PATTERNS = client_patterns("/rooms/(?P[^/]*)/initialSync$", v1=True) + + def __init__(self, hs): + super().__init__() + self.initial_sync_handler = hs.get_initial_sync_handler() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + pagination_config = await PaginationConfig.from_request(self.store, request) + content = await self.initial_sync_handler.room_initial_sync( + room_id=room_id, requester=requester, pagin_config=pagination_config + ) + return 200, content + + +class RoomEventServlet(RestServlet): + PATTERNS = client_patterns( + "/rooms/(?P[^/]*)/event/(?P[^/]*)$", v1=True + ) + + def __init__(self, hs): + super().__init__() + self.clock = hs.get_clock() + self.event_handler = hs.get_event_handler() + self._event_serializer = hs.get_event_client_serializer() + self.auth = hs.get_auth() + + async def on_GET(self, request, room_id, event_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + try: + event = await self.event_handler.get_event( + requester.user, room_id, event_id + ) + except AuthError: + # This endpoint is supposed to return a 404 when the requester does + # not have permission to access the event + # https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-r0-rooms-roomid-event-eventid + raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) + + time_now = self.clock.time_msec() + if event: + event = await self._event_serializer.serialize_event(event, time_now) + return 200, event + + return SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) + + +class RoomEventContextServlet(RestServlet): + PATTERNS = client_patterns( + "/rooms/(?P[^/]*)/context/(?P[^/]*)$", v1=True + ) + + def __init__(self, hs): + super().__init__() + self.clock = hs.get_clock() + self.room_context_handler = hs.get_room_context_handler() + self._event_serializer = hs.get_event_client_serializer() + self.auth = hs.get_auth() + + async def on_GET(self, request, room_id, event_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + limit = parse_integer(request, "limit", default=10) + + # picking the API shape for symmetry with /messages + filter_str = parse_string(request, "filter", encoding="utf-8") + if filter_str: + filter_json = urlparse.unquote(filter_str) + event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) + else: + event_filter = None + + results = await self.room_context_handler.get_event_context( + requester, room_id, event_id, limit, event_filter + ) + + if not results: + raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) + + time_now = self.clock.time_msec() + results["events_before"] = await self._event_serializer.serialize_events( + results["events_before"], time_now + ) + results["event"] = await self._event_serializer.serialize_event( + results["event"], time_now + ) + results["events_after"] = await self._event_serializer.serialize_events( + results["events_after"], time_now + ) + results["state"] = await self._event_serializer.serialize_events( + results["state"], + time_now, + # No need to bundle aggregations for state events + bundle_aggregations=False, + ) + + return 200, results + + +class RoomForgetRestServlet(TransactionRestServlet): + def __init__(self, hs): + super().__init__(hs) + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + + def register(self, http_server): + PATTERNS = "/rooms/(?P[^/]*)/forget" + register_txn_path(self, PATTERNS, http_server) + + async def on_POST(self, request, room_id, txn_id=None): + requester = await self.auth.get_user_by_req(request, allow_guest=False) + + await self.room_member_handler.forget(user=requester.user, room_id=room_id) + + return 200, {} + + def on_PUT(self, request, room_id, txn_id): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id, txn_id + ) + + +# TODO: Needs unit testing +class RoomMembershipRestServlet(TransactionRestServlet): + def __init__(self, hs): + super().__init__(hs) + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + + def register(self, http_server): + # /rooms/$roomid/[invite|join|leave] + PATTERNS = ( + "/rooms/(?P[^/]*)/" + "(?Pjoin|invite|leave|ban|unban|kick)" + ) + register_txn_path(self, PATTERNS, http_server) + + async def on_POST(self, request, room_id, membership_action, txn_id=None): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + if requester.is_guest and membership_action not in { + Membership.JOIN, + Membership.LEAVE, + }: + raise AuthError(403, "Guest access not allowed") + + try: + content = parse_json_object_from_request(request) + except Exception: + # Turns out we used to ignore the body entirely, and some clients + # cheekily send invalid bodies. + content = {} + + if membership_action == "invite" and self._has_3pid_invite_keys(content): + try: + await self.room_member_handler.do_3pid_invite( + room_id, + requester.user, + content["medium"], + content["address"], + content["id_server"], + requester, + txn_id, + content.get("id_access_token"), + ) + except ShadowBanError: + # Pretend the request succeeded. + pass + return 200, {} + + target = requester.user + if membership_action in ["invite", "ban", "unban", "kick"]: + assert_params_in_dict(content, ["user_id"]) + target = UserID.from_string(content["user_id"]) + + event_content = None + if "reason" in content: + event_content = {"reason": content["reason"]} + + try: + await self.room_member_handler.update_membership( + requester=requester, + target=target, + room_id=room_id, + action=membership_action, + txn_id=txn_id, + third_party_signed=content.get("third_party_signed", None), + content=event_content, + ) + except ShadowBanError: + # Pretend the request succeeded. + pass + + return_value = {} + + if membership_action == "join": + return_value["room_id"] = room_id + + return 200, return_value + + def _has_3pid_invite_keys(self, content): + for key in {"id_server", "medium", "address"}: + if key not in content: + return False + return True + + def on_PUT(self, request, room_id, membership_action, txn_id): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id, membership_action, txn_id + ) + + +class RoomRedactEventRestServlet(TransactionRestServlet): + def __init__(self, hs): + super().__init__(hs) + self.event_creation_handler = hs.get_event_creation_handler() + self.auth = hs.get_auth() + + def register(self, http_server): + PATTERNS = "/rooms/(?P[^/]*)/redact/(?P[^/]*)" + register_txn_path(self, PATTERNS, http_server) + + async def on_POST(self, request, room_id, event_id, txn_id=None): + requester = await self.auth.get_user_by_req(request) + content = parse_json_object_from_request(request) + + try: + ( + event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + requester, + { + "type": EventTypes.Redaction, + "content": content, + "room_id": room_id, + "sender": requester.user.to_string(), + "redacts": event_id, + }, + txn_id=txn_id, + ) + event_id = event.event_id + except ShadowBanError: + event_id = "$" + random_string(43) + + set_tag("event_id", event_id) + return 200, {"event_id": event_id} + + def on_PUT(self, request, room_id, event_id, txn_id): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id, event_id, txn_id + ) + + +class RoomTypingRestServlet(RestServlet): + PATTERNS = client_patterns( + "/rooms/(?P[^/]*)/typing/(?P[^/]*)$", v1=True + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.hs = hs + self.presence_handler = hs.get_presence_handler() + self.auth = hs.get_auth() + + # If we're not on the typing writer instance we should scream if we get + # requests. + self._is_typing_writer = ( + hs.config.worker.writers.typing == hs.get_instance_name() + ) + + async def on_PUT(self, request, room_id, user_id): + requester = await self.auth.get_user_by_req(request) + + if not self._is_typing_writer: + raise Exception("Got /typing request on instance that is not typing writer") + + room_id = urlparse.unquote(room_id) + target_user = UserID.from_string(urlparse.unquote(user_id)) + + content = parse_json_object_from_request(request) + + await self.presence_handler.bump_presence_active_time(requester.user) + + # Limit timeout to stop people from setting silly typing timeouts. + timeout = min(content.get("timeout", 30000), 120000) + + # Defer getting the typing handler since it will raise on workers. + typing_handler = self.hs.get_typing_writer_handler() + + try: + if content["typing"]: + await typing_handler.started_typing( + target_user=target_user, + requester=requester, + room_id=room_id, + timeout=timeout, + ) + else: + await typing_handler.stopped_typing( + target_user=target_user, requester=requester, room_id=room_id + ) + except ShadowBanError: + # Pretend this worked without error. + pass + + return 200, {} + + +class RoomAliasListServlet(RestServlet): + PATTERNS = [ + re.compile( + r"^/_matrix/client/unstable/org\.matrix\.msc2432" + r"/rooms/(?P[^/]*)/aliases" + ), + ] + list(client_patterns("/rooms/(?P[^/]*)/aliases$", unstable=False)) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.directory_handler = hs.get_directory_handler() + + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + + alias_list = await self.directory_handler.get_aliases_for_room( + requester, room_id + ) + + return 200, {"aliases": alias_list} + + +class SearchRestServlet(RestServlet): + PATTERNS = client_patterns("/search$", v1=True) + + def __init__(self, hs): + super().__init__() + self.search_handler = hs.get_search_handler() + self.auth = hs.get_auth() + + async def on_POST(self, request): + requester = await self.auth.get_user_by_req(request) + + content = parse_json_object_from_request(request) + + batch = parse_string(request, "next_batch") + results = await self.search_handler.search(requester.user, content, batch) + + return 200, results + + +class JoinedRoomsRestServlet(RestServlet): + PATTERNS = client_patterns("/joined_rooms$", v1=True) + + def __init__(self, hs): + super().__init__() + self.store = hs.get_datastore() + self.auth = hs.get_auth() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + room_ids = await self.store.get_rooms_for_user(requester.user.to_string()) + return 200, {"joined_rooms": list(room_ids)} + + +def register_txn_path(servlet, regex_string, http_server, with_get=False): + """Registers a transaction-based path. + + This registers two paths: + PUT regex_string/$txnid + POST regex_string + + Args: + regex_string (str): The regex string to register. Must NOT have a + trailing $ as this string will be appended to. + http_server : The http_server to register paths with. + with_get: True to also register respective GET paths for the PUTs. + """ + http_server.register_paths( + "POST", + client_patterns(regex_string + "$", v1=True), + servlet.on_POST, + servlet.__class__.__name__, + ) + http_server.register_paths( + "PUT", + client_patterns(regex_string + "/(?P[^/]*)$", v1=True), + servlet.on_PUT, + servlet.__class__.__name__, + ) + if with_get: + http_server.register_paths( + "GET", + client_patterns(regex_string + "/(?P[^/]*)$", v1=True), + servlet.on_GET, + servlet.__class__.__name__, + ) + + +class RoomSpaceSummaryRestServlet(RestServlet): + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/org.matrix.msc2946" + "/rooms/(?P[^/]*)/spaces$" + ), + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self._auth = hs.get_auth() + self._room_summary_handler = hs.get_room_summary_handler() + + async def on_GET( + self, request: SynapseRequest, room_id: str + ) -> Tuple[int, JsonDict]: + requester = await self._auth.get_user_by_req(request, allow_guest=True) + + max_rooms_per_space = parse_integer(request, "max_rooms_per_space") + if max_rooms_per_space is not None and max_rooms_per_space < 0: + raise SynapseError( + 400, + "Value for 'max_rooms_per_space' must be a non-negative integer", + Codes.BAD_JSON, + ) + + return 200, await self._room_summary_handler.get_space_summary( + requester.user.to_string(), + room_id, + suggested_only=parse_boolean(request, "suggested_only", default=False), + max_rooms_per_space=max_rooms_per_space, + ) + + # TODO When switching to the stable endpoint, remove the POST handler. + async def on_POST( + self, request: SynapseRequest, room_id: str + ) -> Tuple[int, JsonDict]: + requester = await self._auth.get_user_by_req(request, allow_guest=True) + content = parse_json_object_from_request(request) + + suggested_only = content.get("suggested_only", False) + if not isinstance(suggested_only, bool): + raise SynapseError( + 400, "'suggested_only' must be a boolean", Codes.BAD_JSON + ) + + max_rooms_per_space = content.get("max_rooms_per_space") + if max_rooms_per_space is not None: + if not isinstance(max_rooms_per_space, int): + raise SynapseError( + 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON + ) + if max_rooms_per_space < 0: + raise SynapseError( + 400, + "Value for 'max_rooms_per_space' must be a non-negative integer", + Codes.BAD_JSON, + ) + + return 200, await self._room_summary_handler.get_space_summary( + requester.user.to_string(), + room_id, + suggested_only=suggested_only, + max_rooms_per_space=max_rooms_per_space, + ) + + +class RoomHierarchyRestServlet(RestServlet): + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/org.matrix.msc2946" + "/rooms/(?P[^/]*)/hierarchy$" + ), + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self._auth = hs.get_auth() + self._room_summary_handler = hs.get_room_summary_handler() + + async def on_GET( + self, request: SynapseRequest, room_id: str + ) -> Tuple[int, JsonDict]: + requester = await self._auth.get_user_by_req(request, allow_guest=True) + + max_depth = parse_integer(request, "max_depth") + if max_depth is not None and max_depth < 0: + raise SynapseError( + 400, "'max_depth' must be a non-negative integer", Codes.BAD_JSON + ) + + limit = parse_integer(request, "limit") + if limit is not None and limit <= 0: + raise SynapseError( + 400, "'limit' must be a positive integer", Codes.BAD_JSON + ) + + return 200, await self._room_summary_handler.get_room_hierarchy( + requester.user.to_string(), + room_id, + suggested_only=parse_boolean(request, "suggested_only", default=False), + max_depth=max_depth, + limit=limit, + from_token=parse_string(request, "from"), + ) + + +class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet): + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/im.nheko.summary" + "/rooms/(?P[^/]*)/summary$" + ), + ) + + def __init__(self, hs: "HomeServer"): + super().__init__(hs) + self._auth = hs.get_auth() + self._room_summary_handler = hs.get_room_summary_handler() + + async def on_GET( + self, request: SynapseRequest, room_identifier: str + ) -> Tuple[int, JsonDict]: + try: + requester = await self._auth.get_user_by_req(request, allow_guest=True) + requester_user_id: Optional[str] = requester.user.to_string() + except MissingClientTokenError: + # auth is optional + requester_user_id = None + + # 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, "via", required=False) + room_id, remote_room_hosts = await self.resolve_room_id( + room_identifier, + remote_room_hosts, + ) + + return 200, await self._room_summary_handler.get_room_summary( + requester_user_id, + room_id, + remote_room_hosts, + ) + + +def register_servlets(hs: "HomeServer", http_server, is_worker=False): + RoomStateEventRestServlet(hs).register(http_server) + RoomMemberListRestServlet(hs).register(http_server) + JoinedRoomMemberListRestServlet(hs).register(http_server) + RoomMessageListRestServlet(hs).register(http_server) + JoinRoomAliasServlet(hs).register(http_server) + RoomMembershipRestServlet(hs).register(http_server) + RoomSendEventRestServlet(hs).register(http_server) + PublicRoomListRestServlet(hs).register(http_server) + RoomStateRestServlet(hs).register(http_server) + RoomRedactEventRestServlet(hs).register(http_server) + RoomTypingRestServlet(hs).register(http_server) + RoomEventContextServlet(hs).register(http_server) + RoomSpaceSummaryRestServlet(hs).register(http_server) + RoomHierarchyRestServlet(hs).register(http_server) + if hs.config.experimental.msc3266_enabled: + RoomSummaryRestServlet(hs).register(http_server) + RoomEventServlet(hs).register(http_server) + JoinedRoomsRestServlet(hs).register(http_server) + RoomAliasListServlet(hs).register(http_server) + SearchRestServlet(hs).register(http_server) + + # Some servlets only get registered for the main process. + if not is_worker: + RoomCreateRestServlet(hs).register(http_server) + RoomForgetRestServlet(hs).register(http_server) + + +def register_deprecated_servlets(hs, http_server): + RoomInitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/room_batch.py b/synapse/rest/client/room_batch.py new file mode 100644 index 0000000000..3172aba605 --- /dev/null +++ b/synapse/rest/client/room_batch.py @@ -0,0 +1,441 @@ +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re + +from synapse.api.constants import EventContentFields, EventTypes +from synapse.api.errors import AuthError, Codes, SynapseError +from synapse.appservice import ApplicationService +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, + parse_string, + parse_strings_from_args, +) +from synapse.rest.client.transactions import HttpTransactionCache +from synapse.types import Requester, UserID, create_requester +from synapse.util.stringutils import random_string + +logger = logging.getLogger(__name__) + + +class RoomBatchSendEventRestServlet(RestServlet): + """ + API endpoint which can insert a chunk of events historically back in time + next to the given `prev_event`. + + `chunk_id` comes from `next_chunk_id `in the response of the batch send + endpoint and is derived from the "insertion" events added to each chunk. + It's not required for the first batch send. + + `state_events_at_start` is used to define the historical state events + needed to auth the events like join events. These events will float + outside of the normal DAG as outlier's and won't be visible in the chat + history which also allows us to insert multiple chunks without having a bunch + of `@mxid joined the room` noise between each chunk. + + `events` is chronological chunk/list of events you want to insert. + There is a reverse-chronological constraint on chunks so once you insert + some messages, you can only insert older ones after that. + tldr; Insert chunks from your most recent history -> oldest history. + + POST /_matrix/client/unstable/org.matrix.msc2716/rooms//batch_send?prev_event=&chunk_id= + { + "events": [ ... ], + "state_events_at_start": [ ... ] + } + """ + + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/org.matrix.msc2716" + "/rooms/(?P[^/]*)/batch_send$" + ), + ) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.store = hs.get_datastore() + self.state_store = hs.get_storage().state + self.event_creation_handler = hs.get_event_creation_handler() + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + self.txns = HttpTransactionCache(hs) + + async def _inherit_depth_from_prev_ids(self, prev_event_ids) -> int: + ( + most_recent_prev_event_id, + most_recent_prev_event_depth, + ) = await self.store.get_max_depth_of(prev_event_ids) + + # We want to insert the historical event after the `prev_event` but before the successor event + # + # We inherit depth from the successor event instead of the `prev_event` + # because events returned from `/messages` are first sorted by `topological_ordering` + # which is just the `depth` and then tie-break with `stream_ordering`. + # + # We mark these inserted historical events as "backfilled" which gives them a + # negative `stream_ordering`. If we use the same depth as the `prev_event`, + # then our historical event will tie-break and be sorted before the `prev_event` + # when it should come after. + # + # We want to use the successor event depth so they appear after `prev_event` because + # it has a larger `depth` but before the successor event because the `stream_ordering` + # is negative before the successor event. + successor_event_ids = await self.store.get_successor_events( + [most_recent_prev_event_id] + ) + + # If we can't find any successor events, then it's a forward extremity of + # historical messages and we can just inherit from the previous historical + # event which we can already assume has the correct depth where we want + # to insert into. + if not successor_event_ids: + depth = most_recent_prev_event_depth + else: + ( + _, + oldest_successor_depth, + ) = await self.store.get_min_depth_of(successor_event_ids) + + depth = oldest_successor_depth + + return depth + + def _create_insertion_event_dict( + self, sender: str, room_id: str, origin_server_ts: int + ): + """Creates an event dict for an "insertion" event with the proper fields + and a random chunk ID. + + Args: + sender: The event author MXID + room_id: The room ID that the event belongs to + origin_server_ts: Timestamp when the event was sent + + Returns: + Tuple of event ID and stream ordering position + """ + + next_chunk_id = random_string(8) + insertion_event = { + "type": EventTypes.MSC2716_INSERTION, + "sender": sender, + "room_id": room_id, + "content": { + EventContentFields.MSC2716_NEXT_CHUNK_ID: next_chunk_id, + EventContentFields.MSC2716_HISTORICAL: True, + }, + "origin_server_ts": origin_server_ts, + } + + return insertion_event + + async def _create_requester_for_user_id_from_app_service( + self, user_id: str, app_service: ApplicationService + ) -> Requester: + """Creates a new requester for the given user_id + and validates that the app service is allowed to control + the given user. + + Args: + user_id: The author MXID that the app service is controlling + app_service: The app service that controls the user + + Returns: + Requester object + """ + + await self.auth.validate_appservice_can_control_user_id(app_service, user_id) + + return create_requester(user_id, app_service=app_service) + + async def on_POST(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=False) + + if not requester.app_service: + raise AuthError( + 403, + "Only application services can use the /batchsend endpoint", + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["state_events_at_start", "events"]) + + prev_events_from_query = parse_strings_from_args(request.args, "prev_event") + chunk_id_from_query = parse_string(request, "chunk_id") + + if prev_events_from_query is None: + raise SynapseError( + 400, + "prev_event query parameter is required when inserting historical messages back in time", + errcode=Codes.MISSING_PARAM, + ) + + # For the event we are inserting next to (`prev_events_from_query`), + # find the most recent auth events (derived from state events) that + # allowed that message to be sent. We will use that as a base + # to auth our historical messages against. + ( + most_recent_prev_event_id, + _, + ) = await self.store.get_max_depth_of(prev_events_from_query) + # mapping from (type, state_key) -> state_event_id + prev_state_map = await self.state_store.get_state_ids_for_event( + most_recent_prev_event_id + ) + # List of state event ID's + prev_state_ids = list(prev_state_map.values()) + auth_event_ids = prev_state_ids + + state_events_at_start = [] + for state_event in body["state_events_at_start"]: + assert_params_in_dict( + state_event, ["type", "origin_server_ts", "content", "sender"] + ) + + logger.debug( + "RoomBatchSendEventRestServlet inserting state_event=%s, auth_event_ids=%s", + state_event, + auth_event_ids, + ) + + event_dict = { + "type": state_event["type"], + "origin_server_ts": state_event["origin_server_ts"], + "content": state_event["content"], + "room_id": room_id, + "sender": state_event["sender"], + "state_key": state_event["state_key"], + } + + # Mark all events as historical + event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True + + # Make the state events float off on their own + fake_prev_event_id = "$" + random_string(43) + + # TODO: This is pretty much the same as some other code to handle inserting state in this file + if event_dict["type"] == EventTypes.Member: + membership = event_dict["content"].get("membership", None) + event_id, _ = await self.room_member_handler.update_membership( + await self._create_requester_for_user_id_from_app_service( + state_event["sender"], requester.app_service + ), + target=UserID.from_string(event_dict["state_key"]), + room_id=room_id, + action=membership, + content=event_dict["content"], + outlier=True, + prev_event_ids=[fake_prev_event_id], + # Make sure to use a copy of this list because we modify it + # later in the loop here. Otherwise it will be the same + # reference and also update in the event when we append later. + auth_event_ids=auth_event_ids.copy(), + ) + else: + # TODO: Add some complement tests that adds state that is not member joins + # and will use this code path. Maybe we only want to support join state events + # and can get rid of this `else`? + ( + event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + await self._create_requester_for_user_id_from_app_service( + state_event["sender"], requester.app_service + ), + event_dict, + outlier=True, + prev_event_ids=[fake_prev_event_id], + # Make sure to use a copy of this list because we modify it + # later in the loop here. Otherwise it will be the same + # reference and also update in the event when we append later. + auth_event_ids=auth_event_ids.copy(), + ) + event_id = event.event_id + + state_events_at_start.append(event_id) + auth_event_ids.append(event_id) + + events_to_create = body["events"] + + inherited_depth = await self._inherit_depth_from_prev_ids( + prev_events_from_query + ) + + # Figure out which chunk to connect to. If they passed in + # chunk_id_from_query let's use it. The chunk ID passed in comes + # from the chunk_id in the "insertion" event from the previous chunk. + last_event_in_chunk = events_to_create[-1] + chunk_id_to_connect_to = chunk_id_from_query + base_insertion_event = None + if chunk_id_from_query: + # All but the first base insertion event should point at a fake + # event, which causes the HS to ask for the state at the start of + # the chunk later. + prev_event_ids = [fake_prev_event_id] + # TODO: Verify the chunk_id_from_query corresponds to an insertion event + pass + # Otherwise, create an insertion event to act as a starting point. + # + # We don't always have an insertion event to start hanging more history + # off of (ideally there would be one in the main DAG, but that's not the + # case if we're wanting to add history to e.g. existing rooms without + # an insertion event), in which case we just create a new insertion event + # that can then get pointed to by a "marker" event later. + else: + prev_event_ids = prev_events_from_query + + base_insertion_event_dict = self._create_insertion_event_dict( + sender=requester.user.to_string(), + room_id=room_id, + origin_server_ts=last_event_in_chunk["origin_server_ts"], + ) + base_insertion_event_dict["prev_events"] = prev_event_ids.copy() + + ( + base_insertion_event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + await self._create_requester_for_user_id_from_app_service( + base_insertion_event_dict["sender"], + requester.app_service, + ), + base_insertion_event_dict, + prev_event_ids=base_insertion_event_dict.get("prev_events"), + auth_event_ids=auth_event_ids, + historical=True, + depth=inherited_depth, + ) + + chunk_id_to_connect_to = base_insertion_event["content"][ + EventContentFields.MSC2716_NEXT_CHUNK_ID + ] + + # Connect this current chunk to the insertion event from the previous chunk + chunk_event = { + "type": EventTypes.MSC2716_CHUNK, + "sender": requester.user.to_string(), + "room_id": room_id, + "content": { + EventContentFields.MSC2716_CHUNK_ID: chunk_id_to_connect_to, + EventContentFields.MSC2716_HISTORICAL: True, + }, + # Since the chunk event is put at the end of the chunk, + # where the newest-in-time event is, copy the origin_server_ts from + # the last event we're inserting + "origin_server_ts": last_event_in_chunk["origin_server_ts"], + } + # Add the chunk event to the end of the chunk (newest-in-time) + events_to_create.append(chunk_event) + + # Add an "insertion" event to the start of each chunk (next to the oldest-in-time + # event in the chunk) so the next chunk can be connected to this one. + insertion_event = self._create_insertion_event_dict( + sender=requester.user.to_string(), + room_id=room_id, + # Since the insertion event is put at the start of the chunk, + # where the oldest-in-time event is, copy the origin_server_ts from + # the first event we're inserting + origin_server_ts=events_to_create[0]["origin_server_ts"], + ) + # Prepend the insertion event to the start of the chunk (oldest-in-time) + events_to_create = [insertion_event] + events_to_create + + event_ids = [] + events_to_persist = [] + for ev in events_to_create: + assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"]) + + event_dict = { + "type": ev["type"], + "origin_server_ts": ev["origin_server_ts"], + "content": ev["content"], + "room_id": room_id, + "sender": ev["sender"], # requester.user.to_string(), + "prev_events": prev_event_ids.copy(), + } + + # Mark all events as historical + event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True + + event, context = await self.event_creation_handler.create_event( + await self._create_requester_for_user_id_from_app_service( + ev["sender"], requester.app_service + ), + event_dict, + prev_event_ids=event_dict.get("prev_events"), + auth_event_ids=auth_event_ids, + historical=True, + depth=inherited_depth, + ) + logger.debug( + "RoomBatchSendEventRestServlet inserting event=%s, prev_event_ids=%s, auth_event_ids=%s", + event, + prev_event_ids, + auth_event_ids, + ) + + assert self.hs.is_mine_id(event.sender), "User must be our own: %s" % ( + event.sender, + ) + + events_to_persist.append((event, context)) + event_id = event.event_id + + event_ids.append(event_id) + prev_event_ids = [event_id] + + # Persist events in reverse-chronological order so they have the + # correct stream_ordering as they are backfilled (which decrements). + # Events are sorted by (topological_ordering, stream_ordering) + # where topological_ordering is just depth. + for (event, context) in reversed(events_to_persist): + ev = await self.event_creation_handler.handle_new_client_event( + await self._create_requester_for_user_id_from_app_service( + event["sender"], requester.app_service + ), + event=event, + context=context, + ) + + # Add the base_insertion_event to the bottom of the list we return + if base_insertion_event is not None: + event_ids.append(base_insertion_event.event_id) + + return 200, { + "state_events": state_events_at_start, + "events": event_ids, + "next_chunk_id": insertion_event["content"][ + EventContentFields.MSC2716_NEXT_CHUNK_ID + ], + } + + def on_GET(self, request, room_id): + return 501, "Not implemented" + + def on_PUT(self, request, room_id): + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id + ) + + +def register_servlets(hs, http_server): + msc2716_enabled = hs.config.experimental.msc2716_enabled + + if msc2716_enabled: + RoomBatchSendEventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/room_keys.py b/synapse/rest/client/room_keys.py new file mode 100644 index 0000000000..263596be86 --- /dev/null +++ b/synapse/rest/client/room_keys.py @@ -0,0 +1,391 @@ +# Copyright 2017, 2018 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import Codes, NotFoundError, SynapseError +from synapse.http.servlet import ( + RestServlet, + parse_json_object_from_request, + parse_string, +) + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class RoomKeysServlet(RestServlet): + PATTERNS = client_patterns( + "/room_keys/keys(/(?P[^/]+))?(/(?P[^/]+))?$" + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.auth = hs.get_auth() + self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() + + async def on_PUT(self, request, room_id, session_id): + """ + Uploads one or more encrypted E2E room keys for backup purposes. + room_id: the ID of the room the keys are for (optional) + session_id: the ID for the E2E room keys for the room (optional) + version: the version of the user's backup which this data is for. + the version must already have been created via the /room_keys/version API. + + Each session has: + * first_message_index: a numeric index indicating the oldest message + encrypted by this session. + * forwarded_count: how many times the uploading client claims this key + has been shared (forwarded) + * is_verified: whether the client that uploaded the keys claims they + were sent by a device which they've verified + * session_data: base64-encrypted data describing the session. + + Returns 200 OK on success with body {} + Returns 403 Forbidden if the version in question is not the most recently + created version (i.e. if this is an old client trying to write to a stale backup) + Returns 404 Not Found if the version in question doesn't exist + + The API is designed to be otherwise agnostic to the room_key encryption + algorithm being used. Sessions are merged with existing ones in the + backup using the heuristics: + * is_verified sessions always win over unverified sessions + * older first_message_index always win over newer sessions + * lower forwarded_count always wins over higher forwarded_count + + We trust the clients not to lie and corrupt their own backups. + It also means that if your access_token is stolen, the attacker could + delete your backup. + + POST /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1 + Content-Type: application/json + + { + "first_message_index": 1, + "forwarded_count": 1, + "is_verified": false, + "session_data": "SSBBTSBBIEZJU0gK" + } + + Or... + + POST /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1 + Content-Type: application/json + + { + "sessions": { + "c0ff33": { + "first_message_index": 1, + "forwarded_count": 1, + "is_verified": false, + "session_data": "SSBBTSBBIEZJU0gK" + } + } + } + + Or... + + POST /room_keys/keys?version=1 HTTP/1.1 + Content-Type: application/json + + { + "rooms": { + "!abc:matrix.org": { + "sessions": { + "c0ff33": { + "first_message_index": 1, + "forwarded_count": 1, + "is_verified": false, + "session_data": "SSBBTSBBIEZJU0gK" + } + } + } + } + } + """ + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + version = parse_string(request, "version") + + if session_id: + body = {"sessions": {session_id: body}} + + if room_id: + body = {"rooms": {room_id: body}} + + ret = await self.e2e_room_keys_handler.upload_room_keys(user_id, version, body) + return 200, ret + + async def on_GET(self, request, room_id, session_id): + """ + Retrieves one or more encrypted E2E room keys for backup purposes. + Symmetric with the PUT version of the API. + + room_id: the ID of the room to retrieve the keys for (optional) + session_id: the ID for the E2E room keys to retrieve the keys for (optional) + version: the version of the user's backup which this data is for. + the version must already have been created via the /change_secret API. + + Returns as follows: + + GET /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1 + { + "first_message_index": 1, + "forwarded_count": 1, + "is_verified": false, + "session_data": "SSBBTSBBIEZJU0gK" + } + + Or... + + GET /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1 + { + "sessions": { + "c0ff33": { + "first_message_index": 1, + "forwarded_count": 1, + "is_verified": false, + "session_data": "SSBBTSBBIEZJU0gK" + } + } + } + + Or... + + GET /room_keys/keys?version=1 HTTP/1.1 + { + "rooms": { + "!abc:matrix.org": { + "sessions": { + "c0ff33": { + "first_message_index": 1, + "forwarded_count": 1, + "is_verified": false, + "session_data": "SSBBTSBBIEZJU0gK" + } + } + } + } + } + """ + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + version = parse_string(request, "version", required=True) + + room_keys = await self.e2e_room_keys_handler.get_room_keys( + user_id, version, room_id, session_id + ) + + # Convert room_keys to the right format to return. + if session_id: + # If the client requests a specific session, but that session was + # not backed up, then return an M_NOT_FOUND. + if room_keys["rooms"] == {}: + raise NotFoundError("No room_keys found") + else: + room_keys = room_keys["rooms"][room_id]["sessions"][session_id] + elif room_id: + # If the client requests all sessions from a room, but no sessions + # are found, then return an empty result rather than an error, so + # that clients don't have to handle an error condition, and an + # empty result is valid. (Similarly if the client requests all + # sessions from the backup, but in that case, room_keys is already + # in the right format, so we don't need to do anything about it.) + if room_keys["rooms"] == {}: + room_keys = {"sessions": {}} + else: + room_keys = room_keys["rooms"][room_id] + + return 200, room_keys + + async def on_DELETE(self, request, room_id, session_id): + """ + Deletes one or more encrypted E2E room keys for a user for backup purposes. + + DELETE /room_keys/keys/!abc:matrix.org/c0ff33?version=1 + HTTP/1.1 200 OK + {} + + room_id: the ID of the room whose keys to delete (optional) + session_id: the ID for the E2E session to delete (optional) + version: the version of the user's backup which this data is for. + the version must already have been created via the /change_secret API. + """ + + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + version = parse_string(request, "version") + + ret = await self.e2e_room_keys_handler.delete_room_keys( + user_id, version, room_id, session_id + ) + return 200, ret + + +class RoomKeysNewVersionServlet(RestServlet): + PATTERNS = client_patterns("/room_keys/version$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.auth = hs.get_auth() + self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() + + async def on_POST(self, request): + """ + Create a new backup version for this user's room_keys with the given + info. The version is allocated by the server and returned to the user + in the response. This API is intended to be used whenever the user + changes the encryption key for their backups, ensuring that backups + encrypted with different keys don't collide. + + It takes out an exclusive lock on this user's room_key backups, to ensure + clients only upload to the current backup. + + The algorithm passed in the version info is a reverse-DNS namespaced + identifier to describe the format of the encrypted backupped keys. + + The auth_data is { user_id: "user_id", nonce: } + encrypted using the algorithm and current encryption key described above. + + POST /room_keys/version + Content-Type: application/json + { + "algorithm": "m.megolm_backup.v1", + "auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K" + } + + HTTP/1.1 200 OK + Content-Type: application/json + { + "version": 12345 + } + """ + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + info = parse_json_object_from_request(request) + + new_version = await self.e2e_room_keys_handler.create_version(user_id, info) + return 200, {"version": new_version} + + # we deliberately don't have a PUT /version, as these things really should + # be immutable to avoid people footgunning + + +class RoomKeysVersionServlet(RestServlet): + PATTERNS = client_patterns("/room_keys/version(/(?P[^/]+))?$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.auth = hs.get_auth() + self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() + + async def on_GET(self, request, version): + """ + Retrieve the version information about a given version of the user's + room_keys backup. If the version part is missing, returns info about the + most current backup version (if any) + + It takes out an exclusive lock on this user's room_key backups, to ensure + clients only upload to the current backup. + + Returns 404 if the given version does not exist. + + GET /room_keys/version/12345 HTTP/1.1 + { + "version": "12345", + "algorithm": "m.megolm_backup.v1", + "auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K" + } + """ + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + + try: + info = await self.e2e_room_keys_handler.get_version_info(user_id, version) + except SynapseError as e: + if e.code == 404: + raise SynapseError(404, "No backup found", Codes.NOT_FOUND) + return 200, info + + async def on_DELETE(self, request, version): + """ + Delete the information about a given version of the user's + room_keys backup. If the version part is missing, deletes the most + current backup version (if any). Doesn't delete the actual room data. + + DELETE /room_keys/version/12345 HTTP/1.1 + HTTP/1.1 200 OK + {} + """ + if version is None: + raise SynapseError(400, "No version specified to delete", Codes.NOT_FOUND) + + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + + await self.e2e_room_keys_handler.delete_version(user_id, version) + return 200, {} + + async def on_PUT(self, request, version): + """ + Update the information about a given version of the user's room_keys backup. + + POST /room_keys/version/12345 HTTP/1.1 + Content-Type: application/json + { + "algorithm": "m.megolm_backup.v1", + "auth_data": { + "public_key": "abcdefg", + "signatures": { + "ed25519:something": "hijklmnop" + } + }, + "version": "12345" + } + + HTTP/1.1 200 OK + Content-Type: application/json + {} + """ + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + info = parse_json_object_from_request(request) + + if version is None: + raise SynapseError( + 400, "No version specified to update", Codes.MISSING_PARAM + ) + + await self.e2e_room_keys_handler.update_version(user_id, version, info) + return 200, {} + + +def register_servlets(hs, http_server): + RoomKeysServlet(hs).register(http_server) + RoomKeysVersionServlet(hs).register(http_server) + RoomKeysNewVersionServlet(hs).register(http_server) diff --git a/synapse/rest/client/room_upgrade_rest_servlet.py b/synapse/rest/client/room_upgrade_rest_servlet.py new file mode 100644 index 0000000000..6d1b083acb --- /dev/null +++ b/synapse/rest/client/room_upgrade_rest_servlet.py @@ -0,0 +1,88 @@ +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import Codes, ShadowBanError, SynapseError +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, +) +from synapse.util import stringutils + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class RoomUpgradeRestServlet(RestServlet): + """Handler for room upgrade requests. + + Handles requests of the form: + + POST /_matrix/client/r0/rooms/$roomid/upgrade HTTP/1.1 + Content-Type: application/json + + { + "new_version": "2", + } + + Creates a new room and shuts down the old one. Returns the ID of the new room. + + Args: + hs (synapse.server.HomeServer): + """ + + PATTERNS = client_patterns( + # /rooms/$roomid/upgrade + "/rooms/(?P[^/]*)/upgrade$" + ) + + def __init__(self, hs): + super().__init__() + self._hs = hs + self._room_creation_handler = hs.get_room_creation_handler() + self._auth = hs.get_auth() + + async def on_POST(self, request, room_id): + requester = await self._auth.get_user_by_req(request) + + content = parse_json_object_from_request(request) + assert_params_in_dict(content, ("new_version",)) + + new_version = KNOWN_ROOM_VERSIONS.get(content["new_version"]) + if new_version is None: + raise SynapseError( + 400, + "Your homeserver does not support this room version", + Codes.UNSUPPORTED_ROOM_VERSION, + ) + + try: + new_room_id = await self._room_creation_handler.upgrade_room( + requester, room_id, new_version + ) + except ShadowBanError: + # Generate a random room ID. + new_room_id = stringutils.random_string(18) + + ret = {"replacement_room": new_room_id} + + return 200, ret + + +def register_servlets(hs, http_server): + RoomUpgradeRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/sendtodevice.py b/synapse/rest/client/sendtodevice.py new file mode 100644 index 0000000000..d537d811d8 --- /dev/null +++ b/synapse/rest/client/sendtodevice.py @@ -0,0 +1,67 @@ +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Tuple + +from synapse.http import servlet +from synapse.http.servlet import assert_params_in_dict, parse_json_object_from_request +from synapse.logging.opentracing import set_tag, trace +from synapse.rest.client.transactions import HttpTransactionCache + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class SendToDeviceRestServlet(servlet.RestServlet): + PATTERNS = client_patterns( + "/sendToDevice/(?P[^/]*)/(?P[^/]*)$" + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.txns = HttpTransactionCache(hs) + self.device_message_handler = hs.get_device_message_handler() + + @trace(opname="sendToDevice") + def on_PUT(self, request, message_type, txn_id): + set_tag("message_type", message_type) + set_tag("txn_id", txn_id) + return self.txns.fetch_or_execute_request( + request, self._put, request, message_type, txn_id + ) + + async def _put(self, request, message_type, txn_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + content = parse_json_object_from_request(request) + assert_params_in_dict(content, ("messages",)) + + await self.device_message_handler.send_device_message( + requester, message_type, content["messages"] + ) + + response: Tuple[int, dict] = (200, {}) + return response + + +def register_servlets(hs, http_server): + SendToDeviceRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/shared_rooms.py b/synapse/rest/client/shared_rooms.py new file mode 100644 index 0000000000..d2e7f04b40 --- /dev/null +++ b/synapse/rest/client/shared_rooms.py @@ -0,0 +1,67 @@ +# Copyright 2020 Half-Shot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from synapse.api.errors import Codes, SynapseError +from synapse.http.servlet import RestServlet +from synapse.types import UserID + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class UserSharedRoomsServlet(RestServlet): + """ + GET /uk.half-shot.msc2666/user/shared_rooms/{user_id} HTTP/1.1 + """ + + PATTERNS = client_patterns( + "/uk.half-shot.msc2666/user/shared_rooms/(?P[^/]*)", + releases=(), # This is an unstable feature + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.user_directory_active = hs.config.update_user_directory + + async def on_GET(self, request, user_id): + + if not self.user_directory_active: + raise SynapseError( + code=400, + msg="The user directory is disabled on this server. Cannot determine shared rooms.", + errcode=Codes.FORBIDDEN, + ) + + UserID.from_string(user_id) + + requester = await self.auth.get_user_by_req(request) + if user_id == requester.user.to_string(): + raise SynapseError( + code=400, + msg="You cannot request a list of shared rooms with yourself", + errcode=Codes.FORBIDDEN, + ) + rooms = await self.store.get_shared_rooms_for_users( + requester.user.to_string(), user_id + ) + + return 200, {"joined": list(rooms)} + + +def register_servlets(hs, http_server): + UserSharedRoomsServlet(hs).register(http_server) diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py new file mode 100644 index 0000000000..e18f4d01b3 --- /dev/null +++ b/synapse/rest/client/sync.py @@ -0,0 +1,532 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import itertools +import logging +from collections import defaultdict +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple + +from synapse.api.constants import Membership, PresenceState +from synapse.api.errors import Codes, StoreError, SynapseError +from synapse.api.filtering import DEFAULT_FILTER_COLLECTION, FilterCollection +from synapse.events.utils import ( + format_event_for_client_v2_without_room_id, + format_event_raw, +) +from synapse.handlers.presence import format_user_presence_state +from synapse.handlers.sync import KnockedSyncResult, SyncConfig +from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string +from synapse.http.site import SynapseRequest +from synapse.types import JsonDict, StreamToken +from synapse.util import json_decoder + +from ._base import client_patterns, set_timeline_upper_limit + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class SyncRestServlet(RestServlet): + """ + + GET parameters:: + timeout(int): How long to wait for new events in milliseconds. + since(batch_token): Batch token when asking for incremental deltas. + set_presence(str): What state the device presence should be set to. + default is "online". + filter(filter_id): A filter to apply to the events returned. + + Response JSON:: + { + "next_batch": // batch token for the next /sync + "presence": // presence data for the user. + "rooms": { + "join": { // Joined rooms being updated. + "${room_id}": { // Id of the room being updated + "event_map": // Map of EventID -> event JSON. + "timeline": { // The recent events in the room if gap is "true" + "limited": // Was the per-room event limit exceeded? + // otherwise the next events in the room. + "events": [] // list of EventIDs in the "event_map". + "prev_batch": // back token for getting previous events. + } + "state": {"events": []} // list of EventIDs updating the + // current state to be what it should + // be at the end of the batch. + "ephemeral": {"events": []} // list of event objects + } + }, + "invite": {}, // Invited rooms being updated. + "leave": {} // Archived rooms being updated. + } + } + """ + + PATTERNS = client_patterns("/sync$") + ALLOWED_PRESENCE = {"online", "offline", "unavailable"} + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.sync_handler = hs.get_sync_handler() + self.clock = hs.get_clock() + self.filtering = hs.get_filtering() + self.presence_handler = hs.get_presence_handler() + self._server_notices_sender = hs.get_server_notices_sender() + self._event_serializer = hs.get_event_client_serializer() + + 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 + + if b"from" in request.args: + # /events used to use 'from', but /sync uses 'since'. + # Lets be helpful and whine if we see a 'from'. + raise SynapseError( + 400, "'from' is not a valid query parameter. Did you mean 'since'?" + ) + + requester = await self.auth.get_user_by_req(request, allow_guest=True) + user = requester.user + device_id = requester.device_id + + timeout = parse_integer(request, "timeout", default=0) + since = parse_string(request, "since") + set_presence = parse_string( + request, + "set_presence", + default="online", + allowed_values=self.ALLOWED_PRESENCE, + ) + filter_id = parse_string(request, "filter") + full_state = parse_boolean(request, "full_state", default=False) + + logger.debug( + "/sync: user=%r, timeout=%r, since=%r, " + "set_presence=%r, filter_id=%r, device_id=%r", + user, + timeout, + since, + set_presence, + filter_id, + device_id, + ) + + request_key = (user, timeout, since, filter_id, full_state, device_id) + + if filter_id is None: + filter_collection = DEFAULT_FILTER_COLLECTION + elif filter_id.startswith("{"): + try: + filter_object = json_decoder.decode(filter_id) + set_timeline_upper_limit( + filter_object, self.hs.config.filter_timeline_limit + ) + except Exception: + raise SynapseError(400, "Invalid filter JSON") + self.filtering.check_valid_filter(filter_object) + filter_collection = FilterCollection(filter_object) + else: + try: + filter_collection = await self.filtering.get_user_filter( + user.localpart, filter_id + ) + except StoreError as err: + if err.code != 404: + raise + # fix up the description and errcode to be more useful + raise SynapseError(400, "No such filter", errcode=Codes.INVALID_PARAM) + + sync_config = SyncConfig( + user=user, + filter_collection=filter_collection, + is_guest=requester.is_guest, + request_key=request_key, + device_id=device_id, + ) + + since_token = None + if since is not None: + since_token = await StreamToken.from_string(self.store, since) + + # 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 + + if affect_presence: + await self.presence_handler.set_state( + user, {"presence": set_presence}, True + ) + + context = await self.presence_handler.user_syncing( + user.to_string(), affect_presence=affect_presence + ) + with context: + sync_result = await self.sync_handler.wait_for_sync_for_user( + requester, + sync_config, + since_token=since_token, + timeout=timeout, + full_state=full_state, + ) + + # the client may have disconnected by now; don't bother to serialize the + # response if so. + if request._disconnected: + logger.info("Client has disconnected; not serializing response.") + return 200, {} + + time_now = self.clock.time_msec() + response_content = await self.encode_response( + time_now, sync_result, requester.access_token_id, filter_collection + ) + + logger.debug("Event formatting complete") + return 200, response_content + + async def encode_response(self, time_now, sync_result, access_token_id, filter): + logger.debug("Formatting events in sync response") + if filter.event_format == "client": + event_formatter = format_event_for_client_v2_without_room_id + elif filter.event_format == "federation": + event_formatter = format_event_raw + else: + raise Exception("Unknown event format %s" % (filter.event_format,)) + + joined = await self.encode_joined( + sync_result.joined, + time_now, + access_token_id, + filter.event_fields, + event_formatter, + ) + + invited = await self.encode_invited( + sync_result.invited, time_now, access_token_id, event_formatter + ) + + knocked = await self.encode_knocked( + sync_result.knocked, time_now, access_token_id, event_formatter + ) + + archived = await self.encode_archived( + sync_result.archived, + time_now, + access_token_id, + filter.event_fields, + event_formatter, + ) + + logger.debug("building sync response dict") + + response: dict = defaultdict(dict) + response["next_batch"] = await sync_result.next_batch.to_string(self.store) + + if sync_result.account_data: + response["account_data"] = {"events": sync_result.account_data} + if sync_result.presence: + response["presence"] = SyncRestServlet.encode_presence( + sync_result.presence, time_now + ) + + if sync_result.to_device: + response["to_device"] = {"events": sync_result.to_device} + + if sync_result.device_lists.changed: + response["device_lists"]["changed"] = list(sync_result.device_lists.changed) + if sync_result.device_lists.left: + response["device_lists"]["left"] = list(sync_result.device_lists.left) + + # We always include this because https://github.com/vector-im/element-android/issues/3725 + # The spec isn't terribly clear on when this can be omitted and how a client would tell + # the difference between "no keys present" and "nothing changed" in terms of whole field + # absent / individual key type entry absent + # Corresponding synapse issue: https://github.com/matrix-org/synapse/issues/10456 + response["device_one_time_keys_count"] = sync_result.device_one_time_keys_count + + # https://github.com/matrix-org/matrix-doc/blob/54255851f642f84a4f1aaf7bc063eebe3d76752b/proposals/2732-olm-fallback-keys.md + # states that this field should always be included, as long as the server supports the feature. + response[ + "org.matrix.msc2732.device_unused_fallback_key_types" + ] = sync_result.device_unused_fallback_key_types + + if joined: + response["rooms"][Membership.JOIN] = joined + if invited: + response["rooms"][Membership.INVITE] = invited + if knocked: + response["rooms"][Membership.KNOCK] = knocked + if archived: + response["rooms"][Membership.LEAVE] = archived + + if sync_result.groups.join: + response["groups"][Membership.JOIN] = sync_result.groups.join + if sync_result.groups.invite: + response["groups"][Membership.INVITE] = sync_result.groups.invite + if sync_result.groups.leave: + response["groups"][Membership.LEAVE] = sync_result.groups.leave + + return response + + @staticmethod + def encode_presence(events, time_now): + return { + "events": [ + { + "type": "m.presence", + "sender": event.user_id, + "content": format_user_presence_state( + event, time_now, include_user_id=False + ), + } + for event in events + ] + } + + async def encode_joined( + self, rooms, time_now, token_id, event_fields, event_formatter + ): + """ + Encode the joined rooms in a sync result + + Args: + rooms(list[synapse.handlers.sync.JoinedSyncResult]): list of sync + results for rooms this user is joined to + time_now(int): current time - used as a baseline for age + calculations + token_id(int): ID of the user's auth token - used for namespacing + of transaction IDs + event_fields(list): List of event fields to include. If empty, + all fields will be returned. + event_formatter (func[dict]): function to convert from federation format + to client format + Returns: + dict[str, dict[str, object]]: the joined rooms list, in our + response format + """ + joined = {} + for room in rooms: + joined[room.room_id] = await self.encode_room( + room, + time_now, + token_id, + joined=True, + only_fields=event_fields, + event_formatter=event_formatter, + ) + + return joined + + async def encode_invited(self, rooms, time_now, token_id, event_formatter): + """ + Encode the invited rooms in a sync result + + Args: + rooms(list[synapse.handlers.sync.InvitedSyncResult]): list of + sync results for rooms this user is invited to + time_now(int): current time - used as a baseline for age + calculations + token_id(int): ID of the user's auth token - used for namespacing + of transaction IDs + event_formatter (func[dict]): function to convert from federation format + to client format + + Returns: + dict[str, dict[str, object]]: the invited rooms list, in our + response format + """ + invited = {} + for room in rooms: + invite = await self._event_serializer.serialize_event( + room.invite, + time_now, + token_id=token_id, + event_format=event_formatter, + include_stripped_room_state=True, + ) + unsigned = dict(invite.get("unsigned", {})) + invite["unsigned"] = unsigned + invited_state = list(unsigned.pop("invite_room_state", [])) + invited_state.append(invite) + invited[room.room_id] = {"invite_state": {"events": invited_state}} + + return invited + + async def encode_knocked( + self, + rooms: List[KnockedSyncResult], + time_now: int, + token_id: int, + event_formatter: Callable[[Dict], Dict], + ) -> Dict[str, Dict[str, Any]]: + """ + Encode the rooms we've knocked on in a sync result. + + Args: + rooms: list of sync results for rooms this user is knocking on + time_now: current time - used as a baseline for age calculations + token_id: ID of the user's auth token - used for namespacing of transaction IDs + event_formatter: function to convert from federation format to client format + + Returns: + The list of rooms the user has knocked on, in our response format. + """ + knocked = {} + for room in rooms: + knock = await self._event_serializer.serialize_event( + room.knock, + time_now, + token_id=token_id, + event_format=event_formatter, + include_stripped_room_state=True, + ) + + # Extract the `unsigned` key from the knock event. + # This is where we (cheekily) store the knock state events + unsigned = knock.setdefault("unsigned", {}) + + # Duplicate the dictionary in order to avoid modifying the original + unsigned = dict(unsigned) + + # 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", [])) + + # Append the actual knock membership event itself as well. This provides + # the client with: + # + # * A knock state event that they can use for easier internal tracking + # * The rough timestamp of when the knock occurred contained within the event + knocked_state.append(knock) + + # Build the `knock_state` dictionary, which will contain the state of the + # room that the client has knocked on + knocked[room.room_id] = {"knock_state": {"events": knocked_state}} + + return knocked + + async def encode_archived( + self, rooms, time_now, token_id, event_fields, event_formatter + ): + """ + Encode the archived rooms in a sync result + + Args: + rooms (list[synapse.handlers.sync.ArchivedSyncResult]): list of + sync results for rooms this user is joined to + time_now(int): current time - used as a baseline for age + calculations + token_id(int): ID of the user's auth token - used for namespacing + of transaction IDs + event_fields(list): List of event fields to include. If empty, + all fields will be returned. + event_formatter (func[dict]): function to convert from federation format + to client format + Returns: + dict[str, dict[str, object]]: The invited rooms list, in our + response format + """ + joined = {} + for room in rooms: + joined[room.room_id] = await self.encode_room( + room, + time_now, + token_id, + joined=False, + only_fields=event_fields, + event_formatter=event_formatter, + ) + + return joined + + async def encode_room( + self, room, time_now, token_id, joined, only_fields, event_formatter + ): + """ + Args: + room (JoinedSyncResult|ArchivedSyncResult): sync result for a + single room + time_now (int): current time - used as a baseline for age + calculations + token_id (int): ID of the user's auth token - used for namespacing + of transaction IDs + joined (bool): True if the user is joined to this room - will mean + we handle ephemeral events + only_fields(list): Optional. The list of event fields to include. + event_formatter (func[dict]): function to convert from federation format + to client format + Returns: + dict[str, object]: the room, encoded in our response format + """ + + def serialize(events): + return self._event_serializer.serialize_events( + events, + time_now=time_now, + # We don't bundle "live" events, as otherwise clients + # will end up double counting annotations. + bundle_aggregations=False, + token_id=token_id, + event_format=event_formatter, + only_event_fields=only_fields, + ) + + state_dict = room.state + timeline_events = room.timeline.events + + state_events = state_dict.values() + + for event in itertools.chain(state_events, timeline_events): + # We've had bug reports that events were coming down under the + # wrong room. + if event.room_id != room.room_id: + logger.warning( + "Event %r is under room %r instead of %r", + event.event_id, + room.room_id, + event.room_id, + ) + + serialized_state = await serialize(state_events) + serialized_timeline = await serialize(timeline_events) + + account_data = room.account_data + + result = { + "timeline": { + "events": serialized_timeline, + "prev_batch": await room.timeline.prev_batch.to_string(self.store), + "limited": room.timeline.limited, + }, + "state": {"events": serialized_state}, + "account_data": {"events": account_data}, + } + + if joined: + ephemeral_events = room.ephemeral + result["ephemeral"] = {"events": ephemeral_events} + result["unread_notifications"] = room.unread_notifications + result["summary"] = room.summary + result["org.matrix.msc2654.unread_count"] = room.unread_count + + return result + + +def register_servlets(hs, http_server): + SyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/tags.py b/synapse/rest/client/tags.py new file mode 100644 index 0000000000..c14f83be18 --- /dev/null +++ b/synapse/rest/client/tags.py @@ -0,0 +1,85 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import AuthError +from synapse.http.servlet import RestServlet, parse_json_object_from_request + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class TagListServlet(RestServlet): + """ + GET /user/{user_id}/rooms/{room_id}/tags HTTP/1.1 + """ + + PATTERNS = client_patterns("/user/(?P[^/]*)/rooms/(?P[^/]*)/tags") + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request, user_id, room_id): + requester = await self.auth.get_user_by_req(request) + if user_id != requester.user.to_string(): + raise AuthError(403, "Cannot get tags for other users.") + + tags = await self.store.get_tags_for_room(user_id, room_id) + + return 200, {"tags": tags} + + +class TagServlet(RestServlet): + """ + PUT /user/{user_id}/rooms/{room_id}/tags/{tag} HTTP/1.1 + DELETE /user/{user_id}/rooms/{room_id}/tags/{tag} HTTP/1.1 + """ + + PATTERNS = client_patterns( + "/user/(?P[^/]*)/rooms/(?P[^/]*)/tags/(?P[^/]*)" + ) + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + self.handler = hs.get_account_data_handler() + + async def on_PUT(self, request, user_id, room_id, tag): + 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.") + + body = parse_json_object_from_request(request) + + await self.handler.add_tag_to_room(user_id, room_id, tag, body) + + return 200, {} + + async def on_DELETE(self, request, user_id, room_id, tag): + 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.") + + await self.handler.remove_tag_from_room(user_id, room_id, tag) + + return 200, {} + + +def register_servlets(hs, http_server): + TagListServlet(hs).register(http_server) + TagServlet(hs).register(http_server) diff --git a/synapse/rest/client/thirdparty.py b/synapse/rest/client/thirdparty.py new file mode 100644 index 0000000000..b5c67c9bb6 --- /dev/null +++ b/synapse/rest/client/thirdparty.py @@ -0,0 +1,111 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging + +from synapse.api.constants import ThirdPartyEntityKind +from synapse.http.servlet import RestServlet + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class ThirdPartyProtocolsServlet(RestServlet): + PATTERNS = client_patterns("/thirdparty/protocols") + + def __init__(self, hs): + super().__init__() + + self.auth = hs.get_auth() + self.appservice_handler = hs.get_application_service_handler() + + async def on_GET(self, request): + await self.auth.get_user_by_req(request, allow_guest=True) + + protocols = await self.appservice_handler.get_3pe_protocols() + return 200, protocols + + +class ThirdPartyProtocolServlet(RestServlet): + PATTERNS = client_patterns("/thirdparty/protocol/(?P[^/]+)$") + + def __init__(self, hs): + super().__init__() + + self.auth = hs.get_auth() + self.appservice_handler = hs.get_application_service_handler() + + async def on_GET(self, request, protocol): + await self.auth.get_user_by_req(request, allow_guest=True) + + protocols = await self.appservice_handler.get_3pe_protocols( + only_protocol=protocol + ) + if protocol in protocols: + return 200, protocols[protocol] + else: + return 404, {"error": "Unknown protocol"} + + +class ThirdPartyUserServlet(RestServlet): + PATTERNS = client_patterns("/thirdparty/user(/(?P[^/]+))?$") + + def __init__(self, hs): + super().__init__() + + self.auth = hs.get_auth() + self.appservice_handler = hs.get_application_service_handler() + + async def on_GET(self, request, protocol): + await self.auth.get_user_by_req(request, allow_guest=True) + + fields = request.args + fields.pop(b"access_token", None) + + results = await self.appservice_handler.query_3pe( + ThirdPartyEntityKind.USER, protocol, fields + ) + + return 200, results + + +class ThirdPartyLocationServlet(RestServlet): + PATTERNS = client_patterns("/thirdparty/location(/(?P[^/]+))?$") + + def __init__(self, hs): + super().__init__() + + self.auth = hs.get_auth() + self.appservice_handler = hs.get_application_service_handler() + + async def on_GET(self, request, protocol): + await self.auth.get_user_by_req(request, allow_guest=True) + + fields = request.args + fields.pop(b"access_token", None) + + results = await self.appservice_handler.query_3pe( + ThirdPartyEntityKind.LOCATION, protocol, fields + ) + + return 200, results + + +def register_servlets(hs, http_server): + ThirdPartyProtocolsServlet(hs).register(http_server) + ThirdPartyProtocolServlet(hs).register(http_server) + ThirdPartyUserServlet(hs).register(http_server) + ThirdPartyLocationServlet(hs).register(http_server) diff --git a/synapse/rest/client/tokenrefresh.py b/synapse/rest/client/tokenrefresh.py new file mode 100644 index 0000000000..b2f858545c --- /dev/null +++ b/synapse/rest/client/tokenrefresh.py @@ -0,0 +1,37 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.api.errors import AuthError +from synapse.http.servlet import RestServlet + +from ._base import client_patterns + + +class TokenRefreshRestServlet(RestServlet): + """ + Exchanges refresh tokens for a pair of an access token and a new refresh + token. + """ + + PATTERNS = client_patterns("/tokenrefresh") + + def __init__(self, hs): + super().__init__() + + async def on_POST(self, request): + raise AuthError(403, "tokenrefresh is no longer supported.") + + +def register_servlets(hs, http_server): + TokenRefreshRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/user_directory.py b/synapse/rest/client/user_directory.py new file mode 100644 index 0000000000..7e8912f0b9 --- /dev/null +++ b/synapse/rest/client/user_directory.py @@ -0,0 +1,79 @@ +# Copyright 2017 Vector Creations Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from synapse.api.errors import SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class UserDirectorySearchRestServlet(RestServlet): + PATTERNS = client_patterns("/user_directory/search$") + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.user_directory_handler = hs.get_user_directory_handler() + + async def on_POST(self, request): + """Searches for users in directory + + Returns: + dict of the form:: + + { + "limited": , # whether there were more results or not + "results": [ # Ordered by best match first + { + "user_id": , + "display_name": , + "avatar_url": + } + ] + } + """ + requester = await self.auth.get_user_by_req(request, allow_guest=False) + user_id = requester.user.to_string() + + if not self.hs.config.user_directory_search_enabled: + return 200, {"limited": False, "results": []} + + body = parse_json_object_from_request(request) + + limit = body.get("limit", 10) + limit = min(limit, 50) + + try: + search_term = body["search_term"] + except Exception: + raise SynapseError(400, "`search_term` is required field") + + results = await self.user_directory_handler.search_users( + user_id, search_term, limit + ) + + return 200, results + + +def register_servlets(hs, http_server): + UserDirectorySearchRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/__init__.py b/synapse/rest/client/v1/__init__.py deleted file mode 100644 index 5e83dba2ed..0000000000 --- a/synapse/rest/client/v1/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/v1/directory.py deleted file mode 100644 index ae92a3df8e..0000000000 --- a/synapse/rest/client/v1/directory.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import logging - -from synapse.api.errors import ( - AuthError, - Codes, - InvalidClientCredentialsError, - NotFoundError, - SynapseError, -) -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.types import RoomAlias - -logger = logging.getLogger(__name__) - - -def register_servlets(hs, http_server): - ClientDirectoryServer(hs).register(http_server) - ClientDirectoryListServer(hs).register(http_server) - ClientAppserviceDirectoryListServer(hs).register(http_server) - - -class ClientDirectoryServer(RestServlet): - PATTERNS = client_patterns("/directory/room/(?P[^/]*)$", v1=True) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.directory_handler = hs.get_directory_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_alias): - room_alias = RoomAlias.from_string(room_alias) - - res = await self.directory_handler.get_association(room_alias) - - return 200, res - - async def on_PUT(self, request, room_alias): - room_alias = RoomAlias.from_string(room_alias) - - content = parse_json_object_from_request(request) - if "room_id" not in content: - raise SynapseError( - 400, 'Missing params: ["room_id"]', errcode=Codes.BAD_JSON - ) - - logger.debug("Got content: %s", content) - logger.debug("Got room name: %s", room_alias.to_string()) - - room_id = content["room_id"] - servers = content["servers"] if "servers" in content else None - - logger.debug("Got room_id: %s", room_id) - logger.debug("Got servers: %s", servers) - - # TODO(erikj): Check types. - - room = await self.store.get_room(room_id) - if room is None: - raise SynapseError(400, "Room does not exist") - - requester = await self.auth.get_user_by_req(request) - - await self.directory_handler.create_association( - requester, room_alias, room_id, servers - ) - - return 200, {} - - async def on_DELETE(self, request, room_alias): - try: - service = self.auth.get_appservice_by_req(request) - room_alias = RoomAlias.from_string(room_alias) - await self.directory_handler.delete_appservice_association( - service, room_alias - ) - logger.info( - "Application service at %s deleted alias %s", - service.url, - room_alias.to_string(), - ) - return 200, {} - except InvalidClientCredentialsError: - # fallback to default user behaviour if they aren't an AS - pass - - requester = await self.auth.get_user_by_req(request) - user = requester.user - - room_alias = RoomAlias.from_string(room_alias) - - await self.directory_handler.delete_association(requester, room_alias) - - logger.info( - "User %s deleted alias %s", user.to_string(), room_alias.to_string() - ) - - return 200, {} - - -class ClientDirectoryListServer(RestServlet): - PATTERNS = client_patterns("/directory/list/room/(?P[^/]*)$", v1=True) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.directory_handler = hs.get_directory_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id): - room = await self.store.get_room(room_id) - if room is None: - raise NotFoundError("Unknown room") - - return 200, {"visibility": "public" if room["is_public"] else "private"} - - async def on_PUT(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - content = parse_json_object_from_request(request) - visibility = content.get("visibility", "public") - - await self.directory_handler.edit_published_room_list( - requester, room_id, visibility - ) - - return 200, {} - - async def on_DELETE(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - await self.directory_handler.edit_published_room_list( - requester, room_id, "private" - ) - - return 200, {} - - -class ClientAppserviceDirectoryListServer(RestServlet): - PATTERNS = client_patterns( - "/directory/list/appservice/(?P[^/]*)/(?P[^/]*)$", v1=True - ) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.directory_handler = hs.get_directory_handler() - self.auth = hs.get_auth() - - def on_PUT(self, request, network_id, room_id): - content = parse_json_object_from_request(request) - visibility = content.get("visibility", "public") - return self._edit(request, network_id, room_id, visibility) - - def on_DELETE(self, request, network_id, room_id): - return self._edit(request, network_id, room_id, "private") - - async def _edit(self, request, network_id, room_id, visibility): - requester = await self.auth.get_user_by_req(request) - if not requester.app_service: - raise AuthError( - 403, "Only appservices can edit the appservice published room list" - ) - - await self.directory_handler.edit_published_appservice_room_list( - requester.app_service.id, network_id, room_id, visibility - ) - - return 200, {} diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py deleted file mode 100644 index ee7454996e..0000000000 --- a/synapse/rest/client/v1/events.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains REST servlets to do with event streaming, /events.""" -import logging - -from synapse.api.errors import SynapseError -from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.streams.config import PaginationConfig - -logger = logging.getLogger(__name__) - - -class EventStreamRestServlet(RestServlet): - PATTERNS = client_patterns("/events$", v1=True) - - DEFAULT_LONGPOLL_TIME_MS = 30000 - - def __init__(self, hs): - super().__init__() - self.event_stream_handler = hs.get_event_stream_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - is_guest = requester.is_guest - room_id = None - if is_guest: - if b"room_id" not in request.args: - raise SynapseError(400, "Guest users must specify room_id param") - if b"room_id" in request.args: - room_id = request.args[b"room_id"][0].decode("ascii") - - pagin_config = await PaginationConfig.from_request(self.store, request) - timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS - if b"timeout" in request.args: - try: - timeout = int(request.args[b"timeout"][0]) - except ValueError: - raise SynapseError(400, "timeout must be in milliseconds.") - - as_client_event = b"raw" not in request.args - - chunk = await self.event_stream_handler.get_stream( - requester.user.to_string(), - pagin_config, - timeout=timeout, - as_client_event=as_client_event, - affect_presence=(not is_guest), - room_id=room_id, - is_guest=is_guest, - ) - - return 200, chunk - - -class EventRestServlet(RestServlet): - PATTERNS = client_patterns("/events/(?P[^/]*)$", v1=True) - - def __init__(self, hs): - super().__init__() - self.clock = hs.get_clock() - self.event_handler = hs.get_event_handler() - self.auth = hs.get_auth() - self._event_serializer = hs.get_event_client_serializer() - - async def on_GET(self, request, event_id): - requester = await self.auth.get_user_by_req(request) - event = await self.event_handler.get_event(requester.user, None, event_id) - - time_now = self.clock.time_msec() - if event: - event = await self._event_serializer.serialize_event(event, time_now) - return 200, event - else: - return 404, "Event not found." - - -def register_servlets(hs, http_server): - EventStreamRestServlet(hs).register(http_server) - EventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/initial_sync.py b/synapse/rest/client/v1/initial_sync.py deleted file mode 100644 index bef1edc838..0000000000 --- a/synapse/rest/client/v1/initial_sync.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from synapse.http.servlet import RestServlet, parse_boolean -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.streams.config import PaginationConfig - - -# TODO: Needs unit testing -class InitialSyncRestServlet(RestServlet): - PATTERNS = client_patterns("/initialSync$", v1=True) - - def __init__(self, hs): - super().__init__() - self.initial_sync_handler = hs.get_initial_sync_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request) - as_client_event = b"raw" not in request.args - pagination_config = await PaginationConfig.from_request(self.store, request) - include_archived = parse_boolean(request, "archived", default=False) - content = await self.initial_sync_handler.snapshot_all_rooms( - user_id=requester.user.to_string(), - pagin_config=pagination_config, - as_client_event=as_client_event, - include_archived=include_archived, - ) - - return 200, content - - -def register_servlets(hs, http_server): - InitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py deleted file mode 100644 index 11567bf32c..0000000000 --- a/synapse/rest/client/v1/login.py +++ /dev/null @@ -1,600 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import re -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional - -from typing_extensions import TypedDict - -from synapse.api.errors import Codes, LoginError, SynapseError -from synapse.api.ratelimiting import Ratelimiter -from synapse.api.urls import CLIENT_API_PREFIX -from synapse.appservice import ApplicationService -from synapse.handlers.sso import SsoIdentityProvider -from synapse.http import get_request_uri -from synapse.http.server import HttpServer, finish_request -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_boolean, - parse_bytes_from_args, - parse_json_object_from_request, - parse_string, -) -from synapse.http.site import SynapseRequest -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.rest.well_known import WellKnownBuilder -from synapse.types import JsonDict, UserID - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -class LoginResponse(TypedDict, total=False): - user_id: str - access_token: str - home_server: str - expires_in_ms: Optional[int] - refresh_token: Optional[str] - device_id: str - well_known: Optional[Dict[str, Any]] - - -class LoginRestServlet(RestServlet): - PATTERNS = client_patterns("/login$", v1=True) - CAS_TYPE = "m.login.cas" - SSO_TYPE = "m.login.sso" - TOKEN_TYPE = "m.login.token" - JWT_TYPE = "org.matrix.login.jwt" - JWT_TYPE_DEPRECATED = "m.login.jwt" - APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service" - REFRESH_TOKEN_PARAM = "org.matrix.msc2918.refresh_token" - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - - # JWT configuration variables. - self.jwt_enabled = hs.config.jwt_enabled - self.jwt_secret = hs.config.jwt_secret - self.jwt_algorithm = hs.config.jwt_algorithm - self.jwt_issuer = hs.config.jwt_issuer - self.jwt_audiences = hs.config.jwt_audiences - - # SSO configuration. - self.saml2_enabled = hs.config.saml2_enabled - self.cas_enabled = hs.config.cas_enabled - self.oidc_enabled = hs.config.oidc_enabled - self._msc2858_enabled = hs.config.experimental.msc2858_enabled - self._msc2918_enabled = hs.config.access_token_lifetime is not None - - self.auth = hs.get_auth() - - self.clock = hs.get_clock() - - self.auth_handler = self.hs.get_auth_handler() - self.registration_handler = hs.get_registration_handler() - self._sso_handler = hs.get_sso_handler() - - self._well_known_builder = WellKnownBuilder(hs) - self._address_ratelimiter = Ratelimiter( - store=hs.get_datastore(), - clock=hs.get_clock(), - rate_hz=self.hs.config.rc_login_address.per_second, - burst_count=self.hs.config.rc_login_address.burst_count, - ) - self._account_ratelimiter = Ratelimiter( - store=hs.get_datastore(), - clock=hs.get_clock(), - rate_hz=self.hs.config.rc_login_account.per_second, - burst_count=self.hs.config.rc_login_account.burst_count, - ) - - def on_GET(self, request: SynapseRequest): - flows = [] - if self.jwt_enabled: - flows.append({"type": LoginRestServlet.JWT_TYPE}) - flows.append({"type": LoginRestServlet.JWT_TYPE_DEPRECATED}) - - if self.cas_enabled: - # we advertise CAS for backwards compat, though MSC1721 renamed it - # to SSO. - flows.append({"type": LoginRestServlet.CAS_TYPE}) - - if self.cas_enabled or self.saml2_enabled or self.oidc_enabled: - sso_flow: JsonDict = { - "type": LoginRestServlet.SSO_TYPE, - "identity_providers": [ - _get_auth_flow_dict_for_idp( - idp, - ) - for idp in self._sso_handler.get_identity_providers().values() - ], - } - - if self._msc2858_enabled: - # backwards-compatibility support for clients which don't - # support the stable API yet - sso_flow["org.matrix.msc2858.identity_providers"] = [ - _get_auth_flow_dict_for_idp(idp, use_unstable_brands=True) - for idp in self._sso_handler.get_identity_providers().values() - ] - - flows.append(sso_flow) - - # While it's valid for us to advertise this login type generally, - # synapse currently only gives out these tokens as part of the - # SSO login flow. - # Generally we don't want to advertise login flows that clients - # don't know how to implement, since they (currently) will always - # fall back to the fallback API if they don't understand one of the - # login flow types returned. - flows.append({"type": LoginRestServlet.TOKEN_TYPE}) - - flows.extend({"type": t} for t in self.auth_handler.get_supported_login_types()) - - flows.append({"type": LoginRestServlet.APPSERVICE_TYPE}) - - return 200, {"flows": flows} - - async def on_POST(self, request: SynapseRequest): - login_submission = parse_json_object_from_request(request) - - if self._msc2918_enabled: - # Check if this login should also issue a refresh token, as per - # MSC2918 - should_issue_refresh_token = parse_boolean( - request, name=LoginRestServlet.REFRESH_TOKEN_PARAM, default=False - ) - else: - should_issue_refresh_token = False - - try: - if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE: - appservice = self.auth.get_appservice_by_req(request) - - if appservice.is_rate_limited(): - await self._address_ratelimiter.ratelimit( - None, request.getClientIP() - ) - - result = await self._do_appservice_login( - login_submission, - appservice, - should_issue_refresh_token=should_issue_refresh_token, - ) - elif self.jwt_enabled and ( - login_submission["type"] == LoginRestServlet.JWT_TYPE - or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED - ): - await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_jwt_login( - login_submission, - should_issue_refresh_token=should_issue_refresh_token, - ) - elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE: - await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_token_login( - login_submission, - should_issue_refresh_token=should_issue_refresh_token, - ) - else: - await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_other_login( - login_submission, - should_issue_refresh_token=should_issue_refresh_token, - ) - except KeyError: - raise SynapseError(400, "Missing JSON keys.") - - well_known_data = self._well_known_builder.get_well_known() - if well_known_data: - result["well_known"] = well_known_data - return 200, result - - async def _do_appservice_login( - self, - login_submission: JsonDict, - appservice: ApplicationService, - should_issue_refresh_token: bool = False, - ): - identifier = login_submission.get("identifier") - logger.info("Got appservice login request with identifier: %r", identifier) - - if not isinstance(identifier, dict): - raise SynapseError( - 400, "Invalid identifier in login submission", Codes.INVALID_PARAM - ) - - # this login flow only supports identifiers of type "m.id.user". - if identifier.get("type") != "m.id.user": - raise SynapseError( - 400, "Unknown login identifier type", Codes.INVALID_PARAM - ) - - user = identifier.get("user") - if not isinstance(user, str): - raise SynapseError(400, "Invalid user in identifier", Codes.INVALID_PARAM) - - if user.startswith("@"): - qualified_user_id = user - else: - qualified_user_id = UserID(user, self.hs.hostname).to_string() - - if not appservice.is_interested_in_user(qualified_user_id): - raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN) - - return await self._complete_login( - qualified_user_id, - login_submission, - ratelimit=appservice.is_rate_limited(), - should_issue_refresh_token=should_issue_refresh_token, - ) - - async def _do_other_login( - self, login_submission: JsonDict, should_issue_refresh_token: bool = False - ) -> LoginResponse: - """Handle non-token/saml/jwt logins - - Args: - login_submission: - should_issue_refresh_token: True if this login should issue - a refresh token alongside the access token. - - Returns: - HTTP response - """ - # Log the request we got, but only certain fields to minimise the chance of - # logging someone's password (even if they accidentally put it in the wrong - # field) - logger.info( - "Got login request with identifier: %r, medium: %r, address: %r, user: %r", - login_submission.get("identifier"), - login_submission.get("medium"), - login_submission.get("address"), - login_submission.get("user"), - ) - canonical_user_id, callback = await self.auth_handler.validate_login( - login_submission, ratelimit=True - ) - result = await self._complete_login( - canonical_user_id, - login_submission, - callback, - should_issue_refresh_token=should_issue_refresh_token, - ) - return result - - async def _complete_login( - self, - user_id: str, - login_submission: JsonDict, - callback: Optional[Callable[[LoginResponse], Awaitable[None]]] = None, - create_non_existent_users: bool = False, - ratelimit: bool = True, - auth_provider_id: Optional[str] = None, - should_issue_refresh_token: bool = False, - ) -> LoginResponse: - """Called when we've successfully authed the user and now need to - actually login them in (e.g. create devices). This gets called on - all successful logins. - - Applies the ratelimiting for successful login attempts against an - account. - - Args: - user_id: ID of the user to register. - login_submission: Dictionary of login information. - callback: Callback function to run after login. - create_non_existent_users: Whether to create the user if they don't - exist. Defaults to False. - ratelimit: Whether to ratelimit the login request. - auth_provider_id: The SSO IdP the user used, if any (just used for the - prometheus metrics). - should_issue_refresh_token: True if this login should issue - a refresh token alongside the access token. - - Returns: - result: Dictionary of account information after successful login. - """ - - # Before we actually log them in we check if they've already logged in - # too often. This happens here rather than before as we don't - # necessarily know the user before now. - if ratelimit: - await self._account_ratelimiter.ratelimit(None, user_id.lower()) - - if create_non_existent_users: - 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 - ) - user_id = canonical_uid - - device_id = login_submission.get("device_id") - initial_display_name = login_submission.get("initial_device_display_name") - ( - device_id, - access_token, - valid_until_ms, - refresh_token, - ) = await self.registration_handler.register_device( - user_id, - device_id, - initial_display_name, - auth_provider_id=auth_provider_id, - should_issue_refresh_token=should_issue_refresh_token, - ) - - result = LoginResponse( - user_id=user_id, - access_token=access_token, - home_server=self.hs.hostname, - device_id=device_id, - ) - - if valid_until_ms is not None: - expires_in_ms = valid_until_ms - self.clock.time_msec() - result["expires_in_ms"] = expires_in_ms - - if refresh_token is not None: - result["refresh_token"] = refresh_token - - if callback is not None: - await callback(result) - - return result - - async def _do_token_login( - self, login_submission: JsonDict, should_issue_refresh_token: bool = False - ) -> LoginResponse: - """ - Handle the final stage of SSO login. - - Args: - login_submission: The JSON request body. - should_issue_refresh_token: True if this login should issue - a refresh token alongside the access token. - - Returns: - The body of the JSON response. - """ - token = login_submission["token"] - auth_handler = self.auth_handler - res = await auth_handler.validate_short_term_login_token(token) - - return await self._complete_login( - res.user_id, - login_submission, - self.auth_handler._sso_login_callback, - auth_provider_id=res.auth_provider_id, - should_issue_refresh_token=should_issue_refresh_token, - ) - - async def _do_jwt_login( - self, login_submission: JsonDict, should_issue_refresh_token: bool = False - ) -> LoginResponse: - token = login_submission.get("token", None) - if token is None: - raise LoginError( - 403, "Token field for JWT is missing", errcode=Codes.FORBIDDEN - ) - - import jwt - - try: - payload = jwt.decode( - token, - self.jwt_secret, - algorithms=[self.jwt_algorithm], - issuer=self.jwt_issuer, - audience=self.jwt_audiences, - ) - except jwt.PyJWTError as e: - # A JWT error occurred, return some info back to the client. - raise LoginError( - 403, - "JWT validation failed: %s" % (str(e),), - errcode=Codes.FORBIDDEN, - ) - - user = payload.get("sub", None) - if user is None: - raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN) - - user_id = UserID(user, self.hs.hostname).to_string() - result = await self._complete_login( - user_id, - login_submission, - create_non_existent_users=True, - should_issue_refresh_token=should_issue_refresh_token, - ) - return result - - -def _get_auth_flow_dict_for_idp( - idp: SsoIdentityProvider, use_unstable_brands: bool = False -) -> JsonDict: - """Return an entry for the login flow dict - - Returns an entry suitable for inclusion in "identity_providers" in the - response to GET /_matrix/client/r0/login - - Args: - idp: the identity provider to describe - use_unstable_brands: whether we should use brand identifiers suitable - for the unstable API - """ - e: JsonDict = {"id": idp.idp_id, "name": idp.idp_name} - if idp.idp_icon: - e["icon"] = idp.idp_icon - if idp.idp_brand: - e["brand"] = idp.idp_brand - # use the stable brand identifier if the unstable identifier isn't defined. - if use_unstable_brands and idp.unstable_idp_brand: - e["brand"] = idp.unstable_idp_brand - return e - - -class RefreshTokenServlet(RestServlet): - PATTERNS = client_patterns( - "/org.matrix.msc2918.refresh_token/refresh$", releases=(), unstable=True - ) - - def __init__(self, hs: "HomeServer"): - self._auth_handler = hs.get_auth_handler() - self._clock = hs.get_clock() - self.access_token_lifetime = hs.config.access_token_lifetime - - async def on_POST( - self, - request: SynapseRequest, - ): - refresh_submission = parse_json_object_from_request(request) - - assert_params_in_dict(refresh_submission, ["refresh_token"]) - token = refresh_submission["refresh_token"] - if not isinstance(token, str): - raise SynapseError(400, "Invalid param: refresh_token", Codes.INVALID_PARAM) - - valid_until_ms = self._clock.time_msec() + self.access_token_lifetime - access_token, refresh_token = await self._auth_handler.refresh_token( - token, valid_until_ms - ) - expires_in_ms = valid_until_ms - self._clock.time_msec() - return ( - 200, - { - "access_token": access_token, - "refresh_token": refresh_token, - "expires_in_ms": expires_in_ms, - }, - ) - - -class SsoRedirectServlet(RestServlet): - PATTERNS = list(client_patterns("/login/(cas|sso)/redirect$", v1=True)) + [ - re.compile( - "^" - + CLIENT_API_PREFIX - + "/r0/login/sso/redirect/(?P[A-Za-z0-9_.~-]+)$" - ) - ] - - def __init__(self, hs: "HomeServer"): - # make sure that the relevant handlers are instantiated, so that they - # register themselves with the main SSOHandler. - if hs.config.cas_enabled: - hs.get_cas_handler() - if hs.config.saml2_enabled: - hs.get_saml_handler() - if hs.config.oidc_enabled: - hs.get_oidc_handler() - self._sso_handler = hs.get_sso_handler() - self._msc2858_enabled = hs.config.experimental.msc2858_enabled - self._public_baseurl = hs.config.public_baseurl - - def register(self, http_server: HttpServer) -> None: - super().register(http_server) - if self._msc2858_enabled: - # expose additional endpoint for MSC2858 support: backwards-compat support - # for clients which don't yet support the stable endpoints. - http_server.register_paths( - "GET", - client_patterns( - "/org.matrix.msc2858/login/sso/redirect/(?P[A-Za-z0-9_.~-]+)$", - releases=(), - unstable=True, - ), - self.on_GET, - self.__class__.__name__, - ) - - async def on_GET( - self, request: SynapseRequest, idp_id: Optional[str] = None - ) -> None: - if not self._public_baseurl: - raise SynapseError(400, "SSO requires a valid public_baseurl") - - # if this isn't the expected hostname, redirect to the right one, so that we - # get our cookies back. - requested_uri = get_request_uri(request) - baseurl_bytes = self._public_baseurl.encode("utf-8") - if not requested_uri.startswith(baseurl_bytes): - # swap out the incorrect base URL for the right one. - # - # The idea here is to redirect from - # https://foo.bar/whatever/_matrix/... - # to - # https://public.baseurl/_matrix/... - # - i = requested_uri.index(b"/_matrix") - new_uri = baseurl_bytes[:-1] + requested_uri[i:] - logger.info( - "Requested URI %s is not canonical: redirecting to %s", - requested_uri.decode("utf-8", errors="replace"), - new_uri.decode("utf-8", errors="replace"), - ) - request.redirect(new_uri) - finish_request(request) - return - - args: Dict[bytes, List[bytes]] = request.args # type: ignore - client_redirect_url = parse_bytes_from_args(args, "redirectUrl", required=True) - sso_url = await self._sso_handler.handle_redirect_request( - request, - client_redirect_url, - idp_id, - ) - logger.info("Redirecting to %s", sso_url) - request.redirect(sso_url) - finish_request(request) - - -class CasTicketServlet(RestServlet): - PATTERNS = client_patterns("/login/cas/ticket", v1=True) - - def __init__(self, hs): - 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, http_server): - LoginRestServlet(hs).register(http_server) - if hs.config.access_token_lifetime is not None: - RefreshTokenServlet(hs).register(http_server) - SsoRedirectServlet(hs).register(http_server) - if hs.config.cas_enabled: - CasTicketServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/logout.py b/synapse/rest/client/v1/logout.py deleted file mode 100644 index 5aa7908d73..0000000000 --- a/synapse/rest/client/v1/logout.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns - -logger = logging.getLogger(__name__) - - -class LogoutRestServlet(RestServlet): - PATTERNS = client_patterns("/logout$", v1=True) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self._auth_handler = hs.get_auth_handler() - self._device_handler = hs.get_device_handler() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request, allow_expired=True) - - if requester.device_id is None: - # The access token wasn't associated with a device. - # Just delete the access token - access_token = self.auth.get_access_token_from_request(request) - await self._auth_handler.delete_access_token(access_token) - else: - await self._device_handler.delete_device( - requester.user.to_string(), requester.device_id - ) - - return 200, {} - - -class LogoutAllRestServlet(RestServlet): - PATTERNS = client_patterns("/logout/all$", v1=True) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self._auth_handler = hs.get_auth_handler() - self._device_handler = hs.get_device_handler() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request, allow_expired=True) - user_id = requester.user.to_string() - - # first delete all of the user's devices - await self._device_handler.delete_all_devices_for_user(user_id) - - # .. and then delete any access tokens which weren't associated with - # devices. - await self._auth_handler.delete_access_tokens_for_user(user_id) - return 200, {} - - -def register_servlets(hs, http_server): - LogoutRestServlet(hs).register(http_server) - LogoutAllRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py deleted file mode 100644 index 2b24fe5aa6..0000000000 --- a/synapse/rest/client/v1/presence.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with presence: /presence/ -""" -import logging - -from synapse.api.errors import AuthError, SynapseError -from synapse.handlers.presence import format_user_presence_state -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.types import UserID - -logger = logging.getLogger(__name__) - - -class PresenceStatusRestServlet(RestServlet): - PATTERNS = client_patterns("/presence/(?P[^/]*)/status", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.presence_handler = hs.get_presence_handler() - self.clock = hs.get_clock() - self.auth = hs.get_auth() - - self._use_presence = hs.config.server.use_presence - - async def on_GET(self, request, user_id): - requester = await self.auth.get_user_by_req(request) - user = UserID.from_string(user_id) - - if not self._use_presence: - return 200, {"presence": "offline"} - - if requester.user != user: - allowed = await self.presence_handler.is_visible( - observed_user=user, observer_user=requester.user - ) - - if not allowed: - raise AuthError(403, "You are not allowed to see their presence.") - - state = await self.presence_handler.get_state(target_user=user) - state = format_user_presence_state( - state, self.clock.time_msec(), include_user_id=False - ) - - return 200, state - - async def on_PUT(self, request, user_id): - requester = await self.auth.get_user_by_req(request) - user = UserID.from_string(user_id) - - if requester.user != user: - raise AuthError(403, "Can only set your own presence state") - - state = {} - - content = parse_json_object_from_request(request) - - try: - state["presence"] = content.pop("presence") - - if "status_msg" in content: - state["status_msg"] = content.pop("status_msg") - if not isinstance(state["status_msg"], str): - raise SynapseError(400, "status_msg must be a string.") - - if content: - raise KeyError() - except SynapseError as e: - raise e - except Exception: - raise SynapseError(400, "Unable to parse state") - - if self._use_presence: - await self.presence_handler.set_state(user, state) - - return 200, {} - - -def register_servlets(hs, http_server): - PresenceStatusRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py deleted file mode 100644 index f42f4b3567..0000000000 --- a/synapse/rest/client/v1/profile.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with profile: /profile/ """ - -from synapse.api.errors import Codes, SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.types import UserID - - -class ProfileDisplaynameRestServlet(RestServlet): - PATTERNS = client_patterns("/profile/(?P[^/]*)/displayname", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.profile_handler = hs.get_profile_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, user_id): - requester_user = None - - if self.hs.config.require_auth_for_profile_requests: - requester = await self.auth.get_user_by_req(request) - requester_user = requester.user - - 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) - - ret = {} - if displayname is not None: - ret["displayname"] = displayname - - return 200, ret - - async def on_PUT(self, request, user_id): - 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.user) - - content = parse_json_object_from_request(request) - - try: - new_name = content["displayname"] - except Exception: - raise SynapseError( - code=400, - msg="Unable to parse name", - errcode=Codes.BAD_JSON, - ) - - await self.profile_handler.set_displayname(user, requester, new_name, is_admin) - - return 200, {} - - -class ProfileAvatarURLRestServlet(RestServlet): - PATTERNS = client_patterns("/profile/(?P[^/]*)/avatar_url", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.profile_handler = hs.get_profile_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, user_id): - requester_user = None - - if self.hs.config.require_auth_for_profile_requests: - requester = await self.auth.get_user_by_req(request) - requester_user = requester.user - - user = UserID.from_string(user_id) - - await self.profile_handler.check_profile_query_allowed(user, requester_user) - - avatar_url = await self.profile_handler.get_avatar_url(user) - - ret = {} - if avatar_url is not None: - ret["avatar_url"] = avatar_url - - return 200, ret - - async def on_PUT(self, request, user_id): - requester = await self.auth.get_user_by_req(request) - user = UserID.from_string(user_id) - is_admin = await self.auth.is_server_admin(requester.user) - - content = parse_json_object_from_request(request) - try: - new_avatar_url = content["avatar_url"] - except KeyError: - raise SynapseError( - 400, "Missing key 'avatar_url'", errcode=Codes.MISSING_PARAM - ) - - await self.profile_handler.set_avatar_url( - user, requester, new_avatar_url, is_admin - ) - - return 200, {} - - -class ProfileRestServlet(RestServlet): - PATTERNS = client_patterns("/profile/(?P[^/]*)", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.profile_handler = hs.get_profile_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, user_id): - requester_user = None - - if self.hs.config.require_auth_for_profile_requests: - requester = await self.auth.get_user_by_req(request) - requester_user = requester.user - - 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 - - return 200, ret - - -def register_servlets(hs, http_server): - ProfileDisplaynameRestServlet(hs).register(http_server) - ProfileAvatarURLRestServlet(hs).register(http_server) - ProfileRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py deleted file mode 100644 index be29a0b39e..0000000000 --- a/synapse/rest/client/v1/push_rule.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from synapse.api.errors import ( - NotFoundError, - StoreError, - SynapseError, - UnrecognizedRequestError, -) -from synapse.http.servlet import ( - RestServlet, - parse_json_value_from_request, - parse_string, -) -from synapse.push.baserules import BASE_RULE_IDS, NEW_RULE_IDS -from synapse.push.clientformat import format_push_rules_for_user -from synapse.push.rulekinds import PRIORITY_CLASS_MAP -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException - - -class PushRuleRestServlet(RestServlet): - PATTERNS = client_patterns("/(?Ppushrules/.*)$", v1=True) - SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = ( - "Unrecognised request: You probably wanted a trailing slash" - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.notifier = hs.get_notifier() - self._is_worker = hs.config.worker_app is not None - - self._users_new_default_push_rules = hs.config.users_new_default_push_rules - - async def on_PUT(self, request, path): - if self._is_worker: - raise Exception("Cannot handle PUT /push_rules on worker") - - spec = _rule_spec_from_path(path.split("/")) - try: - priority_class = _priority_class_from_spec(spec) - except InvalidRuleException as e: - raise SynapseError(400, str(e)) - - requester = await self.auth.get_user_by_req(request) - - if "/" in spec["rule_id"] or "\\" in spec["rule_id"]: - raise SynapseError(400, "rule_id may not contain slashes") - - content = parse_json_value_from_request(request) - - user_id = requester.user.to_string() - - if "attr" in spec: - await self.set_rule_attr(user_id, spec, content) - self.notify_user(user_id) - return 200, {} - - if spec["rule_id"].startswith("."): - # Rule ids starting with '.' are reserved for server default rules. - raise SynapseError(400, "cannot add new rule_ids that start with '.'") - - try: - (conditions, actions) = _rule_tuple_from_request_object( - spec["template"], spec["rule_id"], content - ) - except InvalidRuleException as e: - raise SynapseError(400, str(e)) - - before = parse_string(request, "before") - if before: - before = _namespaced_rule_id(spec, before) - - after = parse_string(request, "after") - if after: - after = _namespaced_rule_id(spec, after) - - try: - await self.store.add_push_rule( - user_id=user_id, - rule_id=_namespaced_rule_id_from_spec(spec), - priority_class=priority_class, - conditions=conditions, - actions=actions, - before=before, - after=after, - ) - self.notify_user(user_id) - except InconsistentRuleException as e: - raise SynapseError(400, str(e)) - except RuleNotFoundException as e: - raise SynapseError(400, str(e)) - - return 200, {} - - async def on_DELETE(self, request, path): - if self._is_worker: - raise Exception("Cannot handle DELETE /push_rules on worker") - - spec = _rule_spec_from_path(path.split("/")) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - namespaced_rule_id = _namespaced_rule_id_from_spec(spec) - - try: - await self.store.delete_push_rule(user_id, namespaced_rule_id) - self.notify_user(user_id) - return 200, {} - except StoreError as e: - if e.code == 404: - raise NotFoundError() - else: - raise - - async def on_GET(self, request, path): - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - # we build up the full structure and then decide which bits of it - # to send which means doing unnecessary work sometimes but is - # is probably not going to make a whole lot of difference - rules = await self.store.get_push_rules_for_user(user_id) - - rules = format_push_rules_for_user(requester.user, rules) - - path = path.split("/")[1:] - - if path == []: - # we're a reference impl: pedantry is our job. - raise UnrecognizedRequestError( - PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR - ) - - if path[0] == "": - return 200, rules - elif path[0] == "global": - result = _filter_ruleset_with_path(rules["global"], path[1:]) - return 200, result - else: - raise UnrecognizedRequestError() - - def notify_user(self, user_id): - stream_id = self.store.get_max_push_rules_stream_id() - self.notifier.on_new_event("push_rules_key", stream_id, users=[user_id]) - - async def set_rule_attr(self, user_id, spec, val): - if spec["attr"] not in ("enabled", "actions"): - # for the sake of potential future expansion, shouldn't report - # 404 in the case of an unknown request so check it corresponds to - # a known attribute first. - raise UnrecognizedRequestError() - - namespaced_rule_id = _namespaced_rule_id_from_spec(spec) - rule_id = spec["rule_id"] - is_default_rule = rule_id.startswith(".") - if is_default_rule: - if namespaced_rule_id not in BASE_RULE_IDS: - raise NotFoundError("Unknown rule %s" % (namespaced_rule_id,)) - if spec["attr"] == "enabled": - if isinstance(val, dict) and "enabled" in val: - val = val["enabled"] - if not isinstance(val, bool): - # Legacy fallback - # This should *actually* take a dict, but many clients pass - # bools directly, so let's not break them. - raise SynapseError(400, "Value for 'enabled' must be boolean") - return await self.store.set_push_rule_enabled( - user_id, namespaced_rule_id, val, is_default_rule - ) - elif spec["attr"] == "actions": - actions = val.get("actions") - _check_actions(actions) - namespaced_rule_id = _namespaced_rule_id_from_spec(spec) - rule_id = spec["rule_id"] - is_default_rule = rule_id.startswith(".") - if is_default_rule: - if user_id in self._users_new_default_push_rules: - rule_ids = NEW_RULE_IDS - else: - rule_ids = BASE_RULE_IDS - - if namespaced_rule_id not in rule_ids: - raise SynapseError(404, "Unknown rule %r" % (namespaced_rule_id,)) - return await self.store.set_push_rule_actions( - user_id, namespaced_rule_id, actions, is_default_rule - ) - else: - raise UnrecognizedRequestError() - - -def _rule_spec_from_path(path): - """Turn a sequence of path components into a rule spec - - Args: - path (sequence[unicode]): the URL path components. - - Returns: - dict: rule spec dict, containing scope/template/rule_id entries, - and possibly attr. - - Raises: - UnrecognizedRequestError if the path components cannot be parsed. - """ - if len(path) < 2: - raise UnrecognizedRequestError() - if path[0] != "pushrules": - raise UnrecognizedRequestError() - - scope = path[1] - path = path[2:] - if scope != "global": - raise UnrecognizedRequestError() - - if len(path) == 0: - raise UnrecognizedRequestError() - - template = path[0] - path = path[1:] - - if len(path) == 0 or len(path[0]) == 0: - raise UnrecognizedRequestError() - - rule_id = path[0] - - spec = {"scope": scope, "template": template, "rule_id": rule_id} - - path = path[1:] - - if len(path) > 0 and len(path[0]) > 0: - spec["attr"] = path[0] - - return spec - - -def _rule_tuple_from_request_object(rule_template, rule_id, req_obj): - if rule_template in ["override", "underride"]: - if "conditions" not in req_obj: - raise InvalidRuleException("Missing 'conditions'") - conditions = req_obj["conditions"] - for c in conditions: - if "kind" not in c: - raise InvalidRuleException("Condition without 'kind'") - elif rule_template == "room": - conditions = [{"kind": "event_match", "key": "room_id", "pattern": rule_id}] - elif rule_template == "sender": - conditions = [{"kind": "event_match", "key": "user_id", "pattern": rule_id}] - elif rule_template == "content": - if "pattern" not in req_obj: - raise InvalidRuleException("Content rule missing 'pattern'") - pat = req_obj["pattern"] - - conditions = [{"kind": "event_match", "key": "content.body", "pattern": pat}] - else: - raise InvalidRuleException("Unknown rule template: %s" % (rule_template,)) - - if "actions" not in req_obj: - raise InvalidRuleException("No actions found") - actions = req_obj["actions"] - - _check_actions(actions) - - return conditions, actions - - -def _check_actions(actions): - if not isinstance(actions, list): - raise InvalidRuleException("No actions found") - - for a in actions: - if a in ["notify", "dont_notify", "coalesce"]: - pass - elif isinstance(a, dict) and "set_tweak" in a: - pass - else: - raise InvalidRuleException("Unrecognised action") - - -def _filter_ruleset_with_path(ruleset, path): - if path == []: - raise UnrecognizedRequestError( - PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR - ) - - if path[0] == "": - return ruleset - template_kind = path[0] - if template_kind not in ruleset: - raise UnrecognizedRequestError() - path = path[1:] - if path == []: - raise UnrecognizedRequestError( - PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR - ) - if path[0] == "": - return ruleset[template_kind] - rule_id = path[0] - - the_rule = None - for r in ruleset[template_kind]: - if r["rule_id"] == rule_id: - the_rule = r - if the_rule is None: - raise NotFoundError - - path = path[1:] - if len(path) == 0: - return the_rule - - attr = path[0] - if attr in the_rule: - # Make sure we return a JSON object as the attribute may be a - # JSON value. - return {attr: the_rule[attr]} - else: - raise UnrecognizedRequestError() - - -def _priority_class_from_spec(spec): - if spec["template"] not in PRIORITY_CLASS_MAP.keys(): - raise InvalidRuleException("Unknown template: %s" % (spec["template"])) - pc = PRIORITY_CLASS_MAP[spec["template"]] - - return pc - - -def _namespaced_rule_id_from_spec(spec): - return _namespaced_rule_id(spec, spec["rule_id"]) - - -def _namespaced_rule_id(spec, rule_id): - return "global/%s/%s" % (spec["template"], rule_id) - - -class InvalidRuleException(Exception): - pass - - -def register_servlets(hs, http_server): - PushRuleRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py deleted file mode 100644 index 18102eca6c..0000000000 --- a/synapse/rest/client/v1/pusher.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import Codes, StoreError, SynapseError -from synapse.http.server import respond_with_html_bytes -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, - parse_string, -) -from synapse.push import PusherConfigException -from synapse.rest.client.v2_alpha._base import client_patterns - -logger = logging.getLogger(__name__) - - -class PushersRestServlet(RestServlet): - PATTERNS = client_patterns("/pushers$", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request) - user = requester.user - - pushers = await self.hs.get_datastore().get_pushers_by_user_id(user.to_string()) - - filtered_pushers = [p.as_dict() for p in pushers] - - return 200, {"pushers": filtered_pushers} - - -class PushersSetRestServlet(RestServlet): - PATTERNS = client_patterns("/pushers/set$", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.notifier = hs.get_notifier() - self.pusher_pool = self.hs.get_pusherpool() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - user = requester.user - - content = parse_json_object_from_request(request) - - if ( - "pushkey" in content - and "app_id" in content - and "kind" in content - and content["kind"] is None - ): - await self.pusher_pool.remove_pusher( - content["app_id"], content["pushkey"], user_id=user.to_string() - ) - return 200, {} - - assert_params_in_dict( - content, - [ - "kind", - "app_id", - "app_display_name", - "device_display_name", - "pushkey", - "lang", - "data", - ], - ) - - logger.debug("set pushkey %s to kind %s", content["pushkey"], content["kind"]) - logger.debug("Got pushers request with body: %r", content) - - append = False - if "append" in content: - append = content["append"] - - if not append: - await self.pusher_pool.remove_pushers_by_app_id_and_pushkey_not_user( - app_id=content["app_id"], - pushkey=content["pushkey"], - not_user_id=user.to_string(), - ) - - try: - await self.pusher_pool.add_pusher( - user_id=user.to_string(), - access_token=requester.access_token_id, - kind=content["kind"], - app_id=content["app_id"], - app_display_name=content["app_display_name"], - device_display_name=content["device_display_name"], - pushkey=content["pushkey"], - lang=content["lang"], - data=content["data"], - profile_tag=content.get("profile_tag", ""), - ) - except PusherConfigException as pce: - raise SynapseError( - 400, "Config Error: " + str(pce), errcode=Codes.MISSING_PARAM - ) - - self.notifier.on_new_replication_data() - - return 200, {} - - -class PushersRemoveRestServlet(RestServlet): - """ - To allow pusher to be delete by clicking a link (ie. GET request) - """ - - PATTERNS = client_patterns("/pushers/remove$", v1=True) - SUCCESS_HTML = b"You have been unsubscribed" - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.notifier = hs.get_notifier() - self.auth = hs.get_auth() - self.pusher_pool = self.hs.get_pusherpool() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, rights="delete_pusher") - user = requester.user - - app_id = parse_string(request, "app_id", required=True) - pushkey = parse_string(request, "pushkey", required=True) - - try: - await self.pusher_pool.remove_pusher( - app_id=app_id, pushkey=pushkey, user_id=user.to_string() - ) - 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, - PushersRemoveRestServlet.SUCCESS_HTML, - ) - return None - - -def register_servlets(hs, http_server): - PushersRestServlet(hs).register(http_server) - PushersSetRestServlet(hs).register(http_server) - PushersRemoveRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py deleted file mode 100644 index ba7250ad8e..0000000000 --- a/synapse/rest/client/v1/room.py +++ /dev/null @@ -1,1152 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with rooms: /rooms/ """ -import logging -import re -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple -from urllib import parse as urlparse - -from synapse.api.constants import EventTypes, Membership -from synapse.api.errors import ( - AuthError, - Codes, - InvalidClientCredentialsError, - MissingClientTokenError, - ShadowBanError, - SynapseError, -) -from synapse.api.filtering import Filter -from synapse.events.utils import format_event_for_client_v2 -from synapse.http.servlet import ( - ResolveRoomIdMixin, - RestServlet, - assert_params_in_dict, - parse_boolean, - parse_integer, - parse_json_object_from_request, - parse_string, - parse_strings_from_args, -) -from synapse.http.site import SynapseRequest -from synapse.logging.opentracing import set_tag -from synapse.rest.client.transactions import HttpTransactionCache -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.storage.state import StateFilter -from synapse.streams.config import PaginationConfig -from synapse.types import JsonDict, StreamToken, ThirdPartyInstanceID, UserID -from synapse.util import json_decoder -from synapse.util.stringutils import parse_and_validate_server_name, random_string - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -class TransactionRestServlet(RestServlet): - def __init__(self, hs): - super().__init__() - self.txns = HttpTransactionCache(hs) - - -class RoomCreateRestServlet(TransactionRestServlet): - # No PATTERN; we have custom dispatch rules here - - def __init__(self, hs): - super().__init__(hs) - self._room_creation_handler = hs.get_room_creation_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - PATTERNS = "/createRoom" - register_txn_path(self, PATTERNS, http_server) - - def on_PUT(self, request, txn_id): - set_tag("txn_id", txn_id) - return self.txns.fetch_or_execute_request(request, self.on_POST, request) - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - - info, _ = await self._room_creation_handler.create_room( - requester, self.get_room_config(request) - ) - - return 200, info - - def get_room_config(self, request): - user_supplied_config = parse_json_object_from_request(request) - return user_supplied_config - - -# TODO: Needs unit testing for generic events -class RoomStateEventRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - 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.auth = hs.get_auth() - - def register(self, http_server): - # /room/$roomid/state/$eventtype - no_state_key = "/rooms/(?P[^/]*)/state/(?P[^/]*)$" - - # /room/$roomid/state/$eventtype/$statekey - state_key = ( - "/rooms/(?P[^/]*)/state/" - "(?P[^/]*)/(?P[^/]*)$" - ) - - http_server.register_paths( - "GET", - client_patterns(state_key, v1=True), - self.on_GET, - self.__class__.__name__, - ) - http_server.register_paths( - "PUT", - client_patterns(state_key, v1=True), - self.on_PUT, - self.__class__.__name__, - ) - http_server.register_paths( - "GET", - client_patterns(no_state_key, v1=True), - self.on_GET_no_state_key, - self.__class__.__name__, - ) - http_server.register_paths( - "PUT", - client_patterns(no_state_key, v1=True), - self.on_PUT_no_state_key, - self.__class__.__name__, - ) - - def on_GET_no_state_key(self, request, room_id, event_type): - return self.on_GET(request, room_id, event_type, "") - - def on_PUT_no_state_key(self, request, room_id, event_type): - return self.on_PUT(request, room_id, event_type, "") - - async def on_GET(self, request, room_id, event_type, state_key): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - format = parse_string( - request, "format", default="content", allowed_values=["content", "event"] - ) - - msg_handler = self.message_handler - data = await msg_handler.get_room_data( - user_id=requester.user.to_string(), - room_id=room_id, - event_type=event_type, - state_key=state_key, - ) - - if not data: - raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - if format == "event": - event = format_event_for_client_v2(data.get_dict()) - return 200, event - elif format == "content": - return 200, data.get_dict()["content"] - - async def on_PUT(self, request, room_id, event_type, state_key, txn_id=None): - requester = await self.auth.get_user_by_req(request) - - if txn_id: - set_tag("txn_id", txn_id) - - content = parse_json_object_from_request(request) - - event_dict = { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - } - - if state_key is not None: - event_dict["state_key"] = state_key - - try: - if event_type == EventTypes.Member: - membership = content.get("membership", None) - event_id, _ = await self.room_member_handler.update_membership( - requester, - target=UserID.from_string(state_key), - room_id=room_id, - action=membership, - content=content, - ) - else: - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, event_dict, txn_id=txn_id - ) - event_id = event.event_id - except ShadowBanError: - event_id = "$" + random_string(43) - - set_tag("event_id", event_id) - ret = {"event_id": event_id} - return 200, ret - - -# TODO: Needs unit testing for generic events + feedback -class RoomSendEventRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.event_creation_handler = hs.get_event_creation_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - # /rooms/$roomid/send/$event_type[/$txn_id] - PATTERNS = "/rooms/(?P[^/]*)/send/(?P[^/]*)" - register_txn_path(self, PATTERNS, http_server, with_get=True) - - async def on_POST(self, request, room_id, event_type, txn_id=None): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - content = parse_json_object_from_request(request) - - event_dict = { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - } - - if b"ts" in request.args and requester.app_service: - event_dict["origin_server_ts"] = parse_integer(request, "ts", 0) - - try: - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, event_dict, txn_id=txn_id - ) - event_id = event.event_id - except ShadowBanError: - event_id = "$" + random_string(43) - - set_tag("event_id", event_id) - return 200, {"event_id": event_id} - - def on_GET(self, request, room_id, event_type, txn_id): - return 200, "Not implemented" - - def on_PUT(self, request, room_id, event_type, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, event_type, txn_id - ) - - -# TODO: Needs unit testing for room ID + alias joins -class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up - self.auth = hs.get_auth() - - def register(self, http_server): - # /join/$room_identifier[/$txn_id] - PATTERNS = "/join/(?P[^/]*)" - register_txn_path(self, PATTERNS, http_server) - - async def on_POST( - self, - request: SynapseRequest, - room_identifier: str, - txn_id: Optional[str] = None, - ): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - try: - content = parse_json_object_from_request(request) - except Exception: - # Turns out we used to ignore the body entirely, and some clients - # cheekily send invalid bodies. - content = {} - - # 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) - room_id, remote_room_hosts = await self.resolve_room_id( - room_identifier, - remote_room_hosts, - ) - - await self.room_member_handler.update_membership( - requester=requester, - target=requester.user, - room_id=room_id, - action="join", - txn_id=txn_id, - remote_room_hosts=remote_room_hosts, - content=content, - third_party_signed=content.get("third_party_signed", None), - ) - - return 200, {"room_id": room_id} - - def on_PUT(self, request, room_identifier, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_identifier, txn_id - ) - - -# TODO: Needs unit testing -class PublicRoomListRestServlet(TransactionRestServlet): - PATTERNS = client_patterns("/publicRooms$", v1=True) - - def __init__(self, hs): - super().__init__(hs) - self.hs = hs - self.auth = hs.get_auth() - - async def on_GET(self, request): - server = parse_string(request, "server") - - try: - await self.auth.get_user_by_req(request, allow_guest=True) - except InvalidClientCredentialsError as e: - # Option to allow servers to require auth when accessing - # /publicRooms via CS API. This is especially helpful in private - # federations. - if not self.hs.config.allow_public_rooms_without_auth: - raise - - # We allow people to not be authed if they're just looking at our - # room list, but require auth when we proxy the request. - # In both cases we call the auth function, as that has the side - # effect of logging who issued this request if an access token was - # provided. - if server: - raise e - - limit: Optional[int] = parse_integer(request, "limit", 0) - since_token = parse_string(request, "since") - - if limit == 0: - # zero is a special value which corresponds to no limit. - limit = None - - handler = self.hs.get_room_list_handler() - if server and server != self.hs.config.server_name: - # Ensure the server is valid. - try: - parse_and_validate_server_name(server) - except ValueError: - raise SynapseError( - 400, - "Invalid server name: %s" % (server,), - Codes.INVALID_PARAM, - ) - - data = await handler.get_remote_public_room_list( - server, limit=limit, since_token=since_token - ) - else: - data = await handler.get_local_public_room_list( - limit=limit, since_token=since_token - ) - - return 200, data - - async def on_POST(self, request): - await self.auth.get_user_by_req(request, allow_guest=True) - - server = parse_string(request, "server") - content = parse_json_object_from_request(request) - - limit: Optional[int] = int(content.get("limit", 100)) - since_token = content.get("since", None) - search_filter = content.get("filter", None) - - include_all_networks = content.get("include_all_networks", False) - third_party_instance_id = content.get("third_party_instance_id", None) - - if include_all_networks: - network_tuple = None - if third_party_instance_id is not None: - raise SynapseError( - 400, "Can't use include_all_networks with an explicit network" - ) - elif third_party_instance_id is None: - network_tuple = ThirdPartyInstanceID(None, None) - else: - network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) - - if limit == 0: - # zero is a special value which corresponds to no limit. - limit = None - - handler = self.hs.get_room_list_handler() - if server and server != self.hs.config.server_name: - # Ensure the server is valid. - try: - parse_and_validate_server_name(server) - except ValueError: - raise SynapseError( - 400, - "Invalid server name: %s" % (server,), - Codes.INVALID_PARAM, - ) - - data = await handler.get_remote_public_room_list( - server, - limit=limit, - since_token=since_token, - search_filter=search_filter, - include_all_networks=include_all_networks, - third_party_instance_id=third_party_instance_id, - ) - - else: - data = await handler.get_local_public_room_list( - limit=limit, - since_token=since_token, - search_filter=search_filter, - network_tuple=network_tuple, - ) - - return 200, data - - -# TODO: Needs unit testing -class RoomMemberListRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/members$", v1=True) - - def __init__(self, hs): - super().__init__() - self.message_handler = hs.get_message_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request, room_id): - # TODO support Pagination stream API (limit/tokens) - requester = await self.auth.get_user_by_req(request, allow_guest=True) - handler = self.message_handler - - # request the state as of a given event, as identified by a stream token, - # for consistency with /messages etc. - # useful for getting the membership in retrospect as of a given /sync - # response. - at_token_string = parse_string(request, "at") - if at_token_string is None: - at_token = None - else: - at_token = await StreamToken.from_string(self.store, at_token_string) - - # let you filter down on particular memberships. - # XXX: this may not be the best shape for this API - we could pass in a filter - # instead, except filters aren't currently aware of memberships. - # See https://github.com/matrix-org/matrix-doc/issues/1337 for more details. - membership = parse_string(request, "membership") - not_membership = parse_string(request, "not_membership") - - events = await handler.get_state_events( - room_id=room_id, - user_id=requester.user.to_string(), - at_token=at_token, - state_filter=StateFilter.from_types([(EventTypes.Member, None)]), - ) - - chunk = [] - - for event in events: - if (membership and event["content"].get("membership") != membership) or ( - not_membership and event["content"].get("membership") == not_membership - ): - continue - chunk.append(event) - - return 200, {"chunk": chunk} - - -# deprecated in favour of /members?membership=join? -# except it does custom AS logic and has a simpler return format -class JoinedRoomMemberListRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/joined_members$", v1=True) - - def __init__(self, hs): - super().__init__() - self.message_handler = hs.get_message_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - users_with_profile = await self.message_handler.get_joined_members( - requester, room_id - ) - - return 200, {"joined": users_with_profile} - - -# TODO: Needs better unit testing -class RoomMessageListRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/messages$", v1=True) - - def __init__(self, hs): - super().__init__() - self.pagination_handler = hs.get_pagination_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - pagination_config = await PaginationConfig.from_request( - self.store, request, default_limit=10 - ) - as_client_event = b"raw" not in request.args - filter_str = parse_string(request, "filter", encoding="utf-8") - if filter_str: - filter_json = urlparse.unquote(filter_str) - event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) - if ( - event_filter - and event_filter.filter_json.get("event_format", "client") - == "federation" - ): - as_client_event = False - else: - event_filter = None - - msgs = await self.pagination_handler.get_messages( - room_id=room_id, - requester=requester, - pagin_config=pagination_config, - as_client_event=as_client_event, - event_filter=event_filter, - ) - - return 200, msgs - - -# TODO: Needs unit testing -class RoomStateRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/state$", v1=True) - - def __init__(self, hs): - super().__init__() - self.message_handler = hs.get_message_handler() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - # Get all the current state for this room - events = await self.message_handler.get_state_events( - room_id=room_id, - user_id=requester.user.to_string(), - is_guest=requester.is_guest, - ) - return 200, events - - -# TODO: Needs unit testing -class RoomInitialSyncRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/initialSync$", v1=True) - - def __init__(self, hs): - super().__init__() - self.initial_sync_handler = hs.get_initial_sync_handler() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - pagination_config = await PaginationConfig.from_request(self.store, request) - content = await self.initial_sync_handler.room_initial_sync( - room_id=room_id, requester=requester, pagin_config=pagination_config - ) - return 200, content - - -class RoomEventServlet(RestServlet): - PATTERNS = client_patterns( - "/rooms/(?P[^/]*)/event/(?P[^/]*)$", v1=True - ) - - def __init__(self, hs): - super().__init__() - self.clock = hs.get_clock() - self.event_handler = hs.get_event_handler() - self._event_serializer = hs.get_event_client_serializer() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id, event_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - try: - event = await self.event_handler.get_event( - requester.user, room_id, event_id - ) - except AuthError: - # This endpoint is supposed to return a 404 when the requester does - # not have permission to access the event - # https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-r0-rooms-roomid-event-eventid - raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - time_now = self.clock.time_msec() - if event: - event = await self._event_serializer.serialize_event(event, time_now) - return 200, event - - return SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - -class RoomEventContextServlet(RestServlet): - PATTERNS = client_patterns( - "/rooms/(?P[^/]*)/context/(?P[^/]*)$", v1=True - ) - - def __init__(self, hs): - super().__init__() - self.clock = hs.get_clock() - self.room_context_handler = hs.get_room_context_handler() - self._event_serializer = hs.get_event_client_serializer() - self.auth = hs.get_auth() - - async def on_GET(self, request, room_id, event_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - limit = parse_integer(request, "limit", default=10) - - # picking the API shape for symmetry with /messages - filter_str = parse_string(request, "filter", encoding="utf-8") - if filter_str: - filter_json = urlparse.unquote(filter_str) - event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) - else: - event_filter = None - - results = await self.room_context_handler.get_event_context( - requester, room_id, event_id, limit, event_filter - ) - - if not results: - raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) - - time_now = self.clock.time_msec() - results["events_before"] = await self._event_serializer.serialize_events( - results["events_before"], time_now - ) - results["event"] = await self._event_serializer.serialize_event( - results["event"], time_now - ) - results["events_after"] = await self._event_serializer.serialize_events( - results["events_after"], time_now - ) - results["state"] = await self._event_serializer.serialize_events( - results["state"], - time_now, - # No need to bundle aggregations for state events - bundle_aggregations=False, - ) - - return 200, results - - -class RoomForgetRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - PATTERNS = "/rooms/(?P[^/]*)/forget" - register_txn_path(self, PATTERNS, http_server) - - async def on_POST(self, request, room_id, txn_id=None): - requester = await self.auth.get_user_by_req(request, allow_guest=False) - - await self.room_member_handler.forget(user=requester.user, room_id=room_id) - - return 200, {} - - def on_PUT(self, request, room_id, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, txn_id - ) - - -# TODO: Needs unit testing -class RoomMembershipRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - # /rooms/$roomid/[invite|join|leave] - PATTERNS = ( - "/rooms/(?P[^/]*)/" - "(?Pjoin|invite|leave|ban|unban|kick)" - ) - register_txn_path(self, PATTERNS, http_server) - - async def on_POST(self, request, room_id, membership_action, txn_id=None): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - if requester.is_guest and membership_action not in { - Membership.JOIN, - Membership.LEAVE, - }: - raise AuthError(403, "Guest access not allowed") - - try: - content = parse_json_object_from_request(request) - except Exception: - # Turns out we used to ignore the body entirely, and some clients - # cheekily send invalid bodies. - content = {} - - if membership_action == "invite" and self._has_3pid_invite_keys(content): - try: - await self.room_member_handler.do_3pid_invite( - room_id, - requester.user, - content["medium"], - content["address"], - content["id_server"], - requester, - txn_id, - content.get("id_access_token"), - ) - except ShadowBanError: - # Pretend the request succeeded. - pass - return 200, {} - - target = requester.user - if membership_action in ["invite", "ban", "unban", "kick"]: - assert_params_in_dict(content, ["user_id"]) - target = UserID.from_string(content["user_id"]) - - event_content = None - if "reason" in content: - event_content = {"reason": content["reason"]} - - try: - await self.room_member_handler.update_membership( - requester=requester, - target=target, - room_id=room_id, - action=membership_action, - txn_id=txn_id, - third_party_signed=content.get("third_party_signed", None), - content=event_content, - ) - except ShadowBanError: - # Pretend the request succeeded. - pass - - return_value = {} - - if membership_action == "join": - return_value["room_id"] = room_id - - return 200, return_value - - def _has_3pid_invite_keys(self, content): - for key in {"id_server", "medium", "address"}: - if key not in content: - return False - return True - - def on_PUT(self, request, room_id, membership_action, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, membership_action, txn_id - ) - - -class RoomRedactEventRestServlet(TransactionRestServlet): - def __init__(self, hs): - super().__init__(hs) - self.event_creation_handler = hs.get_event_creation_handler() - self.auth = hs.get_auth() - - def register(self, http_server): - PATTERNS = "/rooms/(?P[^/]*)/redact/(?P[^/]*)" - register_txn_path(self, PATTERNS, http_server) - - async def on_POST(self, request, room_id, event_id, txn_id=None): - requester = await self.auth.get_user_by_req(request) - content = parse_json_object_from_request(request) - - try: - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, - { - "type": EventTypes.Redaction, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - "redacts": event_id, - }, - txn_id=txn_id, - ) - event_id = event.event_id - except ShadowBanError: - event_id = "$" + random_string(43) - - set_tag("event_id", event_id) - return 200, {"event_id": event_id} - - def on_PUT(self, request, room_id, event_id, txn_id): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id, event_id, txn_id - ) - - -class RoomTypingRestServlet(RestServlet): - PATTERNS = client_patterns( - "/rooms/(?P[^/]*)/typing/(?P[^/]*)$", v1=True - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.presence_handler = hs.get_presence_handler() - self.auth = hs.get_auth() - - # If we're not on the typing writer instance we should scream if we get - # requests. - self._is_typing_writer = ( - hs.config.worker.writers.typing == hs.get_instance_name() - ) - - async def on_PUT(self, request, room_id, user_id): - requester = await self.auth.get_user_by_req(request) - - if not self._is_typing_writer: - raise Exception("Got /typing request on instance that is not typing writer") - - room_id = urlparse.unquote(room_id) - target_user = UserID.from_string(urlparse.unquote(user_id)) - - content = parse_json_object_from_request(request) - - await self.presence_handler.bump_presence_active_time(requester.user) - - # Limit timeout to stop people from setting silly typing timeouts. - timeout = min(content.get("timeout", 30000), 120000) - - # Defer getting the typing handler since it will raise on workers. - typing_handler = self.hs.get_typing_writer_handler() - - try: - if content["typing"]: - await typing_handler.started_typing( - target_user=target_user, - requester=requester, - room_id=room_id, - timeout=timeout, - ) - else: - await typing_handler.stopped_typing( - target_user=target_user, requester=requester, room_id=room_id - ) - except ShadowBanError: - # Pretend this worked without error. - pass - - return 200, {} - - -class RoomAliasListServlet(RestServlet): - PATTERNS = [ - re.compile( - r"^/_matrix/client/unstable/org\.matrix\.msc2432" - r"/rooms/(?P[^/]*)/aliases" - ), - ] + list(client_patterns("/rooms/(?P[^/]*)/aliases$", unstable=False)) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.directory_handler = hs.get_directory_handler() - - async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - alias_list = await self.directory_handler.get_aliases_for_room( - requester, room_id - ) - - return 200, {"aliases": alias_list} - - -class SearchRestServlet(RestServlet): - PATTERNS = client_patterns("/search$", v1=True) - - def __init__(self, hs): - super().__init__() - self.search_handler = hs.get_search_handler() - self.auth = hs.get_auth() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - - content = parse_json_object_from_request(request) - - batch = parse_string(request, "next_batch") - results = await self.search_handler.search(requester.user, content, batch) - - return 200, results - - -class JoinedRoomsRestServlet(RestServlet): - PATTERNS = client_patterns("/joined_rooms$", v1=True) - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.auth = hs.get_auth() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - room_ids = await self.store.get_rooms_for_user(requester.user.to_string()) - return 200, {"joined_rooms": list(room_ids)} - - -def register_txn_path(servlet, regex_string, http_server, with_get=False): - """Registers a transaction-based path. - - This registers two paths: - PUT regex_string/$txnid - POST regex_string - - Args: - regex_string (str): The regex string to register. Must NOT have a - trailing $ as this string will be appended to. - http_server : The http_server to register paths with. - with_get: True to also register respective GET paths for the PUTs. - """ - http_server.register_paths( - "POST", - client_patterns(regex_string + "$", v1=True), - servlet.on_POST, - servlet.__class__.__name__, - ) - http_server.register_paths( - "PUT", - client_patterns(regex_string + "/(?P[^/]*)$", v1=True), - servlet.on_PUT, - servlet.__class__.__name__, - ) - if with_get: - http_server.register_paths( - "GET", - client_patterns(regex_string + "/(?P[^/]*)$", v1=True), - servlet.on_GET, - servlet.__class__.__name__, - ) - - -class RoomSpaceSummaryRestServlet(RestServlet): - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/org.matrix.msc2946" - "/rooms/(?P[^/]*)/spaces$" - ), - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self._auth = hs.get_auth() - self._room_summary_handler = hs.get_room_summary_handler() - - async def on_GET( - self, request: SynapseRequest, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - - max_rooms_per_space = parse_integer(request, "max_rooms_per_space") - if max_rooms_per_space is not None and max_rooms_per_space < 0: - raise SynapseError( - 400, - "Value for 'max_rooms_per_space' must be a non-negative integer", - Codes.BAD_JSON, - ) - - return 200, await self._room_summary_handler.get_space_summary( - requester.user.to_string(), - room_id, - suggested_only=parse_boolean(request, "suggested_only", default=False), - max_rooms_per_space=max_rooms_per_space, - ) - - # TODO When switching to the stable endpoint, remove the POST handler. - async def on_POST( - self, request: SynapseRequest, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - content = parse_json_object_from_request(request) - - suggested_only = content.get("suggested_only", False) - if not isinstance(suggested_only, bool): - raise SynapseError( - 400, "'suggested_only' must be a boolean", Codes.BAD_JSON - ) - - max_rooms_per_space = content.get("max_rooms_per_space") - if max_rooms_per_space is not None: - if not isinstance(max_rooms_per_space, int): - raise SynapseError( - 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON - ) - if max_rooms_per_space < 0: - raise SynapseError( - 400, - "Value for 'max_rooms_per_space' must be a non-negative integer", - Codes.BAD_JSON, - ) - - return 200, await self._room_summary_handler.get_space_summary( - requester.user.to_string(), - room_id, - suggested_only=suggested_only, - max_rooms_per_space=max_rooms_per_space, - ) - - -class RoomHierarchyRestServlet(RestServlet): - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/org.matrix.msc2946" - "/rooms/(?P[^/]*)/hierarchy$" - ), - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self._auth = hs.get_auth() - self._room_summary_handler = hs.get_room_summary_handler() - - async def on_GET( - self, request: SynapseRequest, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - - max_depth = parse_integer(request, "max_depth") - if max_depth is not None and max_depth < 0: - raise SynapseError( - 400, "'max_depth' must be a non-negative integer", Codes.BAD_JSON - ) - - limit = parse_integer(request, "limit") - if limit is not None and limit <= 0: - raise SynapseError( - 400, "'limit' must be a positive integer", Codes.BAD_JSON - ) - - return 200, await self._room_summary_handler.get_room_hierarchy( - requester.user.to_string(), - room_id, - suggested_only=parse_boolean(request, "suggested_only", default=False), - max_depth=max_depth, - limit=limit, - from_token=parse_string(request, "from"), - ) - - -class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet): - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/im.nheko.summary" - "/rooms/(?P[^/]*)/summary$" - ), - ) - - def __init__(self, hs: "HomeServer"): - super().__init__(hs) - self._auth = hs.get_auth() - self._room_summary_handler = hs.get_room_summary_handler() - - async def on_GET( - self, request: SynapseRequest, room_identifier: str - ) -> Tuple[int, JsonDict]: - try: - requester = await self._auth.get_user_by_req(request, allow_guest=True) - requester_user_id: Optional[str] = requester.user.to_string() - except MissingClientTokenError: - # auth is optional - requester_user_id = None - - # 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, "via", required=False) - room_id, remote_room_hosts = await self.resolve_room_id( - room_identifier, - remote_room_hosts, - ) - - return 200, await self._room_summary_handler.get_room_summary( - requester_user_id, - room_id, - remote_room_hosts, - ) - - -def register_servlets(hs: "HomeServer", http_server, is_worker=False): - RoomStateEventRestServlet(hs).register(http_server) - RoomMemberListRestServlet(hs).register(http_server) - JoinedRoomMemberListRestServlet(hs).register(http_server) - RoomMessageListRestServlet(hs).register(http_server) - JoinRoomAliasServlet(hs).register(http_server) - RoomMembershipRestServlet(hs).register(http_server) - RoomSendEventRestServlet(hs).register(http_server) - PublicRoomListRestServlet(hs).register(http_server) - RoomStateRestServlet(hs).register(http_server) - RoomRedactEventRestServlet(hs).register(http_server) - RoomTypingRestServlet(hs).register(http_server) - RoomEventContextServlet(hs).register(http_server) - RoomSpaceSummaryRestServlet(hs).register(http_server) - RoomHierarchyRestServlet(hs).register(http_server) - if hs.config.experimental.msc3266_enabled: - RoomSummaryRestServlet(hs).register(http_server) - RoomEventServlet(hs).register(http_server) - JoinedRoomsRestServlet(hs).register(http_server) - RoomAliasListServlet(hs).register(http_server) - SearchRestServlet(hs).register(http_server) - - # Some servlets only get registered for the main process. - if not is_worker: - RoomCreateRestServlet(hs).register(http_server) - RoomForgetRestServlet(hs).register(http_server) - - -def register_deprecated_servlets(hs, http_server): - RoomInitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/voip.py b/synapse/rest/client/v1/voip.py deleted file mode 100644 index c780ffded5..0000000000 --- a/synapse/rest/client/v1/voip.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import hashlib -import hmac - -from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns - - -class VoipRestServlet(RestServlet): - PATTERNS = client_patterns("/voip/turnServer$", v1=True) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req( - request, self.hs.config.turn_allow_guests - ) - - turnUris = self.hs.config.turn_uris - turnSecret = self.hs.config.turn_shared_secret - turnUsername = self.hs.config.turn_username - turnPassword = self.hs.config.turn_password - userLifetime = self.hs.config.turn_user_lifetime - - if turnUris and turnSecret and userLifetime: - expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 - username = "%d:%s" % (expiry, requester.user.to_string()) - - mac = hmac.new( - turnSecret.encode(), msg=username.encode(), digestmod=hashlib.sha1 - ) - # We need to use standard padded base64 encoding here - # encode_base64 because we need to add the standard padding to get the - # same result as the TURN server. - password = base64.b64encode(mac.digest()).decode("ascii") - - elif turnUris and turnUsername and turnPassword and userLifetime: - username = turnUsername - password = turnPassword - - else: - return 200, {} - - return ( - 200, - { - "username": username, - "password": password, - "ttl": userLifetime / 1000, - "uris": turnUris, - }, - ) - - -def register_servlets(hs, http_server): - VoipRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py deleted file mode 100644 index 5e83dba2ed..0000000000 --- a/synapse/rest/client/v2_alpha/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py deleted file mode 100644 index 0443f4571c..0000000000 --- a/synapse/rest/client/v2_alpha/_base.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains base REST classes for constructing client v1 servlets. -""" -import logging -import re -from typing import Iterable, Pattern - -from synapse.api.errors import InteractiveAuthIncompleteError -from synapse.api.urls import CLIENT_API_PREFIX -from synapse.types import JsonDict - -logger = logging.getLogger(__name__) - - -def client_patterns( - path_regex: str, - releases: Iterable[int] = (0,), - unstable: bool = True, - v1: bool = False, -) -> Iterable[Pattern]: - """Creates a regex compiled client path with the correct client path - prefix. - - Args: - path_regex: The regex string to match. This should NOT have a ^ - as this will be prefixed. - releases: An iterable of releases to include this endpoint under. - unstable: If true, include this endpoint under the "unstable" prefix. - v1: If true, include this endpoint under the "api/v1" prefix. - Returns: - An iterable of patterns. - """ - patterns = [] - - if unstable: - unstable_prefix = CLIENT_API_PREFIX + "/unstable" - patterns.append(re.compile("^" + unstable_prefix + path_regex)) - if v1: - v1_prefix = CLIENT_API_PREFIX + "/api/v1" - patterns.append(re.compile("^" + v1_prefix + path_regex)) - for release in releases: - new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,) - patterns.append(re.compile("^" + new_prefix + path_regex)) - - return patterns - - -def set_timeline_upper_limit(filter_json: JsonDict, filter_timeline_limit: int) -> None: - """ - Enforces a maximum limit of a timeline query. - - Params: - filter_json: The timeline query to modify. - filter_timeline_limit: The maximum limit to allow, passing -1 will - disable enforcing a maximum limit. - """ - if filter_timeline_limit < 0: - return # no upper limits - timeline = filter_json.get("room", {}).get("timeline", {}) - if "limit" in timeline: - filter_json["room"]["timeline"]["limit"] = min( - filter_json["room"]["timeline"]["limit"], filter_timeline_limit - ) - - -def interactive_auth_handler(orig): - """Wraps an on_POST method to handle InteractiveAuthIncompleteErrors - - Takes a on_POST method which returns an Awaitable (errcode, body) response - and adds exception handling to turn a InteractiveAuthIncompleteError into - a 401 response. - - Normal usage is: - - @interactive_auth_handler - async def on_POST(self, request): - # ... - await self.auth_handler.check_auth - """ - - async def wrapped(*args, **kwargs): - try: - return await orig(*args, **kwargs) - except InteractiveAuthIncompleteError as e: - return 401, e.result - - return wrapped diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py deleted file mode 100644 index fb5ad2906e..0000000000 --- a/synapse/rest/client/v2_alpha/account.py +++ /dev/null @@ -1,910 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# Copyright 2017 Vector Creations Ltd -# Copyright 2018 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import logging -import random -from http import HTTPStatus -from typing import TYPE_CHECKING -from urllib.parse import urlparse - -from synapse.api.constants import LoginType -from synapse.api.errors import ( - Codes, - InteractiveAuthIncompleteError, - SynapseError, - ThreepidValidationError, -) -from synapse.config.emailconfig import ThreepidBehaviour -from synapse.handlers.ui_auth import UIAuthSessionDataConstants -from synapse.http.server import finish_request, respond_with_html -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, - parse_string, -) -from synapse.metrics import threepid_send_requests -from synapse.push.mailer import Mailer -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 - -if TYPE_CHECKING: - from synapse.server import HomeServer - - -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_datastore() - self.config = hs.config - self.identity_handler = hs.get_identity_handler() - - if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self.mailer = Mailer( - hs=self.hs, - app_name=self.config.email_app_name, - template_html=self.config.email_password_reset_template_html, - template_text=self.config.email_password_reset_template_text, - ) - - async def on_POST(self, request): - if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: - if self.config.local_threepid_handling_disabled_due_to_email_config: - 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_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) - - # Canonicalise the email address. The addresses are all stored canonicalised - # in the database. This allows the user to reset his password without having to - # know the exact spelling (eg. upper and lower case) of address in the database. - # Stored in the database "foo@bar.com" - # User requests with "FOO@bar.com" would raise a Not Found error - 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 next_link: - # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, next_link) - - await self.identity_handler.ratelimit_request_token_requests( - request, "email", 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_datastore().get_user_id_by_threepid( - "email", email - ) - - if existing_user_id is None: - if self.config.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) - - if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: - assert self.hs.config.account_threepid_delegate_email - - # Have the configured identity server handle the request - ret = await self.identity_handler.requestEmailToken( - self.hs.config.account_threepid_delegate_email, - email, - client_secret, - send_attempt, - next_link, - ) - else: - # Send password reset emails from Synapse - sid = await self.identity_handler.send_threepid_validation( - email, - client_secret, - send_attempt, - self.mailer.send_password_reset_mail, - next_link, - ) - - # Wrap the session id in a JSON object - ret = {"sid": sid} - - threepid_send_requests.labels(type="email", reason="password_reset").observe( - send_attempt - ) - - return 200, ret - - -class PasswordRestServlet(RestServlet): - PATTERNS = client_patterns("/account/password$") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - self.datastore = self.hs.get_datastore() - self.password_policy_handler = hs.get_password_policy_handler() - self._set_password_handler = hs.get_set_password_handler() - - @interactive_auth_handler - async def on_POST(self, request): - body = parse_json_object_from_request(request) - - # we do basic sanity checks here because the auth layer will store these - # in sessions. Pull out the new password provided to us. - new_password = body.pop("new_password", None) - if new_password is not None: - if not isinstance(new_password, str) or len(new_password) > 512: - raise SynapseError(400, "Invalid password") - self.password_policy_handler.validate_password(new_password) - - # there are two possibilities here. Either the user does not have an - # access token, and needs to do a password reset; or they have one and - # need to validate their identity. - # - # In the first case, we offer a couple of means of identifying - # themselves (email and msisdn, though it's unclear if msisdn actually - # works). - # - # In the second case, we require a password to confirm their identity. - - if self.auth.has_access_token(request): - requester = await self.auth.get_user_by_req(request) - try: - params, session_id = await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body, - "modify your account password", - ) - except InteractiveAuthIncompleteError as e: - # The user needs to provide more steps to complete auth, but - # they're not required to provide the password again. - # - # If a password is available now, hash the provided password and - # store it for later. - if new_password: - password_hash = await self.auth_handler.hash(new_password) - await self.auth_handler.set_session_data( - e.session_id, - UIAuthSessionDataConstants.PASSWORD_HASH, - password_hash, - ) - raise - user_id = requester.user.to_string() - else: - requester = None - try: - result, params, session_id = await self.auth_handler.check_ui_auth( - [[LoginType.EMAIL_IDENTITY]], - request, - body, - "modify your account password", - ) - except InteractiveAuthIncompleteError as e: - # The user needs to provide more steps to complete auth, but - # they're not required to provide the password again. - # - # If a password is available now, hash the provided password and - # store it for later. - if new_password: - password_hash = await self.auth_handler.hash(new_password) - await self.auth_handler.set_session_data( - e.session_id, - UIAuthSessionDataConstants.PASSWORD_HASH, - password_hash, - ) - raise - - 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) - - # If we have a password in this request, prefer it. Otherwise, use the - # password hash from an earlier request. - if new_password: - password_hash = await self.auth_handler.hash(new_password) - elif session_id is not None: - password_hash = await self.auth_handler.get_session_data( - session_id, UIAuthSessionDataConstants.PASSWORD_HASH, None - ) - else: - # UI validation was skipped, but the request did not include a new - # password. - password_hash = None - if not password_hash: - raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM) - - logout_devices = params.get("logout_devices", True) - - await self._set_password_handler.set_password( - user_id, password_hash, logout_devices, requester - ) - - return 200, {} - - -class DeactivateAccountRestServlet(RestServlet): - PATTERNS = client_patterns("/account/deactivate$") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - self._deactivate_account_handler = hs.get_deactivate_account_handler() - - @interactive_auth_handler - async def on_POST(self, request): - body = parse_json_object_from_request(request) - erase = body.get("erase", False) - if not isinstance(erase, bool): - raise SynapseError( - HTTPStatus.BAD_REQUEST, - "Param 'erase' must be a boolean, if given", - Codes.BAD_JSON, - ) - - requester = await self.auth.get_user_by_req(request) - - # allow ASes to deactivate their own users - if requester.app_service: - await self._deactivate_account_handler.deactivate_account( - requester.user.to_string(), erase, requester - ) - return 200, {} - - await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body, - "deactivate your account", - ) - result = await self._deactivate_account_handler.deactivate_account( - requester.user.to_string(), - erase, - requester, - id_server=body.get("id_server"), - ) - if result: - id_server_unbind_result = "success" - else: - id_server_unbind_result = "no-support" - - return 200, {"id_server_unbind_result": id_server_unbind_result} - - -class EmailThreepidRequestTokenRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/email/requestToken$") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.config = hs.config - self.identity_handler = hs.get_identity_handler() - self.store = self.hs.get_datastore() - - if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self.mailer = Mailer( - hs=self.hs, - app_name=self.config.email_app_name, - template_html=self.config.email_add_threepid_template_html, - template_text=self.config.email_add_threepid_template_text, - ) - - async def on_POST(self, request): - if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: - if self.config.local_threepid_handling_disabled_due_to_email_config: - 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_json_object_from_request(request) - assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) - client_secret = body["client_secret"] - assert_valid_client_secret(client_secret) - - # Canonicalise the email address. The addresses are all stored canonicalised - # in the database. - # This ensures that the validation email is sent to the canonicalised address - # as it will later be entered into the database. - # Otherwise the email will be sent to "FOO@bar.com" and stored as - # "foo@bar.com" in database. - 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 check_3pid_allowed(self.hs, "email", 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", email - ) - - if next_link: - # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, next_link) - - existing_user_id = await self.store.get_user_id_by_threepid("email", email) - - if existing_user_id is not None: - if self.config.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) - - if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: - assert self.hs.config.account_threepid_delegate_email - - # Have the configured identity server handle the request - ret = await self.identity_handler.requestEmailToken( - self.hs.config.account_threepid_delegate_email, - email, - client_secret, - send_attempt, - next_link, - ) - else: - # Send threepid validation emails from Synapse - sid = await self.identity_handler.send_threepid_validation( - email, - client_secret, - send_attempt, - self.mailer.send_add_threepid_mail, - next_link, - ) - - # Wrap the session id in a JSON object - ret = {"sid": sid} - - threepid_send_requests.labels(type="email", reason="add_threepid").observe( - send_attempt - ) - - return 200, ret - - -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_datastore() - self.identity_handler = hs.get_identity_handler() - - async def on_POST(self, request): - 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 check_3pid_allowed(self.hs, "msisdn", msisdn): - raise SynapseError( - 403, - "Account phone numbers are not authorized on this server", - Codes.THREEPID_DENIED, - ) - - await self.identity_handler.ratelimit_request_token_requests( - request, "msisdn", msisdn - ) - - if next_link: - # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, 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.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, "MSISDN is already in use", Codes.THREEPID_IN_USE) - - if not self.hs.config.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.account_threepid_delegate_msisdn, - country, - phone_number, - client_secret, - send_attempt, - next_link, - ) - - threepid_send_requests.labels(type="msisdn", reason="add_threepid").observe( - send_attempt - ) - - 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): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.config = hs.config - self.clock = hs.get_clock() - self.store = hs.get_datastore() - if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self._failure_email_template = ( - self.config.email_add_threepid_template_failure_html - ) - - async def on_GET(self, request): - if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: - if self.config.local_threepid_handling_disabled_due_to_email_config: - 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" - ) - elif self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: - raise SynapseError( - 400, - "This homeserver is not validating threepids. Use an identity server " - "instead.", - ) - - 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_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 - ) - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.config = hs.config - self.clock = hs.get_clock() - self.store = hs.get_datastore() - self.identity_handler = hs.get_identity_handler() - - async def on_POST(self, request): - if not self.config.account_threepid_delegate_msisdn: - raise SynapseError( - 400, - "This homeserver is not validating phone numbers. Use an identity server " - "instead.", - ) - - body = parse_json_object_from_request(request) - assert_params_in_dict(body, ["client_secret", "sid", "token"]) - assert_valid_client_secret(body["client_secret"]) - - # Proxy submit_token request to msisdn threepid delegate - response = await self.identity_handler.proxy_msisdn_submit_token( - self.config.account_threepid_delegate_msisdn, - body["client_secret"], - body["sid"], - body["token"], - ) - return 200, response - - -class ThreepidRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid$") - - def __init__(self, hs): - 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_datastore() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request) - - threepids = await self.datastore.user_get_threepids(requester.user.to_string()) - - return 200, {"threepids": threepids} - - async def on_POST(self, request): - if not self.hs.config.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): - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - - @interactive_auth_handler - async def on_POST(self, request): - if not self.hs.config.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) - - assert_params_in_dict(body, ["client_secret", "sid"]) - sid = body["sid"] - client_secret = body["client_secret"] - assert_valid_client_secret(client_secret) - - await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body, - "add a third-party identifier to your account", - ) - - 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 ThreepidBindRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/bind$") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.auth = hs.get_auth() - - async def on_POST(self, request): - body = parse_json_object_from_request(request) - - assert_params_in_dict(body, ["id_server", "sid", "client_secret"]) - id_server = body["id_server"] - sid = body["sid"] - id_access_token = body.get("id_access_token") # optional - client_secret = body["client_secret"] - assert_valid_client_secret(client_secret) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - await self.identity_handler.bind_threepid( - client_secret, sid, user_id, id_server, id_access_token - ) - - return 200, {} - - -class ThreepidUnbindRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/unbind$") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.auth = hs.get_auth() - self.datastore = self.hs.get_datastore() - - async def on_POST(self, request): - """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_json_object_from_request(request) - assert_params_in_dict(body, ["medium", "address"]) - - medium = body.get("medium") - address = body.get("address") - id_server = body.get("id_server") - - # 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(), - {"address": address, "medium": medium, "id_server": 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): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - - async def on_POST(self, request): - if not self.hs.config.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - - body = parse_json_object_from_request(request) - assert_params_in_dict(body, ["medium", "address"]) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - try: - ret = await self.auth_handler.delete_threepid( - user_id, body["medium"], body["address"], body.get("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" - - return 200, {"id_server_unbind_result": id_server_unbind_result} - - -def assert_valid_next_link(hs: "HomeServer", next_link: str): - """ - Raises a SynapseError if a given next_link value is invalid - - next_link is valid if the scheme is http(s) and the next_link.domain_whitelist config - option is either empty or contains a domain that matches the one in the given next_link - - Args: - hs: The homeserver object - next_link: The next_link value given by the client - - Raises: - SynapseError: If the next_link is invalid - """ - valid = True - - # Parse the contents of the URL - next_link_parsed = urlparse(next_link) - - # Scheme must not point to the local drive - if next_link_parsed.scheme == "file": - valid = False - - # If the domain whitelist is set, the domain must be in it - if ( - valid - and hs.config.next_link_domain_whitelist is not None - and next_link_parsed.hostname not in hs.config.next_link_domain_whitelist - ): - valid = False - - if not valid: - raise SynapseError( - 400, - "'next_link' domain not included in whitelist, or not http(s)", - errcode=Codes.INVALID_PARAM, - ) - - -class WhoamiRestServlet(RestServlet): - PATTERNS = client_patterns("/account/whoami$") - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request) - - response = {"user_id": requester.user.to_string()} - - # Appservices and similar accounts do not have device IDs - # that we can report on, so exclude them for compliance. - if requester.device_id is not None: - response["device_id"] = requester.device_id - - return 200, response - - -def register_servlets(hs, http_server): - EmailPasswordRequestTokenRestServlet(hs).register(http_server) - PasswordRestServlet(hs).register(http_server) - DeactivateAccountRestServlet(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) - ThreepidAddRestServlet(hs).register(http_server) - ThreepidBindRestServlet(hs).register(http_server) - ThreepidUnbindRestServlet(hs).register(http_server) - ThreepidDeleteRestServlet(hs).register(http_server) - WhoamiRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/account_data.py b/synapse/rest/client/v2_alpha/account_data.py deleted file mode 100644 index 7517e9304e..0000000000 --- a/synapse/rest/client/v2_alpha/account_data.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import AuthError, NotFoundError, SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class AccountDataServlet(RestServlet): - """ - PUT /user/{user_id}/account_data/{account_dataType} HTTP/1.1 - GET /user/{user_id}/account_data/{account_dataType} HTTP/1.1 - """ - - PATTERNS = client_patterns( - "/user/(?P[^/]*)/account_data/(?P[^/]*)" - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.handler = hs.get_account_data_handler() - - async def on_PUT(self, request, user_id, account_data_type): - requester = await self.auth.get_user_by_req(request) - if user_id != requester.user.to_string(): - raise AuthError(403, "Cannot add account data for other users.") - - body = parse_json_object_from_request(request) - - await self.handler.add_account_data_for_user(user_id, account_data_type, body) - - return 200, {} - - async def on_GET(self, request, user_id, account_data_type): - requester = await self.auth.get_user_by_req(request) - if user_id != requester.user.to_string(): - raise AuthError(403, "Cannot get account data for other users.") - - event = await self.store.get_global_account_data_by_type_for_user( - account_data_type, user_id - ) - - if event is None: - raise NotFoundError("Account data not found") - - return 200, event - - -class RoomAccountDataServlet(RestServlet): - """ - PUT /user/{user_id}/rooms/{room_id}/account_data/{account_dataType} HTTP/1.1 - GET /user/{user_id}/rooms/{room_id}/account_data/{account_dataType} HTTP/1.1 - """ - - PATTERNS = client_patterns( - "/user/(?P[^/]*)" - "/rooms/(?P[^/]*)" - "/account_data/(?P[^/]*)" - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.handler = hs.get_account_data_handler() - - async def on_PUT(self, request, user_id, room_id, account_data_type): - requester = await self.auth.get_user_by_req(request) - if user_id != requester.user.to_string(): - raise AuthError(403, "Cannot add account data for other users.") - - body = parse_json_object_from_request(request) - - if account_data_type == "m.fully_read": - raise SynapseError( - 405, - "Cannot set m.fully_read through this API." - " Use /rooms/!roomId:server.name/read_markers", - ) - - await self.handler.add_account_data_to_room( - user_id, room_id, account_data_type, body - ) - - return 200, {} - - async def on_GET(self, request, user_id, room_id, account_data_type): - requester = await self.auth.get_user_by_req(request) - if user_id != requester.user.to_string(): - raise AuthError(403, "Cannot get account data for other users.") - - event = await self.store.get_account_data_for_room_and_type( - user_id, room_id, account_data_type - ) - - if event is None: - raise NotFoundError("Room account data not found") - - return 200, event - - -def register_servlets(hs, http_server): - AccountDataServlet(hs).register(http_server) - RoomAccountDataServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/account_validity.py b/synapse/rest/client/v2_alpha/account_validity.py deleted file mode 100644 index 3ebe401861..0000000000 --- a/synapse/rest/client/v2_alpha/account_validity.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2019 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import SynapseError -from synapse.http.server import respond_with_html -from synapse.http.servlet import RestServlet - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class AccountValidityRenewServlet(RestServlet): - PATTERNS = client_patterns("/account_validity/renew$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - - self.hs = hs - self.account_activity_handler = hs.get_account_validity_handler() - self.auth = hs.get_auth() - 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.invalid_token_template = ( - hs.config.account_validity.account_validity_invalid_token_template - ) - - async def on_GET(self, request): - if b"token" not in request.args: - raise SynapseError(400, "Missing renewal token") - renewal_token = request.args[b"token"][0] - - ( - token_valid, - token_stale, - expiration_ts, - ) = await self.account_activity_handler.renew_account( - renewal_token.decode("utf8") - ) - - if token_valid: - status_code = 200 - response = self.account_renewed_template.render(expiration_ts=expiration_ts) - elif token_stale: - status_code = 200 - response = self.account_previously_renewed_template.render( - expiration_ts=expiration_ts - ) - else: - status_code = 404 - response = self.invalid_token_template.render(expiration_ts=expiration_ts) - - respond_with_html(request, status_code, response) - - -class AccountValiditySendMailServlet(RestServlet): - PATTERNS = client_patterns("/account_validity/send_mail$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - - self.hs = hs - self.account_activity_handler = hs.get_account_validity_handler() - self.auth = hs.get_auth() - self.account_validity_renew_by_email_enabled = ( - hs.config.account_validity.account_validity_renew_by_email_enabled - ) - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request, allow_expired=True) - user_id = requester.user.to_string() - await self.account_activity_handler.send_renewal_email_to_user(user_id) - - return 200, {} - - -def register_servlets(hs, http_server): - AccountValidityRenewServlet(hs).register(http_server) - AccountValiditySendMailServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py deleted file mode 100644 index 6ea1b50a62..0000000000 --- a/synapse/rest/client/v2_alpha/auth.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from typing import TYPE_CHECKING - -from synapse.api.constants import LoginType -from synapse.api.errors import SynapseError -from synapse.api.urls import CLIENT_API_PREFIX -from synapse.http.server import respond_with_html -from synapse.http.servlet import RestServlet, parse_string - -from ._base import client_patterns - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -class AuthRestServlet(RestServlet): - """ - Handles Client / Server API authentication in any situations where it - cannot be handled in the normal flow (with requests to the same endpoint). - Current use is for web fallback auth. - """ - - PATTERNS = client_patterns(r"/auth/(?P[\w\.]*)/fallback/web") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - self.registration_handler = hs.get_registration_handler() - self.recaptcha_template = hs.config.recaptcha_template - self.terms_template = hs.config.terms_template - self.success_template = hs.config.fallback_success_template - - async def on_GET(self, request, stagetype): - session = parse_string(request, "session") - if not session: - raise SynapseError(400, "No session supplied") - - if stagetype == LoginType.RECAPTCHA: - html = self.recaptcha_template.render( - session=session, - myurl="%s/r0/auth/%s/fallback/web" - % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), - sitekey=self.hs.config.recaptcha_public_key, - ) - elif stagetype == LoginType.TERMS: - html = self.terms_template.render( - session=session, - terms_url="%s_matrix/consent?v=%s" - % (self.hs.config.public_baseurl, self.hs.config.user_consent_version), - myurl="%s/r0/auth/%s/fallback/web" - % (CLIENT_API_PREFIX, LoginType.TERMS), - ) - - elif stagetype == LoginType.SSO: - # Display a confirmation page which prompts the user to - # re-authenticate with their SSO provider. - html = await self.auth_handler.start_sso_ui_auth(request, session) - - else: - raise SynapseError(404, "Unknown auth stage type") - - # Render the HTML and return. - respond_with_html(request, 200, html) - return None - - async def on_POST(self, request, stagetype): - - session = parse_string(request, "session") - if not session: - raise SynapseError(400, "No session supplied") - - if stagetype == LoginType.RECAPTCHA: - response = parse_string(request, "g-recaptcha-response") - - if not response: - raise SynapseError(400, "No captcha response supplied") - - authdict = {"response": response, "session": session} - - success = await self.auth_handler.add_oob_auth( - LoginType.RECAPTCHA, authdict, request.getClientIP() - ) - - if success: - html = self.success_template.render() - else: - html = self.recaptcha_template.render( - session=session, - myurl="%s/r0/auth/%s/fallback/web" - % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), - sitekey=self.hs.config.recaptcha_public_key, - ) - elif stagetype == LoginType.TERMS: - authdict = {"session": session} - - success = await self.auth_handler.add_oob_auth( - LoginType.TERMS, authdict, request.getClientIP() - ) - - if success: - html = self.success_template.render() - else: - html = self.terms_template.render( - session=session, - terms_url="%s_matrix/consent?v=%s" - % ( - self.hs.config.public_baseurl, - self.hs.config.user_consent_version, - ), - myurl="%s/r0/auth/%s/fallback/web" - % (CLIENT_API_PREFIX, LoginType.TERMS), - ) - elif stagetype == LoginType.SSO: - # The SSO fallback workflow should not post here, - raise SynapseError(404, "Fallback SSO auth does not support POST requests.") - else: - raise SynapseError(404, "Unknown auth stage type") - - # Render the HTML and return. - respond_with_html(request, 200, html) - return None - - -def register_servlets(hs, http_server): - AuthRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/capabilities.py b/synapse/rest/client/v2_alpha/capabilities.py deleted file mode 100644 index 88e3aac797..0000000000 --- a/synapse/rest/client/v2_alpha/capabilities.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2019 New Vector -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import logging -from typing import TYPE_CHECKING, Tuple - -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, MSC3244_CAPABILITIES -from synapse.http.servlet import RestServlet -from synapse.http.site import SynapseRequest -from synapse.types import JsonDict - -from ._base import client_patterns - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -class CapabilitiesRestServlet(RestServlet): - """End point to expose the capabilities of the server.""" - - PATTERNS = client_patterns("/capabilities$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.config = hs.config - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - - async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - await self.auth.get_user_by_req(request, allow_guest=True) - change_password = self.auth_handler.can_change_password() - - response = { - "capabilities": { - "m.room_versions": { - "default": self.config.default_room_version.identifier, - "available": { - v.identifier: v.disposition - for v in KNOWN_ROOM_VERSIONS.values() - }, - }, - "m.change_password": {"enabled": change_password}, - } - } - - if self.config.experimental.msc3244_enabled: - response["capabilities"]["m.room_versions"][ - "org.matrix.msc3244.room_capabilities" - ] = MSC3244_CAPABILITIES - - return 200, response - - -def register_servlets(hs: "HomeServer", http_server): - CapabilitiesRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/v2_alpha/devices.py deleted file mode 100644 index 8b9674db06..0000000000 --- a/synapse/rest/client/v2_alpha/devices.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# Copyright 2020 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api import errors -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, -) -from synapse.http.site import SynapseRequest - -from ._base import client_patterns, interactive_auth_handler - -logger = logging.getLogger(__name__) - - -class DevicesRestServlet(RestServlet): - PATTERNS = client_patterns("/devices$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.device_handler = hs.get_device_handler() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - devices = await self.device_handler.get_devices_by_user( - requester.user.to_string() - ) - return 200, {"devices": devices} - - -class DeleteDevicesRestServlet(RestServlet): - """ - API for bulk deletion of devices. Accepts a JSON object with a devices - key which lists the device_ids to delete. Requires user interactive auth. - """ - - PATTERNS = client_patterns("/delete_devices") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.device_handler = hs.get_device_handler() - self.auth_handler = hs.get_auth_handler() - - @interactive_auth_handler - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - - try: - body = parse_json_object_from_request(request) - except errors.SynapseError as e: - if e.errcode == errors.Codes.NOT_JSON: - # DELETE - # deal with older clients which didn't pass a JSON dict - # the same as those that pass an empty dict - body = {} - else: - raise e - - assert_params_in_dict(body, ["devices"]) - - await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body, - "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"] - ) - return 200, {} - - -class DeviceRestServlet(RestServlet): - PATTERNS = client_patterns("/devices/(?P[^/]*)$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.device_handler = hs.get_device_handler() - self.auth_handler = hs.get_auth_handler() - - async def on_GET(self, request, device_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - device = await self.device_handler.get_device( - requester.user.to_string(), device_id - ) - return 200, device - - @interactive_auth_handler - async def on_DELETE(self, request, device_id): - requester = await self.auth.get_user_by_req(request) - - try: - body = parse_json_object_from_request(request) - - except errors.SynapseError as e: - if e.errcode == errors.Codes.NOT_JSON: - # deal with older clients which didn't pass a JSON dict - # the same as those that pass an empty dict - body = {} - else: - raise - - await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body, - "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_device(requester.user.to_string(), device_id) - return 200, {} - - async def on_PUT(self, request, device_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - body = parse_json_object_from_request(request) - await self.device_handler.update_device( - requester.user.to_string(), device_id, body - ) - return 200, {} - - -class DehydratedDeviceServlet(RestServlet): - """Retrieve or store a dehydrated device. - - GET /org.matrix.msc2697.v2/dehydrated_device - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "device_id": "dehydrated_device_id", - "device_data": { - "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", - "account": "dehydrated_device" - } - } - - PUT /org.matrix.msc2697/dehydrated_device - Content-Type: application/json - - { - "device_data": { - "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", - "account": "dehydrated_device" - } - } - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "device_id": "dehydrated_device_id" - } - - """ - - PATTERNS = client_patterns("/org.matrix.msc2697.v2/dehydrated_device", releases=()) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.device_handler = hs.get_device_handler() - - async def on_GET(self, request: SynapseRequest): - requester = await self.auth.get_user_by_req(request) - dehydrated_device = await self.device_handler.get_dehydrated_device( - requester.user.to_string() - ) - if dehydrated_device is not None: - (device_id, device_data) = dehydrated_device - result = {"device_id": device_id, "device_data": device_data} - return (200, result) - else: - raise errors.NotFoundError("No dehydrated device available") - - async def on_PUT(self, request: SynapseRequest): - submission = parse_json_object_from_request(request) - requester = await self.auth.get_user_by_req(request) - - if "device_data" not in submission: - raise errors.SynapseError( - 400, - "device_data missing", - errcode=errors.Codes.MISSING_PARAM, - ) - elif not isinstance(submission["device_data"], dict): - raise errors.SynapseError( - 400, - "device_data must be an object", - errcode=errors.Codes.INVALID_PARAM, - ) - - device_id = await self.device_handler.store_dehydrated_device( - requester.user.to_string(), - submission["device_data"], - submission.get("initial_device_display_name", None), - ) - return 200, {"device_id": device_id} - - -class ClaimDehydratedDeviceServlet(RestServlet): - """Claim a dehydrated device. - - POST /org.matrix.msc2697.v2/dehydrated_device/claim - Content-Type: application/json - - { - "device_id": "dehydrated_device_id" - } - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "success": true, - } - - """ - - PATTERNS = client_patterns( - "/org.matrix.msc2697.v2/dehydrated_device/claim", releases=() - ) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.device_handler = hs.get_device_handler() - - async def on_POST(self, request: SynapseRequest): - requester = await self.auth.get_user_by_req(request) - - submission = parse_json_object_from_request(request) - - if "device_id" not in submission: - raise errors.SynapseError( - 400, - "device_id missing", - errcode=errors.Codes.MISSING_PARAM, - ) - elif not isinstance(submission["device_id"], str): - raise errors.SynapseError( - 400, - "device_id must be a string", - errcode=errors.Codes.INVALID_PARAM, - ) - - result = await self.device_handler.rehydrate_device( - requester.user.to_string(), - self.auth.get_access_token_from_request(request), - submission["device_id"], - ) - - return (200, result) - - -def register_servlets(hs, http_server): - DeleteDevicesRestServlet(hs).register(http_server) - DevicesRestServlet(hs).register(http_server) - DeviceRestServlet(hs).register(http_server) - DehydratedDeviceServlet(hs).register(http_server) - ClaimDehydratedDeviceServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/v2_alpha/filter.py deleted file mode 100644 index 411667a9c8..0000000000 --- a/synapse/rest/client/v2_alpha/filter.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import AuthError, NotFoundError, StoreError, SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.types import UserID - -from ._base import client_patterns, set_timeline_upper_limit - -logger = logging.getLogger(__name__) - - -class GetFilterRestServlet(RestServlet): - PATTERNS = client_patterns("/user/(?P[^/]*)/filter/(?P[^/]*)") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.filtering = hs.get_filtering() - - async def on_GET(self, request, user_id, filter_id): - target_user = UserID.from_string(user_id) - requester = await self.auth.get_user_by_req(request) - - if target_user != requester.user: - raise AuthError(403, "Cannot get filters for other users") - - if not self.hs.is_mine(target_user): - raise AuthError(403, "Can only get filters for local users") - - try: - filter_id = int(filter_id) - except Exception: - raise SynapseError(400, "Invalid filter_id") - - try: - filter_collection = await self.filtering.get_user_filter( - user_localpart=target_user.localpart, filter_id=filter_id - ) - except StoreError as e: - if e.code != 404: - raise - raise NotFoundError("No such filter") - - return 200, filter_collection.get_filter_json() - - -class CreateFilterRestServlet(RestServlet): - PATTERNS = client_patterns("/user/(?P[^/]*)/filter") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.filtering = hs.get_filtering() - - async def on_POST(self, request, user_id): - - target_user = UserID.from_string(user_id) - requester = await self.auth.get_user_by_req(request) - - if target_user != requester.user: - raise AuthError(403, "Cannot create filters for other users") - - if not self.hs.is_mine(target_user): - raise AuthError(403, "Can only create filters for local users") - - content = parse_json_object_from_request(request) - set_timeline_upper_limit(content, self.hs.config.filter_timeline_limit) - - filter_id = await self.filtering.add_user_filter( - user_localpart=target_user.localpart, user_filter=content - ) - - return 200, {"filter_id": str(filter_id)} - - -def register_servlets(hs, http_server): - GetFilterRestServlet(hs).register(http_server) - CreateFilterRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/groups.py b/synapse/rest/client/v2_alpha/groups.py deleted file mode 100644 index 6285680c00..0000000000 --- a/synapse/rest/client/v2_alpha/groups.py +++ /dev/null @@ -1,957 +0,0 @@ -# Copyright 2017 Vector Creations Ltd -# Copyright 2018 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from functools import wraps -from typing import TYPE_CHECKING, Optional, Tuple - -from twisted.web.server import Request - -from synapse.api.constants import ( - MAX_GROUP_CATEGORYID_LENGTH, - MAX_GROUP_ROLEID_LENGTH, - MAX_GROUPID_LENGTH, -) -from synapse.api.errors import Codes, SynapseError -from synapse.handlers.groups_local import GroupsLocalHandler -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, -) -from synapse.http.site import SynapseRequest -from synapse.types import GroupID, JsonDict - -from ._base import client_patterns - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -def _validate_group_id(f): - """Wrapper to validate the form of the group ID. - - Can be applied to any on_FOO methods that accepts a group ID as a URL parameter. - """ - - @wraps(f) - def wrapper(self, request: Request, group_id: str, *args, **kwargs): - if not GroupID.is_valid(group_id): - raise SynapseError(400, "%s is not a legal group ID" % (group_id,)) - - return f(self, request, group_id, *args, **kwargs) - - return wrapper - - -class GroupServlet(RestServlet): - """Get the group profile""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/profile$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - group_description = await self.groups_handler.get_group_profile( - group_id, requester_user_id - ) - - return 200, group_description - - @_validate_group_id - async def on_POST( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - assert_params_in_dict( - content, ("name", "avatar_url", "short_description", "long_description") - ) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot create group profiles." - await self.groups_handler.update_group_profile( - group_id, requester_user_id, content - ) - - return 200, {} - - -class GroupSummaryServlet(RestServlet): - """Get the full group summary""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/summary$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - get_group_summary = await self.groups_handler.get_group_summary( - group_id, requester_user_id - ) - - return 200, get_group_summary - - -class GroupSummaryRoomsCatServlet(RestServlet): - """Update/delete a rooms entry in the summary. - - Matches both: - - /groups/:group/summary/rooms/:room_id - - /groups/:group/summary/categories/:category/rooms/:room_id - """ - - PATTERNS = client_patterns( - "/groups/(?P[^/]*)/summary" - "(/categories/(?P[^/]+))?" - "/rooms/(?P[^/]*)$" - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, - request: SynapseRequest, - group_id: str, - category_id: Optional[str], - room_id: str, - ): - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - if category_id == "": - raise SynapseError(400, "category_id cannot be empty", Codes.INVALID_PARAM) - - if category_id and len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: - raise SynapseError( - 400, - "category_id may not be longer than %s characters" - % (MAX_GROUP_CATEGORYID_LENGTH,), - Codes.INVALID_PARAM, - ) - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group summaries." - resp = await self.groups_handler.update_group_summary_room( - group_id, - requester_user_id, - room_id=room_id, - category_id=category_id, - content=content, - ) - - return 200, resp - - @_validate_group_id - async def on_DELETE( - self, request: SynapseRequest, group_id: str, category_id: str, room_id: str - ): - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group profiles." - resp = await self.groups_handler.delete_group_summary_room( - group_id, requester_user_id, room_id=room_id, category_id=category_id - ) - - return 200, resp - - -class GroupCategoryServlet(RestServlet): - """Get/add/update/delete a group category""" - - PATTERNS = client_patterns( - "/groups/(?P[^/]*)/categories/(?P[^/]+)$" - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str, category_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - category = await self.groups_handler.get_group_category( - group_id, requester_user_id, category_id=category_id - ) - - return 200, category - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str, category_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - if not category_id: - raise SynapseError(400, "category_id cannot be empty", Codes.INVALID_PARAM) - - if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: - raise SynapseError( - 400, - "category_id may not be longer than %s characters" - % (MAX_GROUP_CATEGORYID_LENGTH,), - Codes.INVALID_PARAM, - ) - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group categories." - resp = await self.groups_handler.update_group_category( - group_id, requester_user_id, category_id=category_id, content=content - ) - - return 200, resp - - @_validate_group_id - async def on_DELETE( - self, request: SynapseRequest, group_id: str, category_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group categories." - resp = await self.groups_handler.delete_group_category( - group_id, requester_user_id, category_id=category_id - ) - - return 200, resp - - -class GroupCategoriesServlet(RestServlet): - """Get all group categories""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/categories/$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - category = await self.groups_handler.get_group_categories( - group_id, requester_user_id - ) - - return 200, category - - -class GroupRoleServlet(RestServlet): - """Get/add/update/delete a group role""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/roles/(?P[^/]+)$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str, role_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - category = await self.groups_handler.get_group_role( - group_id, requester_user_id, role_id=role_id - ) - - return 200, category - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str, role_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - if not role_id: - raise SynapseError(400, "role_id cannot be empty", Codes.INVALID_PARAM) - - if len(role_id) > MAX_GROUP_ROLEID_LENGTH: - raise SynapseError( - 400, - "role_id may not be longer than %s characters" - % (MAX_GROUP_ROLEID_LENGTH,), - Codes.INVALID_PARAM, - ) - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group roles." - resp = await self.groups_handler.update_group_role( - group_id, requester_user_id, role_id=role_id, content=content - ) - - return 200, resp - - @_validate_group_id - async def on_DELETE( - self, request: SynapseRequest, group_id: str, role_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group roles." - resp = await self.groups_handler.delete_group_role( - group_id, requester_user_id, role_id=role_id - ) - - return 200, resp - - -class GroupRolesServlet(RestServlet): - """Get all group roles""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/roles/$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - category = await self.groups_handler.get_group_roles( - group_id, requester_user_id - ) - - return 200, category - - -class GroupSummaryUsersRoleServlet(RestServlet): - """Update/delete a user's entry in the summary. - - Matches both: - - /groups/:group/summary/users/:room_id - - /groups/:group/summary/roles/:role/users/:user_id - """ - - PATTERNS = client_patterns( - "/groups/(?P[^/]*)/summary" - "(/roles/(?P[^/]+))?" - "/users/(?P[^/]*)$" - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, - request: SynapseRequest, - group_id: str, - role_id: Optional[str], - user_id: str, - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - if role_id == "": - raise SynapseError(400, "role_id cannot be empty", Codes.INVALID_PARAM) - - if role_id and len(role_id) > MAX_GROUP_ROLEID_LENGTH: - raise SynapseError( - 400, - "role_id may not be longer than %s characters" - % (MAX_GROUP_ROLEID_LENGTH,), - Codes.INVALID_PARAM, - ) - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group summaries." - resp = await self.groups_handler.update_group_summary_user( - group_id, - requester_user_id, - user_id=user_id, - role_id=role_id, - content=content, - ) - - return 200, resp - - @_validate_group_id - async def on_DELETE( - self, request: SynapseRequest, group_id: str, role_id: str, user_id: str - ): - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group summaries." - resp = await self.groups_handler.delete_group_summary_user( - group_id, requester_user_id, user_id=user_id, role_id=role_id - ) - - return 200, resp - - -class GroupRoomServlet(RestServlet): - """Get all rooms in a group""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/rooms$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - result = await self.groups_handler.get_rooms_in_group( - group_id, requester_user_id - ) - - return 200, result - - -class GroupUsersServlet(RestServlet): - """Get all users in a group""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/users$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - result = await self.groups_handler.get_users_in_group( - group_id, requester_user_id - ) - - return 200, result - - -class GroupInvitedUsersServlet(RestServlet): - """Get users invited to a group""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/invited_users$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_GET( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - result = await self.groups_handler.get_invited_users_in_group( - group_id, requester_user_id - ) - - return 200, result - - -class GroupSettingJoinPolicyServlet(RestServlet): - """Set group join policy""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/settings/m.join_policy$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group join policy." - result = await self.groups_handler.set_group_join_policy( - group_id, requester_user_id, content - ) - - return 200, result - - -class GroupCreateServlet(RestServlet): - """Create a group""" - - PATTERNS = client_patterns("/create_group$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - self.server_name = hs.hostname - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - # TODO: Create group on remote server - content = parse_json_object_from_request(request) - localpart = content.pop("localpart") - group_id = GroupID(localpart, self.server_name).to_string() - - if not localpart: - raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM) - - if len(group_id) > MAX_GROUPID_LENGTH: - raise SynapseError( - 400, - "Group ID may not be longer than %s characters" % (MAX_GROUPID_LENGTH,), - Codes.INVALID_PARAM, - ) - - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot create groups." - result = await self.groups_handler.create_group( - group_id, requester_user_id, content - ) - - return 200, result - - -class GroupAdminRoomsServlet(RestServlet): - """Add a room to the group""" - - PATTERNS = client_patterns( - "/groups/(?P[^/]*)/admin/rooms/(?P[^/]*)$" - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify rooms in a group." - result = await self.groups_handler.add_room_to_group( - group_id, requester_user_id, room_id, content - ) - - return 200, result - - @_validate_group_id - async def on_DELETE( - self, request: SynapseRequest, group_id: str, room_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group categories." - result = await self.groups_handler.remove_room_from_group( - group_id, requester_user_id, room_id - ) - - return 200, result - - -class GroupAdminRoomsConfigServlet(RestServlet): - """Update the config of a room in a group""" - - PATTERNS = client_patterns( - "/groups/(?P[^/]*)/admin/rooms/(?P[^/]*)" - "/config/(?P[^/]*)$" - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str, room_id: str, config_key: str - ): - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot modify group categories." - result = await self.groups_handler.update_room_in_group( - group_id, requester_user_id, room_id, config_key, content - ) - - return 200, result - - -class GroupAdminUsersInviteServlet(RestServlet): - """Invite a user to the group""" - - PATTERNS = client_patterns( - "/groups/(?P[^/]*)/admin/users/invite/(?P[^/]*)$" - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - self.store = hs.get_datastore() - self.is_mine_id = hs.is_mine_id - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id, user_id - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - config = content.get("config", {}) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot invite users to a group." - result = await self.groups_handler.invite( - group_id, user_id, requester_user_id, config - ) - - return 200, result - - -class GroupAdminUsersKickServlet(RestServlet): - """Kick a user from the group""" - - PATTERNS = client_patterns( - "/groups/(?P[^/]*)/admin/users/remove/(?P[^/]*)$" - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id, user_id - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot kick users from a group." - result = await self.groups_handler.remove_user_from_group( - group_id, user_id, requester_user_id, content - ) - - return 200, result - - -class GroupSelfLeaveServlet(RestServlet): - """Leave a joined group""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/self/leave$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot leave a group for a users." - result = await self.groups_handler.remove_user_from_group( - group_id, requester_user_id, requester_user_id, content - ) - - return 200, result - - -class GroupSelfJoinServlet(RestServlet): - """Attempt to join a group, or knock""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/self/join$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot join a user to a group." - result = await self.groups_handler.join_group( - group_id, requester_user_id, content - ) - - return 200, result - - -class GroupSelfAcceptInviteServlet(RestServlet): - """Accept a group invite""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/self/accept_invite$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - assert isinstance( - self.groups_handler, GroupsLocalHandler - ), "Workers cannot accept an invite to a group." - result = await self.groups_handler.accept_invite( - group_id, requester_user_id, content - ) - - return 200, result - - -class GroupSelfUpdatePublicityServlet(RestServlet): - """Update whether we publicise a users membership of a group""" - - PATTERNS = client_patterns("/groups/(?P[^/]*)/self/update_publicity$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.store = hs.get_datastore() - - @_validate_group_id - async def on_PUT( - self, request: SynapseRequest, group_id: str - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - requester_user_id = requester.user.to_string() - - content = parse_json_object_from_request(request) - publicise = content["publicise"] - await self.store.update_group_publicity(group_id, requester_user_id, publicise) - - return 200, {} - - -class PublicisedGroupsForUserServlet(RestServlet): - """Get the list of groups a user is advertising""" - - PATTERNS = client_patterns("/publicised_groups/(?P[^/]*)$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.store = hs.get_datastore() - self.groups_handler = hs.get_groups_local_handler() - - async def on_GET( - self, request: SynapseRequest, user_id: str - ) -> Tuple[int, JsonDict]: - await self.auth.get_user_by_req(request, allow_guest=True) - - result = await self.groups_handler.get_publicised_groups_for_user(user_id) - - return 200, result - - -class PublicisedGroupsForUsersServlet(RestServlet): - """Get the list of groups a user is advertising""" - - PATTERNS = client_patterns("/publicised_groups$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.store = hs.get_datastore() - self.groups_handler = hs.get_groups_local_handler() - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - await self.auth.get_user_by_req(request, allow_guest=True) - - content = parse_json_object_from_request(request) - user_ids = content["user_ids"] - - result = await self.groups_handler.bulk_get_publicised_groups(user_ids) - - return 200, result - - -class GroupsForUserServlet(RestServlet): - """Get all groups the logged in user is joined to""" - - PATTERNS = client_patterns("/joined_groups$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.groups_handler = hs.get_groups_local_handler() - - async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request, allow_guest=True) - requester_user_id = requester.user.to_string() - - result = await self.groups_handler.get_joined_groups(requester_user_id) - - return 200, result - - -def register_servlets(hs: "HomeServer", http_server): - GroupServlet(hs).register(http_server) - GroupSummaryServlet(hs).register(http_server) - GroupInvitedUsersServlet(hs).register(http_server) - GroupUsersServlet(hs).register(http_server) - GroupRoomServlet(hs).register(http_server) - GroupSettingJoinPolicyServlet(hs).register(http_server) - GroupCreateServlet(hs).register(http_server) - GroupAdminRoomsServlet(hs).register(http_server) - GroupAdminRoomsConfigServlet(hs).register(http_server) - GroupAdminUsersInviteServlet(hs).register(http_server) - GroupAdminUsersKickServlet(hs).register(http_server) - GroupSelfLeaveServlet(hs).register(http_server) - GroupSelfJoinServlet(hs).register(http_server) - GroupSelfAcceptInviteServlet(hs).register(http_server) - GroupsForUserServlet(hs).register(http_server) - GroupCategoryServlet(hs).register(http_server) - GroupCategoriesServlet(hs).register(http_server) - GroupSummaryRoomsCatServlet(hs).register(http_server) - GroupRoleServlet(hs).register(http_server) - GroupRolesServlet(hs).register(http_server) - GroupSelfUpdatePublicityServlet(hs).register(http_server) - GroupSummaryUsersRoleServlet(hs).register(http_server) - PublicisedGroupsForUserServlet(hs).register(http_server) - PublicisedGroupsForUsersServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py deleted file mode 100644 index d0d9d30d40..0000000000 --- a/synapse/rest/client/v2_alpha/keys.py +++ /dev/null @@ -1,344 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# Copyright 2019 New Vector Ltd -# Copyright 2020 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import SynapseError -from synapse.http.servlet import ( - RestServlet, - parse_integer, - parse_json_object_from_request, - parse_string, -) -from synapse.logging.opentracing import log_kv, set_tag, trace -from synapse.types import StreamToken - -from ._base import client_patterns, interactive_auth_handler - -logger = logging.getLogger(__name__) - - -class KeyUploadServlet(RestServlet): - """ - POST /keys/upload HTTP/1.1 - Content-Type: application/json - - { - "device_keys": { - "user_id": "", - "device_id": "", - "valid_until_ts": , - "algorithms": [ - "m.olm.curve25519-aes-sha2", - ] - "keys": { - ":": "", - }, - "signatures:" { - "" { - ":": "" - } } }, - "one_time_keys": { - ":": "" - }, - } - """ - - PATTERNS = client_patterns("/keys/upload(/(?P[^/]+))?$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.auth = hs.get_auth() - self.e2e_keys_handler = hs.get_e2e_keys_handler() - self.device_handler = hs.get_device_handler() - - @trace(opname="upload_keys") - async def on_POST(self, request, device_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - user_id = requester.user.to_string() - body = parse_json_object_from_request(request) - - if device_id is not None: - # Providing the device_id should only be done for setting keys - # for dehydrated devices; however, we allow it for any device for - # compatibility with older clients. - if requester.device_id is not None and device_id != requester.device_id: - dehydrated_device = await self.device_handler.get_dehydrated_device( - user_id - ) - if dehydrated_device is not None and device_id != dehydrated_device[0]: - set_tag("error", True) - log_kv( - { - "message": "Client uploading keys for a different device", - "logged_in_id": requester.device_id, - "key_being_uploaded": device_id, - } - ) - logger.warning( - "Client uploading keys for a different device " - "(logged in as %s, uploading for %s)", - requester.device_id, - device_id, - ) - else: - device_id = requester.device_id - - if device_id is None: - raise SynapseError( - 400, "To upload keys, you must pass device_id when authenticating" - ) - - result = await self.e2e_keys_handler.upload_keys_for_user( - user_id, device_id, body - ) - return 200, result - - -class KeyQueryServlet(RestServlet): - """ - POST /keys/query HTTP/1.1 - Content-Type: application/json - { - "device_keys": { - "": [""] - } } - - HTTP/1.1 200 OK - { - "device_keys": { - "": { - "": { - "user_id": "", // Duplicated to be signed - "device_id": "", // Duplicated to be signed - "valid_until_ts": , - "algorithms": [ // List of supported algorithms - "m.olm.curve25519-aes-sha2", - ], - "keys": { // Must include a ed25519 signing key - ":": "", - }, - "signatures:" { - // Must be signed with device's ed25519 key - "/": { - ":": "" - } - // Must be signed by this server. - "": { - ":": "" - } } } } } } - """ - - PATTERNS = client_patterns("/keys/query$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): - """ - super().__init__() - self.auth = hs.get_auth() - self.e2e_keys_handler = hs.get_e2e_keys_handler() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - user_id = requester.user.to_string() - device_id = requester.device_id - timeout = parse_integer(request, "timeout", 10 * 1000) - body = parse_json_object_from_request(request) - result = await self.e2e_keys_handler.query_devices( - body, timeout, user_id, device_id - ) - return 200, result - - -class KeyChangesServlet(RestServlet): - """Returns the list of changes of keys between two stream tokens (may return - spurious extra results, since we currently ignore the `to` param). - - GET /keys/changes?from=...&to=... - - 200 OK - { "changed": ["@foo:example.com"] } - """ - - PATTERNS = client_patterns("/keys/changes$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): - """ - super().__init__() - self.auth = hs.get_auth() - self.device_handler = hs.get_device_handler() - self.store = hs.get_datastore() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - from_token_string = parse_string(request, "from", required=True) - set_tag("from", from_token_string) - - # We want to enforce they do pass us one, but we ignore it and return - # changes after the "to" as well as before. - set_tag("to", parse_string(request, "to")) - - from_token = await StreamToken.from_string(self.store, from_token_string) - - user_id = requester.user.to_string() - - results = await self.device_handler.get_user_ids_changed(user_id, from_token) - - return 200, results - - -class OneTimeKeyServlet(RestServlet): - """ - POST /keys/claim HTTP/1.1 - { - "one_time_keys": { - "": { - "": "" - } } } - - HTTP/1.1 200 OK - { - "one_time_keys": { - "": { - "": { - ":": "" - } } } } - - """ - - PATTERNS = client_patterns("/keys/claim$") - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.e2e_keys_handler = hs.get_e2e_keys_handler() - - async def on_POST(self, request): - await self.auth.get_user_by_req(request, allow_guest=True) - timeout = parse_integer(request, "timeout", 10 * 1000) - body = parse_json_object_from_request(request) - result = await self.e2e_keys_handler.claim_one_time_keys(body, timeout) - return 200, result - - -class SigningKeyUploadServlet(RestServlet): - """ - POST /keys/device_signing/upload HTTP/1.1 - Content-Type: application/json - - { - } - """ - - PATTERNS = client_patterns("/keys/device_signing/upload$", releases=()) - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.e2e_keys_handler = hs.get_e2e_keys_handler() - self.auth_handler = hs.get_auth_handler() - - @interactive_auth_handler - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - body = parse_json_object_from_request(request) - - await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body, - "add a device signing key to your account", - # Allow skipping of UI auth since this is frequently called directly - # after login and it is silly to ask users to re-auth immediately. - can_skip_ui_auth=True, - ) - - result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) - return 200, result - - -class SignaturesUploadServlet(RestServlet): - """ - POST /keys/signatures/upload HTTP/1.1 - Content-Type: application/json - - { - "@alice:example.com": { - "": { - "user_id": "", - "device_id": "", - "algorithms": [ - "m.olm.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "keys": { - ":": "", - }, - "signatures": { - "": { - ":": ">" - } - } - } - } - } - """ - - PATTERNS = client_patterns("/keys/signatures/upload$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.auth = hs.get_auth() - self.e2e_keys_handler = hs.get_e2e_keys_handler() - - async def on_POST(self, request): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - user_id = requester.user.to_string() - body = parse_json_object_from_request(request) - - result = await self.e2e_keys_handler.upload_signatures_for_device_keys( - user_id, body - ) - return 200, result - - -def register_servlets(hs, http_server): - KeyUploadServlet(hs).register(http_server) - KeyQueryServlet(hs).register(http_server) - KeyChangesServlet(hs).register(http_server) - OneTimeKeyServlet(hs).register(http_server) - SigningKeyUploadServlet(hs).register(http_server) - SignaturesUploadServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/knock.py b/synapse/rest/client/v2_alpha/knock.py deleted file mode 100644 index 7d1bc40658..0000000000 --- a/synapse/rest/client/v2_alpha/knock.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2020 Sorunome -# Copyright 2020 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import logging -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple - -from twisted.web.server import Request - -from synapse.api.constants import Membership -from synapse.api.errors import SynapseError -from synapse.http.servlet import ( - RestServlet, - parse_json_object_from_request, - parse_strings_from_args, -) -from synapse.http.site import SynapseRequest -from synapse.logging.opentracing import set_tag -from synapse.rest.client.transactions import HttpTransactionCache -from synapse.types import JsonDict, RoomAlias, RoomID - -if TYPE_CHECKING: - from synapse.app.homeserver import HomeServer - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class KnockRoomAliasServlet(RestServlet): - """ - POST /knock/{roomIdOrAlias} - """ - - PATTERNS = client_patterns("/knock/(?P[^/]*)") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.txns = HttpTransactionCache(hs) - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - - async def on_POST( - self, - request: SynapseRequest, - room_identifier: str, - txn_id: Optional[str] = None, - ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - - content = parse_json_object_from_request(request) - event_content = None - if "reason" in content: - event_content = {"reason": content["reason"]} - - if RoomID.is_valid(room_identifier): - room_id = room_identifier - - # 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 - ) - elif RoomAlias.is_valid(room_identifier): - handler = self.room_member_handler - room_alias = RoomAlias.from_string(room_identifier) - room_id_obj, remote_room_hosts = await handler.lookup_room_alias(room_alias) - room_id = room_id_obj.to_string() - else: - raise SynapseError( - 400, "%s was not legal room ID or room alias" % (room_identifier,) - ) - - await self.room_member_handler.update_membership( - requester=requester, - target=requester.user, - room_id=room_id, - action=Membership.KNOCK, - txn_id=txn_id, - third_party_signed=None, - remote_room_hosts=remote_room_hosts, - content=event_content, - ) - - return 200, {"room_id": room_id} - - def on_PUT(self, request: Request, room_identifier: str, txn_id: str): - set_tag("txn_id", txn_id) - - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_identifier, txn_id - ) - - -def register_servlets(hs, http_server): - KnockRoomAliasServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/notifications.py b/synapse/rest/client/v2_alpha/notifications.py deleted file mode 100644 index 0ede643c2d..0000000000 --- a/synapse/rest/client/v2_alpha/notifications.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.events.utils import format_event_for_client_v2_without_room_id -from synapse.http.servlet import RestServlet, parse_integer, parse_string - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class NotificationsServlet(RestServlet): - PATTERNS = client_patterns("/notifications$") - - def __init__(self, hs): - super().__init__() - self.store = hs.get_datastore() - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self._event_serializer = hs.get_event_client_serializer() - - async def on_GET(self, request): - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - from_token = parse_string(request, "from", required=False) - limit = parse_integer(request, "limit", default=50) - only = parse_string(request, "only", required=False) - - limit = min(limit, 500) - - push_actions = await self.store.get_push_actions_for_user( - user_id, from_token, limit, only_highlight=(only == "highlight") - ) - - receipts_by_room = await self.store.get_receipts_for_user_with_orderings( - user_id, "m.read" - ) - - notif_event_ids = [pa["event_id"] for pa in push_actions] - notif_events = await self.store.get_events(notif_event_ids) - - returned_push_actions = [] - - next_token = None - - for pa in push_actions: - returned_pa = { - "room_id": pa["room_id"], - "profile_tag": pa["profile_tag"], - "actions": pa["actions"], - "ts": pa["received_ts"], - "event": ( - await self._event_serializer.serialize_event( - notif_events[pa["event_id"]], - self.clock.time_msec(), - event_format=format_event_for_client_v2_without_room_id, - ) - ), - } - - if pa["room_id"] not in receipts_by_room: - returned_pa["read"] = False - else: - receipt = receipts_by_room[pa["room_id"]] - - returned_pa["read"] = ( - receipt["topological_ordering"], - receipt["stream_ordering"], - ) >= (pa["topological_ordering"], pa["stream_ordering"]) - returned_push_actions.append(returned_pa) - next_token = str(pa["stream_ordering"]) - - return 200, {"notifications": returned_push_actions, "next_token": next_token} - - -def register_servlets(hs, http_server): - NotificationsServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/openid.py b/synapse/rest/client/v2_alpha/openid.py deleted file mode 100644 index e8d2673819..0000000000 --- a/synapse/rest/client/v2_alpha/openid.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import logging - -from synapse.api.errors import AuthError -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.util.stringutils import random_string - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class IdTokenServlet(RestServlet): - """ - Get a bearer token that may be passed to a third party to confirm ownership - of a matrix user id. - - The format of the response could be made compatible with the format given - in http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse - - But instead of returning a signed "id_token" the response contains the - name of the issuing matrix homeserver. This means that for now the third - party will need to check the validity of the "id_token" against the - federation /openid/userinfo endpoint of the homeserver. - - Request: - - POST /user/{user_id}/openid/request_token?access_token=... HTTP/1.1 - - {} - - Response: - - HTTP/1.1 200 OK - { - "access_token": "ABDEFGH", - "token_type": "Bearer", - "matrix_server_name": "example.com", - "expires_in": 3600, - } - """ - - PATTERNS = client_patterns("/user/(?P[^/]*)/openid/request_token") - - EXPIRES_MS = 3600 * 1000 - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.clock = hs.get_clock() - self.server_name = hs.config.server_name - - async def on_POST(self, request, user_id): - requester = await self.auth.get_user_by_req(request) - if user_id != requester.user.to_string(): - raise AuthError(403, "Cannot request tokens for other users.") - - # Parse the request body to make sure it's JSON, but ignore the contents - # for now. - parse_json_object_from_request(request) - - token = random_string(24) - ts_valid_until_ms = self.clock.time_msec() + self.EXPIRES_MS - - await self.store.insert_open_id_token(token, ts_valid_until_ms, user_id) - - return ( - 200, - { - "access_token": token, - "token_type": "Bearer", - "matrix_server_name": self.server_name, - "expires_in": self.EXPIRES_MS // 1000, - }, - ) - - -def register_servlets(hs, http_server): - IdTokenServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/password_policy.py b/synapse/rest/client/v2_alpha/password_policy.py deleted file mode 100644 index a83927aee6..0000000000 --- a/synapse/rest/client/v2_alpha/password_policy.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2019 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.http.servlet import RestServlet - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class PasswordPolicyServlet(RestServlet): - PATTERNS = client_patterns("/password_policy$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - - self.policy = hs.config.password_policy - self.enabled = hs.config.password_policy_enabled - - def on_GET(self, request): - if not self.enabled or not self.policy: - return (200, {}) - - policy = {} - - for param in [ - "minimum_length", - "require_digit", - "require_symbol", - "require_lowercase", - "require_uppercase", - ]: - if param in self.policy: - policy["m.%s" % param] = self.policy[param] - - return (200, policy) - - -def register_servlets(hs, http_server): - PasswordPolicyServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py deleted file mode 100644 index 027f8b81fa..0000000000 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2017 Vector Creations Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.constants import ReadReceiptEventFields -from synapse.api.errors import Codes, SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class ReadMarkerRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/read_markers$") - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.receipts_handler = hs.get_receipts_handler() - self.read_marker_handler = hs.get_read_marker_handler() - self.presence_handler = hs.get_presence_handler() - - async def on_POST(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - - await self.presence_handler.bump_presence_active_time(requester.user) - - body = parse_json_object_from_request(request) - read_event_id = body.get("m.read", None) - hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) - - if not isinstance(hidden, bool): - raise SynapseError( - 400, - "Param %s must be a boolean, if given" - % ReadReceiptEventFields.MSC2285_HIDDEN, - Codes.BAD_JSON, - ) - - if read_event_id: - await self.receipts_handler.received_client_receipt( - room_id, - "m.read", - user_id=requester.user.to_string(), - event_id=read_event_id, - hidden=hidden, - ) - - read_marker_event_id = body.get("m.fully_read", None) - if read_marker_event_id: - await self.read_marker_handler.received_client_read_marker( - room_id, - user_id=requester.user.to_string(), - event_id=read_marker_event_id, - ) - - return 200, {} - - -def register_servlets(hs, http_server): - ReadMarkerRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py deleted file mode 100644 index d9ab836cd8..0000000000 --- a/synapse/rest/client/v2_alpha/receipts.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.constants import ReadReceiptEventFields -from synapse.api.errors import Codes, SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class ReceiptRestServlet(RestServlet): - PATTERNS = client_patterns( - "/rooms/(?P[^/]*)" - "/receipt/(?P[^/]*)" - "/(?P[^/]*)$" - ) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.receipts_handler = hs.get_receipts_handler() - self.presence_handler = hs.get_presence_handler() - - async def on_POST(self, request, room_id, receipt_type, event_id): - requester = await self.auth.get_user_by_req(request) - - if receipt_type != "m.read": - raise SynapseError(400, "Receipt type must be 'm.read'") - - body = parse_json_object_from_request(request, allow_empty_body=True) - hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) - - if not isinstance(hidden, bool): - raise SynapseError( - 400, - "Param %s must be a boolean, if given" - % ReadReceiptEventFields.MSC2285_HIDDEN, - Codes.BAD_JSON, - ) - - await self.presence_handler.bump_presence_active_time(requester.user) - - await self.receipts_handler.received_client_receipt( - room_id, - receipt_type, - user_id=requester.user.to_string(), - event_id=event_id, - hidden=hidden, - ) - - return 200, {} - - -def register_servlets(hs, http_server): - ReceiptRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py deleted file mode 100644 index 4d31584acd..0000000000 --- a/synapse/rest/client/v2_alpha/register.py +++ /dev/null @@ -1,879 +0,0 @@ -# Copyright 2015 - 2016 OpenMarket Ltd -# Copyright 2017 Vector Creations Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import hmac -import logging -import random -from typing import List, Union - -import synapse -import synapse.api.auth -import synapse.types -from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType -from synapse.api.errors import ( - Codes, - InteractiveAuthIncompleteError, - SynapseError, - ThreepidValidationError, - UnrecognizedRequestError, -) -from synapse.config import ConfigError -from synapse.config.captcha import CaptchaConfig -from synapse.config.consent import ConsentConfig -from synapse.config.emailconfig import ThreepidBehaviour -from synapse.config.ratelimiting import FederationRateLimitConfig -from synapse.config.registration import RegistrationConfig -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 finish_request, respond_with_html -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_boolean, - parse_json_object_from_request, - parse_string, -) -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 - -# We ought to be using hmac.compare_digest() but on older pythons it doesn't -# exist. It's a _really minor_ security flaw to use plain string comparison -# because the timing attack is so obscured by all the other code here it's -# unlikely to make much difference -if hasattr(hmac, "compare_digest"): - compare_digest = hmac.compare_digest -else: - - def compare_digest(a, b): - return a == b - - -logger = logging.getLogger(__name__) - - -class EmailRegisterRequestTokenRestServlet(RestServlet): - PATTERNS = client_patterns("/register/email/requestToken$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.config = hs.config - - if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self.mailer = Mailer( - hs=self.hs, - app_name=self.config.email_app_name, - template_html=self.config.email_registration_template_html, - template_text=self.config.email_registration_template_text, - ) - - async def on_POST(self, request): - if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF: - if self.hs.config.local_threepid_handling_disabled_due_to_email_config: - 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/v2_alpha/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 check_3pid_allowed(self.hs, "email", email): - 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_datastore().get_user_id_by_threepid( - "email", email - ) - - if existing_user_id is not None: - if self.hs.config.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) - - if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: - assert self.hs.config.account_threepid_delegate_email - - # Have the configured identity server handle the request - ret = await self.identity_handler.requestEmailToken( - self.hs.config.account_threepid_delegate_email, - email, - client_secret, - send_attempt, - next_link, - ) - else: - # Send registration emails from Synapse - sid = await self.identity_handler.send_threepid_validation( - email, - client_secret, - send_attempt, - self.mailer.send_registration_mail, - next_link, - ) - - # Wrap the session id in a JSON object - ret = {"sid": sid} - - threepid_send_requests.labels(type="email", reason="register").observe( - send_attempt - ) - - return 200, ret - - -class MsisdnRegisterRequestTokenRestServlet(RestServlet): - PATTERNS = client_patterns("/register/msisdn/requestToken$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - - async def on_POST(self, request): - 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 check_3pid_allowed(self.hs, "msisdn", msisdn): - 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_datastore().get_user_id_by_threepid( - "msisdn", msisdn - ) - - if existing_user_id is not None: - if self.hs.config.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.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.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[^/]*)/submit_token$", releases=(), unstable=True - ) - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.config = hs.config - self.clock = hs.get_clock() - self.store = hs.get_datastore() - - if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - self._failure_email_template = ( - self.config.email_registration_template_failure_html - ) - - async def on_GET(self, request, medium): - if medium != "email": - raise SynapseError( - 400, "This medium is currently not supported for registration" - ) - if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: - if self.config.local_threepid_handling_disabled_due_to_email_config: - 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_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") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.registration_handler = hs.get_registration_handler() - self.ratelimiter = FederationRateLimiter( - hs.get_clock(), - FederationRateLimitConfig( - # Time window of 2s - window_size=2000, - # Artificially delay requests if rate > sleep_limit/window_size - sleep_limit=1, - # Amount of artificial delay to apply - sleep_msec=1000, - # Error with 429 if more than reject_limit requests are queued - reject_limit=1, - # Allow 1 request at a time - concurrent_requests=1, - ), - ) - - async def on_GET(self, request): - if not self.hs.config.enable_registration: - raise SynapseError( - 403, "Registration has been disabled", errcode=Codes.FORBIDDEN - ) - - ip = request.getClientIP() - with self.ratelimiter.ratelimit(ip) as wait_deferred: - await wait_deferred - - username = parse_string(request, "username", required=True) - - await self.registration_handler.check_username(username) - - return 200, {"available": True} - - -class RegisterRestServlet(RestServlet): - PATTERNS = client_patterns("/register$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - - self.hs = hs - self.auth = hs.get_auth() - self.store = hs.get_datastore() - 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() - self.password_policy_handler = hs.get_password_policy_handler() - self.clock = hs.get_clock() - self._registration_enabled = self.hs.config.enable_registration - self._msc2918_enabled = hs.config.access_token_lifetime is not None - - self._registration_flows = _calculate_registration_flows( - hs.config, self.auth_handler - ) - - @interactive_auth_handler - async def on_POST(self, request): - body = parse_json_object_from_request(request) - - client_addr = request.getClientIP() - - await self.ratelimiter.ratelimit(None, client_addr, update=False) - - kind = b"user" - if b"kind" in request.args: - kind = request.args[b"kind"][0] - - if kind == b"guest": - ret = await self._do_guest_registration(body, address=client_addr) - return ret - elif kind != b"user": - raise UnrecognizedRequestError( - "Do not understand membership kind: %s" % (kind.decode("utf8"),) - ) - - if self._msc2918_enabled: - # Check if this registration should also issue a refresh token, as - # per MSC2918 - should_issue_refresh_token = parse_boolean( - request, name="org.matrix.msc2918.refresh_token", default=False - ) - else: - should_issue_refresh_token = False - - # Pull out the provided username and do basic sanity checks early since - # the auth layer will store these in sessions. - desired_username = None - if "username" in body: - if not isinstance(body["username"], str) or len(body["username"]) > 512: - raise SynapseError(400, "Invalid username") - desired_username = body["username"] - - # fork off as soon as possible for ASes which have completely - # different registration flows to normal users - - # == Application Service Registration == - if body.get("type") == APP_SERVICE_REGISTRATION_TYPE: - if not self.auth.has_access_token(request): - raise SynapseError( - 400, - "Appservice token must be provided when using a type of m.login.application_service", - ) - - # Verify the AS - self.auth.get_appservice_by_req(request) - - # Set the desired user according to the AS API (which uses the - # 'user' key not 'username'). Since this is a new addition, we'll - # fallback to 'username' if they gave one. - desired_username = body.get("user", desired_username) - - # XXX we should check that desired_username is valid. Currently - # we give appservices carte blanche for any insanity in mxids, - # because the IRC bridges rely on being able to register stupid - # IDs. - - access_token = self.auth.get_access_token_from_request(request) - - if not isinstance(desired_username, str): - raise SynapseError(400, "Desired Username is missing or not a string") - - result = await self._do_appservice_registration( - desired_username, - access_token, - body, - should_issue_refresh_token=should_issue_refresh_token, - ) - - return 200, result - elif self.auth.has_access_token(request): - raise SynapseError( - 400, - "An access token should not be provided on requests to /register (except if type is m.login.application_service)", - ) - - # == Normal User Registration == (everyone else) - if not self._registration_enabled: - raise SynapseError(403, "Registration has been disabled", Codes.FORBIDDEN) - - # For regular registration, convert the provided username to lowercase - # before attempting to register it. This should mean that people who try - # to register with upper-case in their usernames don't get a nasty surprise. - # - # Note that we treat usernames case-insensitively in login, so they are - # free to carry on imagining that their username is CrAzYh4cKeR if that - # keeps them happy. - if desired_username is not None: - desired_username = desired_username.lower() - - # Check if this account is upgrading from a guest account. - guest_access_token = body.get("guest_access_token", None) - - # Pull out the provided password and do basic sanity checks early. - # - # Note that we remove the password from the body since the auth layer - # will store the body in the session and we don't want a plaintext - # password store there. - password = body.pop("password", None) - if password is not None: - if not isinstance(password, str) or len(password) > 512: - raise SynapseError(400, "Invalid password") - self.password_policy_handler.validate_password(password) - - if "initial_device_display_name" in body and password is None: - # ignore 'initial_device_display_name' if sent without - # a password to work around a client bug where it sent - # the 'initial_device_display_name' param alone, wiping out - # the original registration params - logger.warning("Ignoring initial_device_display_name without password") - del body["initial_device_display_name"] - - session_id = self.auth_handler.get_session_id(body) - registered_user_id = None - password_hash = None - if session_id: - # if we get a registered user id out of here, it means we previously - # registered a user for this session, so we could just return the - # user here. We carry on and go through the auth checks though, - # for paranoia. - registered_user_id = await self.auth_handler.get_session_data( - session_id, UIAuthSessionDataConstants.REGISTERED_USER_ID, None - ) - # Extract the previously-hashed password from the session. - password_hash = await self.auth_handler.get_session_data( - session_id, UIAuthSessionDataConstants.PASSWORD_HASH, None - ) - - # Ensure that the username is valid. - if desired_username is not None: - await self.registration_handler.check_username( - desired_username, - guest_access_token=guest_access_token, - assigned_user_id=registered_user_id, - ) - - # Check if the user-interactive authentication flows are complete, if - # not this will raise a user-interactive auth error. - try: - auth_result, params, session_id = await self.auth_handler.check_ui_auth( - self._registration_flows, - request, - body, - "register a new account", - ) - except InteractiveAuthIncompleteError as e: - # The user needs to provide more steps to complete auth. - # - # Hash the password and store it with the session since the client - # is not required to provide the password again. - # - # If a password hash was previously stored we will not attempt to - # re-hash and store it for efficiency. This assumes the password - # does not change throughout the authentication flow, but this - # should be fine since the data is meant to be consistent. - if not password_hash and password: - password_hash = await self.auth_handler.hash(password) - await self.auth_handler.set_session_data( - e.session_id, - UIAuthSessionDataConstants.PASSWORD_HASH, - password_hash, - ) - 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 check_3pid_allowed(self.hs, medium, address): - 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 - ) - # don't re-register the threepids - registered = False - else: - # If we have a password in this request, prefer it. Otherwise, there - # might be a password hash from an earlier request. - if password: - password_hash = await self.auth_handler.hash(password) - if not password_hash: - raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM) - - desired_username = params.get("username", None) - guest_access_token = params.get("guest_access_token", None) - - 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/v2_alpha/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 - ) - - registered_user_id = await self.registration_handler.register_user( - localpart=desired_username, - password_hash=password_hash, - guest_access_token=guest_access_token, - threepid=threepid, - 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.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). - await self.auth_handler.set_session_data( - session_id, - UIAuthSessionDataConstants.REGISTERED_USER_ID, - registered_user_id, - ) - - registered = True - - return_dict = await self._create_registration_details( - registered_user_id, - params, - should_issue_refresh_token=should_issue_refresh_token, - ) - - if registered: - await self.registration_handler.post_registration_actions( - user_id=registered_user_id, - auth_result=auth_result, - access_token=return_dict.get("access_token"), - ) - - return 200, return_dict - - async def _do_appservice_registration( - self, username, as_token, body, should_issue_refresh_token: bool = False - ): - user_id = await self.registration_handler.appservice_register( - username, as_token - ) - return await self._create_registration_details( - user_id, - body, - is_appservice_ghost=True, - should_issue_refresh_token=should_issue_refresh_token, - ) - - async def _create_registration_details( - self, - user_id: str, - params: JsonDict, - is_appservice_ghost: bool = False, - should_issue_refresh_token: bool = False, - ): - """Complete registration of newly-registered user - - Allocates device_id if one was not given; also creates access_token. - - Args: - user_id: full canonical @user:id - params: registration parameters, from which we pull device_id, - initial_device_name and inhibit_login - is_appservice_ghost - should_issue_refresh_token: True if this registration should issue - a refresh token alongside the access token. - Returns: - dictionary for response from /register - """ - result = {"user_id": user_id, "home_server": self.hs.hostname} - if not params.get("inhibit_login", False): - device_id = params.get("device_id") - initial_display_name = params.get("initial_device_display_name") - ( - device_id, - access_token, - valid_until_ms, - refresh_token, - ) = await self.registration_handler.register_device( - user_id, - device_id, - initial_display_name, - is_guest=False, - is_appservice_ghost=is_appservice_ghost, - should_issue_refresh_token=should_issue_refresh_token, - ) - - result.update({"access_token": access_token, "device_id": device_id}) - - if valid_until_ms is not None: - expires_in_ms = valid_until_ms - self.clock.time_msec() - result["expires_in_ms"] = expires_in_ms - - if refresh_token is not None: - result["refresh_token"] = refresh_token - - return result - - async def _do_guest_registration(self, params, address=None): - if not self.hs.config.allow_guest_access: - raise SynapseError(403, "Guest access is disabled") - user_id = await self.registration_handler.register_user( - make_guest=True, address=address - ) - - # we don't allow guests to specify their own device_id, because - # we have nowhere to store it. - device_id = synapse.api.auth.GUEST_DEVICE_ID - initial_display_name = params.get("initial_device_display_name") - ( - device_id, - access_token, - valid_until_ms, - refresh_token, - ) = await self.registration_handler.register_device( - user_id, device_id, initial_display_name, is_guest=True - ) - - result = { - "user_id": user_id, - "device_id": device_id, - "access_token": access_token, - "home_server": self.hs.hostname, - } - - if valid_until_ms is not None: - expires_in_ms = valid_until_ms - self.clock.time_msec() - result["expires_in_ms"] = expires_in_ms - - if refresh_token is not None: - result["refresh_token"] = refresh_token - - return 200, result - - -def _calculate_registration_flows( - # technically `config` has to provide *all* of these interfaces, not just one - config: Union[RegistrationConfig, ConsentConfig, CaptchaConfig], - auth_handler: AuthHandler, -) -> List[List[str]]: - """Get a suitable flows list for registration - - Args: - config: server configuration - auth_handler: authorization handler - - 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.registrations_require_3pid - require_msisdn = "msisdn" in config.registrations_require_3pid - - show_msisdn = True - show_email = True - - if config.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]) - - # Prepend m.login.terms to all flows if we're requiring consent - if config.user_consent_at_registration: - for flow in flows: - flow.insert(0, LoginType.TERMS) - - # Prepend recaptcha to all flows if we're requiring captcha - if config.enable_registration_captcha: - for flow in flows: - flow.insert(0, LoginType.RECAPTCHA) - - return flows - - -def register_servlets(hs, http_server): - EmailRegisterRequestTokenRestServlet(hs).register(http_server) - MsisdnRegisterRequestTokenRestServlet(hs).register(http_server) - UsernameAvailabilityRestServlet(hs).register(http_server) - RegistrationSubmitTokenServlet(hs).register(http_server) - RegisterRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/relations.py b/synapse/rest/client/v2_alpha/relations.py deleted file mode 100644 index 0821cd285f..0000000000 --- a/synapse/rest/client/v2_alpha/relations.py +++ /dev/null @@ -1,381 +0,0 @@ -# Copyright 2019 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This class implements the proposed relation APIs from MSC 1849. - -Since the MSC has not been approved all APIs here are unstable and may change at -any time to reflect changes in the MSC. -""" - -import logging - -from synapse.api.constants import EventTypes, RelationTypes -from synapse.api.errors import ShadowBanError, SynapseError -from synapse.http.servlet import ( - RestServlet, - parse_integer, - parse_json_object_from_request, - parse_string, -) -from synapse.rest.client.transactions import HttpTransactionCache -from synapse.storage.relations import ( - AggregationPaginationToken, - PaginationChunk, - RelationPaginationToken, -) -from synapse.util.stringutils import random_string - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class RelationSendServlet(RestServlet): - """Helper API for sending events that have relation data. - - Example API shape to send a 👍 reaction to a room: - - POST /rooms/!foo/send_relation/$bar/m.annotation/m.reaction?key=%F0%9F%91%8D - {} - - { - "event_id": "$foobar" - } - """ - - PATTERN = ( - "/rooms/(?P[^/]*)/send_relation" - "/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)" - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.event_creation_handler = hs.get_event_creation_handler() - self.txns = HttpTransactionCache(hs) - - def register(self, http_server): - http_server.register_paths( - "POST", - client_patterns(self.PATTERN + "$", releases=()), - self.on_PUT_or_POST, - self.__class__.__name__, - ) - http_server.register_paths( - "PUT", - client_patterns(self.PATTERN + "/(?P[^/]*)$", releases=()), - self.on_PUT, - self.__class__.__name__, - ) - - def on_PUT(self, request, *args, **kwargs): - return self.txns.fetch_or_execute_request( - request, self.on_PUT_or_POST, request, *args, **kwargs - ) - - async def on_PUT_or_POST( - self, request, room_id, parent_id, relation_type, event_type, txn_id=None - ): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - if event_type == EventTypes.Member: - # Add relations to a membership is meaningless, so we just deny it - # at the CS API rather than trying to handle it correctly. - raise SynapseError(400, "Cannot send member events with relations") - - content = parse_json_object_from_request(request) - - aggregation_key = parse_string(request, "key", encoding="utf-8") - - content["m.relates_to"] = { - "event_id": parent_id, - "key": aggregation_key, - "rel_type": relation_type, - } - - event_dict = { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - } - - try: - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, event_dict=event_dict, txn_id=txn_id - ) - event_id = event.event_id - except ShadowBanError: - event_id = "$" + random_string(43) - - return 200, {"event_id": event_id} - - -class RelationPaginationServlet(RestServlet): - """API to paginate relations on an event by topological ordering, optionally - filtered by relation type and event type. - """ - - PATTERNS = client_patterns( - "/rooms/(?P[^/]*)/relations/(?P[^/]*)" - "(/(?P[^/]*)(/(?P[^/]*))?)?$", - releases=(), - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.clock = hs.get_clock() - self._event_serializer = hs.get_event_client_serializer() - self.event_handler = hs.get_event_handler() - - async def on_GET( - self, request, room_id, parent_id, relation_type=None, event_type=None - ): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - await self.auth.check_user_in_room_or_world_readable( - room_id, requester.user.to_string(), allow_departed_users=True - ) - - # This gets the original event and checks that a) the event exists and - # b) the user is allowed to view it. - event = await self.event_handler.get_event(requester.user, room_id, parent_id) - - limit = parse_integer(request, "limit", default=5) - from_token_str = parse_string(request, "from") - to_token_str = parse_string(request, "to") - - if event.internal_metadata.is_redacted(): - # If the event is redacted, return an empty list of relations - pagination_chunk = PaginationChunk(chunk=[]) - else: - # Return the relations - from_token = None - if from_token_str: - from_token = RelationPaginationToken.from_string(from_token_str) - - to_token = None - if to_token_str: - to_token = RelationPaginationToken.from_string(to_token_str) - - pagination_chunk = await self.store.get_relations_for_event( - event_id=parent_id, - relation_type=relation_type, - event_type=event_type, - limit=limit, - from_token=from_token, - to_token=to_token, - ) - - events = await self.store.get_events_as_list( - [c["event_id"] for c in pagination_chunk.chunk] - ) - - now = self.clock.time_msec() - # We set bundle_aggregations to False when retrieving the original - # event because we want the content before relations were applied to - # it. - original_event = await self._event_serializer.serialize_event( - event, now, bundle_aggregations=False - ) - # Similarly, we don't allow relations to be applied to relations, so we - # return the original relations without any aggregations on top of them - # here. - events = await self._event_serializer.serialize_events( - events, now, bundle_aggregations=False - ) - - return_value = pagination_chunk.to_dict() - return_value["chunk"] = events - return_value["original_event"] = original_event - - return 200, return_value - - -class RelationAggregationPaginationServlet(RestServlet): - """API to paginate aggregation groups of relations, e.g. paginate the - types and counts of the reactions on the events. - - Example request and response: - - GET /rooms/{room_id}/aggregations/{parent_id} - - { - chunk: [ - { - "type": "m.reaction", - "key": "👍", - "count": 3 - } - ] - } - """ - - PATTERNS = client_patterns( - "/rooms/(?P[^/]*)/aggregations/(?P[^/]*)" - "(/(?P[^/]*)(/(?P[^/]*))?)?$", - releases=(), - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.event_handler = hs.get_event_handler() - - async def on_GET( - self, request, room_id, parent_id, relation_type=None, event_type=None - ): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - await self.auth.check_user_in_room_or_world_readable( - room_id, - requester.user.to_string(), - allow_departed_users=True, - ) - - # This checks that a) the event exists and b) the user is allowed to - # view it. - event = await self.event_handler.get_event(requester.user, room_id, parent_id) - - if relation_type not in (RelationTypes.ANNOTATION, None): - raise SynapseError(400, "Relation type must be 'annotation'") - - limit = parse_integer(request, "limit", default=5) - from_token_str = parse_string(request, "from") - to_token_str = parse_string(request, "to") - - if event.internal_metadata.is_redacted(): - # If the event is redacted, return an empty list of relations - pagination_chunk = PaginationChunk(chunk=[]) - else: - # Return the relations - from_token = None - if from_token_str: - from_token = AggregationPaginationToken.from_string(from_token_str) - - to_token = None - if to_token_str: - to_token = AggregationPaginationToken.from_string(to_token_str) - - pagination_chunk = await self.store.get_aggregation_groups_for_event( - event_id=parent_id, - event_type=event_type, - limit=limit, - from_token=from_token, - to_token=to_token, - ) - - return 200, pagination_chunk.to_dict() - - -class RelationAggregationGroupPaginationServlet(RestServlet): - """API to paginate within an aggregation group of relations, e.g. paginate - all the 👍 reactions on an event. - - Example request and response: - - GET /rooms/{room_id}/aggregations/{parent_id}/m.annotation/m.reaction/👍 - - { - chunk: [ - { - "type": "m.reaction", - "content": { - "m.relates_to": { - "rel_type": "m.annotation", - "key": "👍" - } - } - }, - ... - ] - } - """ - - PATTERNS = client_patterns( - "/rooms/(?P[^/]*)/aggregations/(?P[^/]*)" - "/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)$", - releases=(), - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.clock = hs.get_clock() - self._event_serializer = hs.get_event_client_serializer() - self.event_handler = hs.get_event_handler() - - async def on_GET(self, request, room_id, parent_id, relation_type, event_type, key): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - await self.auth.check_user_in_room_or_world_readable( - room_id, - requester.user.to_string(), - allow_departed_users=True, - ) - - # This checks that a) the event exists and b) the user is allowed to - # view it. - await self.event_handler.get_event(requester.user, room_id, parent_id) - - if relation_type != RelationTypes.ANNOTATION: - raise SynapseError(400, "Relation type must be 'annotation'") - - limit = parse_integer(request, "limit", default=5) - from_token_str = parse_string(request, "from") - to_token_str = parse_string(request, "to") - - from_token = None - if from_token_str: - from_token = RelationPaginationToken.from_string(from_token_str) - - to_token = None - if to_token_str: - to_token = RelationPaginationToken.from_string(to_token_str) - - result = await self.store.get_relations_for_event( - event_id=parent_id, - relation_type=relation_type, - event_type=event_type, - aggregation_key=key, - limit=limit, - from_token=from_token, - to_token=to_token, - ) - - events = await self.store.get_events_as_list( - [c["event_id"] for c in result.chunk] - ) - - now = self.clock.time_msec() - events = await self._event_serializer.serialize_events(events, now) - - return_value = result.to_dict() - return_value["chunk"] = events - - return 200, return_value - - -def register_servlets(hs, http_server): - RelationSendServlet(hs).register(http_server) - RelationPaginationServlet(hs).register(http_server) - RelationAggregationPaginationServlet(hs).register(http_server) - RelationAggregationGroupPaginationServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/report_event.py b/synapse/rest/client/v2_alpha/report_event.py deleted file mode 100644 index 07ea39a8a3..0000000000 --- a/synapse/rest/client/v2_alpha/report_event.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from http import HTTPStatus - -from synapse.api.errors import Codes, SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class ReportEventRestServlet(RestServlet): - PATTERNS = client_patterns("/rooms/(?P[^/]*)/report/(?P[^/]*)$") - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.clock = hs.get_clock() - self.store = hs.get_datastore() - - async def on_POST(self, request, room_id, event_id): - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - body = parse_json_object_from_request(request) - - if not isinstance(body.get("reason", ""), str): - raise SynapseError( - HTTPStatus.BAD_REQUEST, - "Param 'reason' must be a string", - Codes.BAD_JSON, - ) - if not isinstance(body.get("score", 0), int): - raise SynapseError( - HTTPStatus.BAD_REQUEST, - "Param 'score' must be an integer", - Codes.BAD_JSON, - ) - - await self.store.add_event_report( - room_id=room_id, - event_id=event_id, - user_id=user_id, - reason=body.get("reason"), - content=body, - received_ts=self.clock.time_msec(), - ) - - return 200, {} - - -def register_servlets(hs, http_server): - ReportEventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/room.py b/synapse/rest/client/v2_alpha/room.py deleted file mode 100644 index 3172aba605..0000000000 --- a/synapse/rest/client/v2_alpha/room.py +++ /dev/null @@ -1,441 +0,0 @@ -# Copyright 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import re - -from synapse.api.constants import EventContentFields, EventTypes -from synapse.api.errors import AuthError, Codes, SynapseError -from synapse.appservice import ApplicationService -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, - parse_string, - parse_strings_from_args, -) -from synapse.rest.client.transactions import HttpTransactionCache -from synapse.types import Requester, UserID, create_requester -from synapse.util.stringutils import random_string - -logger = logging.getLogger(__name__) - - -class RoomBatchSendEventRestServlet(RestServlet): - """ - API endpoint which can insert a chunk of events historically back in time - next to the given `prev_event`. - - `chunk_id` comes from `next_chunk_id `in the response of the batch send - endpoint and is derived from the "insertion" events added to each chunk. - It's not required for the first batch send. - - `state_events_at_start` is used to define the historical state events - needed to auth the events like join events. These events will float - outside of the normal DAG as outlier's and won't be visible in the chat - history which also allows us to insert multiple chunks without having a bunch - of `@mxid joined the room` noise between each chunk. - - `events` is chronological chunk/list of events you want to insert. - There is a reverse-chronological constraint on chunks so once you insert - some messages, you can only insert older ones after that. - tldr; Insert chunks from your most recent history -> oldest history. - - POST /_matrix/client/unstable/org.matrix.msc2716/rooms//batch_send?prev_event=&chunk_id= - { - "events": [ ... ], - "state_events_at_start": [ ... ] - } - """ - - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/org.matrix.msc2716" - "/rooms/(?P[^/]*)/batch_send$" - ), - ) - - def __init__(self, hs): - super().__init__() - self.hs = hs - self.store = hs.get_datastore() - self.state_store = hs.get_storage().state - self.event_creation_handler = hs.get_event_creation_handler() - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - self.txns = HttpTransactionCache(hs) - - async def _inherit_depth_from_prev_ids(self, prev_event_ids) -> int: - ( - most_recent_prev_event_id, - most_recent_prev_event_depth, - ) = await self.store.get_max_depth_of(prev_event_ids) - - # We want to insert the historical event after the `prev_event` but before the successor event - # - # We inherit depth from the successor event instead of the `prev_event` - # because events returned from `/messages` are first sorted by `topological_ordering` - # which is just the `depth` and then tie-break with `stream_ordering`. - # - # We mark these inserted historical events as "backfilled" which gives them a - # negative `stream_ordering`. If we use the same depth as the `prev_event`, - # then our historical event will tie-break and be sorted before the `prev_event` - # when it should come after. - # - # We want to use the successor event depth so they appear after `prev_event` because - # it has a larger `depth` but before the successor event because the `stream_ordering` - # is negative before the successor event. - successor_event_ids = await self.store.get_successor_events( - [most_recent_prev_event_id] - ) - - # If we can't find any successor events, then it's a forward extremity of - # historical messages and we can just inherit from the previous historical - # event which we can already assume has the correct depth where we want - # to insert into. - if not successor_event_ids: - depth = most_recent_prev_event_depth - else: - ( - _, - oldest_successor_depth, - ) = await self.store.get_min_depth_of(successor_event_ids) - - depth = oldest_successor_depth - - return depth - - def _create_insertion_event_dict( - self, sender: str, room_id: str, origin_server_ts: int - ): - """Creates an event dict for an "insertion" event with the proper fields - and a random chunk ID. - - Args: - sender: The event author MXID - room_id: The room ID that the event belongs to - origin_server_ts: Timestamp when the event was sent - - Returns: - Tuple of event ID and stream ordering position - """ - - next_chunk_id = random_string(8) - insertion_event = { - "type": EventTypes.MSC2716_INSERTION, - "sender": sender, - "room_id": room_id, - "content": { - EventContentFields.MSC2716_NEXT_CHUNK_ID: next_chunk_id, - EventContentFields.MSC2716_HISTORICAL: True, - }, - "origin_server_ts": origin_server_ts, - } - - return insertion_event - - async def _create_requester_for_user_id_from_app_service( - self, user_id: str, app_service: ApplicationService - ) -> Requester: - """Creates a new requester for the given user_id - and validates that the app service is allowed to control - the given user. - - Args: - user_id: The author MXID that the app service is controlling - app_service: The app service that controls the user - - Returns: - Requester object - """ - - await self.auth.validate_appservice_can_control_user_id(app_service, user_id) - - return create_requester(user_id, app_service=app_service) - - async def on_POST(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=False) - - if not requester.app_service: - raise AuthError( - 403, - "Only application services can use the /batchsend endpoint", - ) - - body = parse_json_object_from_request(request) - assert_params_in_dict(body, ["state_events_at_start", "events"]) - - prev_events_from_query = parse_strings_from_args(request.args, "prev_event") - chunk_id_from_query = parse_string(request, "chunk_id") - - if prev_events_from_query is None: - raise SynapseError( - 400, - "prev_event query parameter is required when inserting historical messages back in time", - errcode=Codes.MISSING_PARAM, - ) - - # For the event we are inserting next to (`prev_events_from_query`), - # find the most recent auth events (derived from state events) that - # allowed that message to be sent. We will use that as a base - # to auth our historical messages against. - ( - most_recent_prev_event_id, - _, - ) = await self.store.get_max_depth_of(prev_events_from_query) - # mapping from (type, state_key) -> state_event_id - prev_state_map = await self.state_store.get_state_ids_for_event( - most_recent_prev_event_id - ) - # List of state event ID's - prev_state_ids = list(prev_state_map.values()) - auth_event_ids = prev_state_ids - - state_events_at_start = [] - for state_event in body["state_events_at_start"]: - assert_params_in_dict( - state_event, ["type", "origin_server_ts", "content", "sender"] - ) - - logger.debug( - "RoomBatchSendEventRestServlet inserting state_event=%s, auth_event_ids=%s", - state_event, - auth_event_ids, - ) - - event_dict = { - "type": state_event["type"], - "origin_server_ts": state_event["origin_server_ts"], - "content": state_event["content"], - "room_id": room_id, - "sender": state_event["sender"], - "state_key": state_event["state_key"], - } - - # Mark all events as historical - event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True - - # Make the state events float off on their own - fake_prev_event_id = "$" + random_string(43) - - # TODO: This is pretty much the same as some other code to handle inserting state in this file - if event_dict["type"] == EventTypes.Member: - membership = event_dict["content"].get("membership", None) - event_id, _ = await self.room_member_handler.update_membership( - await self._create_requester_for_user_id_from_app_service( - state_event["sender"], requester.app_service - ), - target=UserID.from_string(event_dict["state_key"]), - room_id=room_id, - action=membership, - content=event_dict["content"], - outlier=True, - prev_event_ids=[fake_prev_event_id], - # Make sure to use a copy of this list because we modify it - # later in the loop here. Otherwise it will be the same - # reference and also update in the event when we append later. - auth_event_ids=auth_event_ids.copy(), - ) - else: - # TODO: Add some complement tests that adds state that is not member joins - # and will use this code path. Maybe we only want to support join state events - # and can get rid of this `else`? - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - await self._create_requester_for_user_id_from_app_service( - state_event["sender"], requester.app_service - ), - event_dict, - outlier=True, - prev_event_ids=[fake_prev_event_id], - # Make sure to use a copy of this list because we modify it - # later in the loop here. Otherwise it will be the same - # reference and also update in the event when we append later. - auth_event_ids=auth_event_ids.copy(), - ) - event_id = event.event_id - - state_events_at_start.append(event_id) - auth_event_ids.append(event_id) - - events_to_create = body["events"] - - inherited_depth = await self._inherit_depth_from_prev_ids( - prev_events_from_query - ) - - # Figure out which chunk to connect to. If they passed in - # chunk_id_from_query let's use it. The chunk ID passed in comes - # from the chunk_id in the "insertion" event from the previous chunk. - last_event_in_chunk = events_to_create[-1] - chunk_id_to_connect_to = chunk_id_from_query - base_insertion_event = None - if chunk_id_from_query: - # All but the first base insertion event should point at a fake - # event, which causes the HS to ask for the state at the start of - # the chunk later. - prev_event_ids = [fake_prev_event_id] - # TODO: Verify the chunk_id_from_query corresponds to an insertion event - pass - # Otherwise, create an insertion event to act as a starting point. - # - # We don't always have an insertion event to start hanging more history - # off of (ideally there would be one in the main DAG, but that's not the - # case if we're wanting to add history to e.g. existing rooms without - # an insertion event), in which case we just create a new insertion event - # that can then get pointed to by a "marker" event later. - else: - prev_event_ids = prev_events_from_query - - base_insertion_event_dict = self._create_insertion_event_dict( - sender=requester.user.to_string(), - room_id=room_id, - origin_server_ts=last_event_in_chunk["origin_server_ts"], - ) - base_insertion_event_dict["prev_events"] = prev_event_ids.copy() - - ( - base_insertion_event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - await self._create_requester_for_user_id_from_app_service( - base_insertion_event_dict["sender"], - requester.app_service, - ), - base_insertion_event_dict, - prev_event_ids=base_insertion_event_dict.get("prev_events"), - auth_event_ids=auth_event_ids, - historical=True, - depth=inherited_depth, - ) - - chunk_id_to_connect_to = base_insertion_event["content"][ - EventContentFields.MSC2716_NEXT_CHUNK_ID - ] - - # Connect this current chunk to the insertion event from the previous chunk - chunk_event = { - "type": EventTypes.MSC2716_CHUNK, - "sender": requester.user.to_string(), - "room_id": room_id, - "content": { - EventContentFields.MSC2716_CHUNK_ID: chunk_id_to_connect_to, - EventContentFields.MSC2716_HISTORICAL: True, - }, - # Since the chunk event is put at the end of the chunk, - # where the newest-in-time event is, copy the origin_server_ts from - # the last event we're inserting - "origin_server_ts": last_event_in_chunk["origin_server_ts"], - } - # Add the chunk event to the end of the chunk (newest-in-time) - events_to_create.append(chunk_event) - - # Add an "insertion" event to the start of each chunk (next to the oldest-in-time - # event in the chunk) so the next chunk can be connected to this one. - insertion_event = self._create_insertion_event_dict( - sender=requester.user.to_string(), - room_id=room_id, - # Since the insertion event is put at the start of the chunk, - # where the oldest-in-time event is, copy the origin_server_ts from - # the first event we're inserting - origin_server_ts=events_to_create[0]["origin_server_ts"], - ) - # Prepend the insertion event to the start of the chunk (oldest-in-time) - events_to_create = [insertion_event] + events_to_create - - event_ids = [] - events_to_persist = [] - for ev in events_to_create: - assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"]) - - event_dict = { - "type": ev["type"], - "origin_server_ts": ev["origin_server_ts"], - "content": ev["content"], - "room_id": room_id, - "sender": ev["sender"], # requester.user.to_string(), - "prev_events": prev_event_ids.copy(), - } - - # Mark all events as historical - event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True - - event, context = await self.event_creation_handler.create_event( - await self._create_requester_for_user_id_from_app_service( - ev["sender"], requester.app_service - ), - event_dict, - prev_event_ids=event_dict.get("prev_events"), - auth_event_ids=auth_event_ids, - historical=True, - depth=inherited_depth, - ) - logger.debug( - "RoomBatchSendEventRestServlet inserting event=%s, prev_event_ids=%s, auth_event_ids=%s", - event, - prev_event_ids, - auth_event_ids, - ) - - assert self.hs.is_mine_id(event.sender), "User must be our own: %s" % ( - event.sender, - ) - - events_to_persist.append((event, context)) - event_id = event.event_id - - event_ids.append(event_id) - prev_event_ids = [event_id] - - # Persist events in reverse-chronological order so they have the - # correct stream_ordering as they are backfilled (which decrements). - # Events are sorted by (topological_ordering, stream_ordering) - # where topological_ordering is just depth. - for (event, context) in reversed(events_to_persist): - ev = await self.event_creation_handler.handle_new_client_event( - await self._create_requester_for_user_id_from_app_service( - event["sender"], requester.app_service - ), - event=event, - context=context, - ) - - # Add the base_insertion_event to the bottom of the list we return - if base_insertion_event is not None: - event_ids.append(base_insertion_event.event_id) - - return 200, { - "state_events": state_events_at_start, - "events": event_ids, - "next_chunk_id": insertion_event["content"][ - EventContentFields.MSC2716_NEXT_CHUNK_ID - ], - } - - def on_GET(self, request, room_id): - return 501, "Not implemented" - - def on_PUT(self, request, room_id): - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id - ) - - -def register_servlets(hs, http_server): - msc2716_enabled = hs.config.experimental.msc2716_enabled - - if msc2716_enabled: - RoomBatchSendEventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/room_keys.py b/synapse/rest/client/v2_alpha/room_keys.py deleted file mode 100644 index 263596be86..0000000000 --- a/synapse/rest/client/v2_alpha/room_keys.py +++ /dev/null @@ -1,391 +0,0 @@ -# Copyright 2017, 2018 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import Codes, NotFoundError, SynapseError -from synapse.http.servlet import ( - RestServlet, - parse_json_object_from_request, - parse_string, -) - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class RoomKeysServlet(RestServlet): - PATTERNS = client_patterns( - "/room_keys/keys(/(?P[^/]+))?(/(?P[^/]+))?$" - ) - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.auth = hs.get_auth() - self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() - - async def on_PUT(self, request, room_id, session_id): - """ - Uploads one or more encrypted E2E room keys for backup purposes. - room_id: the ID of the room the keys are for (optional) - session_id: the ID for the E2E room keys for the room (optional) - version: the version of the user's backup which this data is for. - the version must already have been created via the /room_keys/version API. - - Each session has: - * first_message_index: a numeric index indicating the oldest message - encrypted by this session. - * forwarded_count: how many times the uploading client claims this key - has been shared (forwarded) - * is_verified: whether the client that uploaded the keys claims they - were sent by a device which they've verified - * session_data: base64-encrypted data describing the session. - - Returns 200 OK on success with body {} - Returns 403 Forbidden if the version in question is not the most recently - created version (i.e. if this is an old client trying to write to a stale backup) - Returns 404 Not Found if the version in question doesn't exist - - The API is designed to be otherwise agnostic to the room_key encryption - algorithm being used. Sessions are merged with existing ones in the - backup using the heuristics: - * is_verified sessions always win over unverified sessions - * older first_message_index always win over newer sessions - * lower forwarded_count always wins over higher forwarded_count - - We trust the clients not to lie and corrupt their own backups. - It also means that if your access_token is stolen, the attacker could - delete your backup. - - POST /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1 - Content-Type: application/json - - { - "first_message_index": 1, - "forwarded_count": 1, - "is_verified": false, - "session_data": "SSBBTSBBIEZJU0gK" - } - - Or... - - POST /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1 - Content-Type: application/json - - { - "sessions": { - "c0ff33": { - "first_message_index": 1, - "forwarded_count": 1, - "is_verified": false, - "session_data": "SSBBTSBBIEZJU0gK" - } - } - } - - Or... - - POST /room_keys/keys?version=1 HTTP/1.1 - Content-Type: application/json - - { - "rooms": { - "!abc:matrix.org": { - "sessions": { - "c0ff33": { - "first_message_index": 1, - "forwarded_count": 1, - "is_verified": false, - "session_data": "SSBBTSBBIEZJU0gK" - } - } - } - } - } - """ - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - body = parse_json_object_from_request(request) - version = parse_string(request, "version") - - if session_id: - body = {"sessions": {session_id: body}} - - if room_id: - body = {"rooms": {room_id: body}} - - ret = await self.e2e_room_keys_handler.upload_room_keys(user_id, version, body) - return 200, ret - - async def on_GET(self, request, room_id, session_id): - """ - Retrieves one or more encrypted E2E room keys for backup purposes. - Symmetric with the PUT version of the API. - - room_id: the ID of the room to retrieve the keys for (optional) - session_id: the ID for the E2E room keys to retrieve the keys for (optional) - version: the version of the user's backup which this data is for. - the version must already have been created via the /change_secret API. - - Returns as follows: - - GET /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1 - { - "first_message_index": 1, - "forwarded_count": 1, - "is_verified": false, - "session_data": "SSBBTSBBIEZJU0gK" - } - - Or... - - GET /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1 - { - "sessions": { - "c0ff33": { - "first_message_index": 1, - "forwarded_count": 1, - "is_verified": false, - "session_data": "SSBBTSBBIEZJU0gK" - } - } - } - - Or... - - GET /room_keys/keys?version=1 HTTP/1.1 - { - "rooms": { - "!abc:matrix.org": { - "sessions": { - "c0ff33": { - "first_message_index": 1, - "forwarded_count": 1, - "is_verified": false, - "session_data": "SSBBTSBBIEZJU0gK" - } - } - } - } - } - """ - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - version = parse_string(request, "version", required=True) - - room_keys = await self.e2e_room_keys_handler.get_room_keys( - user_id, version, room_id, session_id - ) - - # Convert room_keys to the right format to return. - if session_id: - # If the client requests a specific session, but that session was - # not backed up, then return an M_NOT_FOUND. - if room_keys["rooms"] == {}: - raise NotFoundError("No room_keys found") - else: - room_keys = room_keys["rooms"][room_id]["sessions"][session_id] - elif room_id: - # If the client requests all sessions from a room, but no sessions - # are found, then return an empty result rather than an error, so - # that clients don't have to handle an error condition, and an - # empty result is valid. (Similarly if the client requests all - # sessions from the backup, but in that case, room_keys is already - # in the right format, so we don't need to do anything about it.) - if room_keys["rooms"] == {}: - room_keys = {"sessions": {}} - else: - room_keys = room_keys["rooms"][room_id] - - return 200, room_keys - - async def on_DELETE(self, request, room_id, session_id): - """ - Deletes one or more encrypted E2E room keys for a user for backup purposes. - - DELETE /room_keys/keys/!abc:matrix.org/c0ff33?version=1 - HTTP/1.1 200 OK - {} - - room_id: the ID of the room whose keys to delete (optional) - session_id: the ID for the E2E session to delete (optional) - version: the version of the user's backup which this data is for. - the version must already have been created via the /change_secret API. - """ - - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - version = parse_string(request, "version") - - ret = await self.e2e_room_keys_handler.delete_room_keys( - user_id, version, room_id, session_id - ) - return 200, ret - - -class RoomKeysNewVersionServlet(RestServlet): - PATTERNS = client_patterns("/room_keys/version$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.auth = hs.get_auth() - self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() - - async def on_POST(self, request): - """ - Create a new backup version for this user's room_keys with the given - info. The version is allocated by the server and returned to the user - in the response. This API is intended to be used whenever the user - changes the encryption key for their backups, ensuring that backups - encrypted with different keys don't collide. - - It takes out an exclusive lock on this user's room_key backups, to ensure - clients only upload to the current backup. - - The algorithm passed in the version info is a reverse-DNS namespaced - identifier to describe the format of the encrypted backupped keys. - - The auth_data is { user_id: "user_id", nonce: } - encrypted using the algorithm and current encryption key described above. - - POST /room_keys/version - Content-Type: application/json - { - "algorithm": "m.megolm_backup.v1", - "auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K" - } - - HTTP/1.1 200 OK - Content-Type: application/json - { - "version": 12345 - } - """ - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - info = parse_json_object_from_request(request) - - new_version = await self.e2e_room_keys_handler.create_version(user_id, info) - return 200, {"version": new_version} - - # we deliberately don't have a PUT /version, as these things really should - # be immutable to avoid people footgunning - - -class RoomKeysVersionServlet(RestServlet): - PATTERNS = client_patterns("/room_keys/version(/(?P[^/]+))?$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.auth = hs.get_auth() - self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler() - - async def on_GET(self, request, version): - """ - Retrieve the version information about a given version of the user's - room_keys backup. If the version part is missing, returns info about the - most current backup version (if any) - - It takes out an exclusive lock on this user's room_key backups, to ensure - clients only upload to the current backup. - - Returns 404 if the given version does not exist. - - GET /room_keys/version/12345 HTTP/1.1 - { - "version": "12345", - "algorithm": "m.megolm_backup.v1", - "auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K" - } - """ - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - - try: - info = await self.e2e_room_keys_handler.get_version_info(user_id, version) - except SynapseError as e: - if e.code == 404: - raise SynapseError(404, "No backup found", Codes.NOT_FOUND) - return 200, info - - async def on_DELETE(self, request, version): - """ - Delete the information about a given version of the user's - room_keys backup. If the version part is missing, deletes the most - current backup version (if any). Doesn't delete the actual room data. - - DELETE /room_keys/version/12345 HTTP/1.1 - HTTP/1.1 200 OK - {} - """ - if version is None: - raise SynapseError(400, "No version specified to delete", Codes.NOT_FOUND) - - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - - await self.e2e_room_keys_handler.delete_version(user_id, version) - return 200, {} - - async def on_PUT(self, request, version): - """ - Update the information about a given version of the user's room_keys backup. - - POST /room_keys/version/12345 HTTP/1.1 - Content-Type: application/json - { - "algorithm": "m.megolm_backup.v1", - "auth_data": { - "public_key": "abcdefg", - "signatures": { - "ed25519:something": "hijklmnop" - } - }, - "version": "12345" - } - - HTTP/1.1 200 OK - Content-Type: application/json - {} - """ - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - info = parse_json_object_from_request(request) - - if version is None: - raise SynapseError( - 400, "No version specified to update", Codes.MISSING_PARAM - ) - - await self.e2e_room_keys_handler.update_version(user_id, version, info) - return 200, {} - - -def register_servlets(hs, http_server): - RoomKeysServlet(hs).register(http_server) - RoomKeysVersionServlet(hs).register(http_server) - RoomKeysNewVersionServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py deleted file mode 100644 index 6d1b083acb..0000000000 --- a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import Codes, ShadowBanError, SynapseError -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, -) -from synapse.util import stringutils - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class RoomUpgradeRestServlet(RestServlet): - """Handler for room upgrade requests. - - Handles requests of the form: - - POST /_matrix/client/r0/rooms/$roomid/upgrade HTTP/1.1 - Content-Type: application/json - - { - "new_version": "2", - } - - Creates a new room and shuts down the old one. Returns the ID of the new room. - - Args: - hs (synapse.server.HomeServer): - """ - - PATTERNS = client_patterns( - # /rooms/$roomid/upgrade - "/rooms/(?P[^/]*)/upgrade$" - ) - - def __init__(self, hs): - super().__init__() - self._hs = hs - self._room_creation_handler = hs.get_room_creation_handler() - self._auth = hs.get_auth() - - async def on_POST(self, request, room_id): - requester = await self._auth.get_user_by_req(request) - - content = parse_json_object_from_request(request) - assert_params_in_dict(content, ("new_version",)) - - new_version = KNOWN_ROOM_VERSIONS.get(content["new_version"]) - if new_version is None: - raise SynapseError( - 400, - "Your homeserver does not support this room version", - Codes.UNSUPPORTED_ROOM_VERSION, - ) - - try: - new_room_id = await self._room_creation_handler.upgrade_room( - requester, room_id, new_version - ) - except ShadowBanError: - # Generate a random room ID. - new_room_id = stringutils.random_string(18) - - ret = {"replacement_room": new_room_id} - - return 200, ret - - -def register_servlets(hs, http_server): - RoomUpgradeRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/sendtodevice.py b/synapse/rest/client/v2_alpha/sendtodevice.py deleted file mode 100644 index d537d811d8..0000000000 --- a/synapse/rest/client/v2_alpha/sendtodevice.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from typing import Tuple - -from synapse.http import servlet -from synapse.http.servlet import assert_params_in_dict, parse_json_object_from_request -from synapse.logging.opentracing import set_tag, trace -from synapse.rest.client.transactions import HttpTransactionCache - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class SendToDeviceRestServlet(servlet.RestServlet): - PATTERNS = client_patterns( - "/sendToDevice/(?P[^/]*)/(?P[^/]*)$" - ) - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.txns = HttpTransactionCache(hs) - self.device_message_handler = hs.get_device_message_handler() - - @trace(opname="sendToDevice") - def on_PUT(self, request, message_type, txn_id): - set_tag("message_type", message_type) - set_tag("txn_id", txn_id) - return self.txns.fetch_or_execute_request( - request, self._put, request, message_type, txn_id - ) - - async def _put(self, request, message_type, txn_id): - requester = await self.auth.get_user_by_req(request, allow_guest=True) - - content = parse_json_object_from_request(request) - assert_params_in_dict(content, ("messages",)) - - await self.device_message_handler.send_device_message( - requester, message_type, content["messages"] - ) - - response: Tuple[int, dict] = (200, {}) - return response - - -def register_servlets(hs, http_server): - SendToDeviceRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/shared_rooms.py b/synapse/rest/client/v2_alpha/shared_rooms.py deleted file mode 100644 index d2e7f04b40..0000000000 --- a/synapse/rest/client/v2_alpha/shared_rooms.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2020 Half-Shot -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import logging - -from synapse.api.errors import Codes, SynapseError -from synapse.http.servlet import RestServlet -from synapse.types import UserID - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class UserSharedRoomsServlet(RestServlet): - """ - GET /uk.half-shot.msc2666/user/shared_rooms/{user_id} HTTP/1.1 - """ - - PATTERNS = client_patterns( - "/uk.half-shot.msc2666/user/shared_rooms/(?P[^/]*)", - releases=(), # This is an unstable feature - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.user_directory_active = hs.config.update_user_directory - - async def on_GET(self, request, user_id): - - if not self.user_directory_active: - raise SynapseError( - code=400, - msg="The user directory is disabled on this server. Cannot determine shared rooms.", - errcode=Codes.FORBIDDEN, - ) - - UserID.from_string(user_id) - - requester = await self.auth.get_user_by_req(request) - if user_id == requester.user.to_string(): - raise SynapseError( - code=400, - msg="You cannot request a list of shared rooms with yourself", - errcode=Codes.FORBIDDEN, - ) - rooms = await self.store.get_shared_rooms_for_users( - requester.user.to_string(), user_id - ) - - return 200, {"joined": list(rooms)} - - -def register_servlets(hs, http_server): - UserSharedRoomsServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py deleted file mode 100644 index e18f4d01b3..0000000000 --- a/synapse/rest/client/v2_alpha/sync.py +++ /dev/null @@ -1,532 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import itertools -import logging -from collections import defaultdict -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple - -from synapse.api.constants import Membership, PresenceState -from synapse.api.errors import Codes, StoreError, SynapseError -from synapse.api.filtering import DEFAULT_FILTER_COLLECTION, FilterCollection -from synapse.events.utils import ( - format_event_for_client_v2_without_room_id, - format_event_raw, -) -from synapse.handlers.presence import format_user_presence_state -from synapse.handlers.sync import KnockedSyncResult, SyncConfig -from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string -from synapse.http.site import SynapseRequest -from synapse.types import JsonDict, StreamToken -from synapse.util import json_decoder - -from ._base import client_patterns, set_timeline_upper_limit - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - - -class SyncRestServlet(RestServlet): - """ - - GET parameters:: - timeout(int): How long to wait for new events in milliseconds. - since(batch_token): Batch token when asking for incremental deltas. - set_presence(str): What state the device presence should be set to. - default is "online". - filter(filter_id): A filter to apply to the events returned. - - Response JSON:: - { - "next_batch": // batch token for the next /sync - "presence": // presence data for the user. - "rooms": { - "join": { // Joined rooms being updated. - "${room_id}": { // Id of the room being updated - "event_map": // Map of EventID -> event JSON. - "timeline": { // The recent events in the room if gap is "true" - "limited": // Was the per-room event limit exceeded? - // otherwise the next events in the room. - "events": [] // list of EventIDs in the "event_map". - "prev_batch": // back token for getting previous events. - } - "state": {"events": []} // list of EventIDs updating the - // current state to be what it should - // be at the end of the batch. - "ephemeral": {"events": []} // list of event objects - } - }, - "invite": {}, // Invited rooms being updated. - "leave": {} // Archived rooms being updated. - } - } - """ - - PATTERNS = client_patterns("/sync$") - ALLOWED_PRESENCE = {"online", "offline", "unavailable"} - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.sync_handler = hs.get_sync_handler() - self.clock = hs.get_clock() - self.filtering = hs.get_filtering() - self.presence_handler = hs.get_presence_handler() - self._server_notices_sender = hs.get_server_notices_sender() - self._event_serializer = hs.get_event_client_serializer() - - 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 - - if b"from" in request.args: - # /events used to use 'from', but /sync uses 'since'. - # Lets be helpful and whine if we see a 'from'. - raise SynapseError( - 400, "'from' is not a valid query parameter. Did you mean 'since'?" - ) - - requester = await self.auth.get_user_by_req(request, allow_guest=True) - user = requester.user - device_id = requester.device_id - - timeout = parse_integer(request, "timeout", default=0) - since = parse_string(request, "since") - set_presence = parse_string( - request, - "set_presence", - default="online", - allowed_values=self.ALLOWED_PRESENCE, - ) - filter_id = parse_string(request, "filter") - full_state = parse_boolean(request, "full_state", default=False) - - logger.debug( - "/sync: user=%r, timeout=%r, since=%r, " - "set_presence=%r, filter_id=%r, device_id=%r", - user, - timeout, - since, - set_presence, - filter_id, - device_id, - ) - - request_key = (user, timeout, since, filter_id, full_state, device_id) - - if filter_id is None: - filter_collection = DEFAULT_FILTER_COLLECTION - elif filter_id.startswith("{"): - try: - filter_object = json_decoder.decode(filter_id) - set_timeline_upper_limit( - filter_object, self.hs.config.filter_timeline_limit - ) - except Exception: - raise SynapseError(400, "Invalid filter JSON") - self.filtering.check_valid_filter(filter_object) - filter_collection = FilterCollection(filter_object) - else: - try: - filter_collection = await self.filtering.get_user_filter( - user.localpart, filter_id - ) - except StoreError as err: - if err.code != 404: - raise - # fix up the description and errcode to be more useful - raise SynapseError(400, "No such filter", errcode=Codes.INVALID_PARAM) - - sync_config = SyncConfig( - user=user, - filter_collection=filter_collection, - is_guest=requester.is_guest, - request_key=request_key, - device_id=device_id, - ) - - since_token = None - if since is not None: - since_token = await StreamToken.from_string(self.store, since) - - # 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 - - if affect_presence: - await self.presence_handler.set_state( - user, {"presence": set_presence}, True - ) - - context = await self.presence_handler.user_syncing( - user.to_string(), affect_presence=affect_presence - ) - with context: - sync_result = await self.sync_handler.wait_for_sync_for_user( - requester, - sync_config, - since_token=since_token, - timeout=timeout, - full_state=full_state, - ) - - # the client may have disconnected by now; don't bother to serialize the - # response if so. - if request._disconnected: - logger.info("Client has disconnected; not serializing response.") - return 200, {} - - time_now = self.clock.time_msec() - response_content = await self.encode_response( - time_now, sync_result, requester.access_token_id, filter_collection - ) - - logger.debug("Event formatting complete") - return 200, response_content - - async def encode_response(self, time_now, sync_result, access_token_id, filter): - logger.debug("Formatting events in sync response") - if filter.event_format == "client": - event_formatter = format_event_for_client_v2_without_room_id - elif filter.event_format == "federation": - event_formatter = format_event_raw - else: - raise Exception("Unknown event format %s" % (filter.event_format,)) - - joined = await self.encode_joined( - sync_result.joined, - time_now, - access_token_id, - filter.event_fields, - event_formatter, - ) - - invited = await self.encode_invited( - sync_result.invited, time_now, access_token_id, event_formatter - ) - - knocked = await self.encode_knocked( - sync_result.knocked, time_now, access_token_id, event_formatter - ) - - archived = await self.encode_archived( - sync_result.archived, - time_now, - access_token_id, - filter.event_fields, - event_formatter, - ) - - logger.debug("building sync response dict") - - response: dict = defaultdict(dict) - response["next_batch"] = await sync_result.next_batch.to_string(self.store) - - if sync_result.account_data: - response["account_data"] = {"events": sync_result.account_data} - if sync_result.presence: - response["presence"] = SyncRestServlet.encode_presence( - sync_result.presence, time_now - ) - - if sync_result.to_device: - response["to_device"] = {"events": sync_result.to_device} - - if sync_result.device_lists.changed: - response["device_lists"]["changed"] = list(sync_result.device_lists.changed) - if sync_result.device_lists.left: - response["device_lists"]["left"] = list(sync_result.device_lists.left) - - # We always include this because https://github.com/vector-im/element-android/issues/3725 - # The spec isn't terribly clear on when this can be omitted and how a client would tell - # the difference between "no keys present" and "nothing changed" in terms of whole field - # absent / individual key type entry absent - # Corresponding synapse issue: https://github.com/matrix-org/synapse/issues/10456 - response["device_one_time_keys_count"] = sync_result.device_one_time_keys_count - - # https://github.com/matrix-org/matrix-doc/blob/54255851f642f84a4f1aaf7bc063eebe3d76752b/proposals/2732-olm-fallback-keys.md - # states that this field should always be included, as long as the server supports the feature. - response[ - "org.matrix.msc2732.device_unused_fallback_key_types" - ] = sync_result.device_unused_fallback_key_types - - if joined: - response["rooms"][Membership.JOIN] = joined - if invited: - response["rooms"][Membership.INVITE] = invited - if knocked: - response["rooms"][Membership.KNOCK] = knocked - if archived: - response["rooms"][Membership.LEAVE] = archived - - if sync_result.groups.join: - response["groups"][Membership.JOIN] = sync_result.groups.join - if sync_result.groups.invite: - response["groups"][Membership.INVITE] = sync_result.groups.invite - if sync_result.groups.leave: - response["groups"][Membership.LEAVE] = sync_result.groups.leave - - return response - - @staticmethod - def encode_presence(events, time_now): - return { - "events": [ - { - "type": "m.presence", - "sender": event.user_id, - "content": format_user_presence_state( - event, time_now, include_user_id=False - ), - } - for event in events - ] - } - - async def encode_joined( - self, rooms, time_now, token_id, event_fields, event_formatter - ): - """ - Encode the joined rooms in a sync result - - Args: - rooms(list[synapse.handlers.sync.JoinedSyncResult]): list of sync - results for rooms this user is joined to - time_now(int): current time - used as a baseline for age - calculations - token_id(int): ID of the user's auth token - used for namespacing - of transaction IDs - event_fields(list): List of event fields to include. If empty, - all fields will be returned. - event_formatter (func[dict]): function to convert from federation format - to client format - Returns: - dict[str, dict[str, object]]: the joined rooms list, in our - response format - """ - joined = {} - for room in rooms: - joined[room.room_id] = await self.encode_room( - room, - time_now, - token_id, - joined=True, - only_fields=event_fields, - event_formatter=event_formatter, - ) - - return joined - - async def encode_invited(self, rooms, time_now, token_id, event_formatter): - """ - Encode the invited rooms in a sync result - - Args: - rooms(list[synapse.handlers.sync.InvitedSyncResult]): list of - sync results for rooms this user is invited to - time_now(int): current time - used as a baseline for age - calculations - token_id(int): ID of the user's auth token - used for namespacing - of transaction IDs - event_formatter (func[dict]): function to convert from federation format - to client format - - Returns: - dict[str, dict[str, object]]: the invited rooms list, in our - response format - """ - invited = {} - for room in rooms: - invite = await self._event_serializer.serialize_event( - room.invite, - time_now, - token_id=token_id, - event_format=event_formatter, - include_stripped_room_state=True, - ) - unsigned = dict(invite.get("unsigned", {})) - invite["unsigned"] = unsigned - invited_state = list(unsigned.pop("invite_room_state", [])) - invited_state.append(invite) - invited[room.room_id] = {"invite_state": {"events": invited_state}} - - return invited - - async def encode_knocked( - self, - rooms: List[KnockedSyncResult], - time_now: int, - token_id: int, - event_formatter: Callable[[Dict], Dict], - ) -> Dict[str, Dict[str, Any]]: - """ - Encode the rooms we've knocked on in a sync result. - - Args: - rooms: list of sync results for rooms this user is knocking on - time_now: current time - used as a baseline for age calculations - token_id: ID of the user's auth token - used for namespacing of transaction IDs - event_formatter: function to convert from federation format to client format - - Returns: - The list of rooms the user has knocked on, in our response format. - """ - knocked = {} - for room in rooms: - knock = await self._event_serializer.serialize_event( - room.knock, - time_now, - token_id=token_id, - event_format=event_formatter, - include_stripped_room_state=True, - ) - - # Extract the `unsigned` key from the knock event. - # This is where we (cheekily) store the knock state events - unsigned = knock.setdefault("unsigned", {}) - - # Duplicate the dictionary in order to avoid modifying the original - unsigned = dict(unsigned) - - # 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", [])) - - # Append the actual knock membership event itself as well. This provides - # the client with: - # - # * A knock state event that they can use for easier internal tracking - # * The rough timestamp of when the knock occurred contained within the event - knocked_state.append(knock) - - # Build the `knock_state` dictionary, which will contain the state of the - # room that the client has knocked on - knocked[room.room_id] = {"knock_state": {"events": knocked_state}} - - return knocked - - async def encode_archived( - self, rooms, time_now, token_id, event_fields, event_formatter - ): - """ - Encode the archived rooms in a sync result - - Args: - rooms (list[synapse.handlers.sync.ArchivedSyncResult]): list of - sync results for rooms this user is joined to - time_now(int): current time - used as a baseline for age - calculations - token_id(int): ID of the user's auth token - used for namespacing - of transaction IDs - event_fields(list): List of event fields to include. If empty, - all fields will be returned. - event_formatter (func[dict]): function to convert from federation format - to client format - Returns: - dict[str, dict[str, object]]: The invited rooms list, in our - response format - """ - joined = {} - for room in rooms: - joined[room.room_id] = await self.encode_room( - room, - time_now, - token_id, - joined=False, - only_fields=event_fields, - event_formatter=event_formatter, - ) - - return joined - - async def encode_room( - self, room, time_now, token_id, joined, only_fields, event_formatter - ): - """ - Args: - room (JoinedSyncResult|ArchivedSyncResult): sync result for a - single room - time_now (int): current time - used as a baseline for age - calculations - token_id (int): ID of the user's auth token - used for namespacing - of transaction IDs - joined (bool): True if the user is joined to this room - will mean - we handle ephemeral events - only_fields(list): Optional. The list of event fields to include. - event_formatter (func[dict]): function to convert from federation format - to client format - Returns: - dict[str, object]: the room, encoded in our response format - """ - - def serialize(events): - return self._event_serializer.serialize_events( - events, - time_now=time_now, - # We don't bundle "live" events, as otherwise clients - # will end up double counting annotations. - bundle_aggregations=False, - token_id=token_id, - event_format=event_formatter, - only_event_fields=only_fields, - ) - - state_dict = room.state - timeline_events = room.timeline.events - - state_events = state_dict.values() - - for event in itertools.chain(state_events, timeline_events): - # We've had bug reports that events were coming down under the - # wrong room. - if event.room_id != room.room_id: - logger.warning( - "Event %r is under room %r instead of %r", - event.event_id, - room.room_id, - event.room_id, - ) - - serialized_state = await serialize(state_events) - serialized_timeline = await serialize(timeline_events) - - account_data = room.account_data - - result = { - "timeline": { - "events": serialized_timeline, - "prev_batch": await room.timeline.prev_batch.to_string(self.store), - "limited": room.timeline.limited, - }, - "state": {"events": serialized_state}, - "account_data": {"events": account_data}, - } - - if joined: - ephemeral_events = room.ephemeral - result["ephemeral"] = {"events": ephemeral_events} - result["unread_notifications"] = room.unread_notifications - result["summary"] = room.summary - result["org.matrix.msc2654.unread_count"] = room.unread_count - - return result - - -def register_servlets(hs, http_server): - SyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/tags.py b/synapse/rest/client/v2_alpha/tags.py deleted file mode 100644 index c14f83be18..0000000000 --- a/synapse/rest/client/v2_alpha/tags.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import AuthError -from synapse.http.servlet import RestServlet, parse_json_object_from_request - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class TagListServlet(RestServlet): - """ - GET /user/{user_id}/rooms/{room_id}/tags HTTP/1.1 - """ - - PATTERNS = client_patterns("/user/(?P[^/]*)/rooms/(?P[^/]*)/tags") - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.store = hs.get_datastore() - - async def on_GET(self, request, user_id, room_id): - requester = await self.auth.get_user_by_req(request) - if user_id != requester.user.to_string(): - raise AuthError(403, "Cannot get tags for other users.") - - tags = await self.store.get_tags_for_room(user_id, room_id) - - return 200, {"tags": tags} - - -class TagServlet(RestServlet): - """ - PUT /user/{user_id}/rooms/{room_id}/tags/{tag} HTTP/1.1 - DELETE /user/{user_id}/rooms/{room_id}/tags/{tag} HTTP/1.1 - """ - - PATTERNS = client_patterns( - "/user/(?P[^/]*)/rooms/(?P[^/]*)/tags/(?P[^/]*)" - ) - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - self.handler = hs.get_account_data_handler() - - async def on_PUT(self, request, user_id, room_id, tag): - 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.") - - body = parse_json_object_from_request(request) - - await self.handler.add_tag_to_room(user_id, room_id, tag, body) - - return 200, {} - - async def on_DELETE(self, request, user_id, room_id, tag): - 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.") - - await self.handler.remove_tag_from_room(user_id, room_id, tag) - - return 200, {} - - -def register_servlets(hs, http_server): - TagListServlet(hs).register(http_server) - TagServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/thirdparty.py b/synapse/rest/client/v2_alpha/thirdparty.py deleted file mode 100644 index b5c67c9bb6..0000000000 --- a/synapse/rest/client/v2_alpha/thirdparty.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import logging - -from synapse.api.constants import ThirdPartyEntityKind -from synapse.http.servlet import RestServlet - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class ThirdPartyProtocolsServlet(RestServlet): - PATTERNS = client_patterns("/thirdparty/protocols") - - def __init__(self, hs): - super().__init__() - - self.auth = hs.get_auth() - self.appservice_handler = hs.get_application_service_handler() - - async def on_GET(self, request): - await self.auth.get_user_by_req(request, allow_guest=True) - - protocols = await self.appservice_handler.get_3pe_protocols() - return 200, protocols - - -class ThirdPartyProtocolServlet(RestServlet): - PATTERNS = client_patterns("/thirdparty/protocol/(?P[^/]+)$") - - def __init__(self, hs): - super().__init__() - - self.auth = hs.get_auth() - self.appservice_handler = hs.get_application_service_handler() - - async def on_GET(self, request, protocol): - await self.auth.get_user_by_req(request, allow_guest=True) - - protocols = await self.appservice_handler.get_3pe_protocols( - only_protocol=protocol - ) - if protocol in protocols: - return 200, protocols[protocol] - else: - return 404, {"error": "Unknown protocol"} - - -class ThirdPartyUserServlet(RestServlet): - PATTERNS = client_patterns("/thirdparty/user(/(?P[^/]+))?$") - - def __init__(self, hs): - super().__init__() - - self.auth = hs.get_auth() - self.appservice_handler = hs.get_application_service_handler() - - async def on_GET(self, request, protocol): - await self.auth.get_user_by_req(request, allow_guest=True) - - fields = request.args - fields.pop(b"access_token", None) - - results = await self.appservice_handler.query_3pe( - ThirdPartyEntityKind.USER, protocol, fields - ) - - return 200, results - - -class ThirdPartyLocationServlet(RestServlet): - PATTERNS = client_patterns("/thirdparty/location(/(?P[^/]+))?$") - - def __init__(self, hs): - super().__init__() - - self.auth = hs.get_auth() - self.appservice_handler = hs.get_application_service_handler() - - async def on_GET(self, request, protocol): - await self.auth.get_user_by_req(request, allow_guest=True) - - fields = request.args - fields.pop(b"access_token", None) - - results = await self.appservice_handler.query_3pe( - ThirdPartyEntityKind.LOCATION, protocol, fields - ) - - return 200, results - - -def register_servlets(hs, http_server): - ThirdPartyProtocolsServlet(hs).register(http_server) - ThirdPartyProtocolServlet(hs).register(http_server) - ThirdPartyUserServlet(hs).register(http_server) - ThirdPartyLocationServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/tokenrefresh.py b/synapse/rest/client/v2_alpha/tokenrefresh.py deleted file mode 100644 index b2f858545c..0000000000 --- a/synapse/rest/client/v2_alpha/tokenrefresh.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2015, 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from synapse.api.errors import AuthError -from synapse.http.servlet import RestServlet - -from ._base import client_patterns - - -class TokenRefreshRestServlet(RestServlet): - """ - Exchanges refresh tokens for a pair of an access token and a new refresh - token. - """ - - PATTERNS = client_patterns("/tokenrefresh") - - def __init__(self, hs): - super().__init__() - - async def on_POST(self, request): - raise AuthError(403, "tokenrefresh is no longer supported.") - - -def register_servlets(hs, http_server): - TokenRefreshRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/user_directory.py b/synapse/rest/client/v2_alpha/user_directory.py deleted file mode 100644 index 7e8912f0b9..0000000000 --- a/synapse/rest/client/v2_alpha/user_directory.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2017 Vector Creations Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.api.errors import SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request - -from ._base import client_patterns - -logger = logging.getLogger(__name__) - - -class UserDirectorySearchRestServlet(RestServlet): - PATTERNS = client_patterns("/user_directory/search$") - - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.user_directory_handler = hs.get_user_directory_handler() - - async def on_POST(self, request): - """Searches for users in directory - - Returns: - dict of the form:: - - { - "limited": , # whether there were more results or not - "results": [ # Ordered by best match first - { - "user_id": , - "display_name": , - "avatar_url": - } - ] - } - """ - requester = await self.auth.get_user_by_req(request, allow_guest=False) - user_id = requester.user.to_string() - - if not self.hs.config.user_directory_search_enabled: - return 200, {"limited": False, "results": []} - - body = parse_json_object_from_request(request) - - limit = body.get("limit", 10) - limit = min(limit, 50) - - try: - search_term = body["search_term"] - except Exception: - raise SynapseError(400, "`search_term` is required field") - - results = await self.user_directory_handler.search_users( - user_id, search_term, limit - ) - - return 200, results - - -def register_servlets(hs, http_server): - UserDirectorySearchRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/voip.py b/synapse/rest/client/voip.py new file mode 100644 index 0000000000..f53020520d --- /dev/null +++ b/synapse/rest/client/voip.py @@ -0,0 +1,73 @@ +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import hashlib +import hmac + +from synapse.http.servlet import RestServlet +from synapse.rest.client._base import client_patterns + + +class VoipRestServlet(RestServlet): + PATTERNS = client_patterns("/voip/turnServer$", v1=True) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req( + request, self.hs.config.turn_allow_guests + ) + + turnUris = self.hs.config.turn_uris + turnSecret = self.hs.config.turn_shared_secret + turnUsername = self.hs.config.turn_username + turnPassword = self.hs.config.turn_password + userLifetime = self.hs.config.turn_user_lifetime + + if turnUris and turnSecret and userLifetime: + expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 + username = "%d:%s" % (expiry, requester.user.to_string()) + + mac = hmac.new( + turnSecret.encode(), msg=username.encode(), digestmod=hashlib.sha1 + ) + # We need to use standard padded base64 encoding here + # encode_base64 because we need to add the standard padding to get the + # same result as the TURN server. + password = base64.b64encode(mac.digest()).decode("ascii") + + elif turnUris and turnUsername and turnPassword and userLifetime: + username = turnUsername + password = turnPassword + + else: + return 200, {} + + return ( + 200, + { + "username": username, + "password": password, + "ttl": userLifetime / 1000, + "uris": turnUris, + }, + ) + + +def register_servlets(hs, http_server): + VoipRestServlet(hs).register(http_server) -- cgit 1.4.1