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

Commit

Permalink
Merge pull request #126 from matrix-org/babolivier/displayname_reg
Browse files Browse the repository at this point in the history
Allow modules to set a display name on registration
  • Loading branch information
babolivier authored Mar 23, 2022
2 parents f20caae + fd8bdbf commit 816ae75
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 130 deletions.
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

0 comments on commit 816ae75

Please sign in to comment.