# -*- coding: utf-8 -*- # 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. """Contains functions for registering clients.""" import logging from twisted.internet import defer from synapse import types from synapse.api.constants import MAX_USERID_LENGTH, LoginType from synapse.api.errors import ( AuthError, Codes, ConsentNotGivenError, LimitExceededError, RegistrationError, SynapseError, ) from synapse.config.server import is_threepid_reserved from synapse.http.servlet import assert_params_in_dict from synapse.replication.http.login import RegisterDeviceReplicationServlet from synapse.replication.http.register import ( ReplicationPostRegisterActionsServlet, ReplicationRegisterServlet, ) from synapse.types import RoomAlias, RoomID, UserID, create_requester from synapse.util.async_helpers import Linearizer from ._base import BaseHandler logger = logging.getLogger(__name__) class RegistrationHandler(BaseHandler): def __init__(self, hs): """ Args: hs (synapse.server.HomeServer): """ super(RegistrationHandler, self).__init__(hs) self.hs = hs self.auth = hs.get_auth() self._auth_handler = hs.get_auth_handler() self.profile_handler = hs.get_profile_handler() self.user_directory_handler = hs.get_user_directory_handler() self.identity_handler = self.hs.get_handlers().identity_handler self.ratelimiter = hs.get_registration_ratelimiter() self._next_generated_user_id = None self.macaroon_gen = hs.get_macaroon_generator() self._generate_user_id_linearizer = Linearizer( name="_generate_user_id_linearizer" ) self._server_notices_mxid = hs.config.server_notices_mxid if hs.config.worker_app: self._register_client = ReplicationRegisterServlet.make_client(hs) self._register_device_client = RegisterDeviceReplicationServlet.make_client( hs ) self._post_registration_client = ReplicationPostRegisterActionsServlet.make_client( hs ) else: self.device_handler = hs.get_device_handler() self.pusher_pool = hs.get_pusherpool() self.session_lifetime = hs.config.session_lifetime @defer.inlineCallbacks def check_username(self, localpart, guest_access_token=None, assigned_user_id=None): if types.contains_invalid_mxid_characters(localpart): raise SynapseError( 400, "User ID can only contain characters a-z, 0-9, or '=_-./'", Codes.INVALID_USERNAME, ) if not localpart: raise SynapseError(400, "User ID cannot be empty", Codes.INVALID_USERNAME) if localpart[0] == "_": raise SynapseError( 400, "User ID may not begin with _", Codes.INVALID_USERNAME ) user = UserID(localpart, self.hs.hostname) user_id = user.to_string() if assigned_user_id: if user_id == assigned_user_id: return else: raise SynapseError( 400, "A different user ID has already been registered for this session", ) self.check_user_id_not_appservice_exclusive(user_id) if len(user_id) > MAX_USERID_LENGTH: raise SynapseError( 400, "User ID may not be longer than %s characters" % (MAX_USERID_LENGTH,), Codes.INVALID_USERNAME, ) users = yield self.store.get_users_by_id_case_insensitive(user_id) if users: if not guest_access_token: raise SynapseError( 400, "User ID already taken.", errcode=Codes.USER_IN_USE ) user_data = yield self.auth.get_user_by_access_token(guest_access_token) if not user_data["is_guest"] or user_data["user"].localpart != localpart: raise AuthError( 403, "Cannot register taken user ID without valid guest " "credentials for that user.", errcode=Codes.FORBIDDEN, ) @defer.inlineCallbacks def register_user( self, localpart=None, password=None, guest_access_token=None, make_guest=False, admin=False, threepid=None, user_type=None, default_display_name=None, address=None, bind_emails=[], ): """Registers a new client on the server. Args: localpart : The local part of the user ID to register. If None, one will be generated. password (unicode) : The password to assign to this user so they can login again. This can be None which means they cannot login again via a password (e.g. the user is an application service user). user_type (str|None): type of user. One of the values from api.constants.UserTypes, or None for a normal user. default_display_name (unicode|None): if set, the new user's displayname will be set to this. Defaults to 'localpart'. address (str|None): the IP address used to perform the registration. bind_emails (List[str]): list of emails to bind to this account. Returns: Deferred[str]: user_id Raises: RegistrationError if there was a problem registering. """ yield self.auth.check_auth_blocking(threepid=threepid) password_hash = None if password: password_hash = yield self._auth_handler.hash(password) if localpart: yield self.check_username(localpart, guest_access_token=guest_access_token) was_guest = guest_access_token is not None if not was_guest: try: int(localpart) raise RegistrationError( 400, "Numeric user IDs are reserved for guest users." ) except ValueError: pass user = UserID(localpart, self.hs.hostname) user_id = user.to_string() if was_guest: # If the user was a guest then they already have a profile default_display_name = None elif default_display_name is None: default_display_name = localpart yield self.register_with_store( user_id=user_id, password_hash=password_hash, was_guest=was_guest, make_guest=make_guest, create_profile_with_displayname=default_display_name, admin=admin, user_type=user_type, address=address, ) if self.hs.config.user_directory_search_all_users: profile = yield self.store.get_profileinfo(localpart) yield self.user_directory_handler.handle_local_profile_change( user_id, profile ) else: # autogen a sequential user ID attempts = 0 user = None while not user: localpart = yield self._generate_user_id(attempts > 0) user = UserID(localpart, self.hs.hostname) user_id = user.to_string() yield self.check_user_id_not_appservice_exclusive(user_id) if default_display_name is None: default_display_name = localpart try: yield self.register_with_store( user_id=user_id, password_hash=password_hash, make_guest=make_guest, create_profile_with_displayname=default_display_name, address=address, ) except SynapseError: # if user id is taken, just generate another user = None user_id = None attempts += 1 if not self.hs.config.user_consent_at_registration: yield self._auto_join_rooms(user_id) else: logger.info( "Skipping auto-join for %s because consent is required at registration", user_id, ) # Bind any specified emails to this account current_time = self.hs.get_clock().time_msec() for email in bind_emails: # generate threepid dict threepid_dict = { "medium": "email", "address": email, "validated_at": current_time, } # Bind email to new account yield self._register_email_threepid(user_id, threepid_dict, None, False) return user_id @defer.inlineCallbacks def _auto_join_rooms(self, user_id): """Automatically joins users to auto join rooms - creating the room in the first place if the user is the first to be created. Args: user_id(str): The user to join """ # auto-join the user to any rooms we're supposed to dump them into fake_requester = create_requester(user_id) # try to create the room if we're the first real user on the server. Note # that an auto-generated support user is not a real user and will never be # the user to create the room should_auto_create_rooms = False is_support = yield self.store.is_support_user(user_id) # There is an edge case where the first user is the support user, then # the room is never created, though this seems unlikely and # recoverable from given the support user being involved in the first # place. if self.hs.config.autocreate_auto_join_rooms and not is_support: count = yield self.store.count_all_users() should_auto_create_rooms = count == 1 for r in self.hs.config.auto_join_rooms: logger.info("Auto-joining %s to %s", user_id, r) try: if should_auto_create_rooms: room_alias = RoomAlias.from_string(r) if self.hs.hostname != room_alias.domain: logger.warning( "Cannot create room alias %s, " "it does not match server domain", r, ) else: # create room expects the localpart of the room alias room_alias_localpart = room_alias.localpart # getting the RoomCreationHandler during init gives a dependency # loop yield self.hs.get_room_creation_handler().create_room( fake_requester, config={ "preset": "public_chat", "room_alias_name": room_alias_localpart, }, ratelimit=False, ) else: yield self._join_user_to_room(fake_requester, r) except ConsentNotGivenError as e: # Technically not necessary to pull out this error though # moving away from bare excepts is a good thing to do. logger.error("Failed to join new user to %r: %r", r, e) except Exception as e: logger.error("Failed to join new user to %r: %r", r, e) @defer.inlineCallbacks def post_consent_actions(self, user_id): """A series of registration actions that can only be carried out once consent has been granted Args: user_id (str): The user to join """ yield self._auto_join_rooms(user_id) @defer.inlineCallbacks def appservice_register(self, user_localpart, as_token): user = UserID(user_localpart, self.hs.hostname) user_id = user.to_string() service = self.store.get_app_service_by_token(as_token) if not service: raise AuthError(403, "Invalid application service token.") if not service.is_interested_in_user(user_id): raise SynapseError( 400, "Invalid user localpart for this application service.", errcode=Codes.EXCLUSIVE, ) service_id = service.id if service.is_exclusive_user(user_id) else None yield self.check_user_id_not_appservice_exclusive( user_id, allowed_appservice=service ) yield self.register_with_store( user_id=user_id, password_hash="", appservice_id=service_id, create_profile_with_displayname=user.localpart, ) return user_id def check_user_id_not_appservice_exclusive(self, user_id, allowed_appservice=None): # don't allow people to register the server notices mxid if self._server_notices_mxid is not None: if user_id == self._server_notices_mxid: raise SynapseError( 400, "This user ID is reserved.", errcode=Codes.EXCLUSIVE ) # valid user IDs must not clash with any user ID namespaces claimed by # application services. services = self.store.get_app_services() interested_services = [ s for s in services if s.is_interested_in_user(user_id) and s != allowed_appservice ] for service in interested_services: if service.is_exclusive_user(user_id): raise SynapseError( 400, "This user ID is reserved by an application service.", errcode=Codes.EXCLUSIVE, ) @defer.inlineCallbacks def _generate_user_id(self, reseed=False): if reseed or self._next_generated_user_id is None: with (yield self._generate_user_id_linearizer.queue(())): if reseed or self._next_generated_user_id is None: self._next_generated_user_id = ( yield self.store.find_next_generated_user_id_localpart() ) id = self._next_generated_user_id self._next_generated_user_id += 1 return str(id) @defer.inlineCallbacks def _join_user_to_room(self, requester, room_identifier): room_member_handler = self.hs.get_room_member_handler() if RoomID.is_valid(room_identifier): room_id = room_identifier elif RoomAlias.is_valid(room_identifier): room_alias = RoomAlias.from_string(room_identifier) room_id, remote_room_hosts = ( yield room_member_handler.lookup_room_alias(room_alias) ) room_id = room_id.to_string() else: raise SynapseError( 400, "%s was not legal room ID or room alias" % (room_identifier,) ) yield room_member_handler.update_membership( requester=requester, target=requester.user, room_id=room_id, remote_room_hosts=remote_room_hosts, action="join", ratelimit=False, ) def register_with_store( self, user_id, password_hash=None, was_guest=False, make_guest=False, appservice_id=None, create_profile_with_displayname=None, admin=False, user_type=None, address=None, ): """Register user in the datastore. Args: user_id (str): The desired user ID to register. password_hash (str|None): Optional. The password hash for this user. was_guest (bool): Optional. Whether this is a guest account being upgraded to a non-guest account. make_guest (boolean): True if the the new user should be guest, false to add a regular user account. appservice_id (str|None): The ID of the appservice registering the user. create_profile_with_displayname (unicode|None): Optionally create a profile for the user, setting their displayname to the given value admin (boolean): is an admin user? user_type (str|None): type of user. One of the values from api.constants.UserTypes, or None for a normal user. address (str|None): the IP address used to perform the registration. Returns: Deferred """ # Don't rate limit for app services if appservice_id is None and address is not None: time_now = self.clock.time() allowed, time_allowed = self.ratelimiter.can_do_action( address, time_now_s=time_now, rate_hz=self.hs.config.rc_registration.per_second, burst_count=self.hs.config.rc_registration.burst_count, ) if not allowed: raise LimitExceededError( retry_after_ms=int(1000 * (time_allowed - time_now)) ) if self.hs.config.worker_app: return self._register_client( user_id=user_id, password_hash=password_hash, was_guest=was_guest, make_guest=make_guest, appservice_id=appservice_id, create_profile_with_displayname=create_profile_with_displayname, admin=admin, user_type=user_type, address=address, ) else: return self.store.register_user( user_id=user_id, password_hash=password_hash, was_guest=was_guest, make_guest=make_guest, appservice_id=appservice_id, create_profile_with_displayname=create_profile_with_displayname, admin=admin, user_type=user_type, ) @defer.inlineCallbacks def register_device(self, user_id, device_id, initial_display_name, is_guest=False): """Register a device for a user and generate an access token. The access token will be limited by the homeserver's session_lifetime config. Args: user_id (str): full canonical @user:id device_id (str|None): The device ID to check, or None to generate a new one. initial_display_name (str|None): An optional display name for the device. is_guest (bool): Whether this is a guest account Returns: defer.Deferred[tuple[str, str]]: Tuple of device ID and access token """ if self.hs.config.worker_app: r = yield self._register_device_client( user_id=user_id, device_id=device_id, initial_display_name=initial_display_name, is_guest=is_guest, ) return r["device_id"], r["access_token"] valid_until_ms = None if self.session_lifetime is not None: if is_guest: raise Exception( "session_lifetime is not currently implemented for guest access" ) valid_until_ms = self.clock.time_msec() + self.session_lifetime device_id = yield self.device_handler.check_device_registered( user_id, device_id, initial_display_name ) if is_guest: assert valid_until_ms is None access_token = self.macaroon_gen.generate_access_token( user_id, ["guest = true"] ) else: access_token = yield self._auth_handler.get_access_token_for_user_id( user_id, device_id=device_id, valid_until_ms=valid_until_ms ) return (device_id, access_token) @defer.inlineCallbacks def post_registration_actions( self, user_id, auth_result, access_token, bind_email, bind_msisdn ): """A user has completed registration Args: user_id (str): The user ID that consented auth_result (dict): The authenticated credentials of the newly registered user. access_token (str|None): The access token of the newly logged in device, or None if `inhibit_login` enabled. bind_email (bool): Whether to bind the email with the identity server. bind_msisdn (bool): Whether to bind the msisdn with the identity server. """ if self.hs.config.worker_app: yield self._post_registration_client( user_id=user_id, auth_result=auth_result, access_token=access_token, bind_email=bind_email, bind_msisdn=bind_msisdn, ) return if auth_result and LoginType.EMAIL_IDENTITY in auth_result: threepid = auth_result[LoginType.EMAIL_IDENTITY] # Necessary due to auth checks prior to the threepid being # written to the db if is_threepid_reserved( self.hs.config.mau_limits_reserved_threepids, threepid ): yield self.store.upsert_monthly_active_user(user_id) yield self._register_email_threepid( user_id, threepid, access_token, bind_email ) if auth_result and LoginType.MSISDN in auth_result: threepid = auth_result[LoginType.MSISDN] yield self._register_msisdn_threepid(user_id, threepid, bind_msisdn) if auth_result and LoginType.TERMS in auth_result: yield self._on_user_consented(user_id, self.hs.config.user_consent_version) @defer.inlineCallbacks def _on_user_consented(self, user_id, consent_version): """A user consented to the terms on registration Args: user_id (str): The user ID that consented. consent_version (str): version of the policy the user has consented to. """ logger.info("%s has consented to the privacy policy", user_id) yield self.store.user_set_consent_version(user_id, consent_version) yield self.post_consent_actions(user_id) @defer.inlineCallbacks def _register_email_threepid(self, user_id, threepid, token, bind_email): """Add an email address as a 3pid identifier Also adds an email pusher for the email address, if configured in the HS config Also optionally binds emails to the given user_id on the identity server Must be called on master. Args: user_id (str): id of user threepid (object): m.login.email.identity auth response token (str|None): access_token for the user, or None if not logged in. bind_email (bool): true if the client requested the email to be bound at the identity server Returns: defer.Deferred: """ reqd = ("medium", "address", "validated_at") if any(x not in threepid for x in reqd): # This will only happen if the ID server returns a malformed response logger.info("Can't add incomplete 3pid") return yield self._auth_handler.add_threepid( user_id, threepid["medium"], threepid["address"], threepid["validated_at"] ) # And we add an email pusher for them by default, but only # if email notifications are enabled (so people don't start # getting mail spam where they weren't before if email # notifs are set up on a home server) if ( self.hs.config.email_enable_notifs and self.hs.config.email_notif_for_new_users and token ): # Pull the ID of the access token back out of the db # It would really make more sense for this to be passed # up when the access token is saved, but that's quite an # invasive change I'd rather do separately. user_tuple = yield self.store.get_user_by_access_token(token) token_id = user_tuple["token_id"] yield self.pusher_pool.add_pusher( user_id=user_id, access_token=token_id, kind="email", app_id="m.email", app_display_name="Email Notifications", device_display_name=threepid["address"], pushkey=threepid["address"], lang=None, # We don't know a user's language here data={}, ) if bind_email: logger.info("bind_email specified: binding") logger.debug("Binding emails %s to %s" % (threepid, user_id)) yield self.identity_handler.bind_threepid( threepid["threepid_creds"], user_id ) else: logger.info("bind_email not specified: not binding email") @defer.inlineCallbacks def _register_msisdn_threepid(self, user_id, threepid, bind_msisdn): """Add a phone number as a 3pid identifier Also optionally binds msisdn to the given user_id on the identity server Must be called on master. Args: user_id (str): id of user threepid (object): m.login.msisdn auth response bind_msisdn (bool): true if the client requested the msisdn to be bound at the identity server Returns: defer.Deferred: """ try: assert_params_in_dict(threepid, ["medium", "address", "validated_at"]) except SynapseError as ex: if ex.errcode == Codes.MISSING_PARAM: # This will only happen if the ID server returns a malformed response logger.info("Can't add incomplete 3pid") return None raise yield self._auth_handler.add_threepid( user_id, threepid["medium"], threepid["address"], threepid["validated_at"] ) if bind_msisdn: logger.info("bind_msisdn specified: binding") logger.debug("Binding msisdn %s to %s", threepid, user_id) yield self.identity_handler.bind_threepid( threepid["threepid_creds"], user_id ) else: logger.info("bind_msisdn not specified: not binding msisdn")