Skip to content

Commit 723e5a8

Browse files
committed
Add user_may_create_room_with_visibility spamchecker module API callback
WIP
1 parent 17e6b32 commit 723e5a8

File tree

6 files changed

+100
-3
lines changed

6 files changed

+100
-3
lines changed

docs/modules/spam_checker_callbacks.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,27 @@ The value of the first callback that does not return `synapse.module_api.NOT_SPA
180180
be used. If this happens, Synapse will not call any of the subsequent implementations of
181181
this callback.
182182

183+
### `user_may_create_room_with_visibility`
184+
185+
_First introduced in Synapse vX.X.X_
186+
187+
```python
188+
async def user_may_create_room_with_visibility(user_id: str, visibility: str) -> Union["synapse.module_api.NOT_SPAM", "synapse.module_api.errors.Codes"]
189+
```
190+
191+
Called when processing a room creation request or updating join rules for an existing room.
192+
193+
The callback must return one of:
194+
- `synapse.module_api.NOT_SPAM`, to allow the operation. Other callbacks may still
195+
decide to reject it.
196+
- `synapse.module_api.errors.Codes` to reject the operation with an error code. In case
197+
of doubt, `synapse.module_api.errors.Codes.FORBIDDEN` is a good error code.
198+
199+
If multiple modules implement this callback, they will be considered in order. If a
200+
callback returns `synapse.module_api.NOT_SPAM`, Synapse falls through to the next one.
201+
The value of the first callback that does not return `synapse.module_api.NOT_SPAM` will
202+
be used. If this happens, Synapse will not call any of the subsequent implementations of
203+
this callback.
183204

184205

185206
### `user_may_create_room_alias`

docs/spam_checker.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ well as some specific methods:
2727

2828
* `user_may_invite`
2929
* `user_may_create_room`
30+
* `user_may_create_room_with_visibility`
3031
* `user_may_create_room_alias`
3132
* `user_may_publish_room`
3233
* `check_username_for_spam`

synapse/handlers/room.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,10 @@ async def create_room(
784784
Codes.MISSING_PARAM,
785785
)
786786

787+
# The spec says rooms should default to private visibility if
788+
# `visibility` is not specified.
789+
visibility = config.get("visibility", "private")
790+
787791
if not is_requester_admin:
788792
spam_check = await self._spam_checker_module_callbacks.user_may_create_room(
789793
user_id
@@ -795,6 +799,17 @@ async def create_room(
795799
errcode=spam_check[0],
796800
additional_fields=spam_check[1],
797801
)
802+
spam_check = await self._spam_checker_module_callbacks.user_may_create_room_with_visibility(
803+
user_id,
804+
visibility,
805+
)
806+
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
807+
raise SynapseError(
808+
403,
809+
"You are not permitted to create rooms with visibility {visibility}",
810+
errcode=spam_check[0],
811+
additional_fields=spam_check[1],
812+
)
798813

799814
if ratelimit:
800815
# Rate limit once in advance, but don't rate limit the individual
@@ -887,9 +902,6 @@ async def create_room(
887902
% (user_id,),
888903
)
889904

890-
# The spec says rooms should default to private visibility if
891-
# `visibility` is not specified.
892-
visibility = config.get("visibility", "private")
893905
is_public = visibility == "public"
894906

895907
self._validate_room_config(config, visibility)

synapse/module_api/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
SHOULD_DROP_FEDERATED_EVENT_CALLBACK,
100100
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK,
101101
USER_MAY_CREATE_ROOM_CALLBACK,
102+
USER_MAY_CREATE_ROOM_WITH_VISIBILITY_CALLBACK,
102103
USER_MAY_INVITE_CALLBACK,
103104
USER_MAY_JOIN_ROOM_CALLBACK,
104105
USER_MAY_PUBLISH_ROOM_CALLBACK,
@@ -307,6 +308,9 @@ def register_spam_checker_callbacks(
307308
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
308309
user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
309310
user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
311+
user_may_create_room_with_visibility: Optional[
312+
USER_MAY_CREATE_ROOM_WITH_VISIBILITY_CALLBACK
313+
] = None,
310314
user_may_create_room_alias: Optional[
311315
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
312316
] = None,
@@ -335,6 +339,7 @@ def register_spam_checker_callbacks(
335339
check_registration_for_spam=check_registration_for_spam,
336340
check_media_file_for_spam=check_media_file_for_spam,
337341
check_login_for_spam=check_login_for_spam,
342+
user_may_create_room_with_visibility=user_may_create_room_with_visibility,
338343
)
339344

340345
def register_account_validity_callbacks(

synapse/module_api/callbacks/spamchecker_callbacks.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@
136136
]
137137
],
138138
]
139+
USER_MAY_CREATE_ROOM_WITH_VISIBILITY_CALLBACK = Callable[
140+
[str, str],
141+
Awaitable[
142+
Union[
143+
Literal["NOT_SPAM"],
144+
Codes,
145+
]
146+
],
147+
]
139148
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[
140149
[str, RoomAlias],
141150
Awaitable[
@@ -332,6 +341,9 @@ def __init__(self, hs: "synapse.server.HomeServer") -> None:
332341
USER_MAY_SEND_3PID_INVITE_CALLBACK
333342
] = []
334343
self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = []
344+
self._user_may_create_room_with_visibility_callbacks: List[
345+
USER_MAY_CREATE_ROOM_WITH_VISIBILITY_CALLBACK
346+
] = []
335347
self._user_may_create_room_alias_callbacks: List[
336348
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
337349
] = []
@@ -367,6 +379,9 @@ def register_callbacks(
367379
] = None,
368380
check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
369381
check_login_for_spam: Optional[CHECK_LOGIN_FOR_SPAM_CALLBACK] = None,
382+
user_may_create_room_with_visibility: Optional[
383+
USER_MAY_CREATE_ROOM_WITH_VISIBILITY_CALLBACK
384+
] = None,
370385
) -> None:
371386
"""Register callbacks from module for each hook."""
372387
if check_event_for_spam is not None:
@@ -391,6 +406,11 @@ def register_callbacks(
391406
if user_may_create_room is not None:
392407
self._user_may_create_room_callbacks.append(user_may_create_room)
393408

409+
if user_may_create_room_with_visibility is not None:
410+
self._user_may_create_room_with_visibility_callbacks.append(
411+
user_may_create_room_with_visibility,
412+
)
413+
394414
if user_may_create_room_alias is not None:
395415
self._user_may_create_room_alias_callbacks.append(
396416
user_may_create_room_alias,
@@ -653,6 +673,29 @@ async def user_may_create_room(
653673

654674
return self.NOT_SPAM
655675

676+
async def user_may_create_room_with_visibility(
677+
self, userid: str, visibility: str
678+
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
679+
"""Checks if a given user may create a room with a given visibility
680+
Args:
681+
userid: The ID of the user attempting to create a room
682+
visibility: The visibility of the room to be created
683+
"""
684+
for callback in self._user_may_create_room_with_visibility_callbacks:
685+
with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
686+
res = await delay_cancellation(callback(userid, visibility))
687+
if res is self.NOT_SPAM:
688+
continue
689+
elif isinstance(res, synapse.api.errors.Codes):
690+
return res, {}
691+
else:
692+
logger.warning(
693+
"Module returned invalid value, rejecting room creation as spam"
694+
)
695+
return synapse.api.errors.Codes.FORBIDDEN, {}
696+
697+
return self.NOT_SPAM
698+
656699
async def user_may_create_room_alias(
657700
self, userid: str, room_alias: RoomAlias
658701
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:

synapse/rest/client/room.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ def __init__(self, hs: "HomeServer"):
198198
self.delayed_events_handler = hs.get_delayed_events_handler()
199199
self.auth = hs.get_auth()
200200
self._max_event_delay_ms = hs.config.server.max_event_delay_ms
201+
self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker
201202

202203
def register(self, http_server: HttpServer) -> None:
203204
# /rooms/$roomid/state/$eventtype
@@ -289,6 +290,20 @@ async def on_PUT(
289290

290291
content = parse_json_object_from_request(request)
291292

293+
if event_type == EventTypes.JoinRules:
294+
visibility = "public" # XXXTODO: determine visibility from content
295+
spam_check = await self._spam_checker_module_callbacks.user_may_create_room_with_visibility(
296+
requester.user.to_string(), visibility
297+
)
298+
299+
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
300+
raise SynapseError(
301+
403,
302+
"You are not permitted to change room visibility to {visibility}",
303+
errcode=spam_check[0],
304+
additional_fields=spam_check[1],
305+
)
306+
292307
origin_server_ts = None
293308
if requester.app_service:
294309
origin_server_ts = parse_integer(request, "ts")

0 commit comments

Comments
 (0)