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

Commit 381e73e

Browse files
committed
Add 3pid unbind callback to module API
1 parent 2d82cda commit 381e73e

File tree

5 files changed

+179
-38
lines changed

5 files changed

+179
-38
lines changed

docs/modules/third_party_rules_callbacks.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,24 @@ server_.
265265

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

268+
### `on_threepid_unbind`
269+
270+
_First introduced in Synapse v1.63.0_
271+
272+
```python
273+
async def on_threepid_unbind(
274+
user_id: str, medium: str, address: str, identity_server: str
275+
) -> Tuple[bool, bool]:
276+
```
277+
278+
Called before a threepid association is removed.
279+
280+
It should return a tuple of 2 booleans reporting if a changed happened for the first, and if unbind
281+
needs to stop there for the second (True value). In this case no other module unbind will be
282+
called, and the default unbind made to the IS that was used on bind will also be skipped.
283+
In any case the mapping will be removed from the Synapse 3pid remote table, except if an Exception
284+
was raised at some point.
285+
268286
## Example
269287

270288
The example below is a module that implements the third-party rules callback

synapse/events/third_party_rules.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
ON_PROFILE_UPDATE_CALLBACK = Callable[[str, ProfileInfo, bool, bool], Awaitable]
4646
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK = Callable[[str, bool, bool], Awaitable]
4747
ON_THREEPID_BIND_CALLBACK = Callable[[str, str, str], Awaitable]
48+
ON_THREEPID_UNBIND_CALLBACK = Callable[
49+
[str, str, str, str], Awaitable[Tuple[bool, bool]]
50+
]
4851

4952

5053
def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
@@ -174,6 +177,7 @@ def __init__(self, hs: "HomeServer"):
174177
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
175178
] = []
176179
self._on_threepid_bind_callbacks: List[ON_THREEPID_BIND_CALLBACK] = []
180+
self._on_threepid_unbind_callbacks: List[ON_THREEPID_UNBIND_CALLBACK] = []
177181

178182
def register_third_party_rules_callbacks(
179183
self,
@@ -193,6 +197,7 @@ def register_third_party_rules_callbacks(
193197
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
194198
] = None,
195199
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
200+
on_threepid_unbind: Optional[ON_THREEPID_UNBIND_CALLBACK] = None,
196201
) -> None:
197202
"""Register callbacks from modules for each hook."""
198203
if check_event_allowed is not None:
@@ -230,6 +235,9 @@ def register_third_party_rules_callbacks(
230235
if on_threepid_bind is not None:
231236
self._on_threepid_bind_callbacks.append(on_threepid_bind)
232237

238+
if on_threepid_unbind is not None:
239+
self._on_threepid_unbind_callbacks.append(on_threepid_unbind)
240+
233241
async def check_event_allowed(
234242
self, event: EventBase, context: EventContext
235243
) -> Tuple[bool, Optional[dict]]:
@@ -523,3 +531,42 @@ async def on_threepid_bind(self, user_id: str, medium: str, address: str) -> Non
523531
logger.exception(
524532
"Failed to run module API callback %s: %s", callback, e
525533
)
534+
535+
async def on_threepid_unbind(
536+
self, user_id: str, medium: str, address: str, identity_server: str
537+
) -> Tuple[bool, bool]:
538+
"""Called before a threepid association is removed.
539+
540+
Note that this callback is called before an association is deleted on the
541+
local homeserver.
542+
543+
Args:
544+
user_id: the user being associated with the threepid.
545+
medium: the threepid's medium.
546+
address: the threepid's address.
547+
identity_server: the identity server where the threepid was successfully registered.
548+
549+
Returns:
550+
A tuple of 2 booleans reporting if a changed happened for the first, and if unbind
551+
needs to stop there for the second (True value). In this case no other module unbind will be
552+
called, and the default unbind made to the IS that was used on bind will also be skipped.
553+
In any case the mapping will be removed from the Synapse 3pid remote table, except if an Exception
554+
was raised at some point.
555+
"""
556+
557+
global_changed = True
558+
for callback in self._on_threepid_unbind_callbacks:
559+
try:
560+
(changed, stop) = await callback(
561+
user_id, medium, address, identity_server
562+
)
563+
global_changed &= changed
564+
if stop:
565+
return (global_changed, True)
566+
except Exception as e:
567+
logger.exception(
568+
"Failed to run module API callback %s: %s", callback, e
569+
)
570+
raise e
571+
572+
return (global_changed, False)

synapse/handlers/identity.py

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -294,49 +294,67 @@ async def try_unbind_threepid_with_id_server(
294294
server doesn't support unbinding
295295
"""
296296

297-
if not valid_id_server_location(id_server):
298-
raise SynapseError(
299-
400,
300-
"id_server must be a valid hostname with optional port and path components",
301-
)
297+
medium = threepid["medium"]
298+
address = threepid["address"]
299+
300+
(
301+
changed,
302+
stop,
303+
) = await self.hs.get_third_party_event_rules().on_threepid_unbind(
304+
mxid, medium, address, id_server
305+
)
302306

303-
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
304-
url_bytes = b"/_matrix/identity/api/v1/3pid/unbind"
307+
# If a module wants to take over unbind it will return stop = True,
308+
# in this case we should just purge the table from the 3pid record
309+
if not stop:
310+
if not valid_id_server_location(id_server):
311+
raise SynapseError(
312+
400,
313+
"id_server must be a valid hostname with optional port and path components",
314+
)
305315

306-
content = {
307-
"mxid": mxid,
308-
"threepid": {"medium": threepid["medium"], "address": threepid["address"]},
309-
}
316+
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
317+
url_bytes = b"/_matrix/identity/api/v1/3pid/unbind"
310318

311-
# we abuse the federation http client to sign the request, but we have to send it
312-
# using the normal http client since we don't want the SRV lookup and want normal
313-
# 'browser-like' HTTPS.
314-
auth_headers = self.federation_http_client.build_auth_headers(
315-
destination=None,
316-
method=b"POST",
317-
url_bytes=url_bytes,
318-
content=content,
319-
destination_is=id_server.encode("ascii"),
320-
)
321-
headers = {b"Authorization": auth_headers}
319+
content = {
320+
"mxid": mxid,
321+
"threepid": {
322+
"medium": threepid["medium"],
323+
"address": threepid["address"],
324+
},
325+
}
322326

323-
try:
324-
# Use the blacklisting http client as this call is only to identity servers
325-
# provided by a client
326-
await self.blacklisting_http_client.post_json_get_json(
327-
url, content, headers
327+
# we abuse the federation http client to sign the request, but we have to send it
328+
# using the normal http client since we don't want the SRV lookup and want normal
329+
# 'browser-like' HTTPS.
330+
auth_headers = self.federation_http_client.build_auth_headers(
331+
destination=None,
332+
method=b"POST",
333+
url_bytes=url_bytes,
334+
content=content,
335+
destination_is=id_server.encode("ascii"),
328336
)
329-
changed = True
330-
except HttpResponseException as e:
331-
changed = False
332-
if e.code in (400, 404, 501):
333-
# The remote server probably doesn't support unbinding (yet)
334-
logger.warning("Received %d response while unbinding threepid", e.code)
335-
else:
336-
logger.error("Failed to unbind threepid on identity server: %s", e)
337-
raise SynapseError(500, "Failed to contact identity server")
338-
except RequestTimedOutError:
339-
raise SynapseError(500, "Timed out contacting identity server")
337+
headers = {b"Authorization": auth_headers}
338+
339+
try:
340+
# Use the blacklisting http client as this call is only to identity servers
341+
# provided by a client
342+
await self.blacklisting_http_client.post_json_get_json(
343+
url, content, headers
344+
)
345+
changed &= True
346+
except HttpResponseException as e:
347+
changed &= False
348+
if e.code in (400, 404, 501):
349+
# The remote server probably doesn't support unbinding (yet)
350+
logger.warning(
351+
"Received %d response while unbinding threepid", e.code
352+
)
353+
else:
354+
logger.error("Failed to unbind threepid on identity server: %s", e)
355+
raise SynapseError(500, "Failed to contact identity server")
356+
except RequestTimedOutError:
357+
raise SynapseError(500, "Timed out contacting identity server")
340358

341359
await self.store.remove_user_bound_threepid(
342360
user_id=mxid,

synapse/module_api/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
ON_NEW_EVENT_CALLBACK,
6868
ON_PROFILE_UPDATE_CALLBACK,
6969
ON_THREEPID_BIND_CALLBACK,
70+
ON_THREEPID_UNBIND_CALLBACK,
7071
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
7172
)
7273
from synapse.handlers.account_data import ON_ACCOUNT_DATA_UPDATED_CALLBACK
@@ -317,6 +318,7 @@ def register_third_party_rules_callbacks(
317318
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
318319
] = None,
319320
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
321+
on_threepid_unbind: Optional[ON_THREEPID_UNBIND_CALLBACK] = None,
320322
) -> None:
321323
"""Registers callbacks for third party event rules capabilities.
322324
@@ -333,6 +335,7 @@ def register_third_party_rules_callbacks(
333335
on_profile_update=on_profile_update,
334336
on_user_deactivation_status_changed=on_user_deactivation_status_changed,
335337
on_threepid_bind=on_threepid_bind,
338+
on_threepid_unbind=on_threepid_unbind,
336339
)
337340

338341
def register_presence_router_callbacks(

tests/rest/client/test_third_party_rules.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,3 +937,58 @@ def test_on_threepid_bind(self) -> None:
937937

938938
# Check that the mock was called with the right parameters
939939
self.assertEqual(args, (user_id, "email", "foo@example.com"))
940+
941+
def test_on_threepid_unbind(self) -> None:
942+
"""Tests that the on_threepid_unbind module callback is called correctly before
943+
removing a 3PID mapping.
944+
"""
945+
# Register a mocked callback.
946+
threepid_unbind_mock = Mock(return_value=make_awaitable(None))
947+
third_party_rules = self.hs.get_third_party_event_rules()
948+
third_party_rules._on_threepid_unbind_callbacks.append(threepid_unbind_mock)
949+
950+
# Register an admin user.
951+
self.register_user("admin", "password", admin=True)
952+
admin_tok = self.login("admin", "password")
953+
954+
# Also register a normal user we can modify.
955+
user_id = self.register_user("user", "password")
956+
957+
# Add a 3PID to the user.
958+
channel = self.make_request(
959+
"PUT",
960+
"/_synapse/admin/v2/users/%s" % user_id,
961+
{
962+
"threepids": [
963+
{
964+
"medium": "email",
965+
"address": "foo@example.com",
966+
},
967+
],
968+
},
969+
access_token=admin_tok,
970+
)
971+
self.assertEqual(channel.code, 200, channel.json_body)
972+
973+
# Remove the 3PID mapping.
974+
channel = self.make_request(
975+
"DELETE",
976+
"/_synapse/admin/v2/users/%s" % user_id,
977+
{
978+
"threepids": [
979+
{
980+
"medium": "email",
981+
"address": "foo@example.com",
982+
},
983+
],
984+
},
985+
access_token=admin_tok,
986+
)
987+
self.assertEqual(channel.code, 200, channel.json_body)
988+
989+
# Check that the mock was called once.
990+
threepid_unbind_mock.assert_called_once()
991+
args = threepid_unbind_mock.call_args[0]
992+
993+
# Check that the mock was called with the right parameters
994+
self.assertEqual(args, (user_id, "email", "foo@example.com"))

0 commit comments

Comments
 (0)