Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Mainline username generation #121

Merged
merged 5 commits into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/11743.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a config flag to inhibit M_USER_IN_USE during registration.
1 change: 1 addition & 0 deletions changelog.d/11790.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a module callback to set username at registration.
62 changes: 62 additions & 0 deletions docs/modules/password_auth_provider_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,68 @@ device ID), and the (now deactivated) access token.

If multiple modules implement this callback, Synapse runs them all in order.

### `get_username_for_registration`

_First introduced in Synapse v1.52.0_

```python
async def get_username_for_registration(
uia_results: Dict[str, Any],
params: Dict[str, Any],
) -> Optional[str]
```

Called when registering a new user. The module can return a username to set for the user
being registered by returning it as a string, or `None` if it doesn't wish to force a
username for this user. If a username is returned, it will be used as the local part of a
user's full Matrix ID (e.g. it's `alice` in `@alice:example.com`).

This callback is called once [User-Interactive Authentication](https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api)
has been completed by the user. It is not called when registering a user via SSO. It is
passed two dictionaries, which include the information that the user has provided during
the registration process.

The first dictionary contains the results of the [User-Interactive Authentication](https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api)
flow followed by the user. Its keys are the identifiers of every step involved in the flow,
associated with either a boolean value indicating whether the step was correctly completed,
or additional information (e.g. email address, phone number...). A list of most existing
identifiers can be found in the [Matrix specification](https://spec.matrix.org/v1.1/client-server-api/#authentication-types).
Here's an example featuring all currently supported keys:

```python
{
"m.login.dummy": True, # Dummy authentication
"m.login.terms": True, # User has accepted the terms of service for the homeserver
"m.login.recaptcha": True, # User has completed the recaptcha challenge
"m.login.email.identity": { # User has provided and verified an email address
"medium": "email",
"address": "alice@example.com",
"validated_at": 1642701357084,
},
"m.login.msisdn": { # User has provided and verified a phone number
"medium": "msisdn",
"address": "33123456789",
"validated_at": 1642701357084,
},
"org.matrix.msc3231.login.registration_token": "sometoken", # User has registered through the flow described in MSC3231
}
```

The second dictionary contains the parameters provided by the user's client in the request
to `/_matrix/client/v3/register`. See the [Matrix specification](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3register)
for a complete list of these parameters.

If the module cannot, or does not wish to, generate a username for this user, it must
return `None`.

If multiple modules implement this callback, they will be considered in order. If a
callback returns `None`, Synapse falls through to the next one. The value of the first
callback that does not return `None` will be used. If this happens, Synapse will not call
any of the subsequent implementations of this callback. If every callback return `None`,
the username provided by the user is used, if any (otherwise one is automatically
generated).


## Example

The example module below implements authentication checkers for two different login types:
Expand Down
17 changes: 10 additions & 7 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1318,13 +1318,6 @@ oembed:
#
#disable_msisdn_registration: true

# Derive the user's matrix ID from a type of 3PID used when registering.
# This overrides any matrix ID the user proposes when calling /register
# The 3PID type should be present in registrations_require_3pid to avoid
# users failing to register if they don't specify the right kind of 3pid.
#
#register_mxid_from_3pid: email

# Uncomment to set the display name of new users to their email address,
# rather than using the default heuristic.
#
Expand Down Expand Up @@ -1564,6 +1557,16 @@ account_threepid_delegates:
#
#bind_new_user_emails_to_sydent: https://example.com:8091

# Whether to inhibit errors raised when registering a new account if the user ID
# already exists. If turned on, that requests to /register/available will always
# show a user ID as available, and Synapse won't raise an error when starting
# a registration with a user ID that already exists. However, Synapse will still
# raise an error if the registration completes and the username conflicts.
#
# Defaults to false.
#
#inhibit_user_in_use_error: true


## Metrics ###

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def exec_file(path_segments):
# We pin black so that our tests don't start failing on new releases.
CONDITIONAL_REQUIREMENTS["lint"] = [
"isort==5.7.0",
"black==21.6b0",
"black==21.12b0",
"flake8-comprehensions",
"flake8-bugbear==21.3.2",
"flake8",
Expand Down
20 changes: 12 additions & 8 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def read_config(self, config, **kwargs):
"registration_requires_token", False
)
self.registration_shared_secret = config.get("registration_shared_secret")
self.register_mxid_from_3pid = config.get("register_mxid_from_3pid")
self.register_just_use_email_for_display_name = config.get(
"register_just_use_email_for_display_name", False
)
Expand Down Expand Up @@ -189,6 +188,8 @@ def read_config(self, config, **kwargs):
self.bind_new_user_emails_to_sydent.strip("/")
)

self.inhibit_user_in_use_error = config.get("inhibit_user_in_use_error", False)

def generate_config_section(self, generate_secrets=False, **kwargs):
if generate_secrets:
registration_shared_secret = 'registration_shared_secret: "%s"' % (
Expand Down Expand Up @@ -230,13 +231,6 @@ def generate_config_section(self, generate_secrets=False, **kwargs):
#
#disable_msisdn_registration: true

# Derive the user's matrix ID from a type of 3PID used when registering.
# This overrides any matrix ID the user proposes when calling /register
# The 3PID type should be present in registrations_require_3pid to avoid
# users failing to register if they don't specify the right kind of 3pid.
#
#register_mxid_from_3pid: email

# Uncomment to set the display name of new users to their email address,
# rather than using the default heuristic.
#
Expand Down Expand Up @@ -475,6 +469,16 @@ def generate_config_section(self, generate_secrets=False, **kwargs):
# without validation.
#
#bind_new_user_emails_to_sydent: https://example.com:8091

# Whether to inhibit errors raised when registering a new account if the user ID
# already exists. If turned on, that requests to /register/available will always
# show a user ID as available, and Synapse won't raise an error when starting
# a registration with a user ID that already exists. However, Synapse will still
# raise an error if the registration completes and the username conflicts.
#
# Defaults to false.
#
#inhibit_user_in_use_error: true
"""
% locals()
)
Expand Down
58 changes: 58 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,10 @@ def run(*args: Tuple, **kwargs: Dict) -> Awaitable:
Optional[Tuple[str, Optional[Callable[["LoginResponse"], Awaitable[None]]]]]
],
]
GET_USERNAME_FOR_REGISTRATION_CALLBACK = Callable[
[JsonDict, JsonDict],
Awaitable[Optional[str]],
]


class PasswordAuthProvider:
Expand All @@ -1978,6 +1982,9 @@ def __init__(self) -> None:
# lists of callbacks
self.check_3pid_auth_callbacks: List[CHECK_3PID_AUTH_CALLBACK] = []
self.on_logged_out_callbacks: List[ON_LOGGED_OUT_CALLBACK] = []
self.get_username_for_registration_callbacks: List[
GET_USERNAME_FOR_REGISTRATION_CALLBACK
] = []

# Mapping from login type to login parameters
self._supported_login_types: Dict[str, Iterable[str]] = {}
Expand All @@ -1992,6 +1999,9 @@ def register_password_auth_provider_callbacks(
auth_checkers: Optional[
Dict[Tuple[str, Tuple[str, ...]], CHECK_AUTH_CALLBACK]
] = None,
get_username_for_registration: Optional[
GET_USERNAME_FOR_REGISTRATION_CALLBACK
] = None,
) -> None:
# Register check_3pid_auth callback
if check_3pid_auth is not None:
Expand Down Expand Up @@ -2036,6 +2046,11 @@ def register_password_auth_provider_callbacks(
# Add the new method to the list of auth_checker_callbacks for this login type
self.auth_checker_callbacks.setdefault(login_type, []).append(callback)

if get_username_for_registration is not None:
self.get_username_for_registration_callbacks.append(
get_username_for_registration,
)

def get_supported_login_types(self) -> Mapping[str, Iterable[str]]:
"""Get the login types supported by this password provider

Expand Down Expand Up @@ -2191,3 +2206,46 @@ async def on_logged_out(
except Exception as e:
logger.warning("Failed to run module API callback %s: %s", callback, e)
continue

async def get_username_for_registration(
self,
uia_results: JsonDict,
params: JsonDict,
) -> Optional[str]:
"""Defines the username to use when registering the user, using the credentials
and parameters provided during the UIA flow.

Stops at the first callback that returns a string.

Args:
uia_results: The credentials provided during the UIA flow.
params: The parameters provided by the registration request.

Returns:
The localpart to use when registering this user, or None if no module
returned a localpart.
"""
for callback in self.get_username_for_registration_callbacks:
try:
res = await callback(uia_results, params)

if isinstance(res, str):
return res
elif res is not None:
# mypy complains that this line is unreachable because it assumes the
# data returned by the module fits the expected type. We just want
# to make sure this is the case.
logger.warning( # type: ignore[unreachable]
"Ignoring non-string value returned by"
" get_username_for_registration callback %s: %s",
callback,
res,
)
except Exception as e:
logger.error(
"Module raised an exception in get_username_for_registration: %s",
e,
)
raise SynapseError(code=500, msg="Internal Server Error")

return None
28 changes: 14 additions & 14 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ async def check_username(
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):
raise SynapseError(
Expand Down Expand Up @@ -171,23 +172,22 @@ async def check_username(

users = await self.store.get_users_by_id_case_insensitive(user_id)
if users:
if not guest_access_token:
if not inhibit_user_in_use_error and not guest_access_token:
raise SynapseError(
400, "User ID already taken.", errcode=Codes.USER_IN_USE
)

# Retrieve guest user information from provided access token
user_data = await self.auth.get_user_by_access_token(guest_access_token)
if (
not user_data.is_guest
or UserID.from_string(user_data.user_id).localpart != localpart
):
raise AuthError(
403,
"Cannot register taken user ID without valid guest "
"credentials for that user.",
errcode=Codes.FORBIDDEN,
)
if guest_access_token:
user_data = await self.auth.get_user_by_access_token(guest_access_token)
if (
not user_data.is_guest
or UserID.from_string(user_data.user_id).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:
try:
Expand Down
1 change: 0 additions & 1 deletion synapse/logging/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
def get_thread_resource_usage() -> "Optional[resource.struct_rusage]":
return resource.getrusage(RUSAGE_THREAD)


except Exception:
# If the system doesn't support resource.getrusage(RUSAGE_THREAD) then we
# won't track resource usage.
Expand Down
22 changes: 22 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from synapse.handlers.auth import (
CHECK_3PID_AUTH_CALLBACK,
CHECK_AUTH_CALLBACK,
GET_USERNAME_FOR_REGISTRATION_CALLBACK,
ON_LOGGED_OUT_CALLBACK,
AuthHandler,
)
Expand Down Expand Up @@ -163,6 +164,7 @@ def __init__(self, hs: "HomeServer", auth_handler: AuthHandler) -> None:
self._presence_stream = hs.get_event_sources().sources.presence
self._state = hs.get_state_handler()
self._clock: Clock = hs.get_clock()
self._registration_handler = hs.get_registration_handler()
self._send_email_handler = hs.get_send_email_handler()
self.custom_template_dir = hs.config.server.custom_template_directory

Expand Down Expand Up @@ -296,6 +298,9 @@ def register_password_auth_provider_callbacks(
auth_checkers: Optional[
Dict[Tuple[str, Tuple[str, ...]], CHECK_AUTH_CALLBACK]
] = None,
get_username_for_registration: Optional[
GET_USERNAME_FOR_REGISTRATION_CALLBACK
] = None,
) -> None:
"""Registers callbacks for password auth provider capabilities.

Expand All @@ -305,6 +310,7 @@ def register_password_auth_provider_callbacks(
check_3pid_auth=check_3pid_auth,
on_logged_out=on_logged_out,
auth_checkers=auth_checkers,
get_username_for_registration=get_username_for_registration,
)

def register_web_resource(self, path: str, resource: Resource):
Expand Down Expand Up @@ -1124,6 +1130,22 @@ async def get_room_state(

return {key: state_events[event_id] for key, event_id in state_ids.items()}

async def check_username(self, username: str) -> None:
"""Checks if the provided username uses the grammar defined in the Matrix
specification, and is already being used by an existing user.

Added in Synapse v1.52.0.

Args:
username: The username to check. This is the local part of the user's full
Matrix user ID, i.e. it's "alice" if the full user ID is "@alice:foo.com".

Raises:
SynapseError with the errcode "M_USER_IN_USE" if the username is already in
use.
"""
await self._registration_handler.check_username(username)


class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room
Expand Down
Loading