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

Commit 816ae75

Browse files
authored
Merge pull request #126 from matrix-org/babolivier/displayname_reg
Allow modules to set a display name on registration
2 parents f20caae + fd8bdbf commit 816ae75

File tree

12 files changed

+209
-130
lines changed

12 files changed

+209
-130
lines changed

changelog.d/11927.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use the proper type for the Content-Length header in the `UploadResource`.

changelog.d/12009.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Enable modules to set a custom display name when registering a user.

changelog.d/12025.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update the `olddeps` CI job to use an old version of `markupsafe`.

docs/modules/password_auth_provider_callbacks.md

+31-4
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ If the authentication is unsuccessful, the module must return `None`.
8585
If multiple modules implement this callback, they will be considered in order. If a
8686
callback returns `None`, Synapse falls through to the next one. The value of the first
8787
callback that does not return `None` will be used. If this happens, Synapse will not call
88-
any of the subsequent implementations of this callback. If every callback return `None`,
88+
any of the subsequent implementations of this callback. If every callback returns `None`,
8989
the authentication is denied.
9090

9191
### `on_logged_out`
@@ -162,10 +162,38 @@ return `None`.
162162
If multiple modules implement this callback, they will be considered in order. If a
163163
callback returns `None`, Synapse falls through to the next one. The value of the first
164164
callback that does not return `None` will be used. If this happens, Synapse will not call
165-
any of the subsequent implementations of this callback. If every callback return `None`,
165+
any of the subsequent implementations of this callback. If every callback returns `None`,
166166
the username provided by the user is used, if any (otherwise one is automatically
167167
generated).
168168

169+
### `get_displayname_for_registration`
170+
171+
_First introduced in Synapse v1.54.0_
172+
173+
```python
174+
async def get_displayname_for_registration(
175+
uia_results: Dict[str, Any],
176+
params: Dict[str, Any],
177+
) -> Optional[str]
178+
```
179+
180+
Called when registering a new user. The module can return a display name to set for the
181+
user being registered by returning it as a string, or `None` if it doesn't wish to force a
182+
display name for this user.
183+
184+
This callback is called once [User-Interactive Authentication](https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api)
185+
has been completed by the user. It is not called when registering a user via SSO. It is
186+
passed two dictionaries, which include the information that the user has provided during
187+
the registration process. These dictionaries are identical to the ones passed to
188+
[`get_username_for_registration`](#get_username_for_registration), so refer to the
189+
documentation of this callback for more information about them.
190+
191+
If multiple modules implement this callback, they will be considered in order. If a
192+
callback returns `None`, Synapse falls through to the next one. The value of the first
193+
callback that does not return `None` will be used. If this happens, Synapse will not call
194+
any of the subsequent implementations of this callback. If every callback returns `None`,
195+
the username will be used (e.g. `alice` if the user being registered is `@alice:example.com`).
196+
169197
## `is_3pid_allowed`
170198

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

200227
```python
201228
from typing import Awaitable, Callable, Optional, Tuple

synapse/config/registration.py

-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ def read_config(self, config, **kwargs):
4343
"registration_requires_token", False
4444
)
4545
self.registration_shared_secret = config.get("registration_shared_secret")
46-
self.register_just_use_email_for_display_name = config.get(
47-
"register_just_use_email_for_display_name", False
48-
)
4946

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

synapse/handlers/auth.py

+58
Original file line numberDiff line numberDiff line change
@@ -2064,6 +2064,10 @@ def run(*args: Tuple, **kwargs: Dict) -> Awaitable:
20642064
[JsonDict, JsonDict],
20652065
Awaitable[Optional[str]],
20662066
]
2067+
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK = Callable[
2068+
[JsonDict, JsonDict],
2069+
Awaitable[Optional[str]],
2070+
]
20672071
IS_3PID_ALLOWED_CALLBACK = Callable[[str, str, bool], Awaitable[bool]]
20682072

20692073

@@ -2080,6 +2084,9 @@ def __init__(self) -> None:
20802084
self.get_username_for_registration_callbacks: List[
20812085
GET_USERNAME_FOR_REGISTRATION_CALLBACK
20822086
] = []
2087+
self.get_displayname_for_registration_callbacks: List[
2088+
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK
2089+
] = []
20832090
self.is_3pid_allowed_callbacks: List[IS_3PID_ALLOWED_CALLBACK] = []
20842091

20852092
# Mapping from login type to login parameters
@@ -2099,6 +2106,9 @@ def register_password_auth_provider_callbacks(
20992106
get_username_for_registration: Optional[
21002107
GET_USERNAME_FOR_REGISTRATION_CALLBACK
21012108
] = None,
2109+
get_displayname_for_registration: Optional[
2110+
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK
2111+
] = None,
21022112
) -> None:
21032113
# Register check_3pid_auth callback
21042114
if check_3pid_auth is not None:
@@ -2148,6 +2158,11 @@ def register_password_auth_provider_callbacks(
21482158
get_username_for_registration,
21492159
)
21502160

2161+
if get_displayname_for_registration is not None:
2162+
self.get_displayname_for_registration_callbacks.append(
2163+
get_displayname_for_registration,
2164+
)
2165+
21512166
if is_3pid_allowed is not None:
21522167
self.is_3pid_allowed_callbacks.append(is_3pid_allowed)
21532168

@@ -2350,6 +2365,49 @@ async def get_username_for_registration(
23502365

23512366
return None
23522367

2368+
async def get_displayname_for_registration(
2369+
self,
2370+
uia_results: JsonDict,
2371+
params: JsonDict,
2372+
) -> Optional[str]:
2373+
"""Defines the display name to use when registering the user, using the
2374+
credentials and parameters provided during the UIA flow.
2375+
2376+
Stops at the first callback that returns a tuple containing at least one string.
2377+
2378+
Args:
2379+
uia_results: The credentials provided during the UIA flow.
2380+
params: The parameters provided by the registration request.
2381+
2382+
Returns:
2383+
A tuple which first element is the display name, and the second is an MXC URL
2384+
to the user's avatar.
2385+
"""
2386+
for callback in self.get_displayname_for_registration_callbacks:
2387+
try:
2388+
res = await callback(uia_results, params)
2389+
2390+
if isinstance(res, str):
2391+
return res
2392+
elif res is not None:
2393+
# mypy complains that this line is unreachable because it assumes the
2394+
# data returned by the module fits the expected type. We just want
2395+
# to make sure this is the case.
2396+
logger.warning( # type: ignore[unreachable]
2397+
"Ignoring non-string value returned by"
2398+
" get_displayname_for_registration callback %s: %s",
2399+
callback,
2400+
res,
2401+
)
2402+
except Exception as e:
2403+
logger.error(
2404+
"Module raised an exception in get_displayname_for_registration: %s",
2405+
e,
2406+
)
2407+
raise SynapseError(code=500, msg="Internal Server Error")
2408+
2409+
return None
2410+
23532411
async def is_3pid_allowed(
23542412
self,
23552413
medium: str,

synapse/module_api/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
from synapse.handlers.auth import (
7272
CHECK_3PID_AUTH_CALLBACK,
7373
CHECK_AUTH_CALLBACK,
74+
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK,
7475
GET_USERNAME_FOR_REGISTRATION_CALLBACK,
7576
IS_3PID_ALLOWED_CALLBACK,
7677
ON_LOGGED_OUT_CALLBACK,
@@ -317,6 +318,9 @@ def register_password_auth_provider_callbacks(
317318
get_username_for_registration: Optional[
318319
GET_USERNAME_FOR_REGISTRATION_CALLBACK
319320
] = None,
321+
get_displayname_for_registration: Optional[
322+
GET_DISPLAYNAME_FOR_REGISTRATION_CALLBACK
323+
] = None,
320324
) -> None:
321325
"""Registers callbacks for password auth provider capabilities.
322326
@@ -328,6 +332,7 @@ def register_password_auth_provider_callbacks(
328332
is_3pid_allowed=is_3pid_allowed,
329333
auth_checkers=auth_checkers,
330334
get_username_for_registration=get_username_for_registration,
335+
get_displayname_for_registration=get_displayname_for_registration,
331336
)
332337

333338
def register_background_update_controller_callbacks(

synapse/rest/client/register.py

+6-68
Original file line numberDiff line numberDiff line change
@@ -695,26 +695,18 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
695695
session_id
696696
)
697697

698-
# TODO: This won't be needed anymore once https://github.com/matrix-org/matrix-dinsic/issues/793
699-
# is resolved.
700-
desired_display_name = body.get("display_name")
701-
if auth_result:
702-
if LoginType.EMAIL_IDENTITY in auth_result:
703-
address = auth_result[LoginType.EMAIL_IDENTITY]["address"]
704-
if (
705-
self.hs.config.registration.register_just_use_email_for_display_name
706-
):
707-
desired_display_name = address
708-
else:
709-
# Custom mapping between email address and display name
710-
desired_display_name = _map_email_to_displayname(address)
698+
display_name = await (
699+
self.password_auth_provider.get_displayname_for_registration(
700+
auth_result, params
701+
)
702+
)
711703

712704
registered_user_id = await self.registration_handler.register_user(
713705
localpart=desired_username,
714706
password_hash=password_hash,
715707
guest_access_token=guest_access_token,
716-
default_display_name=desired_display_name,
717708
threepid=threepid,
709+
default_display_name=display_name,
718710
address=client_addr,
719711
user_agent_ips=entries,
720712
)
@@ -876,60 +868,6 @@ async def _do_guest_registration(
876868
return 200, result
877869

878870

879-
def cap(name: str) -> str:
880-
"""Capitalise parts of a name containing different words, including those
881-
separated by hyphens.
882-
For example, 'John-Doe'
883-
884-
Args:
885-
The name to parse
886-
"""
887-
if not name:
888-
return name
889-
890-
# Split the name by whitespace then hyphens, capitalizing each part then
891-
# joining it back together.
892-
capatilized_name = " ".join(
893-
"-".join(part.capitalize() for part in space_part.split("-"))
894-
for space_part in name.split()
895-
)
896-
return capatilized_name
897-
898-
899-
def _map_email_to_displayname(address: str) -> str:
900-
"""Custom mapping from an email address to a user displayname
901-
902-
Args:
903-
address: The email address to process
904-
Returns:
905-
The new displayname
906-
"""
907-
# Split the part before and after the @ in the email.
908-
# Replace all . with spaces in the first part
909-
parts = address.replace(".", " ").split("@")
910-
911-
# Figure out which org this email address belongs to
912-
org_parts = parts[1].split(" ")
913-
914-
# If this is a ...matrix.org email, mark them as an Admin
915-
if org_parts[-2] == "matrix" and org_parts[-1] == "org":
916-
org = "Tchap Admin"
917-
918-
# Is this is a ...gouv.fr address, set the org to whatever is before
919-
# gouv.fr. If there isn't anything (a @gouv.fr email) simply mark their
920-
# org as "gouv"
921-
elif org_parts[-2] == "gouv" and org_parts[-1] == "fr":
922-
org = org_parts[-3] if len(org_parts) > 2 else org_parts[-2]
923-
924-
# Otherwise, mark their org as the email's second-level domain name
925-
else:
926-
org = org_parts[-2]
927-
928-
desired_display_name = cap(parts[0]) + " [" + cap(org) + "]"
929-
930-
return desired_display_name
931-
932-
933871
def _calculate_registration_flows(
934872
config: HomeServerConfig, auth_handler: AuthHandler
935873
) -> List[List[str]]:

synapse/rest/media/v1/upload_resource.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ async def _async_render_OPTIONS(self, request: SynapseRequest) -> None:
4949

5050
async def _async_render_POST(self, request: SynapseRequest) -> None:
5151
requester = await self.auth.get_user_by_req(request)
52-
content_length = request.getHeader("Content-Length")
53-
if content_length is None:
52+
raw_content_length = request.getHeader("Content-Length")
53+
if raw_content_length is None:
5454
raise SynapseError(msg="Request must specify a Content-Length", code=400)
55-
if int(content_length) > self.max_upload_size:
55+
try:
56+
content_length = int(raw_content_length)
57+
except ValueError:
58+
raise SynapseError(msg="Content-Length value is invalid", code=400)
59+
if content_length > self.max_upload_size:
5660
raise SynapseError(
5761
msg="Upload request body is too large",
5862
code=413,
@@ -66,7 +70,8 @@ async def _async_render_POST(self, request: SynapseRequest) -> None:
6670
upload_name: Optional[str] = upload_name_bytes.decode("utf8")
6771
except UnicodeDecodeError:
6872
raise SynapseError(
69-
msg="Invalid UTF-8 filename parameter: %r" % (upload_name), code=400
73+
msg="Invalid UTF-8 filename parameter: %r" % (upload_name_bytes,),
74+
code=400,
7075
)
7176

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

0 commit comments

Comments
 (0)