summary refs log tree commit diff
path: root/synapse/rest/client/account.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--synapse/rest/client/account.py612
1 files changed, 5 insertions, 607 deletions
diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py

index 8daa449f9e..455ddda484 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py
@@ -21,28 +21,20 @@ # import logging import random -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, List, Literal, Optional, Tuple from urllib.parse import urlparse -from synapse._pydantic_compat import HAS_PYDANTIC_V2 - -if TYPE_CHECKING or HAS_PYDANTIC_V2: - from pydantic.v1 import StrictBool, StrictStr, constr -else: - from pydantic import StrictBool, StrictStr, constr - import attr -from typing_extensions import Literal from twisted.web.server import Request +from synapse._pydantic_compat import StrictBool, StrictStr, constr from synapse.api.constants import LoginType from synapse.api.errors import ( Codes, InteractiveAuthIncompleteError, NotFoundError, SynapseError, - ThreepidValidationError, ) from synapse.handlers.ui_auth import UIAuthSessionDataConstants from synapse.http.server import HttpServer, finish_request, respond_with_html @@ -54,19 +46,13 @@ from synapse.http.servlet import ( parse_string, ) from synapse.http.site import SynapseRequest -from synapse.metrics import threepid_send_requests -from synapse.push.mailer import Mailer from synapse.types import JsonDict from synapse.types.rest import RequestBodyModel from synapse.types.rest.client import ( AuthenticationData, - ClientSecretStr, - EmailRequestTokenBody, - MsisdnRequestTokenBody, + ClientSecretStr ) -from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string -from synapse.util.threepids import check_3pid_allowed, validate_email from ._base import client_patterns, interactive_auth_handler @@ -77,80 +63,6 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -class EmailPasswordRequestTokenRestServlet(RestServlet): - PATTERNS = client_patterns("/account/password/email/requestToken$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.datastore = hs.get_datastores().main - self.config = hs.config - self.identity_handler = hs.get_identity_handler() - - if self.config.email.can_verify_email: - self.mailer = Mailer( - hs=self.hs, - app_name=self.config.email.email_app_name, - template_html=self.config.email.email_password_reset_template_html, - template_text=self.config.email.email_password_reset_template_text, - ) - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - if not self.config.email.can_verify_email: - logger.warning( - "User password resets have been disabled due to lack of email config" - ) - raise SynapseError( - 400, "Email-based password resets have been disabled on this server" - ) - - body = parse_and_validate_json_object_from_request( - request, EmailRequestTokenBody - ) - - if body.next_link: - # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, body.next_link) - - await self.identity_handler.ratelimit_request_token_requests( - request, "email", body.email - ) - - # The email will be sent to the stored address. - # This avoids a potential account hijack by requesting a password reset to - # an email address which is controlled by the attacker but which, after - # canonicalisation, matches the one in our database. - existing_user_id = await self.hs.get_datastores().main.get_user_id_by_threepid( - "email", body.email - ) - - if existing_user_id is None: - if self.config.server.request_token_inhibit_3pid_errors: - # Make the client think the operation succeeded. See the rationale in the - # comments for request_token_inhibit_3pid_errors. - # Also wait for some random amount of time between 100ms and 1s to make it - # look like we did something. - await self.hs.get_clock().sleep(random.randint(1, 10) / 10) - return 200, {"sid": random_string(16)} - - raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) - - # Send password reset emails from Synapse - sid = await self.identity_handler.send_threepid_validation( - body.email, - body.client_secret, - body.send_attempt, - self.mailer.send_password_reset_mail, - body.next_link, - ) - threepid_send_requests.labels(type="email", reason="password_reset").observe( - body.send_attempt - ) - - # Wrap the session id in a JSON object - return 200, {"sid": sid} - - class PasswordRestServlet(RestServlet): PATTERNS = client_patterns("/account/password$") @@ -211,30 +123,8 @@ class PasswordRestServlet(RestServlet): "modify your account password", ) - if LoginType.EMAIL_IDENTITY in result: - threepid = result[LoginType.EMAIL_IDENTITY] - if "medium" not in threepid or "address" not in threepid: - raise SynapseError(500, "Malformed threepid") - if threepid["medium"] == "email": - # For emails, canonicalise the address. - # We store all email addresses canonicalised in the DB. - # (See add_threepid in synapse/handlers/auth.py) - try: - threepid["address"] = validate_email(threepid["address"]) - except ValueError as e: - raise SynapseError(400, str(e)) - # if using email, we must know about the email they're authing with! - threepid_user_id = await self.datastore.get_user_id_by_threepid( - threepid["medium"], threepid["address"] - ) - if not threepid_user_id: - raise SynapseError( - 404, "Email address not found", Codes.NOT_FOUND - ) - user_id = threepid_user_id - else: - logger.error("Auth succeeded but no known type! %r", result.keys()) - raise SynapseError(500, "", Codes.UNKNOWN) + logger.error("Auth succeeded but no known type (hint: 3PID auth was removed)! %r", result.keys()) + raise SynapseError(500, "", Codes.UNKNOWN) except InteractiveAuthIncompleteError as e: # The user needs to provide more steps to complete auth, but @@ -326,486 +216,6 @@ class DeactivateAccountRestServlet(RestServlet): return 200, {"id_server_unbind_result": id_server_unbind_result} -class EmailThreepidRequestTokenRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/email/requestToken$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.config = hs.config - self.identity_handler = hs.get_identity_handler() - self.store = self.hs.get_datastores().main - - if self.config.email.can_verify_email: - self.mailer = Mailer( - hs=self.hs, - app_name=self.config.email.email_app_name, - template_html=self.config.email.email_add_threepid_template_html, - template_text=self.config.email.email_add_threepid_template_text, - ) - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - if not self.hs.config.registration.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - - if not self.config.email.can_verify_email: - logger.warning( - "Adding emails have been disabled due to lack of an email config" - ) - raise SynapseError( - 400, - "Adding an email to your account is disabled on this server", - ) - - body = parse_and_validate_json_object_from_request( - request, EmailRequestTokenBody - ) - - if not await check_3pid_allowed(self.hs, "email", body.email): - raise SynapseError( - 403, - "Your email domain is not authorized on this server", - Codes.THREEPID_DENIED, - ) - - await self.identity_handler.ratelimit_request_token_requests( - request, "email", body.email - ) - - if body.next_link: - # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, body.next_link) - - existing_user_id = await self.store.get_user_id_by_threepid("email", body.email) - - if existing_user_id is not None: - if self.config.server.request_token_inhibit_3pid_errors: - # Make the client think the operation succeeded. See the rationale in the - # comments for request_token_inhibit_3pid_errors. - # Also wait for some random amount of time between 100ms and 1s to make it - # look like we did something. - await self.hs.get_clock().sleep(random.randint(1, 10) / 10) - return 200, {"sid": random_string(16)} - - raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - - # Send threepid validation emails from Synapse - sid = await self.identity_handler.send_threepid_validation( - body.email, - body.client_secret, - body.send_attempt, - self.mailer.send_add_threepid_mail, - body.next_link, - ) - - threepid_send_requests.labels(type="email", reason="add_threepid").observe( - body.send_attempt - ) - - # Wrap the session id in a JSON object - return 200, {"sid": sid} - - -class MsisdnThreepidRequestTokenRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/msisdn/requestToken$") - - def __init__(self, hs: "HomeServer"): - self.hs = hs - super().__init__() - self.store = self.hs.get_datastores().main - self.identity_handler = hs.get_identity_handler() - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - body = parse_and_validate_json_object_from_request( - request, MsisdnRequestTokenBody - ) - msisdn = phone_number_to_msisdn(body.country, body.phone_number) - logger.info("Request #%s to verify ownership of %s", body.send_attempt, msisdn) - - if not await check_3pid_allowed(self.hs, "msisdn", msisdn): - raise SynapseError( - 403, - # TODO: is this error message accurate? Looks like we've only rejected - # this phone number, not necessarily all phone numbers - "Account phone numbers are not authorized on this server", - Codes.THREEPID_DENIED, - ) - - await self.identity_handler.ratelimit_request_token_requests( - request, "msisdn", msisdn - ) - - if body.next_link: - # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, body.next_link) - - existing_user_id = await self.store.get_user_id_by_threepid("msisdn", msisdn) - - if existing_user_id is not None: - if self.hs.config.server.request_token_inhibit_3pid_errors: - # Make the client think the operation succeeded. See the rationale in the - # comments for request_token_inhibit_3pid_errors. - # Also wait for some random amount of time between 100ms and 1s to make it - # look like we did something. - await self.hs.get_clock().sleep(random.randint(1, 10) / 10) - return 200, {"sid": random_string(16)} - - logger.info("MSISDN %s is already in use by %s", msisdn, existing_user_id) - raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) - - if not self.hs.config.registration.account_threepid_delegate_msisdn: - logger.warning( - "No upstream msisdn account_threepid_delegate configured on the server to " - "handle this request" - ) - raise SynapseError( - 400, - "Adding phone numbers to user account is not supported by this homeserver", - ) - - ret = await self.identity_handler.requestMsisdnToken( - self.hs.config.registration.account_threepid_delegate_msisdn, - body.country, - body.phone_number, - body.client_secret, - body.send_attempt, - body.next_link, - ) - - threepid_send_requests.labels(type="msisdn", reason="add_threepid").observe( - body.send_attempt - ) - logger.info("MSISDN %s: got response from identity server: %s", msisdn, ret) - - return 200, ret - - -class AddThreepidEmailSubmitTokenServlet(RestServlet): - """Handles 3PID validation token submission for adding an email to a user's account""" - - PATTERNS = client_patterns( - "/add_threepid/email/submit_token$", releases=(), unstable=True - ) - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.config = hs.config - self.clock = hs.get_clock() - self.store = hs.get_datastores().main - if self.config.email.can_verify_email: - self._failure_email_template = ( - self.config.email.email_add_threepid_template_failure_html - ) - - async def on_GET(self, request: Request) -> None: - if not self.config.email.can_verify_email: - logger.warning( - "Adding emails have been disabled due to lack of an email config" - ) - raise SynapseError( - 400, "Adding an email to your account is disabled on this server" - ) - - sid = parse_string(request, "sid", required=True) - token = parse_string(request, "token", required=True) - client_secret = parse_string(request, "client_secret", required=True) - assert_valid_client_secret(client_secret) - - # Attempt to validate a 3PID session - try: - # Mark the session as valid - next_link = await self.store.validate_threepid_session( - sid, client_secret, token, self.clock.time_msec() - ) - - # Perform a 302 redirect if next_link is set - if next_link: - request.setResponseCode(302) - request.setHeader("Location", next_link) - finish_request(request) - return None - - # Otherwise show the success template - html = self.config.email.email_add_threepid_template_success_html_content - status_code = 200 - except ThreepidValidationError as e: - status_code = e.code - - # Show a failure page with a reason - template_vars = {"failure_reason": e.msg} - html = self._failure_email_template.render(**template_vars) - - respond_with_html(request, status_code, html) - - -class AddThreepidMsisdnSubmitTokenServlet(RestServlet): - """Handles 3PID validation token submission for adding a phone number to a user's - account - """ - - PATTERNS = client_patterns( - "/add_threepid/msisdn/submit_token$", releases=(), unstable=True - ) - - class PostBody(RequestBodyModel): - client_secret: ClientSecretStr - sid: StrictStr - token: StrictStr - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.config = hs.config - self.clock = hs.get_clock() - self.store = hs.get_datastores().main - self.identity_handler = hs.get_identity_handler() - - async def on_POST(self, request: Request) -> Tuple[int, JsonDict]: - if not self.config.registration.account_threepid_delegate_msisdn: - raise SynapseError( - 400, - "This homeserver is not validating phone numbers. Use an identity server " - "instead.", - ) - - body = parse_and_validate_json_object_from_request(request, self.PostBody) - - # Proxy submit_token request to msisdn threepid delegate - response = await self.identity_handler.proxy_msisdn_submit_token( - self.config.registration.account_threepid_delegate_msisdn, - body.client_secret, - body.sid, - body.token, - ) - return 200, response - - -class ThreepidRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid$") - # This is used as a proxy for all the 3pid endpoints. - - CATEGORY = "Client API requests" - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - self.datastore = self.hs.get_datastores().main - - async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - - threepids = await self.datastore.user_get_threepids(requester.user.to_string()) - - return 200, {"threepids": [attr.asdict(t) for t in threepids]} - - # NOTE(dmr): I have chosen not to use Pydantic to parse this request's body, because - # the endpoint is deprecated. (If you really want to, you could do this by reusing - # ThreePidBindRestServelet.PostBody with an `alias_generator` to handle - # `threePidCreds` versus `three_pid_creds`. - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - if self.hs.config.experimental.msc3861.enabled: - raise NotFoundError(errcode=Codes.UNRECOGNIZED) - - if not self.hs.config.registration.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - body = parse_json_object_from_request(request) - - threepid_creds = body.get("threePidCreds") or body.get("three_pid_creds") - if threepid_creds is None: - raise SynapseError( - 400, "Missing param three_pid_creds", Codes.MISSING_PARAM - ) - assert_params_in_dict(threepid_creds, ["client_secret", "sid"]) - - sid = threepid_creds["sid"] - client_secret = threepid_creds["client_secret"] - assert_valid_client_secret(client_secret) - - validation_session = await self.identity_handler.validate_threepid_session( - client_secret, sid - ) - if validation_session: - await self.auth_handler.add_threepid( - user_id, - validation_session["medium"], - validation_session["address"], - validation_session["validated_at"], - ) - return 200, {} - - raise SynapseError( - 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED - ) - - -class ThreepidAddRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/add$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - - class PostBody(RequestBodyModel): - auth: Optional[AuthenticationData] = None - client_secret: ClientSecretStr - sid: StrictStr - - @interactive_auth_handler - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - if not self.hs.config.registration.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - body = parse_and_validate_json_object_from_request(request, self.PostBody) - - await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body.dict(exclude_unset=True), - "add a third-party identifier to your account", - ) - - validation_session = await self.identity_handler.validate_threepid_session( - body.client_secret, body.sid - ) - if validation_session: - await self.auth_handler.add_threepid( - user_id, - validation_session["medium"], - validation_session["address"], - validation_session["validated_at"], - ) - return 200, {} - - raise SynapseError( - 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED - ) - - -class ThreepidBindRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/bind$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.auth = hs.get_auth() - - class PostBody(RequestBodyModel): - client_secret: ClientSecretStr - id_access_token: StrictStr - id_server: StrictStr - sid: StrictStr - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - body = parse_and_validate_json_object_from_request(request, self.PostBody) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - await self.identity_handler.bind_threepid( - body.client_secret, body.sid, user_id, body.id_server, body.id_access_token - ) - - return 200, {} - - -class ThreepidUnbindRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/unbind$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.identity_handler = hs.get_identity_handler() - self.auth = hs.get_auth() - self.datastore = self.hs.get_datastores().main - - class PostBody(RequestBodyModel): - address: StrictStr - id_server: Optional[StrictStr] = None - medium: Literal["email", "msisdn"] - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - """Unbind the given 3pid from a specific identity server, or identity servers that are - known to have this 3pid bound - """ - requester = await self.auth.get_user_by_req(request) - body = parse_and_validate_json_object_from_request(request, self.PostBody) - - # Attempt to unbind the threepid from an identity server. If id_server is None, try to - # unbind from all identity servers this threepid has been added to in the past - result = await self.identity_handler.try_unbind_threepid( - requester.user.to_string(), body.medium, body.address, body.id_server - ) - return 200, {"id_server_unbind_result": "success" if result else "no-support"} - - -class ThreepidDeleteRestServlet(RestServlet): - PATTERNS = client_patterns("/account/3pid/delete$") - - def __init__(self, hs: "HomeServer"): - super().__init__() - self.hs = hs - self.auth = hs.get_auth() - self.auth_handler = hs.get_auth_handler() - - class PostBody(RequestBodyModel): - address: StrictStr - id_server: Optional[StrictStr] = None - medium: Literal["email", "msisdn"] - - async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - if not self.hs.config.registration.enable_3pid_changes: - raise SynapseError( - 400, "3PID changes are disabled on this server", Codes.FORBIDDEN - ) - - body = parse_and_validate_json_object_from_request(request, self.PostBody) - - requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - - try: - # Attempt to remove any known bindings of this third-party ID - # and user ID from identity servers. - ret = await self.hs.get_identity_handler().try_unbind_threepid( - user_id, body.medium, body.address, body.id_server - ) - except Exception: - # NB. This endpoint should succeed if there is nothing to - # delete, so it should only throw if something is wrong - # that we ought to care about. - logger.exception("Failed to remove threepid") - raise SynapseError(500, "Failed to remove threepid") - - if ret: - id_server_unbind_result = "success" - else: - id_server_unbind_result = "no-support" - - # Delete the local association of this user ID and third-party ID. - await self.auth_handler.delete_local_threepid( - user_id, body.medium, body.address - ) - - return 200, {"id_server_unbind_result": id_server_unbind_result} - - def assert_valid_next_link(hs: "HomeServer", next_link: str) -> None: """ Raises a SynapseError if a given next_link value is invalid @@ -901,20 +311,8 @@ class AccountStatusRestServlet(RestServlet): def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: if hs.config.worker.worker_app is None: if not hs.config.experimental.msc3861.enabled: - EmailPasswordRequestTokenRestServlet(hs).register(http_server) DeactivateAccountRestServlet(hs).register(http_server) PasswordRestServlet(hs).register(http_server) - EmailThreepidRequestTokenRestServlet(hs).register(http_server) - MsisdnThreepidRequestTokenRestServlet(hs).register(http_server) - AddThreepidEmailSubmitTokenServlet(hs).register(http_server) - AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server) - ThreepidRestServlet(hs).register(http_server) - if hs.config.worker.worker_app is None: - ThreepidBindRestServlet(hs).register(http_server) - ThreepidUnbindRestServlet(hs).register(http_server) - if not hs.config.experimental.msc3861.enabled: - ThreepidAddRestServlet(hs).register(http_server) - ThreepidDeleteRestServlet(hs).register(http_server) WhoamiRestServlet(hs).register(http_server) if hs.config.worker.worker_app is None and hs.config.experimental.msc3720_enabled: