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

Commit be76cd8

Browse files
authored
Allow admins to require a manual approval process before new accounts can be used (using MSC3866) (#13556)
1 parent 8625ad8 commit be76cd8

File tree

21 files changed

+731
-34
lines changed

21 files changed

+731
-34
lines changed

changelog.d/13556.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow server admins to require a manual approval process before new accounts can be used (using [MSC3866](https://github.com/matrix-org/matrix-spec-proposals/pull/3866)).

synapse/_scripts/synapse_port_db.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
"redactions": ["have_censored"],
108108
"room_stats_state": ["is_federatable"],
109109
"local_media_repository": ["safe_from_quarantine"],
110-
"users": ["shadow_banned"],
110+
"users": ["shadow_banned", "approved"],
111111
"e2e_fallback_keys_json": ["used"],
112112
"access_tokens": ["used"],
113113
"device_lists_changes_in_room": ["converted_to_destinations"],

synapse/api/constants.py

+11
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,14 @@ class PublicRoomsFilterFields:
269269

270270
GENERIC_SEARCH_TERM: Final = "generic_search_term"
271271
ROOM_TYPES: Final = "room_types"
272+
273+
274+
class ApprovalNoticeMedium:
275+
"""Identifier for the medium this server will use to serve notice of approval for a
276+
specific user's registration.
277+
278+
As defined in https://github.com/matrix-org/matrix-spec-proposals/blob/babolivier/m_not_approved/proposals/3866-user-not-approved-error.md
279+
"""
280+
281+
NONE = "org.matrix.msc3866.none"
282+
EMAIL = "org.matrix.msc3866.email"

synapse/api/errors.py

+16
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class Codes(str, Enum):
106106
# Part of MSC3895.
107107
UNABLE_DUE_TO_PARTIAL_STATE = "ORG.MATRIX.MSC3895_UNABLE_DUE_TO_PARTIAL_STATE"
108108

109+
USER_AWAITING_APPROVAL = "ORG.MATRIX.MSC3866_USER_AWAITING_APPROVAL"
110+
109111

110112
class CodeMessageException(RuntimeError):
111113
"""An exception with integer code and message string attributes.
@@ -566,6 +568,20 @@ def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
566568
return cs_error(self.msg, self.errcode, **extra)
567569

568570

571+
class NotApprovedError(SynapseError):
572+
def __init__(
573+
self,
574+
msg: str,
575+
approval_notice_medium: str,
576+
):
577+
super().__init__(
578+
code=403,
579+
msg=msg,
580+
errcode=Codes.USER_AWAITING_APPROVAL,
581+
additional_fields={"approval_notice_medium": approval_notice_medium},
582+
)
583+
584+
569585
def cs_error(msg: str, code: str = Codes.UNKNOWN, **kwargs: Any) -> "JsonDict":
570586
"""Utility method for constructing an error response for client-server
571587
interactions.

synapse/config/experimental.py

+19
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,25 @@
1414

1515
from typing import Any
1616

17+
import attr
18+
1719
from synapse.config._base import Config
1820
from synapse.types import JsonDict
1921

2022

23+
@attr.s(auto_attribs=True, frozen=True, slots=True)
24+
class MSC3866Config:
25+
"""Configuration for MSC3866 (mandating approval for new users)"""
26+
27+
# Whether the base support for the approval process is enabled. This includes the
28+
# ability for administrators to check and update the approval of users, even if no
29+
# approval is currently required.
30+
enabled: bool = False
31+
# Whether to require that new users are approved by an admin before their account
32+
# can be used. Note that this setting is ignored if 'enabled' is false.
33+
require_approval_for_new_accounts: bool = False
34+
35+
2136
class ExperimentalConfig(Config):
2237
"""Config section for enabling experimental features"""
2338

@@ -97,6 +112,10 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
97112
# MSC3852: Expose last seen user agent field on /_matrix/client/v3/devices.
98113
self.msc3852_enabled: bool = experimental.get("msc3852_enabled", False)
99114

115+
# MSC3866: M_USER_AWAITING_APPROVAL error code
116+
raw_msc3866_config = experimental.get("msc3866", {})
117+
self.msc3866 = MSC3866Config(**raw_msc3866_config)
118+
100119
# MSC3881: Remotely toggle push notifications for another client
101120
self.msc3881_enabled: bool = experimental.get("msc3881_enabled", False)
102121

synapse/handlers/admin.py

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(self, hs: "HomeServer"):
3232
self.store = hs.get_datastores().main
3333
self._storage_controllers = hs.get_storage_controllers()
3434
self._state_storage_controller = self._storage_controllers.state
35+
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
3536

3637
async def get_whois(self, user: UserID) -> JsonDict:
3738
connections = []
@@ -75,6 +76,10 @@ async def get_user(self, user: UserID) -> Optional[JsonDict]:
7576
"is_guest",
7677
}
7778

79+
if self._msc3866_enabled:
80+
# Only include the approved flag if support for MSC3866 is enabled.
81+
user_info_to_return.add("approved")
82+
7883
# Restrict returned keys to a known set.
7984
user_info_dict = {
8085
key: value

synapse/handlers/auth.py

+11
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,17 @@ async def check_user_exists(self, user_id: str) -> Optional[str]:
10091009
return res[0]
10101010
return None
10111011

1012+
async def is_user_approved(self, user_id: str) -> bool:
1013+
"""Checks if a user is approved and therefore can be allowed to log in.
1014+
1015+
Args:
1016+
user_id: the user to check the approval status of.
1017+
1018+
Returns:
1019+
A boolean that is True if the user is approved, False otherwise.
1020+
"""
1021+
return await self.store.is_user_approved(user_id)
1022+
10121023
async def _find_user_id_and_pwd_hash(
10131024
self, user_id: str
10141025
) -> Optional[Tuple[str, str]]:

synapse/handlers/register.py

+8
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ async def register_user(
220220
by_admin: bool = False,
221221
user_agent_ips: Optional[List[Tuple[str, str]]] = None,
222222
auth_provider_id: Optional[str] = None,
223+
approved: bool = False,
223224
) -> str:
224225
"""Registers a new client on the server.
225226
@@ -246,6 +247,8 @@ async def register_user(
246247
user_agent_ips: Tuples of user-agents and IP addresses used
247248
during the registration process.
248249
auth_provider_id: The SSO IdP the user used, if any.
250+
approved: True if the new user should be considered already
251+
approved by an administrator.
249252
Returns:
250253
The registered user_id.
251254
Raises:
@@ -307,6 +310,7 @@ async def register_user(
307310
user_type=user_type,
308311
address=address,
309312
shadow_banned=shadow_banned,
313+
approved=approved,
310314
)
311315

312316
profile = await self.store.get_profileinfo(localpart)
@@ -695,6 +699,7 @@ async def register_with_store(
695699
user_type: Optional[str] = None,
696700
address: Optional[str] = None,
697701
shadow_banned: bool = False,
702+
approved: bool = False,
698703
) -> None:
699704
"""Register user in the datastore.
700705
@@ -713,6 +718,7 @@ async def register_with_store(
713718
api.constants.UserTypes, or None for a normal user.
714719
address: the IP address used to perform the registration.
715720
shadow_banned: Whether to shadow-ban the user
721+
approved: Whether to mark the user as approved by an administrator
716722
"""
717723
if self.hs.config.worker.worker_app:
718724
await self._register_client(
@@ -726,6 +732,7 @@ async def register_with_store(
726732
user_type=user_type,
727733
address=address,
728734
shadow_banned=shadow_banned,
735+
approved=approved,
729736
)
730737
else:
731738
await self.store.register_user(
@@ -738,6 +745,7 @@ async def register_with_store(
738745
admin=admin,
739746
user_type=user_type,
740747
shadow_banned=shadow_banned,
748+
approved=approved,
741749
)
742750

743751
# Only call the account validity module(s) on the main process, to avoid

synapse/replication/http/register.py

+5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ async def _serialize_payload( # type: ignore[override]
5151
user_type: Optional[str],
5252
address: Optional[str],
5353
shadow_banned: bool,
54+
approved: bool,
5455
) -> JsonDict:
5556
"""
5657
Args:
@@ -68,6 +69,8 @@ async def _serialize_payload( # type: ignore[override]
6869
or None for a normal user.
6970
address: the IP address used to perform the regitration.
7071
shadow_banned: Whether to shadow-ban the user
72+
approved: Whether the user should be considered already approved by an
73+
administrator.
7174
"""
7275
return {
7376
"password_hash": password_hash,
@@ -79,6 +82,7 @@ async def _serialize_payload( # type: ignore[override]
7982
"user_type": user_type,
8083
"address": address,
8184
"shadow_banned": shadow_banned,
85+
"approved": approved,
8286
}
8387

8488
async def _handle_request( # type: ignore[override]
@@ -99,6 +103,7 @@ async def _handle_request( # type: ignore[override]
99103
user_type=content["user_type"],
100104
address=content["address"],
101105
shadow_banned=content["shadow_banned"],
106+
approved=content["approved"],
102107
)
103108

104109
return 200, {}

synapse/rest/admin/users.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(self, hs: "HomeServer"):
6969
self.store = hs.get_datastores().main
7070
self.auth = hs.get_auth()
7171
self.admin_handler = hs.get_admin_handler()
72+
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
7273

7374
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
7475
await assert_requester_is_admin(self.auth, request)
@@ -95,6 +96,13 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
9596
guests = parse_boolean(request, "guests", default=True)
9697
deactivated = parse_boolean(request, "deactivated", default=False)
9798

99+
# If support for MSC3866 is not enabled, apply no filtering based on the
100+
# `approved` column.
101+
if self._msc3866_enabled:
102+
approved = parse_boolean(request, "approved", default=True)
103+
else:
104+
approved = True
105+
98106
order_by = parse_string(
99107
request,
100108
"order_by",
@@ -115,8 +123,22 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
115123
direction = parse_string(request, "dir", default="f", allowed_values=("f", "b"))
116124

117125
users, total = await self.store.get_users_paginate(
118-
start, limit, user_id, name, guests, deactivated, order_by, direction
126+
start,
127+
limit,
128+
user_id,
129+
name,
130+
guests,
131+
deactivated,
132+
order_by,
133+
direction,
134+
approved,
119135
)
136+
137+
# If support for MSC3866 is not enabled, don't show the approval flag.
138+
if not self._msc3866_enabled:
139+
for user in users:
140+
del user["approved"]
141+
120142
ret = {"users": users, "total": total}
121143
if (start + limit) < total:
122144
ret["next_token"] = str(start + len(users))
@@ -163,6 +185,7 @@ def __init__(self, hs: "HomeServer"):
163185
self.deactivate_account_handler = hs.get_deactivate_account_handler()
164186
self.registration_handler = hs.get_registration_handler()
165187
self.pusher_pool = hs.get_pusherpool()
188+
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
166189

167190
async def on_GET(
168191
self, request: SynapseRequest, user_id: str
@@ -239,6 +262,15 @@ async def on_PUT(
239262
HTTPStatus.BAD_REQUEST, "'deactivated' parameter is not of type boolean"
240263
)
241264

265+
approved: Optional[bool] = None
266+
if "approved" in body and self._msc3866_enabled:
267+
approved = body["approved"]
268+
if not isinstance(approved, bool):
269+
raise SynapseError(
270+
HTTPStatus.BAD_REQUEST,
271+
"'approved' parameter is not of type boolean",
272+
)
273+
242274
# convert List[Dict[str, str]] into List[Tuple[str, str]]
243275
if external_ids is not None:
244276
new_external_ids = [
@@ -343,6 +375,9 @@ async def on_PUT(
343375
if "user_type" in body:
344376
await self.store.set_user_type(target_user, user_type)
345377

378+
if approved is not None:
379+
await self.store.update_user_approval_status(target_user, approved)
380+
346381
user = await self.admin_handler.get_user(target_user)
347382
assert user is not None
348383

@@ -355,13 +390,18 @@ async def on_PUT(
355390
if password is not None:
356391
password_hash = await self.auth_handler.hash(password)
357392

393+
new_user_approved = True
394+
if self._msc3866_enabled and approved is not None:
395+
new_user_approved = approved
396+
358397
user_id = await self.registration_handler.register_user(
359398
localpart=target_user.localpart,
360399
password_hash=password_hash,
361400
admin=set_admin_to,
362401
default_display_name=displayname,
363402
user_type=user_type,
364403
by_admin=True,
404+
approved=new_user_approved,
365405
)
366406

367407
if threepids is not None:
@@ -550,6 +590,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
550590
user_type=user_type,
551591
default_display_name=displayname,
552592
by_admin=True,
593+
approved=True,
553594
)
554595

555596
result = await register._create_registration_details(user_id, body)

0 commit comments

Comments
 (0)