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

Add a callback to react to 3PID associations #12302

Merged
merged 4 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all 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/12302.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a module callback to react to new 3PID (email address, phone number) associations.
18 changes: 18 additions & 0 deletions docs/modules/third_party_rules_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,24 @@ admin API.

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

### `on_threepid_bind`

_First introduced in Synapse v1.56.0_

```python
async def on_threepid_bind(user_id: str, medium: str, address: str) -> None:
```

Called after creating an association between a local user and a third-party identifier
babolivier marked this conversation as resolved.
Show resolved Hide resolved
(email address, phone number). The module is given the Matrix ID of the user the
association is for, as well as the medium (`email` or `msisdn`) and address of the
third-party identifier.

Note that this callback is _not_ called after a successful association on an _identity
server_.

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

## Example

The example below is a module that implements the third-party rules callback
Expand Down
26 changes: 26 additions & 0 deletions synapse/events/third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
CHECK_CAN_DEACTIVATE_USER_CALLBACK = Callable[[str, bool], Awaitable[bool]]
ON_PROFILE_UPDATE_CALLBACK = Callable[[str, ProfileInfo, bool, bool], Awaitable]
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK = Callable[[str, bool, bool], Awaitable]
ON_THREEPID_BIND_CALLBACK = Callable[[str, str, str], Awaitable]


def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
Expand Down Expand Up @@ -169,6 +170,7 @@ def __init__(self, hs: "HomeServer"):
self._on_user_deactivation_status_changed_callbacks: List[
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = []
self._on_threepid_bind_callbacks: List[ON_THREEPID_BIND_CALLBACK] = []

def register_third_party_rules_callbacks(
self,
Expand All @@ -187,6 +189,7 @@ def register_third_party_rules_callbacks(
on_user_deactivation_status_changed: Optional[
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = None,
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
) -> None:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
Expand Down Expand Up @@ -221,6 +224,9 @@ def register_third_party_rules_callbacks(
on_user_deactivation_status_changed,
)

if on_threepid_bind is not None:
self._on_threepid_bind_callbacks.append(on_threepid_bind)

async def check_event_allowed(
self, event: EventBase, context: EventContext
) -> Tuple[bool, Optional[dict]]:
Expand Down Expand Up @@ -479,3 +485,23 @@ async def on_user_deactivation_status_changed(
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)

async def on_threepid_bind(self, user_id: str, medium: str, address: str) -> None:
"""Called after a threepid association has been verified and stored.

Note that this callback is called when an association is created on the
local homeserver, not when it's created on an identity server (and then kept track
of so that it can be unbound on the same IS later on).

Args:
user_id: the user being associated with the threepid.
medium: the threepid's medium.
address: the threepid's address.
"""
for callback in self._on_threepid_bind_callbacks:
try:
await callback(user_id, medium, address)
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)
3 changes: 3 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def __init__(self, hs: "HomeServer"):
self.macaroon_gen = hs.get_macaroon_generator()
self._password_enabled = hs.config.auth.password_enabled
self._password_localdb_enabled = hs.config.auth.password_localdb_enabled
self._third_party_rules = hs.get_third_party_event_rules()

# Ratelimiter for failed auth during UIA. Uses same ratelimit config
# as per `rc_login.failed_attempts`.
Expand Down Expand Up @@ -1505,6 +1506,8 @@ async def add_threepid(
user_id, medium, address, validated_at, self.hs.get_clock().time_msec()
)

await self._third_party_rules.on_threepid_bind(user_id, medium, address)

async def delete_threepid(
self, user_id: str, medium: str, address: str, id_server: Optional[str] = None
) -> bool:
Expand Down
3 changes: 3 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
ON_CREATE_ROOM_CALLBACK,
ON_NEW_EVENT_CALLBACK,
ON_PROFILE_UPDATE_CALLBACK,
ON_THREEPID_BIND_CALLBACK,
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
)
from synapse.handlers.account_validity import (
Expand Down Expand Up @@ -293,6 +294,7 @@ def register_third_party_rules_callbacks(
on_user_deactivation_status_changed: Optional[
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = None,
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
) -> None:
"""Registers callbacks for third party event rules capabilities.

Expand All @@ -308,6 +310,7 @@ def register_third_party_rules_callbacks(
check_can_deactivate_user=check_can_deactivate_user,
on_profile_update=on_profile_update,
on_user_deactivation_status_changed=on_user_deactivation_status_changed,
on_threepid_bind=on_threepid_bind,
)

def register_presence_router_callbacks(
Expand Down
41 changes: 41 additions & 0 deletions tests/rest/client/test_third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,3 +896,44 @@ def test_check_can_shutdown_room(self) -> None:

# Check that the mock was called with the right room ID
self.assertEqual(args[1], self.room_id)

def test_on_threepid_bind(self) -> None:
"""Tests that the on_threepid_bind module callback is called correctly after
associating a 3PID to an account.
"""
# Register a mocked callback.
threepid_bind_mock = Mock(return_value=make_awaitable(None))
third_party_rules = self.hs.get_third_party_event_rules()
third_party_rules._on_threepid_bind_callbacks.append(threepid_bind_mock)

# Register an admin user.
self.register_user("admin", "password", admin=True)
admin_tok = self.login("admin", "password")

# Also register a normal user we can modify.
user_id = self.register_user("user", "password")

# Add a 3PID to the user.
channel = self.make_request(
"PUT",
"/_synapse/admin/v2/users/%s" % user_id,
{
"threepids": [
{
"medium": "email",
"address": "foo@example.com",
},
],
},
access_token=admin_tok,
)

# Check that the shutdown was blocked
self.assertEqual(channel.code, 200, channel.json_body)

# Check that the mock was called once.
threepid_bind_mock.assert_called_once()
args = threepid_bind_mock.call_args[0]

# Check that the mock was called with the right parameters
self.assertEqual(args, (user_id, "email", "foo@example.com"))