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

Add third_party module callbacks to check if a user can delete a room and deactivate a user #12028

Merged
merged 15 commits into from
Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/12028.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add third party event rules callbacks `check_can_shutdown_room` and `check_can_deactivate_user`.
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
44 changes: 44 additions & 0 deletions docs/modules/third_party_rules_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,50 @@ deny an incoming event, see [`check_event_for_spam`](spam_checker_callbacks.md#c

If multiple modules implement this callback, Synapse runs them all in order.

### `check_can_shutdown_room`

_First introduced in Synapse v1.5X.0_
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

```python
async def check_can_shutdown_room(
user_id: str
room_id: str,
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
) -> bool:
```

Called when a user requests the shutdown of a room. The module must return a
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
boolean indicating whether the shutdown can go through. If the callback returns `False`,
the shutdown will not proceed and the caller will see a `M_FOBIDDEN` error.
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

If multiple modules implement this callback, they will be considered in order. If a
callback returns `True`, Synapse falls through to the next one. The value of the first
callback that does not return `True` will be used. If this happens, Synapse will not call
any of the subsequent implementations of this callback.

### `check_can_deactivate_user`

_First introduced in Synapse v1.5X.0_
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

```python
async def check_can_deactivate_user(
requester: "synapse.types.Requester",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this a bit more, is there a reason we need to pass the Requester down to the module instead of just the user ID? If not I'd prefer if we passed the user ID for consistency (I know on_create_room already passes a Requester to modules, we'll probably need to fix it later).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it was mostly so you could tell if the requester was an admin or not by inspecting the object. I can do an is_admin flag (or maybe there is a Module API to determine this) but I was going for convenience.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed Requester, second parameter is now either the admin's user id, or None if no admin made the request.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having thought about it...I can't think of a reason to care who the admin is... going to use by_admin

user_id: str,
) -> bool:
```

Called when a user requests the deactivation of a user. User deactivation may be
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
performed by an admin or the user themselves, so developers are encouraged to check the
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
requester when implementing this callback. The module must return a
boolean indicating whether the deactivation can go through. If the callback returns `False`,
the deactivation will not proceed and the caller will see a `M_FOBIDDEN` error.
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

If multiple modules implement this callback, they will be considered in order. If a
callback returns `True`, Synapse falls through to the next one. The value of the first
callback that does not return `True` will be used. If this happens, Synapse will not call
any of the subsequent implementations of this callback.



## Example

The example below is a module that implements the third-party rules callback
Expand Down
56 changes: 56 additions & 0 deletions synapse/events/third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
[str, StateMap[EventBase], str], Awaitable[bool]
]
ON_NEW_EVENT_CALLBACK = Callable[[EventBase, StateMap[EventBase]], Awaitable]
CHECK_CAN_SHUTDOWN_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]]
CHECK_CAN_DEACTIVATE_USER_CALLBACK = Callable[[Requester, str], Awaitable[bool]]


def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
Expand Down Expand Up @@ -154,6 +156,8 @@ def __init__(self, hs: "HomeServer"):
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
] = []
self._on_new_event_callbacks: List[ON_NEW_EVENT_CALLBACK] = []
self._check_can_shutdown_room: List[CHECK_CAN_SHUTDOWN_ROOM_CALLBACK] = []
self._check_can_deactivate_user: List[CHECK_CAN_DEACTIVATE_USER_CALLBACK] = []
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

def register_third_party_rules_callbacks(
self,
Expand All @@ -166,6 +170,8 @@ def register_third_party_rules_callbacks(
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
] = None,
on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None,
check_can_shutdown_room: Optional[CHECK_CAN_SHUTDOWN_ROOM_CALLBACK] = None,
check_can_deactivate_user: Optional[CHECK_CAN_DEACTIVATE_USER_CALLBACK] = None,
) -> None:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
Expand All @@ -187,6 +193,12 @@ def register_third_party_rules_callbacks(
if on_new_event is not None:
self._on_new_event_callbacks.append(on_new_event)

if check_can_shutdown_room is not None:
self._check_can_shutdown_room.append(check_can_shutdown_room)

if check_can_deactivate_user is not None:
self._check_can_deactivate_user.append(check_can_deactivate_user)

async def check_event_allowed(
self, event: EventBase, context: EventContext
) -> Tuple[bool, Optional[dict]]:
Expand Down Expand Up @@ -353,6 +365,50 @@ async def on_new_event(self, event_id: str) -> None:
"Failed to run module API callback %s: %s", callback, e
)

async def check_can_shutdown_room(self, user_id: str, room_id: str) -> bool:
"""Intercept requests to shutdown a room. If `False` is returned, the
room should not be shut down.
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

Args:
requester: The ID of the user requesting the shutdown.
room_id: The ID of the room.

Raises:
ModuleFailureError if a callback raised any exception.
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
"""
for callback in self._check_can_shutdown_room:
try:
if await callback(user_id, room_id) is False:
return False
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)
return True

async def check_can_deactivate_user(
self, requester: Requester, user_id: str
) -> bool:
"""Intercept requests to deactivate a user. If `False` is returned, the
user should not be deactivated.

Args:
requester
user_id: The ID of the room.

Raises:
ModuleFailureError if a callback raised any exception.
"""
for callback in self._check_can_deactivate_user:
try:
if await callback(requester, user_id) is False:
return False
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)
return True

async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]:
"""Given a room ID, return the state events of that room.

Expand Down
12 changes: 11 additions & 1 deletion synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from synapse.api.errors import SynapseError
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.types import Requester, UserID, create_requester
from synapse.types import Codes, Requester, UserID, create_requester

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand All @@ -41,6 +41,7 @@ def __init__(self, hs: "HomeServer"):

# Flag that indicates whether the process to part users from rooms is running
self._user_parter_running = False
self._third_party_rules = hs.get_third_party_event_rules()

# Start the user parter loop so it can resume parting users from rooms where
# it left off (if it has work left to do).
Expand Down Expand Up @@ -73,6 +74,15 @@ async def deactivate_account(
Returns:
True if identity server supports removing threepids, otherwise False.
"""

# Check if this user can be deactivated
if not await self._third_party_rules.check_can_deactivate_user(
requester, user_id
):
raise SynapseError(
403, "Deactivation of this user is forbidden", Codes.FORBIDDEN
)

# FIXME: Theoretically there is a race here wherein user resets
# password using threepid.

Expand Down
8 changes: 8 additions & 0 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,7 @@ def __init__(self, hs: "HomeServer"):
self.room_member_handler = hs.get_room_member_handler()
self._room_creation_handler = hs.get_room_creation_handler()
self._replication = hs.get_replication_data_handler()
self._third_party_rules = hs.get_third_party_event_rules()
self.event_creation_handler = hs.get_event_creation_handler()
self.store = hs.get_datastore()

Expand Down Expand Up @@ -1548,6 +1549,13 @@ async def shutdown_room(
if not RoomID.is_valid(room_id):
raise SynapseError(400, "%s is not a legal room ID" % (room_id,))

if not await self._third_party_rules.check_can_shutdown_room(
requester_user_id, room_id
):
raise SynapseError(
403, "Shutdown of this room is forbidden", Codes.FORBIDDEN
)

# Action the block first (even if the room doesn't exist yet)
if block:
# This will work even if the room is already blocked, but that is
Expand Down
6 changes: 6 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
USER_MAY_SEND_3PID_INVITE_CALLBACK,
)
from synapse.events.third_party_rules import (
CHECK_CAN_DEACTIVATE_USER_CALLBACK,
CHECK_CAN_SHUTDOWN_ROOM_CALLBACK,
CHECK_EVENT_ALLOWED_CALLBACK,
CHECK_THREEPID_CAN_BE_INVITED_CALLBACK,
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK,
Expand Down Expand Up @@ -278,6 +280,8 @@ def register_third_party_rules_callbacks(
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
] = None,
on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None,
check_can_shutdown_room: Optional[CHECK_CAN_SHUTDOWN_ROOM_CALLBACK] = None,
check_can_deactivate_user: Optional[CHECK_CAN_DEACTIVATE_USER_CALLBACK] = None,
) -> None:
"""Registers callbacks for third party event rules capabilities.

Expand All @@ -289,6 +293,8 @@ def register_third_party_rules_callbacks(
check_threepid_can_be_invited=check_threepid_can_be_invited,
check_visibility_can_be_modified=check_visibility_can_be_modified,
on_new_event=on_new_event,
check_can_shutdown_room=check_can_shutdown_room,
check_can_deactivate_user=check_can_deactivate_user,
)

def register_presence_router_callbacks(
Expand Down