diff options
author | reivilibre <38398653+reivilibre@users.noreply.github.com> | 2021-08-17 12:57:58 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-17 11:57:58 +0000 |
commit | 642a42eddece60afbbd5e5a6659fa9b939238b4a (patch) | |
tree | fde46b103f636a302d6a47aadf368e19e4fc43da /synapse/rest/client/v2_alpha | |
parent | Always list fallback key types in /sync (#10623) (diff) | |
download | synapse-642a42eddece60afbbd5e5a6659fa9b939238b4a.tar.xz |
Flatten the synapse.rest.client package (#10600)
Diffstat (limited to 'synapse/rest/client/v2_alpha')
30 files changed, 0 insertions, 6875 deletions
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<user_id>[^/]*)/account_data/(?P<account_data_type>[^/]*)" - ) - - 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<user_id>[^/]*)" - "/rooms/(?P<room_id>[^/]*)" - "/account_data/(?P<account_data_type>[^/]*)" - ) - - 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<stagetype>[\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<device_id>[^/]*)$") - - 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<user_id>[^/]*)/filter/(?P<filter_id>[^/]*)") - - 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<user_id>[^/]*)/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<group_id>[^/]*)/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<group_id>[^/]*)/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<group_id>[^/]*)/summary" - "(/categories/(?P<category_id>[^/]+))?" - "/rooms/(?P<room_id>[^/]*)$" - ) - - 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<group_id>[^/]*)/categories/(?P<category_id>[^/]+)$" - ) - - 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<group_id>[^/]*)/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<group_id>[^/]*)/roles/(?P<role_id>[^/]+)$") - - 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<group_id>[^/]*)/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<group_id>[^/]*)/summary" - "(/roles/(?P<role_id>[^/]+))?" - "/users/(?P<user_id>[^/]*)$" - ) - - 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<group_id>[^/]*)/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<group_id>[^/]*)/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<group_id>[^/]*)/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<group_id>[^/]*)/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<group_id>[^/]*)/admin/rooms/(?P<room_id>[^/]*)$" - ) - - 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<group_id>[^/]*)/admin/rooms/(?P<room_id>[^/]*)" - "/config/(?P<config_key>[^/]*)$" - ) - - 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<group_id>[^/]*)/admin/users/invite/(?P<user_id>[^/]*)$" - ) - - 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<group_id>[^/]*)/admin/users/remove/(?P<user_id>[^/]*)$" - ) - - 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<group_id>[^/]*)/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<group_id>[^/]*)/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<group_id>[^/]*)/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<group_id>[^/]*)/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<user_id>[^/]*)$") - - 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": "<user_id>", - "device_id": "<device_id>", - "valid_until_ts": <millisecond_timestamp>, - "algorithms": [ - "m.olm.curve25519-aes-sha2", - ] - "keys": { - "<algorithm>:<device_id>": "<key_base64>", - }, - "signatures:" { - "<user_id>" { - "<algorithm>:<device_id>": "<signature_base64>" - } } }, - "one_time_keys": { - "<algorithm>:<key_id>": "<key_base64>" - }, - } - """ - - PATTERNS = client_patterns("/keys/upload(/(?P<device_id>[^/]+))?$") - - 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": { - "<user_id>": ["<device_id>"] - } } - - HTTP/1.1 200 OK - { - "device_keys": { - "<user_id>": { - "<device_id>": { - "user_id": "<user_id>", // Duplicated to be signed - "device_id": "<device_id>", // Duplicated to be signed - "valid_until_ts": <millisecond_timestamp>, - "algorithms": [ // List of supported algorithms - "m.olm.curve25519-aes-sha2", - ], - "keys": { // Must include a ed25519 signing key - "<algorithm>:<key_id>": "<key_base64>", - }, - "signatures:" { - // Must be signed with device's ed25519 key - "<user_id>/<device_id>": { - "<algorithm>:<key_id>": "<signature_base64>" - } - // Must be signed by this server. - "<server_name>": { - "<algorithm>:<key_id>": "<signature_base64>" - } } } } } } - """ - - 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": { - "<user_id>": { - "<device_id>": "<algorithm>" - } } } - - HTTP/1.1 200 OK - { - "one_time_keys": { - "<user_id>": { - "<device_id>": { - "<algorithm>:<key_id>": "<key_base64>" - } } } } - - """ - - 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": { - "<device_id>": { - "user_id": "<user_id>", - "device_id": "<device_id>", - "algorithms": [ - "m.olm.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "keys": { - "<algorithm>:<device_id>": "<key_base64>", - }, - "signatures": { - "<signing_user_id>": { - "<algorithm>:<signing_key_base64>": "<signature_base64>>" - } - } - } - } - } - """ - - 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<room_identifier>[^/]*)") - - 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<user_id>[^/]*)/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<room_id>[^/]*)/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<room_id>[^/]*)" - "/receipt/(?P<receipt_type>[^/]*)" - "/(?P<event_id>[^/]*)$" - ) - - 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<medium>[^/]*)/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<room_id>[^/]*)/send_relation" - "/(?P<parent_id>[^/]*)/(?P<relation_type>[^/]*)/(?P<event_type>[^/]*)" - ) - - 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<txn_id>[^/]*)$", 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<room_id>[^/]*)/relations/(?P<parent_id>[^/]*)" - "(/(?P<relation_type>[^/]*)(/(?P<event_type>[^/]*))?)?$", - 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<room_id>[^/]*)/aggregations/(?P<parent_id>[^/]*)" - "(/(?P<relation_type>[^/]*)(/(?P<event_type>[^/]*))?)?$", - 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<room_id>[^/]*)/aggregations/(?P<parent_id>[^/]*)" - "/(?P<relation_type>[^/]*)/(?P<event_type>[^/]*)/(?P<key>[^/]*)$", - 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<room_id>[^/]*)/report/(?P<event_id>[^/]*)$") - - 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/<roomID>/batch_send?prev_event=<eventID>&chunk_id=<chunkID> - { - "events": [ ... ], - "state_events_at_start": [ ... ] - } - """ - - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/org.matrix.msc2716" - "/rooms/(?P<room_id>[^/]*)/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<room_id>[^/]+))?(/(?P<session_id>[^/]+))?$" - ) - - 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: <random string> } - 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<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_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<room_id>[^/]*)/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<message_type>[^/]*)/(?P<txn_id>[^/]*)$" - ) - - 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<user_id>[^/]*)", - 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<str>): 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<str>): 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<str>): 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<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/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<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/tags/(?P<tag>[^/]*)" - ) - - 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<protocol>[^/]+)$") - - 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<protocol>[^/]+))?$") - - 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<protocol>[^/]+))?$") - - 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": <bool>, # whether there were more results or not - "results": [ # Ordered by best match first - { - "user_id": <user_id>, - "display_name": <display_name>, - "avatar_url": <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) |