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/account.py | |
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/account.py')
-rw-r--r-- | synapse/rest/client/account.py | 910 |
1 files changed, 910 insertions, 0 deletions
diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py new file mode 100644 index 0000000000..fb5ad2906e --- /dev/null +++ b/synapse/rest/client/account.py @@ -0,0 +1,910 @@ +# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import random +from http import HTTPStatus +from typing import TYPE_CHECKING +from urllib.parse import urlparse + +from synapse.api.constants import LoginType +from synapse.api.errors import ( + Codes, + InteractiveAuthIncompleteError, + SynapseError, + ThreepidValidationError, +) +from synapse.config.emailconfig import ThreepidBehaviour +from synapse.handlers.ui_auth import UIAuthSessionDataConstants +from synapse.http.server import finish_request, respond_with_html +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, + parse_string, +) +from synapse.metrics import threepid_send_requests +from synapse.push.mailer import Mailer +from synapse.util.msisdn import phone_number_to_msisdn +from synapse.util.stringutils import assert_valid_client_secret, random_string +from synapse.util.threepids import check_3pid_allowed, validate_email + +from ._base import client_patterns, interactive_auth_handler + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +logger = logging.getLogger(__name__) + + +class EmailPasswordRequestTokenRestServlet(RestServlet): + PATTERNS = client_patterns("/account/password/email/requestToken$") + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.hs = hs + self.datastore = hs.get_datastore() + self.config = hs.config + self.identity_handler = hs.get_identity_handler() + + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self.mailer = Mailer( + hs=self.hs, + app_name=self.config.email_app_name, + template_html=self.config.email_password_reset_template_html, + template_text=self.config.email_password_reset_template_text, + ) + + async def on_POST(self, request): + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + logger.warning( + "User password resets have been disabled due to lack of email config" + ) + raise SynapseError( + 400, "Email-based password resets have been disabled on this server" + ) + + body = parse_json_object_from_request(request) + + assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) + + # Extract params from body + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + # Canonicalise the email address. The addresses are all stored canonicalised + # in the database. This allows the user to reset his password without having to + # know the exact spelling (eg. upper and lower case) of address in the database. + # Stored in the database "foo@bar.com" + # User requests with "FOO@bar.com" would raise a Not Found error + try: + email = validate_email(body["email"]) + except ValueError as e: + raise SynapseError(400, str(e)) + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param + + if next_link: + # Raise if the provided next_link value isn't valid + assert_valid_next_link(self.hs, next_link) + + await self.identity_handler.ratelimit_request_token_requests( + request, "email", email + ) + + # The email will be sent to the stored address. + # This avoids a potential account hijack by requesting a password reset to + # an email address which is controlled by the attacker but which, after + # canonicalisation, matches the one in our database. + existing_user_id = await self.hs.get_datastore().get_user_id_by_threepid( + "email", email + ) + + if existing_user_id is None: + if self.config.request_token_inhibit_3pid_errors: + # Make the client think the operation succeeded. See the rationale in the + # comments for request_token_inhibit_3pid_errors. + # Also wait for some random amount of time between 100ms and 1s to make it + # look like we did something. + await self.hs.get_clock().sleep(random.randint(1, 10) / 10) + return 200, {"sid": random_string(16)} + + raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) + + if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + assert self.hs.config.account_threepid_delegate_email + + # Have the configured identity server handle the request + ret = await self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate_email, + email, + client_secret, + send_attempt, + next_link, + ) + else: + # Send password reset emails from Synapse + sid = await self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_password_reset_mail, + next_link, + ) + + # Wrap the session id in a JSON object + ret = {"sid": sid} + + threepid_send_requests.labels(type="email", reason="password_reset").observe( + send_attempt + ) + + return 200, ret + + +class PasswordRestServlet(RestServlet): + PATTERNS = client_patterns("/account/password$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + self.datastore = self.hs.get_datastore() + self.password_policy_handler = hs.get_password_policy_handler() + self._set_password_handler = hs.get_set_password_handler() + + @interactive_auth_handler + async def on_POST(self, request): + body = parse_json_object_from_request(request) + + # we do basic sanity checks here because the auth layer will store these + # in sessions. Pull out the new password provided to us. + new_password = body.pop("new_password", None) + if new_password is not None: + if not isinstance(new_password, str) or len(new_password) > 512: + raise SynapseError(400, "Invalid password") + self.password_policy_handler.validate_password(new_password) + + # there are two possibilities here. Either the user does not have an + # access token, and needs to do a password reset; or they have one and + # need to validate their identity. + # + # In the first case, we offer a couple of means of identifying + # themselves (email and msisdn, though it's unclear if msisdn actually + # works). + # + # In the second case, we require a password to confirm their identity. + + if self.auth.has_access_token(request): + requester = await self.auth.get_user_by_req(request) + try: + params, session_id = await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "modify your account password", + ) + except InteractiveAuthIncompleteError as e: + # The user needs to provide more steps to complete auth, but + # they're not required to provide the password again. + # + # If a password is available now, hash the provided password and + # store it for later. + if new_password: + password_hash = await self.auth_handler.hash(new_password) + await self.auth_handler.set_session_data( + e.session_id, + UIAuthSessionDataConstants.PASSWORD_HASH, + password_hash, + ) + raise + user_id = requester.user.to_string() + else: + requester = None + try: + result, params, session_id = await self.auth_handler.check_ui_auth( + [[LoginType.EMAIL_IDENTITY]], + request, + body, + "modify your account password", + ) + except InteractiveAuthIncompleteError as e: + # The user needs to provide more steps to complete auth, but + # they're not required to provide the password again. + # + # If a password is available now, hash the provided password and + # store it for later. + if new_password: + password_hash = await self.auth_handler.hash(new_password) + await self.auth_handler.set_session_data( + e.session_id, + UIAuthSessionDataConstants.PASSWORD_HASH, + password_hash, + ) + raise + + if LoginType.EMAIL_IDENTITY in result: + threepid = result[LoginType.EMAIL_IDENTITY] + if "medium" not in threepid or "address" not in threepid: + raise SynapseError(500, "Malformed threepid") + if threepid["medium"] == "email": + # For emails, canonicalise the address. + # We store all email addresses canonicalised in the DB. + # (See add_threepid in synapse/handlers/auth.py) + try: + threepid["address"] = validate_email(threepid["address"]) + except ValueError as e: + raise SynapseError(400, str(e)) + # if using email, we must know about the email they're authing with! + threepid_user_id = await self.datastore.get_user_id_by_threepid( + threepid["medium"], threepid["address"] + ) + if not threepid_user_id: + raise SynapseError(404, "Email address not found", Codes.NOT_FOUND) + user_id = threepid_user_id + else: + logger.error("Auth succeeded but no known type! %r", result.keys()) + raise SynapseError(500, "", Codes.UNKNOWN) + + # If we have a password in this request, prefer it. Otherwise, use the + # password hash from an earlier request. + if new_password: + password_hash = await self.auth_handler.hash(new_password) + elif session_id is not None: + password_hash = await self.auth_handler.get_session_data( + session_id, UIAuthSessionDataConstants.PASSWORD_HASH, None + ) + else: + # UI validation was skipped, but the request did not include a new + # password. + password_hash = None + if not password_hash: + raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM) + + logout_devices = params.get("logout_devices", True) + + await self._set_password_handler.set_password( + user_id, password_hash, logout_devices, requester + ) + + return 200, {} + + +class DeactivateAccountRestServlet(RestServlet): + PATTERNS = client_patterns("/account/deactivate$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + self._deactivate_account_handler = hs.get_deactivate_account_handler() + + @interactive_auth_handler + async def on_POST(self, request): + body = parse_json_object_from_request(request) + erase = body.get("erase", False) + if not isinstance(erase, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'erase' must be a boolean, if given", + Codes.BAD_JSON, + ) + + requester = await self.auth.get_user_by_req(request) + + # allow ASes to deactivate their own users + if requester.app_service: + await self._deactivate_account_handler.deactivate_account( + requester.user.to_string(), erase, requester + ) + return 200, {} + + await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "deactivate your account", + ) + result = await self._deactivate_account_handler.deactivate_account( + requester.user.to_string(), + erase, + requester, + id_server=body.get("id_server"), + ) + if result: + id_server_unbind_result = "success" + else: + id_server_unbind_result = "no-support" + + return 200, {"id_server_unbind_result": id_server_unbind_result} + + +class EmailThreepidRequestTokenRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/email/requestToken$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.config = hs.config + self.identity_handler = hs.get_identity_handler() + self.store = self.hs.get_datastore() + + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self.mailer = Mailer( + hs=self.hs, + app_name=self.config.email_app_name, + template_html=self.config.email_add_threepid_template_html, + template_text=self.config.email_add_threepid_template_text, + ) + + async def on_POST(self, request): + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + logger.warning( + "Adding emails have been disabled due to lack of an email config" + ) + raise SynapseError( + 400, "Adding an email to your account is disabled on this server" + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + # Canonicalise the email address. The addresses are all stored canonicalised + # in the database. + # This ensures that the validation email is sent to the canonicalised address + # as it will later be entered into the database. + # Otherwise the email will be sent to "FOO@bar.com" and stored as + # "foo@bar.com" in database. + try: + email = validate_email(body["email"]) + except ValueError as e: + raise SynapseError(400, str(e)) + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param + + if not check_3pid_allowed(self.hs, "email", email): + raise SynapseError( + 403, + "Your email domain is not authorized on this server", + Codes.THREEPID_DENIED, + ) + + await self.identity_handler.ratelimit_request_token_requests( + request, "email", email + ) + + if next_link: + # Raise if the provided next_link value isn't valid + assert_valid_next_link(self.hs, next_link) + + existing_user_id = await self.store.get_user_id_by_threepid("email", email) + + if existing_user_id is not None: + if self.config.request_token_inhibit_3pid_errors: + # Make the client think the operation succeeded. See the rationale in the + # comments for request_token_inhibit_3pid_errors. + # Also wait for some random amount of time between 100ms and 1s to make it + # look like we did something. + await self.hs.get_clock().sleep(random.randint(1, 10) / 10) + return 200, {"sid": random_string(16)} + + raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) + + if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + assert self.hs.config.account_threepid_delegate_email + + # Have the configured identity server handle the request + ret = await self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate_email, + email, + client_secret, + send_attempt, + next_link, + ) + else: + # Send threepid validation emails from Synapse + sid = await self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_add_threepid_mail, + next_link, + ) + + # Wrap the session id in a JSON object + ret = {"sid": sid} + + threepid_send_requests.labels(type="email", reason="add_threepid").observe( + send_attempt + ) + + return 200, ret + + +class MsisdnThreepidRequestTokenRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/msisdn/requestToken$") + + def __init__(self, hs: "HomeServer"): + self.hs = hs + super().__init__() + self.store = self.hs.get_datastore() + self.identity_handler = hs.get_identity_handler() + + async def on_POST(self, request): + body = parse_json_object_from_request(request) + assert_params_in_dict( + body, ["client_secret", "country", "phone_number", "send_attempt"] + ) + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + country = body["country"] + phone_number = body["phone_number"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param + + msisdn = phone_number_to_msisdn(country, phone_number) + + if not check_3pid_allowed(self.hs, "msisdn", msisdn): + raise SynapseError( + 403, + "Account phone numbers are not authorized on this server", + Codes.THREEPID_DENIED, + ) + + await self.identity_handler.ratelimit_request_token_requests( + request, "msisdn", msisdn + ) + + if next_link: + # Raise if the provided next_link value isn't valid + assert_valid_next_link(self.hs, next_link) + + existing_user_id = await self.store.get_user_id_by_threepid("msisdn", msisdn) + + if existing_user_id is not None: + if self.hs.config.request_token_inhibit_3pid_errors: + # Make the client think the operation succeeded. See the rationale in the + # comments for request_token_inhibit_3pid_errors. + # Also wait for some random amount of time between 100ms and 1s to make it + # look like we did something. + await self.hs.get_clock().sleep(random.randint(1, 10) / 10) + return 200, {"sid": random_string(16)} + + raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) + + if not self.hs.config.account_threepid_delegate_msisdn: + logger.warning( + "No upstream msisdn account_threepid_delegate configured on the server to " + "handle this request" + ) + raise SynapseError( + 400, + "Adding phone numbers to user account is not supported by this homeserver", + ) + + ret = await self.identity_handler.requestMsisdnToken( + self.hs.config.account_threepid_delegate_msisdn, + country, + phone_number, + client_secret, + send_attempt, + next_link, + ) + + threepid_send_requests.labels(type="msisdn", reason="add_threepid").observe( + send_attempt + ) + + return 200, ret + + +class AddThreepidEmailSubmitTokenServlet(RestServlet): + """Handles 3PID validation token submission for adding an email to a user's account""" + + PATTERNS = client_patterns( + "/add_threepid/email/submit_token$", releases=(), unstable=True + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.config = hs.config + self.clock = hs.get_clock() + self.store = hs.get_datastore() + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: + self._failure_email_template = ( + self.config.email_add_threepid_template_failure_html + ) + + async def on_GET(self, request): + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + logger.warning( + "Adding emails have been disabled due to lack of an email config" + ) + raise SynapseError( + 400, "Adding an email to your account is disabled on this server" + ) + elif self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + raise SynapseError( + 400, + "This homeserver is not validating threepids. Use an identity server " + "instead.", + ) + + sid = parse_string(request, "sid", required=True) + token = parse_string(request, "token", required=True) + client_secret = parse_string(request, "client_secret", required=True) + assert_valid_client_secret(client_secret) + + # Attempt to validate a 3PID session + try: + # Mark the session as valid + next_link = await self.store.validate_threepid_session( + sid, client_secret, token, self.clock.time_msec() + ) + + # Perform a 302 redirect if next_link is set + if next_link: + request.setResponseCode(302) + request.setHeader("Location", next_link) + finish_request(request) + return None + + # Otherwise show the success template + html = self.config.email_add_threepid_template_success_html_content + status_code = 200 + except ThreepidValidationError as e: + status_code = e.code + + # Show a failure page with a reason + template_vars = {"failure_reason": e.msg} + html = self._failure_email_template.render(**template_vars) + + respond_with_html(request, status_code, html) + + +class AddThreepidMsisdnSubmitTokenServlet(RestServlet): + """Handles 3PID validation token submission for adding a phone number to a user's + account + """ + + PATTERNS = client_patterns( + "/add_threepid/msisdn/submit_token$", releases=(), unstable=True + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super().__init__() + self.config = hs.config + self.clock = hs.get_clock() + self.store = hs.get_datastore() + self.identity_handler = hs.get_identity_handler() + + async def on_POST(self, request): + if not self.config.account_threepid_delegate_msisdn: + raise SynapseError( + 400, + "This homeserver is not validating phone numbers. Use an identity server " + "instead.", + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["client_secret", "sid", "token"]) + assert_valid_client_secret(body["client_secret"]) + + # Proxy submit_token request to msisdn threepid delegate + response = await self.identity_handler.proxy_msisdn_submit_token( + self.config.account_threepid_delegate_msisdn, + body["client_secret"], + body["sid"], + body["token"], + ) + return 200, response + + +class ThreepidRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + self.datastore = self.hs.get_datastore() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + + threepids = await self.datastore.user_get_threepids(requester.user.to_string()) + + return 200, {"threepids": threepids} + + async def on_POST(self, request): + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) + + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + threepid_creds = body.get("threePidCreds") or body.get("three_pid_creds") + if threepid_creds is None: + raise SynapseError( + 400, "Missing param three_pid_creds", Codes.MISSING_PARAM + ) + assert_params_in_dict(threepid_creds, ["client_secret", "sid"]) + + sid = threepid_creds["sid"] + client_secret = threepid_creds["client_secret"] + assert_valid_client_secret(client_secret) + + validation_session = await self.identity_handler.validate_threepid_session( + client_secret, sid + ) + if validation_session: + await self.auth_handler.add_threepid( + user_id, + validation_session["medium"], + validation_session["address"], + validation_session["validated_at"], + ) + return 200, {} + + raise SynapseError( + 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED + ) + + +class ThreepidAddRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/add$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + + @interactive_auth_handler + async def on_POST(self, request): + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) + + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + body = parse_json_object_from_request(request) + + assert_params_in_dict(body, ["client_secret", "sid"]) + sid = body["sid"] + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + await self.auth_handler.validate_user_via_ui_auth( + requester, + request, + body, + "add a third-party identifier to your account", + ) + + validation_session = await self.identity_handler.validate_threepid_session( + client_secret, sid + ) + if validation_session: + await self.auth_handler.add_threepid( + user_id, + validation_session["medium"], + validation_session["address"], + validation_session["validated_at"], + ) + return 200, {} + + raise SynapseError( + 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED + ) + + +class ThreepidBindRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/bind$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.auth = hs.get_auth() + + async def on_POST(self, request): + body = parse_json_object_from_request(request) + + assert_params_in_dict(body, ["id_server", "sid", "client_secret"]) + id_server = body["id_server"] + sid = body["sid"] + id_access_token = body.get("id_access_token") # optional + client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) + + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + await self.identity_handler.bind_threepid( + client_secret, sid, user_id, id_server, id_access_token + ) + + return 200, {} + + +class ThreepidUnbindRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/unbind$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.identity_handler = hs.get_identity_handler() + self.auth = hs.get_auth() + self.datastore = self.hs.get_datastore() + + async def on_POST(self, request): + """Unbind the given 3pid from a specific identity server, or identity servers that are + known to have this 3pid bound + """ + requester = await self.auth.get_user_by_req(request) + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["medium", "address"]) + + medium = body.get("medium") + address = body.get("address") + id_server = body.get("id_server") + + # Attempt to unbind the threepid from an identity server. If id_server is None, try to + # unbind from all identity servers this threepid has been added to in the past + result = await self.identity_handler.try_unbind_threepid( + requester.user.to_string(), + {"address": address, "medium": medium, "id_server": id_server}, + ) + return 200, {"id_server_unbind_result": "success" if result else "no-support"} + + +class ThreepidDeleteRestServlet(RestServlet): + PATTERNS = client_patterns("/account/3pid/delete$") + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_auth_handler() + + async def on_POST(self, request): + if not self.hs.config.enable_3pid_changes: + raise SynapseError( + 400, "3PID changes are disabled on this server", Codes.FORBIDDEN + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["medium", "address"]) + + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + try: + ret = await self.auth_handler.delete_threepid( + user_id, body["medium"], body["address"], body.get("id_server") + ) + except Exception: + # NB. This endpoint should succeed if there is nothing to + # delete, so it should only throw if something is wrong + # that we ought to care about. + logger.exception("Failed to remove threepid") + raise SynapseError(500, "Failed to remove threepid") + + if ret: + id_server_unbind_result = "success" + else: + id_server_unbind_result = "no-support" + + return 200, {"id_server_unbind_result": id_server_unbind_result} + + +def assert_valid_next_link(hs: "HomeServer", next_link: str): + """ + Raises a SynapseError if a given next_link value is invalid + + next_link is valid if the scheme is http(s) and the next_link.domain_whitelist config + option is either empty or contains a domain that matches the one in the given next_link + + Args: + hs: The homeserver object + next_link: The next_link value given by the client + + Raises: + SynapseError: If the next_link is invalid + """ + valid = True + + # Parse the contents of the URL + next_link_parsed = urlparse(next_link) + + # Scheme must not point to the local drive + if next_link_parsed.scheme == "file": + valid = False + + # If the domain whitelist is set, the domain must be in it + if ( + valid + and hs.config.next_link_domain_whitelist is not None + and next_link_parsed.hostname not in hs.config.next_link_domain_whitelist + ): + valid = False + + if not valid: + raise SynapseError( + 400, + "'next_link' domain not included in whitelist, or not http(s)", + errcode=Codes.INVALID_PARAM, + ) + + +class WhoamiRestServlet(RestServlet): + PATTERNS = client_patterns("/account/whoami$") + + def __init__(self, hs): + super().__init__() + self.auth = hs.get_auth() + + async def on_GET(self, request): + requester = await self.auth.get_user_by_req(request) + + response = {"user_id": requester.user.to_string()} + + # Appservices and similar accounts do not have device IDs + # that we can report on, so exclude them for compliance. + if requester.device_id is not None: + response["device_id"] = requester.device_id + + return 200, response + + +def register_servlets(hs, http_server): + EmailPasswordRequestTokenRestServlet(hs).register(http_server) + PasswordRestServlet(hs).register(http_server) + DeactivateAccountRestServlet(hs).register(http_server) + EmailThreepidRequestTokenRestServlet(hs).register(http_server) + MsisdnThreepidRequestTokenRestServlet(hs).register(http_server) + AddThreepidEmailSubmitTokenServlet(hs).register(http_server) + AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server) + ThreepidRestServlet(hs).register(http_server) + ThreepidAddRestServlet(hs).register(http_server) + ThreepidBindRestServlet(hs).register(http_server) + ThreepidUnbindRestServlet(hs).register(http_server) + ThreepidDeleteRestServlet(hs).register(http_server) + WhoamiRestServlet(hs).register(http_server) |