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

Allow modules to set a display name on registration #126

Merged
merged 5 commits into from
Mar 23, 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/11927.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use the proper type for the Content-Length header in the `UploadResource`.
1 change: 1 addition & 0 deletions changelog.d/12009.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enable modules to set a custom display name when registering a user.
1 change: 1 addition & 0 deletions changelog.d/12025.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update the `olddeps` CI job to use an old version of `markupsafe`.
35 changes: 31 additions & 4 deletions docs/modules/password_auth_provider_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ If the authentication is unsuccessful, the module 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`,
any of the subsequent implementations of this callback. If every callback returns `None`,
the authentication is denied.

### `on_logged_out`
Expand Down Expand Up @@ -162,10 +162,38 @@ 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`,
any of the subsequent implementations of this callback. If every callback returns `None`,
the username provided by the user is used, if any (otherwise one is automatically
generated).

### `get_displayname_for_registration`

_First introduced in Synapse v1.54.0_

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

Called when registering a new user. The module can return a display name to set for the
user being registered by returning it as a string, or `None` if it doesn't wish to force a
display name for this user.

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. These dictionaries are identical to the ones passed to
[`get_username_for_registration`](#get_username_for_registration), so refer to the
documentation of this callback for more information about them.

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 returns `None`,
the username will be used (e.g. `alice` if the user being registered is `@alice:example.com`).

## `is_3pid_allowed`

_First introduced in Synapse v1.53.0_
Expand Down Expand Up @@ -194,8 +222,7 @@ The example module below implements authentication checkers for two different lo
- Is checked by the method: `self.check_my_login`
- `m.login.password` (defined in [the spec](https://matrix.org/docs/spec/client_server/latest#password-based))
- Expects a `password` field to be sent to `/login`
- Is checked by the method: `self.check_pass`

- Is checked by the method: `self.check_pass`

```python
from typing import Awaitable, Callable, Optional, Tuple
Expand Down
3 changes: 0 additions & 3 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ def read_config(self, config, **kwargs):
"registration_requires_token", False
)
self.registration_shared_secret = config.get("registration_shared_secret")
self.register_just_use_email_for_display_name = config.get(
"register_just_use_email_for_display_name", False
)

self.bcrypt_rounds = config.get("bcrypt_rounds", 12)

Expand Down
58 changes: 58 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2064,6 +2064,10 @@ def run(*args: Tuple, **kwargs: Dict) -> Awaitable:
[JsonDict, JsonDict],
Awaitable[Optional[str]],
]
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK = Callable[
[JsonDict, JsonDict],
Awaitable[Optional[str]],
]
IS_3PID_ALLOWED_CALLBACK = Callable[[str, str, bool], Awaitable[bool]]


Expand All @@ -2080,6 +2084,9 @@ def __init__(self) -> None:
self.get_username_for_registration_callbacks: List[
GET_USERNAME_FOR_REGISTRATION_CALLBACK
] = []
self.get_displayname_for_registration_callbacks: List[
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK
] = []
self.is_3pid_allowed_callbacks: List[IS_3PID_ALLOWED_CALLBACK] = []

# Mapping from login type to login parameters
Expand All @@ -2099,6 +2106,9 @@ def register_password_auth_provider_callbacks(
get_username_for_registration: Optional[
GET_USERNAME_FOR_REGISTRATION_CALLBACK
] = None,
get_displayname_for_registration: Optional[
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK
] = None,
) -> None:
# Register check_3pid_auth callback
if check_3pid_auth is not None:
Expand Down Expand Up @@ -2148,6 +2158,11 @@ def register_password_auth_provider_callbacks(
get_username_for_registration,
)

if get_displayname_for_registration is not None:
self.get_displayname_for_registration_callbacks.append(
get_displayname_for_registration,
)

if is_3pid_allowed is not None:
self.is_3pid_allowed_callbacks.append(is_3pid_allowed)

Expand Down Expand Up @@ -2350,6 +2365,49 @@ async def get_username_for_registration(

return None

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

Stops at the first callback that returns a tuple containing at least one string.

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

Returns:
A tuple which first element is the display name, and the second is an MXC URL
to the user's avatar.
"""
for callback in self.get_displayname_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_displayname_for_registration callback %s: %s",
callback,
res,
)
except Exception as e:
logger.error(
"Module raised an exception in get_displayname_for_registration: %s",
e,
)
raise SynapseError(code=500, msg="Internal Server Error")

return None

async def is_3pid_allowed(
self,
medium: str,
Expand Down
5 changes: 5 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from synapse.handlers.auth import (
CHECK_3PID_AUTH_CALLBACK,
CHECK_AUTH_CALLBACK,
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK,
GET_USERNAME_FOR_REGISTRATION_CALLBACK,
IS_3PID_ALLOWED_CALLBACK,
ON_LOGGED_OUT_CALLBACK,
Expand Down Expand Up @@ -317,6 +318,9 @@ def register_password_auth_provider_callbacks(
get_username_for_registration: Optional[
GET_USERNAME_FOR_REGISTRATION_CALLBACK
] = None,
get_displayname_for_registration: Optional[
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK
] = None,
) -> None:
"""Registers callbacks for password auth provider capabilities.

Expand All @@ -328,6 +332,7 @@ def register_password_auth_provider_callbacks(
is_3pid_allowed=is_3pid_allowed,
auth_checkers=auth_checkers,
get_username_for_registration=get_username_for_registration,
get_displayname_for_registration=get_displayname_for_registration,
)

def register_background_update_controller_callbacks(
Expand Down
74 changes: 6 additions & 68 deletions synapse/rest/client/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,26 +695,18 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
session_id
)

# TODO: This won't be needed anymore once https://github.com/matrix-org/matrix-dinsic/issues/793
# is resolved.
desired_display_name = body.get("display_name")
if auth_result:
if LoginType.EMAIL_IDENTITY in auth_result:
address = auth_result[LoginType.EMAIL_IDENTITY]["address"]
if (
self.hs.config.registration.register_just_use_email_for_display_name
):
desired_display_name = address
else:
# Custom mapping between email address and display name
desired_display_name = _map_email_to_displayname(address)
display_name = await (
self.password_auth_provider.get_displayname_for_registration(
auth_result, params
)
)

registered_user_id = await self.registration_handler.register_user(
localpart=desired_username,
password_hash=password_hash,
guest_access_token=guest_access_token,
default_display_name=desired_display_name,
threepid=threepid,
default_display_name=display_name,
address=client_addr,
user_agent_ips=entries,
)
Expand Down Expand Up @@ -876,60 +868,6 @@ async def _do_guest_registration(
return 200, result


def cap(name: str) -> str:
"""Capitalise parts of a name containing different words, including those
separated by hyphens.
For example, 'John-Doe'

Args:
The name to parse
"""
if not name:
return name

# Split the name by whitespace then hyphens, capitalizing each part then
# joining it back together.
capatilized_name = " ".join(
"-".join(part.capitalize() for part in space_part.split("-"))
for space_part in name.split()
)
return capatilized_name


def _map_email_to_displayname(address: str) -> str:
"""Custom mapping from an email address to a user displayname

Args:
address: The email address to process
Returns:
The new displayname
"""
# Split the part before and after the @ in the email.
# Replace all . with spaces in the first part
parts = address.replace(".", " ").split("@")

# Figure out which org this email address belongs to
org_parts = parts[1].split(" ")

# If this is a ...matrix.org email, mark them as an Admin
if org_parts[-2] == "matrix" and org_parts[-1] == "org":
org = "Tchap Admin"

# Is this is a ...gouv.fr address, set the org to whatever is before
# gouv.fr. If there isn't anything (a @gouv.fr email) simply mark their
# org as "gouv"
elif org_parts[-2] == "gouv" and org_parts[-1] == "fr":
org = org_parts[-3] if len(org_parts) > 2 else org_parts[-2]

# Otherwise, mark their org as the email's second-level domain name
else:
org = org_parts[-2]

desired_display_name = cap(parts[0]) + " [" + cap(org) + "]"

return desired_display_name


def _calculate_registration_flows(
config: HomeServerConfig, auth_handler: AuthHandler
) -> List[List[str]]:
Expand Down
13 changes: 9 additions & 4 deletions synapse/rest/media/v1/upload_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ async def _async_render_OPTIONS(self, request: SynapseRequest) -> None:

async def _async_render_POST(self, request: SynapseRequest) -> None:
requester = await self.auth.get_user_by_req(request)
content_length = request.getHeader("Content-Length")
if content_length is None:
raw_content_length = request.getHeader("Content-Length")
if raw_content_length is None:
raise SynapseError(msg="Request must specify a Content-Length", code=400)
if int(content_length) > self.max_upload_size:
try:
content_length = int(raw_content_length)
except ValueError:
raise SynapseError(msg="Content-Length value is invalid", code=400)
if content_length > self.max_upload_size:
raise SynapseError(
msg="Upload request body is too large",
code=413,
Expand All @@ -66,7 +70,8 @@ async def _async_render_POST(self, request: SynapseRequest) -> None:
upload_name: Optional[str] = upload_name_bytes.decode("utf8")
except UnicodeDecodeError:
raise SynapseError(
msg="Invalid UTF-8 filename parameter: %r" % (upload_name), code=400
msg="Invalid UTF-8 filename parameter: %r" % (upload_name_bytes,),
code=400,
)

# If the name is falsey (e.g. an empty byte string) ensure it is None.
Expand Down
Loading