mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
36df9c5e36
Behind a configuration flag this adds + to the list of allowed characters in Matrix IDs. The main feature this enables is using full E.164 phone numbers as Matrix IDs.
1055 lines
42 KiB
Python
1055 lines
42 KiB
Python
# Copyright 2014 - 2016 OpenMarket Ltd
|
|
# Copyright 2021 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.
|
|
|
|
"""Contains functions for registering clients."""
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
|
|
|
|
from prometheus_client import Counter
|
|
from typing_extensions import TypedDict
|
|
|
|
from synapse import types
|
|
from synapse.api.constants import (
|
|
MAX_USERID_LENGTH,
|
|
EventContentFields,
|
|
EventTypes,
|
|
JoinRules,
|
|
LoginType,
|
|
)
|
|
from synapse.api.errors import (
|
|
AuthError,
|
|
Codes,
|
|
ConsentNotGivenError,
|
|
InvalidClientTokenError,
|
|
SynapseError,
|
|
)
|
|
from synapse.appservice import ApplicationService
|
|
from synapse.config.server import is_threepid_reserved
|
|
from synapse.handlers.device import DeviceHandler
|
|
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.spam_checker_api import RegistrationBehaviour
|
|
from synapse.types import GUEST_USER_ID_PATTERN, RoomAlias, UserID, create_requester
|
|
from synapse.types.state import StateFilter
|
|
|
|
if TYPE_CHECKING:
|
|
from synapse.server import HomeServer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
registration_counter = Counter(
|
|
"synapse_user_registrations_total",
|
|
"Number of new users registered (since restart)",
|
|
["guest", "shadow_banned", "auth_provider"],
|
|
)
|
|
|
|
login_counter = Counter(
|
|
"synapse_user_logins_total",
|
|
"Number of user logins (since restart)",
|
|
["guest", "auth_provider"],
|
|
)
|
|
|
|
|
|
def init_counters_for_auth_provider(auth_provider_id: str) -> None:
|
|
"""Ensure the prometheus counters for the given auth provider are initialised
|
|
|
|
This fixes a problem where the counters are not reported for a given auth provider
|
|
until the user first logs in/registers.
|
|
"""
|
|
for is_guest in (True, False):
|
|
login_counter.labels(guest=is_guest, auth_provider=auth_provider_id)
|
|
for shadow_banned in (True, False):
|
|
registration_counter.labels(
|
|
guest=is_guest,
|
|
shadow_banned=shadow_banned,
|
|
auth_provider=auth_provider_id,
|
|
)
|
|
|
|
|
|
class LoginDict(TypedDict):
|
|
device_id: str
|
|
access_token: str
|
|
valid_until_ms: Optional[int]
|
|
refresh_token: Optional[str]
|
|
|
|
|
|
class RegistrationHandler:
|
|
def __init__(self, hs: "HomeServer"):
|
|
self.store = hs.get_datastores().main
|
|
self._storage_controllers = hs.get_storage_controllers()
|
|
self.clock = hs.get_clock()
|
|
self.hs = hs
|
|
self.auth = hs.get_auth()
|
|
self.auth_blocking = hs.get_auth_blocking()
|
|
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_identity_handler()
|
|
self.ratelimiter = hs.get_registration_ratelimiter()
|
|
self.macaroon_gen = hs.get_macaroon_generator()
|
|
self._account_validity_handler = hs.get_account_validity_handler()
|
|
self._user_consent_version = self.hs.config.consent.user_consent_version
|
|
self._server_notices_mxid = hs.config.servernotices.server_notices_mxid
|
|
self._server_name = hs.hostname
|
|
|
|
self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker
|
|
|
|
if hs.config.worker.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._register_device_client = self.register_device_inner
|
|
self.pusher_pool = hs.get_pusherpool()
|
|
|
|
self.session_lifetime = hs.config.registration.session_lifetime
|
|
self.nonrefreshable_access_token_lifetime = (
|
|
hs.config.registration.nonrefreshable_access_token_lifetime
|
|
)
|
|
self.refreshable_access_token_lifetime = (
|
|
hs.config.registration.refreshable_access_token_lifetime
|
|
)
|
|
self.refresh_token_lifetime = hs.config.registration.refresh_token_lifetime
|
|
|
|
init_counters_for_auth_provider("")
|
|
|
|
async def check_username(
|
|
self,
|
|
localpart: str,
|
|
guest_access_token: Optional[str] = None,
|
|
assigned_user_id: Optional[str] = None,
|
|
inhibit_user_in_use_error: bool = False,
|
|
) -> None:
|
|
if types.contains_invalid_mxid_characters(
|
|
localpart, self.hs.config.experimental.msc4009_e164_mxids
|
|
):
|
|
extra_chars = (
|
|
"=_-./+" if self.hs.config.experimental.msc4009_e164_mxids else "=_-./"
|
|
)
|
|
raise SynapseError(
|
|
400,
|
|
f"User ID can only contain characters a-z, 0-9, or '{extra_chars}'",
|
|
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 = await self.store.get_users_by_id_case_insensitive(user_id)
|
|
if users:
|
|
if not inhibit_user_in_use_error and not guest_access_token:
|
|
raise SynapseError(
|
|
400, "User ID already taken.", errcode=Codes.USER_IN_USE
|
|
)
|
|
if guest_access_token:
|
|
user_data = await 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,
|
|
)
|
|
|
|
if guest_access_token is None and GUEST_USER_ID_PATTERN.fullmatch(localpart):
|
|
raise SynapseError(
|
|
400,
|
|
"Numeric user IDs are reserved for guest users.",
|
|
errcode=Codes.INVALID_USERNAME,
|
|
)
|
|
|
|
async def register_user(
|
|
self,
|
|
localpart: Optional[str] = None,
|
|
password_hash: Optional[str] = None,
|
|
guest_access_token: Optional[str] = None,
|
|
make_guest: bool = False,
|
|
admin: bool = False,
|
|
threepid: Optional[dict] = None,
|
|
user_type: Optional[str] = None,
|
|
default_display_name: Optional[str] = None,
|
|
address: Optional[str] = None,
|
|
bind_emails: Optional[Iterable[str]] = None,
|
|
by_admin: bool = False,
|
|
user_agent_ips: Optional[List[Tuple[str, str]]] = None,
|
|
auth_provider_id: Optional[str] = None,
|
|
approved: bool = False,
|
|
) -> str:
|
|
"""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_hash: The hashed 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).
|
|
guest_access_token: The access token used when this was a guest
|
|
account.
|
|
make_guest: True if the the new user should be guest,
|
|
false to add a regular user account.
|
|
admin: True if the user should be registered as a server admin.
|
|
threepid: The threepid used for registering, if any.
|
|
user_type: type of user. One of the values from
|
|
api.constants.UserTypes, or None for a normal user.
|
|
default_display_name: if set, the new user's displayname
|
|
will be set to this. Defaults to 'localpart'.
|
|
address: the IP address used to perform the registration.
|
|
bind_emails: list of emails to bind to this account.
|
|
by_admin: True if this registration is being made via the
|
|
admin api, otherwise False.
|
|
user_agent_ips: Tuples of user-agents and IP addresses used
|
|
during the registration process.
|
|
auth_provider_id: The SSO IdP the user used, if any.
|
|
approved: True if the new user should be considered already
|
|
approved by an administrator.
|
|
Returns:
|
|
The registered user_id.
|
|
Raises:
|
|
SynapseError if there was a problem registering.
|
|
"""
|
|
bind_emails = bind_emails or []
|
|
|
|
await self.check_registration_ratelimit(address)
|
|
|
|
result = await self._spam_checker_module_callbacks.check_registration_for_spam(
|
|
threepid,
|
|
localpart,
|
|
user_agent_ips or [],
|
|
auth_provider_id=auth_provider_id,
|
|
)
|
|
|
|
if result == RegistrationBehaviour.DENY:
|
|
logger.info(
|
|
"Blocked registration of %r",
|
|
localpart,
|
|
)
|
|
# We return a 429 to make it not obvious that they've been
|
|
# denied.
|
|
raise SynapseError(429, "Rate limited")
|
|
|
|
shadow_banned = result == RegistrationBehaviour.SHADOW_BAN
|
|
if shadow_banned:
|
|
logger.info(
|
|
"Shadow banning registration of %r",
|
|
localpart,
|
|
)
|
|
|
|
# do not check_auth_blocking if the call is coming through the Admin API
|
|
if not by_admin:
|
|
await self.auth_blocking.check_auth_blocking(threepid=threepid)
|
|
|
|
if localpart is not None:
|
|
await self.check_username(localpart, guest_access_token=guest_access_token)
|
|
|
|
was_guest = guest_access_token is not None
|
|
|
|
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
|
|
|
|
await 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,
|
|
shadow_banned=shadow_banned,
|
|
approved=approved,
|
|
)
|
|
|
|
profile = await self.store.get_profileinfo(localpart)
|
|
await self.user_directory_handler.handle_local_profile_change(
|
|
user_id, profile
|
|
)
|
|
|
|
else:
|
|
# autogen a sequential user ID
|
|
fail_count = 0
|
|
# If a default display name is not given, generate one.
|
|
generate_display_name = default_display_name is None
|
|
# This breaks on successful registration *or* errors after 10 failures.
|
|
while True:
|
|
# Fail after being unable to find a suitable ID a few times
|
|
if fail_count > 10:
|
|
raise SynapseError(500, "Unable to find a suitable guest user ID")
|
|
|
|
generated_localpart = await self.store.generate_user_id()
|
|
user = UserID(generated_localpart, self.hs.hostname)
|
|
user_id = user.to_string()
|
|
self.check_user_id_not_appservice_exclusive(user_id)
|
|
if generate_display_name:
|
|
default_display_name = generated_localpart
|
|
try:
|
|
await 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,
|
|
shadow_banned=shadow_banned,
|
|
)
|
|
|
|
# Successfully registered
|
|
break
|
|
except SynapseError:
|
|
# if user id is taken, just generate another
|
|
fail_count += 1
|
|
|
|
registration_counter.labels(
|
|
guest=make_guest,
|
|
shadow_banned=shadow_banned,
|
|
auth_provider=(auth_provider_id or ""),
|
|
).inc()
|
|
|
|
# If the user does not need to consent at registration, auto-join any
|
|
# configured rooms.
|
|
if not self.hs.config.consent.user_consent_at_registration:
|
|
if (
|
|
not self.hs.config.registration.auto_join_rooms_for_guests
|
|
and make_guest
|
|
):
|
|
logger.info(
|
|
"Skipping auto-join for %s because auto-join for guests is disabled",
|
|
user_id,
|
|
)
|
|
else:
|
|
await 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
|
|
await self._register_email_threepid(user_id, threepid_dict, None)
|
|
|
|
return user_id
|
|
|
|
async def _create_and_join_rooms(self, user_id: str) -> None:
|
|
"""
|
|
Create the auto-join rooms and join or invite the user to them.
|
|
|
|
This should only be called when the first "real" user registers.
|
|
|
|
Args:
|
|
user_id: The user to join
|
|
"""
|
|
# Getting the handlers during init gives a dependency loop.
|
|
room_creation_handler = self.hs.get_room_creation_handler()
|
|
room_member_handler = self.hs.get_room_member_handler()
|
|
|
|
# Generate a stub for how the rooms will be configured.
|
|
stub_config = {
|
|
"preset": self.hs.config.registration.autocreate_auto_join_room_preset,
|
|
}
|
|
|
|
# If the configuration provides a user ID to create rooms with, use
|
|
# that instead of the first user registered.
|
|
requires_join = False
|
|
if self.hs.config.registration.auto_join_user_id:
|
|
fake_requester = create_requester(
|
|
self.hs.config.registration.auto_join_user_id,
|
|
authenticated_entity=self._server_name,
|
|
)
|
|
|
|
# If the room requires an invite, add the user to the list of invites.
|
|
if self.hs.config.registration.auto_join_room_requires_invite:
|
|
stub_config["invite"] = [user_id]
|
|
|
|
# If the room is being created by a different user, the first user
|
|
# registered needs to join it. Note that in the case of an invitation
|
|
# being necessary this will occur after the invite was sent.
|
|
requires_join = True
|
|
else:
|
|
fake_requester = create_requester(
|
|
user_id, authenticated_entity=self._server_name
|
|
)
|
|
|
|
# Choose whether to federate the new room.
|
|
if not self.hs.config.registration.autocreate_auto_join_rooms_federated:
|
|
stub_config["creation_content"] = {EventContentFields.FEDERATE: False}
|
|
|
|
for r in self.hs.config.registration.auto_join_rooms:
|
|
logger.info("Auto-joining %s to %s", user_id, r)
|
|
|
|
try:
|
|
room_alias = RoomAlias.from_string(r)
|
|
|
|
if self.hs.hostname != room_alias.domain:
|
|
# If the alias is remote, try to join the room. This might fail
|
|
# because the room might be invite only, but we don't have any local
|
|
# user in the room to invite this one with, so at this point that's
|
|
# the best we can do.
|
|
logger.info(
|
|
"Cannot automatically create room with alias %s as it isn't"
|
|
" local, trying to join the room instead",
|
|
r,
|
|
)
|
|
|
|
(
|
|
room,
|
|
remote_room_hosts,
|
|
) = await room_member_handler.lookup_room_alias(room_alias)
|
|
room_id = room.to_string()
|
|
|
|
await room_member_handler.update_membership(
|
|
requester=create_requester(
|
|
user_id, authenticated_entity=self._server_name
|
|
),
|
|
target=UserID.from_string(user_id),
|
|
room_id=room_id,
|
|
remote_room_hosts=remote_room_hosts,
|
|
action="join",
|
|
ratelimit=False,
|
|
)
|
|
else:
|
|
# A shallow copy is OK here since the only key that is
|
|
# modified is room_alias_name.
|
|
config = stub_config.copy()
|
|
# create room expects the localpart of the room alias
|
|
config["room_alias_name"] = room_alias.localpart
|
|
|
|
room_id, _, _ = await room_creation_handler.create_room(
|
|
fake_requester,
|
|
config=config,
|
|
ratelimit=False,
|
|
)
|
|
|
|
# If the room does not require an invite, but another user
|
|
# created it, then ensure the first user joins it.
|
|
if requires_join:
|
|
await room_member_handler.update_membership(
|
|
requester=create_requester(
|
|
user_id, authenticated_entity=self._server_name
|
|
),
|
|
target=UserID.from_string(user_id),
|
|
room_id=room_id,
|
|
# Since it was just created, there are no remote hosts.
|
|
remote_room_hosts=[],
|
|
action="join",
|
|
ratelimit=False,
|
|
)
|
|
except Exception as e:
|
|
logger.error("Failed to join new user to %r: %r", r, e)
|
|
|
|
async def _join_rooms(self, user_id: str) -> None:
|
|
"""
|
|
Join or invite the user to the auto-join rooms.
|
|
|
|
Args:
|
|
user_id: The user to join
|
|
"""
|
|
room_member_handler = self.hs.get_room_member_handler()
|
|
|
|
for r in self.hs.config.registration.auto_join_rooms:
|
|
logger.info("Auto-joining %s to %s", user_id, r)
|
|
|
|
try:
|
|
room_alias = RoomAlias.from_string(r)
|
|
|
|
if RoomAlias.is_valid(r):
|
|
(
|
|
room,
|
|
remote_room_hosts,
|
|
) = await room_member_handler.lookup_room_alias(room_alias)
|
|
room_id = room.to_string()
|
|
else:
|
|
raise SynapseError(
|
|
400, "%s was not legal room ID or room alias" % (r,)
|
|
)
|
|
|
|
# Calculate whether the room requires an invite or can be
|
|
# joined directly. By default, we consider the room as requiring an
|
|
# invite if the homeserver is in the room (unless told otherwise by the
|
|
# join rules). Otherwise we consider it as being joinable, at the risk of
|
|
# failing to join, but in this case there's little more we can do since
|
|
# we don't have a local user in the room to craft up an invite with.
|
|
requires_invite = await self.store.is_host_joined(
|
|
room_id,
|
|
self._server_name,
|
|
)
|
|
|
|
if requires_invite:
|
|
# If the server is in the room, check if the room is public.
|
|
state = await self._storage_controllers.state.get_current_state_ids(
|
|
room_id, StateFilter.from_types([(EventTypes.JoinRules, "")])
|
|
)
|
|
|
|
event_id = state.get((EventTypes.JoinRules, ""))
|
|
if event_id:
|
|
join_rules_event = await self.store.get_event(
|
|
event_id, allow_none=True
|
|
)
|
|
if join_rules_event:
|
|
join_rule = join_rules_event.content.get("join_rule", None)
|
|
requires_invite = (
|
|
join_rule and join_rule != JoinRules.PUBLIC
|
|
)
|
|
|
|
# Send the invite, if necessary.
|
|
if requires_invite:
|
|
# If an invite is required, there must be a auto-join user ID.
|
|
assert self.hs.config.registration.auto_join_user_id
|
|
|
|
await room_member_handler.update_membership(
|
|
requester=create_requester(
|
|
self.hs.config.registration.auto_join_user_id,
|
|
authenticated_entity=self._server_name,
|
|
),
|
|
target=UserID.from_string(user_id),
|
|
room_id=room_id,
|
|
remote_room_hosts=remote_room_hosts,
|
|
action="invite",
|
|
ratelimit=False,
|
|
)
|
|
|
|
# Send the join.
|
|
await room_member_handler.update_membership(
|
|
requester=create_requester(
|
|
user_id, authenticated_entity=self._server_name
|
|
),
|
|
target=UserID.from_string(user_id),
|
|
room_id=room_id,
|
|
remote_room_hosts=remote_room_hosts,
|
|
action="join",
|
|
ratelimit=False,
|
|
)
|
|
|
|
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)
|
|
|
|
async def _auto_join_rooms(self, user_id: str) -> None:
|
|
"""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: The user to join
|
|
"""
|
|
# If there are no rooms to auto-join, just bail.
|
|
if not self.hs.config.registration.auto_join_rooms:
|
|
return
|
|
|
|
# auto-join the user to any rooms we're supposed to dump them into
|
|
|
|
# try to create the room if we're the first real user on the server. Note
|
|
# that an auto-generated support or bot user is not a real user and will never be
|
|
# the user to create the room
|
|
should_auto_create_rooms = False
|
|
if (
|
|
self.hs.config.registration.autocreate_auto_join_rooms
|
|
and await self.store.is_real_user(user_id)
|
|
):
|
|
count = await self.store.count_real_users()
|
|
should_auto_create_rooms = count == 1
|
|
|
|
if should_auto_create_rooms:
|
|
await self._create_and_join_rooms(user_id)
|
|
else:
|
|
await self._join_rooms(user_id)
|
|
|
|
async def post_consent_actions(self, user_id: str) -> None:
|
|
"""A series of registration actions that can only be carried out once consent
|
|
has been granted
|
|
|
|
Args:
|
|
user_id: The user to join
|
|
"""
|
|
await self._auto_join_rooms(user_id)
|
|
|
|
async def appservice_register(self, user_localpart: str, as_token: str) -> str:
|
|
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 InvalidClientTokenError()
|
|
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
|
|
|
|
self.check_user_id_not_appservice_exclusive(user_id, allowed_appservice=service)
|
|
|
|
await 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: str, allowed_appservice: Optional[ApplicationService] = None
|
|
) -> 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,
|
|
)
|
|
|
|
async def check_registration_ratelimit(self, address: Optional[str]) -> None:
|
|
"""A simple helper method to check whether the registration rate limit has been hit
|
|
for a given IP address
|
|
|
|
Args:
|
|
address: the IP address used to perform the registration. If this is
|
|
None, no ratelimiting will be performed.
|
|
|
|
Raises:
|
|
LimitExceededError: If the rate limit has been exceeded.
|
|
"""
|
|
if not address:
|
|
return
|
|
|
|
await self.ratelimiter.ratelimit(None, address)
|
|
|
|
async def register_with_store(
|
|
self,
|
|
user_id: str,
|
|
password_hash: Optional[str] = None,
|
|
was_guest: bool = False,
|
|
make_guest: bool = False,
|
|
appservice_id: Optional[str] = None,
|
|
create_profile_with_displayname: Optional[str] = None,
|
|
admin: bool = False,
|
|
user_type: Optional[str] = None,
|
|
address: Optional[str] = None,
|
|
shadow_banned: bool = False,
|
|
approved: bool = False,
|
|
) -> None:
|
|
"""Register user in the datastore.
|
|
|
|
Args:
|
|
user_id: The desired user ID to register.
|
|
password_hash: Optional. The password hash for this user.
|
|
was_guest: Optional. Whether this is a guest account being
|
|
upgraded to a non-guest account.
|
|
make_guest: True if the the new user should be guest,
|
|
false to add a regular user account.
|
|
appservice_id: The ID of the appservice registering the user.
|
|
create_profile_with_displayname: Optionally create a
|
|
profile for the user, setting their displayname to the given value
|
|
admin: is an admin user?
|
|
user_type: type of user. One of the values from
|
|
api.constants.UserTypes, or None for a normal user.
|
|
address: the IP address used to perform the registration.
|
|
shadow_banned: Whether to shadow-ban the user
|
|
approved: Whether to mark the user as approved by an administrator
|
|
"""
|
|
if self.hs.config.worker.worker_app:
|
|
await 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,
|
|
shadow_banned=shadow_banned,
|
|
approved=approved,
|
|
)
|
|
else:
|
|
await 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,
|
|
shadow_banned=shadow_banned,
|
|
approved=approved,
|
|
)
|
|
|
|
# Only call the account validity module(s) on the main process, to avoid
|
|
# repeating e.g. database writes on all of the workers.
|
|
await self._account_validity_handler.on_user_registration(user_id)
|
|
|
|
async def register_device(
|
|
self,
|
|
user_id: str,
|
|
device_id: Optional[str],
|
|
initial_display_name: Optional[str],
|
|
is_guest: bool = False,
|
|
is_appservice_ghost: bool = False,
|
|
auth_provider_id: Optional[str] = None,
|
|
should_issue_refresh_token: bool = False,
|
|
auth_provider_session_id: Optional[str] = None,
|
|
) -> Tuple[str, str, Optional[int], Optional[str]]:
|
|
"""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: full canonical @user:id
|
|
device_id: The device ID to check, or None to generate a new one.
|
|
initial_display_name: An optional display name for the device.
|
|
is_guest: Whether this is a guest account
|
|
auth_provider_id: The SSO IdP the user used, if any.
|
|
should_issue_refresh_token: Whether it should also issue a refresh token
|
|
auth_provider_session_id: The session ID received during login from the SSO IdP.
|
|
Returns:
|
|
Tuple of device ID, access token, access token expiration time and refresh token
|
|
"""
|
|
res = await self._register_device_client(
|
|
user_id=user_id,
|
|
device_id=device_id,
|
|
initial_display_name=initial_display_name,
|
|
is_guest=is_guest,
|
|
is_appservice_ghost=is_appservice_ghost,
|
|
should_issue_refresh_token=should_issue_refresh_token,
|
|
auth_provider_id=auth_provider_id,
|
|
auth_provider_session_id=auth_provider_session_id,
|
|
)
|
|
|
|
login_counter.labels(
|
|
guest=is_guest,
|
|
auth_provider=(auth_provider_id or ""),
|
|
).inc()
|
|
|
|
return (
|
|
res["device_id"],
|
|
res["access_token"],
|
|
res["valid_until_ms"],
|
|
res["refresh_token"],
|
|
)
|
|
|
|
async def register_device_inner(
|
|
self,
|
|
user_id: str,
|
|
device_id: Optional[str],
|
|
initial_display_name: Optional[str],
|
|
is_guest: bool = False,
|
|
is_appservice_ghost: bool = False,
|
|
should_issue_refresh_token: bool = False,
|
|
auth_provider_id: Optional[str] = None,
|
|
auth_provider_session_id: Optional[str] = None,
|
|
) -> LoginDict:
|
|
"""Helper for register_device
|
|
|
|
Does the bits that need doing on the main process. Not for use outside this
|
|
class and RegisterDeviceReplicationServlet.
|
|
"""
|
|
assert not self.hs.config.worker.worker_app
|
|
now_ms = self.clock.time_msec()
|
|
access_token_expiry = None
|
|
if self.session_lifetime is not None:
|
|
if is_guest:
|
|
raise Exception(
|
|
"session_lifetime is not currently implemented for guest access"
|
|
)
|
|
access_token_expiry = now_ms + self.session_lifetime
|
|
|
|
if self.nonrefreshable_access_token_lifetime is not None:
|
|
if access_token_expiry is not None:
|
|
# Don't allow the non-refreshable access token to outlive the
|
|
# session.
|
|
access_token_expiry = min(
|
|
now_ms + self.nonrefreshable_access_token_lifetime,
|
|
access_token_expiry,
|
|
)
|
|
else:
|
|
access_token_expiry = now_ms + self.nonrefreshable_access_token_lifetime
|
|
|
|
refresh_token = None
|
|
refresh_token_id = None
|
|
|
|
# This can only run on the main process.
|
|
assert isinstance(self.device_handler, DeviceHandler)
|
|
|
|
registered_device_id = await self.device_handler.check_device_registered(
|
|
user_id,
|
|
device_id,
|
|
initial_display_name,
|
|
auth_provider_id=auth_provider_id,
|
|
auth_provider_session_id=auth_provider_session_id,
|
|
)
|
|
if is_guest:
|
|
assert access_token_expiry is None
|
|
access_token = self.macaroon_gen.generate_guest_access_token(user_id)
|
|
else:
|
|
if should_issue_refresh_token:
|
|
# A refreshable access token lifetime must be configured
|
|
# since we're told to issue a refresh token (the caller checks
|
|
# that this value is set before setting this flag).
|
|
assert self.refreshable_access_token_lifetime is not None
|
|
|
|
# Set the expiry time of the refreshable access token
|
|
access_token_expiry = now_ms + self.refreshable_access_token_lifetime
|
|
|
|
# Set the refresh token expiry time (if configured)
|
|
refresh_token_expiry = None
|
|
if self.refresh_token_lifetime is not None:
|
|
refresh_token_expiry = now_ms + self.refresh_token_lifetime
|
|
|
|
# Set an ultimate session expiry time (if configured)
|
|
ultimate_session_expiry_ts = None
|
|
if self.session_lifetime is not None:
|
|
ultimate_session_expiry_ts = now_ms + self.session_lifetime
|
|
|
|
# Also ensure that the issued tokens don't outlive the
|
|
# session.
|
|
# (It would be weird to configure a homeserver with a shorter
|
|
# session lifetime than token lifetime, but may as well handle
|
|
# it.)
|
|
access_token_expiry = min(
|
|
access_token_expiry, ultimate_session_expiry_ts
|
|
)
|
|
if refresh_token_expiry is not None:
|
|
refresh_token_expiry = min(
|
|
refresh_token_expiry, ultimate_session_expiry_ts
|
|
)
|
|
|
|
(
|
|
refresh_token,
|
|
refresh_token_id,
|
|
) = await self._auth_handler.create_refresh_token_for_user_id(
|
|
user_id,
|
|
device_id=registered_device_id,
|
|
expiry_ts=refresh_token_expiry,
|
|
ultimate_session_expiry_ts=ultimate_session_expiry_ts,
|
|
)
|
|
|
|
access_token = await self._auth_handler.create_access_token_for_user_id(
|
|
user_id,
|
|
device_id=registered_device_id,
|
|
valid_until_ms=access_token_expiry,
|
|
is_appservice_ghost=is_appservice_ghost,
|
|
refresh_token_id=refresh_token_id,
|
|
)
|
|
|
|
return {
|
|
"device_id": registered_device_id,
|
|
"access_token": access_token,
|
|
"valid_until_ms": access_token_expiry,
|
|
"refresh_token": refresh_token,
|
|
}
|
|
|
|
async def post_registration_actions(
|
|
self, user_id: str, auth_result: dict, access_token: Optional[str]
|
|
) -> None:
|
|
"""A user has completed registration
|
|
|
|
Args:
|
|
user_id: The user ID that consented
|
|
auth_result: The authenticated credentials of the newly registered user.
|
|
access_token: The access token of the newly logged in device, or
|
|
None if `inhibit_login` enabled.
|
|
"""
|
|
# TODO: 3pid registration can actually happen on the workers. Consider
|
|
# refactoring it.
|
|
if self.hs.config.worker.worker_app:
|
|
await self._post_registration_client(
|
|
user_id=user_id, auth_result=auth_result, access_token=access_token
|
|
)
|
|
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.server.mau_limits_reserved_threepids, threepid
|
|
):
|
|
await self.store.upsert_monthly_active_user(user_id)
|
|
|
|
await self._register_email_threepid(user_id, threepid, access_token)
|
|
|
|
if auth_result and LoginType.MSISDN in auth_result:
|
|
threepid = auth_result[LoginType.MSISDN]
|
|
await self._register_msisdn_threepid(user_id, threepid)
|
|
|
|
if auth_result and LoginType.TERMS in auth_result:
|
|
# The terms type should only exist if consent is enabled.
|
|
assert self._user_consent_version is not None
|
|
await self._on_user_consented(user_id, self._user_consent_version)
|
|
|
|
async def _on_user_consented(self, user_id: str, consent_version: str) -> None:
|
|
"""A user consented to the terms on registration
|
|
|
|
Args:
|
|
user_id: The user ID that consented.
|
|
consent_version: version of the policy the user has consented to.
|
|
"""
|
|
logger.info("%s has consented to the privacy policy", user_id)
|
|
await self.store.user_set_consent_version(user_id, consent_version)
|
|
await self.post_consent_actions(user_id)
|
|
|
|
async def _register_email_threepid(
|
|
self, user_id: str, threepid: dict, token: Optional[str]
|
|
) -> None:
|
|
"""Add an email address as a 3pid identifier
|
|
|
|
Also adds an email pusher for the email address, if configured in the
|
|
HS config
|
|
|
|
Must be called on master.
|
|
|
|
Args:
|
|
user_id: id of user
|
|
threepid: m.login.email.identity auth response
|
|
token: access_token for the user, or None if not logged in.
|
|
"""
|
|
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
|
|
|
|
await 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 homeserver)
|
|
if (
|
|
self.hs.config.email.email_enable_notifs
|
|
and self.hs.config.email.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 = await self.store.get_user_by_access_token(token)
|
|
# The token better still exist.
|
|
assert user_tuple
|
|
device_id = user_tuple.device_id
|
|
|
|
await self.pusher_pool.add_or_update_pusher(
|
|
user_id=user_id,
|
|
device_id=device_id,
|
|
kind="email",
|
|
app_id="m.email",
|
|
app_display_name="Email Notifications",
|
|
device_display_name=threepid["address"],
|
|
pushkey=threepid["address"],
|
|
lang=None,
|
|
data={},
|
|
)
|
|
|
|
async def _register_msisdn_threepid(self, user_id: str, threepid: dict) -> None:
|
|
"""Add a phone number as a 3pid identifier
|
|
|
|
Must be called on master.
|
|
|
|
Args:
|
|
user_id: id of user
|
|
threepid: m.login.msisdn auth response
|
|
"""
|
|
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
|
|
|
|
await self._auth_handler.add_threepid(
|
|
user_id,
|
|
threepid["medium"],
|
|
threepid["address"],
|
|
threepid["validated_at"],
|
|
)
|