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

Add module API callbacks for adding and deleting local 3PID associations #15044

Merged
merged 10 commits into from
Feb 27, 2023
1 change: 1 addition & 0 deletions changelog.d/15044.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add two new Third Party Rules module API callbacks: [`on_add_user_third_party_identifier`](https://matrix-org.github.io/synapse/v1.79/modules/third_party_rules_callbacks.html#on_add_user_third_party_identifier) and [`on_remove_user_third_party_identifier`](https://matrix-org.github.io/synapse/v1.79/modules/third_party_rules_callbacks.html#on_remove_user_third_party_identifier).
45 changes: 44 additions & 1 deletion docs/modules/third_party_rules_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ If multiple modules implement this callback, Synapse runs them all in order.

_First introduced in Synapse v1.56.0_

**<span style="color:red">
This callback is deprecated in favour of the `on_add_user_third_party_identifier` callback, which
features the same functionality. The only difference is in name.
</span>**

```python
async def on_threepid_bind(user_id: str, medium: str, address: str) -> None:
```
Expand All @@ -265,6 +270,44 @@ server_.

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

### `on_add_user_third_party_identifier`

_First introduced in Synapse v1.79.0_

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

Called after successfully creating an association between a user and a third-party identifier
(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 (i.e. an email address).

Note that this callback is _not_ called if a user attempts to bind their third-party identifier
to an identity server (via a call to [`POST
/_matrix/client/v3/account/3pid/bind`](https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3account3pidbind)).

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

### `on_remove_user_third_party_identifier`

_First introduced in Synapse v1.79.0_

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

Called after successfully removing an association between a user and a third-party identifier
(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 (i.e. an email address).

Note that this callback is _not_ called if a user attempts to unbind their third-party
identifier from an identity server (via a call to [`POST
/_matrix/client/v3/account/3pid/unbind`](https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3account3pidunbind)).

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 Expand Up @@ -297,4 +340,4 @@ class EventCensorer:
)
event_dict["content"] = new_event_content
return event_dict
```
```
24 changes: 24 additions & 0 deletions docs/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ process, for example:
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
```

# Upgrading to v1.79.0

## The `on_threepid_bind` module callback method has been deprecated

Synapse v1.79.0 deprecates the
[`on_threepid_bind`](modules/third_party_rules_callbacks.md#on_threepid_bind)
"third-party rules" Synapse module callback method in favour of a new module method,
[`on_add_user_third_party_identifier`](modules/third_party_rules_callbacks.md#on_add_user_third_party_identifier).
`on_threepid_bind` will be removed in a future version of Synapse. You should check whether any Synapse
modules in use in your deployment are making use of `on_threepid_bind`, and update them where possible.

The arguments and functionality of the new method are the same.

The justification behind the name change is that the old method's name, `on_threepid_bind`, was
misleading. A user is considered to "bind" their third-party ID to their Matrix ID only if they
do so via an [identity server](https://spec.matrix.org/latest/identity-service-api/)
(so that users on other homeservers may find them). But this method was not called in that case -
it was only called when a user added a third-party identifier on the local homeserver.

Module developers may also be interested in the related
[`on_remove_user_third_party_identifier`](modules/third_party_rules_callbacks.md#on_remove_user_third_party_identifier)
module callback method that was also added in Synapse v1.79.0. This new method is called when a
user removes a third-party identifier from their account.

# Upgrading to v1.78.0

## Deprecate the `/_synapse/admin/v1/media/<server_name>/delete` admin API
Expand Down
63 changes: 63 additions & 0 deletions synapse/events/third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
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]
ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable]
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable]


def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
Expand Down Expand Up @@ -174,6 +176,12 @@ def __init__(self, hs: "HomeServer"):
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = []
self._on_threepid_bind_callbacks: List[ON_THREEPID_BIND_CALLBACK] = []
self._on_add_user_third_party_identifier_callbacks: List[
ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
] = []
self._on_remove_user_third_party_identifier_callbacks: List[
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
] = []

def register_third_party_rules_callbacks(
self,
Expand All @@ -193,6 +201,12 @@ def register_third_party_rules_callbacks(
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = None,
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
on_add_user_third_party_identifier: Optional[
ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
] = None,
on_remove_user_third_party_identifier: Optional[
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
] = None,
) -> None:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
Expand Down Expand Up @@ -230,6 +244,11 @@ def register_third_party_rules_callbacks(
if on_threepid_bind is not None:
self._on_threepid_bind_callbacks.append(on_threepid_bind)

if on_add_user_third_party_identifier is not None:
self._on_add_user_third_party_identifier_callbacks.append(
on_add_user_third_party_identifier
)

async def check_event_allowed(
self,
event: EventBase,
Expand Down Expand Up @@ -513,6 +532,9 @@ async def on_threepid_bind(self, user_id: str, medium: str, address: str) -> Non
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).

THIS MODULE CALLBACK METHOD HAS BEEN DEPRECATED. Please use the
`on_add_user_third_party_identifier` callback method instead.

Args:
user_id: the user being associated with the threepid.
medium: the threepid's medium.
Expand All @@ -525,3 +547,44 @@ async def on_threepid_bind(self, user_id: str, medium: str, address: str) -> Non
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)

async def on_add_user_third_party_identifier(
self, user_id: str, medium: str, address: str
) -> None:
"""Called when an association between a user's Matrix ID and a third-party ID
(email, phone number) has successfully been registered on the homeserver.

Args:
user_id: The User ID included in the association.
medium: The medium of the third-party ID (email, msisdn).
address: The address of the third-party ID (i.e. an email address).
"""
for callback in self._on_add_user_third_party_identifier_callbacks:
try:
await callback(user_id, medium, address)
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)

async def on_remove_user_third_party_identifier(
self, user_id: str, medium: str, address: str
) -> None:
"""Called when an association between a user's Matrix ID and a third-party ID
(email, phone number) has been successfully removed on the homeserver.

This is called *after* any known bindings on identity servers for this
association have been removed.

Args:
user_id: The User ID included in the removed association.
medium: The medium of the third-party ID (email, msisdn).
address: The address of the third-party ID (i.e. an email address).
"""
for callback in self._on_remove_user_third_party_identifier_callbacks:
try:
await callback(user_id, medium, address)
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)
49 changes: 31 additions & 18 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,17 @@ async def delete_access_tokens_for_user(
async def add_threepid(
self, user_id: str, medium: str, address: str, validated_at: int
) -> None:
"""
Adds an association between a user's Matrix ID and a third-party ID (email,
phone number).

Args:
user_id: The ID of the user to associate.
medium: The medium of the third-party ID (email, msisdn).
address: The address of the third-party ID (i.e. an email address).
validated_at: The timestamp in ms of when the validation that the user owns
this third-party ID occurred.
"""
# check if medium has a valid value
if medium not in ["email", "msisdn"]:
raise SynapseError(
Expand All @@ -1567,42 +1578,44 @@ async def add_threepid(
user_id, medium, address, validated_at, self.hs.get_clock().time_msec()
)

# Inform Synapse modules that a 3PID association has been created.
await self._third_party_rules.on_add_user_third_party_identifier(
user_id, medium, address
)

# Deprecated method for informing Synapse modules that a 3PID association
# has successfully been created.
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:
"""Attempts to unbind the 3pid on the identity servers and deletes it
from the local database.
async def delete_local_threepid(
self, user_id: str, medium: str, address: str
) -> None:
"""Deletes an association between a third-party ID and a user ID from the local
database. This method does not unbind the association from any identity servers.

If `medium` is 'email' and a pusher is associated with this third-party ID, the
pusher will also be deleted.

Args:
user_id: ID of user to remove the 3pid from.
medium: The medium of the 3pid being removed: "email" or "msisdn".
address: The 3pid address to remove.
id_server: Use the given identity server when unbinding
any threepids. If None then will attempt to unbind using the
identity server specified when binding (if known).

Returns:
Returns True if successfully unbound the 3pid on
the identity server, False if identity server doesn't support the
unbind API.
"""

# 'Canonicalise' email addresses as per above
if medium == "email":
address = canonicalise_email(address)

result = await self.hs.get_identity_handler().try_unbind_threepid(
user_id, medium, address, id_server
await self.store.user_delete_threepid(user_id, medium, address)

# Inform Synapse modules that a 3PID association has been deleted.
await self._third_party_rules.on_remove_user_third_party_identifier(
user_id, medium, address
)

await self.store.user_delete_threepid(user_id, medium, address)
if medium == "email":
await self.store.delete_pusher_by_app_id_pushkey_user_id(
app_id="m.email", pushkey=address, user_id=user_id
)
return result

async def hash(self, password: str) -> str:
"""Computes a secure hash of password.
Expand Down
20 changes: 11 additions & 9 deletions synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,26 +100,28 @@ async def deactivate_account(
# unbinding
identity_server_supports_unbinding = True

# Retrieve the 3PIDs this user has bound to an identity server
threepids = await self.store.user_get_bound_threepids(user_id)

for threepid in threepids:
# Attempt to unbind any known bound threepids to this account from identity
# server(s).
bound_threepids = await self.store.user_get_bound_threepids(user_id)
for threepid in bound_threepids:
try:
result = await self._identity_handler.try_unbind_threepid(
user_id, threepid["medium"], threepid["address"], id_server
)
identity_server_supports_unbinding &= result
except Exception:
# Do we want this to be a fatal error or should we carry on?
logger.exception("Failed to remove threepid from ID server")
raise SynapseError(400, "Failed to remove threepid from ID server")
await self.store.user_delete_threepid(

identity_server_supports_unbinding &= result

# Remove any local threepid associations for this account.
local_threepids = await self.store.user_get_threepids(user_id)
for threepid in local_threepids:
await self._auth_handler.delete_local_threepid(
user_id, threepid["medium"], threepid["address"]
)

# Remove all 3PIDs this user has bound to the homeserver
await self.store.user_delete_threepids(user_id)

# delete any devices belonging to the user, which will also
# delete corresponding access tokens.
await self._device_handler.delete_all_devices_for_user(user_id)
Expand Down
10 changes: 10 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@
CHECK_EVENT_ALLOWED_CALLBACK,
CHECK_THREEPID_CAN_BE_INVITED_CALLBACK,
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK,
ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK,
ON_CREATE_ROOM_CALLBACK,
ON_NEW_EVENT_CALLBACK,
ON_PROFILE_UPDATE_CALLBACK,
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK,
ON_THREEPID_BIND_CALLBACK,
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
)
Expand Down Expand Up @@ -357,6 +359,12 @@ def register_third_party_rules_callbacks(
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = None,
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
on_add_user_third_party_identifier: Optional[
ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
] = None,
on_remove_user_third_party_identifier: Optional[
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
] = None,
) -> None:
"""Registers callbacks for third party event rules capabilities.

Expand All @@ -373,6 +381,8 @@ def register_third_party_rules_callbacks(
on_profile_update=on_profile_update,
on_user_deactivation_status_changed=on_user_deactivation_status_changed,
on_threepid_bind=on_threepid_bind,
on_add_user_third_party_identifier=on_add_user_third_party_identifier,
on_remove_user_third_party_identifier=on_remove_user_third_party_identifier,
)

def register_presence_router_callbacks(
Expand Down
11 changes: 9 additions & 2 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,13 +304,20 @@ async def on_PUT(
# remove old threepids
for medium, address in del_threepids:
try:
await self.auth_handler.delete_threepid(
user_id, medium, address, None
# Attempt to remove any known bindings of this third-party ID
# and user ID from identity servers.
await self.hs.get_identity_handler().try_unbind_threepid(
user_id, medium, address, id_server=None
)
except Exception:
logger.exception("Failed to remove threepids")
raise SynapseError(500, "Failed to remove threepids")

# Delete the local association of this user ID and third-party ID.
await self.auth_handler.delete_local_threepid(
user_id, medium, address
)

# add new threepids
current_time = self.hs.get_clock().time_msec()
for medium, address in add_threepids:
Expand Down
9 changes: 8 additions & 1 deletion synapse/rest/client/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,9 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
user_id = requester.user.to_string()

try:
ret = await self.auth_handler.delete_threepid(
# Attempt to remove any known bindings of this third-party ID
# and user ID from identity servers.
ret = await self.hs.get_identity_handler().try_unbind_threepid(
user_id, body.medium, body.address, body.id_server
)
except Exception:
Expand All @@ -783,6 +785,11 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
else:
id_server_unbind_result = "no-support"

# Delete the local association of this user ID and third-party ID.
await self.auth_handler.delete_local_threepid(
user_id, body.medium, body.address
)

return 200, {"id_server_unbind_result": id_server_unbind_result}


Expand Down
Loading