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

Commit 115b439

Browse files
committed
Change admin APIs to support checking and updating the approval status of a user
1 parent 73b4791 commit 115b439

File tree

6 files changed

+243
-2
lines changed

6 files changed

+243
-2
lines changed

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 = []
@@ -74,6 +75,10 @@ async def get_user(self, user: UserID) -> Optional[JsonDict]:
7475
"is_guest",
7576
}
7677

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

synapse/handlers/register.py

+8
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ async def register_user(
217217
by_admin: bool = False,
218218
user_agent_ips: Optional[List[Tuple[str, str]]] = None,
219219
auth_provider_id: Optional[str] = None,
220+
approved: bool = False,
220221
) -> str:
221222
"""Registers a new client on the server.
222223
@@ -243,6 +244,8 @@ async def register_user(
243244
user_agent_ips: Tuples of user-agents and IP addresses used
244245
during the registration process.
245246
auth_provider_id: The SSO IdP the user used, if any.
247+
approved: True if the new user should be considered already
248+
approved by an administrator.
246249
Returns:
247250
The registered user_id.
248251
Raises:
@@ -304,6 +307,7 @@ async def register_user(
304307
user_type=user_type,
305308
address=address,
306309
shadow_banned=shadow_banned,
310+
approved=approved,
307311
)
308312

309313
profile = await self.store.get_profileinfo(localpart)
@@ -692,6 +696,7 @@ async def register_with_store(
692696
user_type: Optional[str] = None,
693697
address: Optional[str] = None,
694698
shadow_banned: bool = False,
699+
approved: bool = False,
695700
) -> None:
696701
"""Register user in the datastore.
697702
@@ -710,6 +715,7 @@ async def register_with_store(
710715
api.constants.UserTypes, or None for a normal user.
711716
address: the IP address used to perform the registration.
712717
shadow_banned: Whether to shadow-ban the user
718+
approved: Whether to mark the user as approved by an administrator
713719
"""
714720
if self.hs.config.worker.worker_app:
715721
await self._register_client(
@@ -723,6 +729,7 @@ async def register_with_store(
723729
user_type=user_type,
724730
address=address,
725731
shadow_banned=shadow_banned,
732+
approved=approved,
726733
)
727734
else:
728735
await self.store.register_user(
@@ -735,6 +742,7 @@ async def register_with_store(
735742
admin=admin,
736743
user_type=user_type,
737744
shadow_banned=shadow_banned,
745+
approved=approved,
738746
)
739747

740748
# 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 = False
394+
if self._msc3866_enabled and approved is True:
395+
new_user_approved = True
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)

synapse/storage/databases/main/__init__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ async def get_users_paginate(
203203
deactivated: bool = False,
204204
order_by: str = UserSortOrder.USER_ID.value,
205205
direction: str = "f",
206+
approved: bool = True,
206207
) -> Tuple[List[JsonDict], int]:
207208
"""Function to retrieve a paginated list of users from
208209
users list. This will return a json list of users and the
@@ -217,6 +218,7 @@ async def get_users_paginate(
217218
deactivated: whether to include deactivated users
218219
order_by: the sort order of the returned list
219220
direction: sort ascending or descending
221+
approved: whether to include approved users
220222
Returns:
221223
A tuple of a list of mappings from user to information and a count of total users.
222224
"""
@@ -249,6 +251,11 @@ def get_users_paginate_txn(
249251
if not deactivated:
250252
filters.append("deactivated = 0")
251253

254+
if not approved:
255+
# We ignore NULL values for the approved flag because these should only
256+
# be already existing users that we consider as already approved.
257+
filters.append("approved = 0")
258+
252259
where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""
253260

254261
sql_base = f"""
@@ -262,7 +269,7 @@ def get_users_paginate_txn(
262269

263270
sql = f"""
264271
SELECT name, user_type, is_guest, admin, deactivated, shadow_banned,
265-
displayname, avatar_url, creation_ts * 1000 as creation_ts
272+
displayname, avatar_url, creation_ts * 1000 as creation_ts, approved
266273
{sql_base}
267274
ORDER BY {order_by_column} {order}, u.name ASC
268275
LIMIT ? OFFSET ?

0 commit comments

Comments
 (0)