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

Implement MSC1915 - 3PID unbind APIs #4982

Merged
merged 9 commits into from
Apr 3, 2019
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/4982.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Track which identity server is used when binding a threepid and use that for unbinding, as per MSC1915.
7 changes: 6 additions & 1 deletion synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,14 +912,18 @@ def add_threepid(self, user_id, medium, address, validated_at):
)

@defer.inlineCallbacks
def delete_threepid(self, user_id, medium, address):
def delete_threepid(self, user_id, medium, address, id_server=None):
"""Attempts to unbind the 3pid on the identity servers and deletes it
from the local database.

Args:
user_id (str)
medium (str)
address (str)
id_server (str|None): 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:
Deferred[bool]: Returns True if successfully unbound the 3pid on
Expand All @@ -937,6 +941,7 @@ def delete_threepid(self, user_id, medium, address):
{
'medium': medium,
'address': address,
'id_server': id_server,
},
)

Expand Down
6 changes: 5 additions & 1 deletion synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ def __init__(self, hs):
hs.get_reactor().callWhenRunning(self._start_user_parting)

@defer.inlineCallbacks
def deactivate_account(self, user_id, erase_data):
def deactivate_account(self, user_id, erase_data, id_server=None):
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
"""Deactivate a user's account

Args:
user_id (str): ID of user to be deactivated
erase_data (bool): whether to GDPR-erase the user's data
id_server (str|None): 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:
Deferred[bool]: True if identity server supports removing
Expand All @@ -74,6 +77,7 @@ def deactivate_account(self, user_id, erase_data):
{
'medium': threepid['medium'],
'address': threepid['address'],
'id_server': id_server,
},
)
identity_server_supports_unbinding &= result
Expand Down
73 changes: 60 additions & 13 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ def bind_threepid(self, creds, mxid):
}
)
logger.debug("bound threepid %r to %s", creds, mxid)

# Remember where we bound the threepid
yield self.store.add_user_bound_threepid(
user_id=mxid,
medium=data["medium"],
address=data["address"],
id_server=id_server,
)
except CodeMessageException as e:
data = json.loads(e.msg) # XXX WAT?
defer.returnValue(data)
Expand All @@ -142,30 +150,61 @@ def try_unbind_threepid(self, mxid, threepid):

Args:
mxid (str): Matrix user ID of binding to be removed
threepid (dict): Dict with medium & address of binding to be removed
threepid (dict): Dict with medium & address of binding to be
removed, and an optional id_server.

Raises:
SynapseError: If we failed to contact the identity server

Returns:
Deferred[bool]: True on success, otherwise False if the identity
server doesn't support unbinding
server doesn't support unbinding (or no identity server found to
contact).
"""
logger.debug("unbinding threepid %r from %s", threepid, mxid)
if not self.trusted_id_servers:
logger.warn("Can't unbind threepid: no trusted ID servers set in config")
if threepid.get("id_server"):
Copy link
Member

Choose a reason for hiding this comment

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

why is this in the dict rather than a function param? (and anyway, the docstring needs updating)

Copy link
Member Author

Choose a reason for hiding this comment

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

Broadly because this is how its handled in the bind path where the threepid dict contains the identity server. I don't mind pulling it out but then we probably want to also pull out medium and address

id_servers = [threepid["id_server"]]
else:
id_servers = yield self.store.get_id_servers_user_bound(
user_id=mxid,
medium=threepid["medium"],
address=threepid["address"],
)

# We don't know where to unbind, so we don't have a choice but to return
if not id_servers:
defer.returnValue(False)
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved

# We don't track what ID server we added 3pids on (perhaps we ought to)
# but we assume that any of the servers in the trusted list are in the
# same ID server federation, so we can pick any one of them to send the
# deletion request to.
id_server = next(iter(self.trusted_id_servers))
changed = True
for id_server in id_servers:
changed &= yield self.try_unbind_threepid_with_id_server(
mxid, threepid, id_server,
)

defer.returnValue(changed)

@defer.inlineCallbacks
def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server):
"""Removes a binding from an identity server

Args:
mxid (str): Matrix user ID of binding to be removed
threepid (dict): Dict with medium & address of binding to be removed
id_server (str): Identity server to unbind from

Raises:
SynapseError: If we failed to contact the identity server

Returns:
Deferred[bool]: True on success, otherwise False if the identity
server doesn't support unbinding
"""
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
content = {
"mxid": mxid,
"threepid": threepid,
"threepid": {
"medium": threepid["medium"],
"address": threepid["address"],
},
}

# we abuse the federation http client to sign the request, but we have to send it
Expand All @@ -188,16 +227,24 @@ def try_unbind_threepid(self, mxid, threepid):
content,
headers,
)
changed = True
except HttpResponseException as e:
changed = False
if e.code in (400, 404, 501,):
# The remote server probably doesn't support unbinding (yet)
logger.warn("Received %d response while unbinding threepid", e.code)
defer.returnValue(False)
else:
logger.error("Failed to unbind threepid on identity server: %s", e)
raise SynapseError(502, "Failed to contact identity server")

defer.returnValue(True)
yield self.store.remove_user_bound_threepid(
user_id=mxid,
medium=threepid["medium"],
address=threepid["address"],
id_server=id_server,
)

defer.returnValue(changed)

@defer.inlineCallbacks
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
Expand Down
5 changes: 3 additions & 2 deletions synapse/rest/client/v2_alpha/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def on_POST(self, request):
)
result = yield self._deactivate_account_handler.deactivate_account(
requester.user.to_string(), erase,
id_server=body.get("id_server"),
)
if result:
id_server_unbind_result = "success"
Expand Down Expand Up @@ -363,7 +364,7 @@ def on_POST(self, request):


class ThreepidDeleteRestServlet(RestServlet):
PATTERNS = client_v2_patterns("/account/3pid/delete$", releases=())
PATTERNS = client_v2_patterns("/account/3pid/delete$")

def __init__(self, hs):
super(ThreepidDeleteRestServlet, self).__init__()
Expand All @@ -380,7 +381,7 @@ def on_POST(self, request):

try:
ret = yield self.auth_handler.delete_threepid(
user_id, body['medium'], body['address']
user_id, body['medium'], body['address'], body.get("id_server"),
)
except Exception:
# NB. This endpoint should succeed if there is nothing to
Expand Down
112 changes: 112 additions & 0 deletions synapse/storage/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,83 @@ def user_delete_threepid(self, user_id, medium, address):
desc="user_delete_threepids",
)

def add_user_bound_threepid(self, user_id, medium, address, id_server):
"""The server proxied a bind request to the given identity server on
behalf of the given user. We need to remember this in case the user
asks us to unbind the threepid.

Args:
user_id (str)
medium (str)
address (str)
id_server (str)

Returns:
Deferred
"""
# We need to use an upsert, in case they user had already bound the
# threepid
return self._simple_upsert(
table="user_threepid_id_server",
keyvalues={
"user_id": user_id,
"medium": medium,
"address": address,
"id_server": id_server,
},
values={},
insertion_values={},
desc="add_user_bound_threepid",
)

def remove_user_bound_threepid(self, user_id, medium, address, id_server):
"""The server proxied an unbind request to the given identity server on
behalf of the given user, so we remove the mapping of threepid to
identity server.

Args:
user_id (str)
medium (str)
address (str)
id_server (str)

Returns:
Deferred
"""
return self._simple_delete(
table="user_threepid_id_server",
keyvalues={
"user_id": user_id,
"medium": medium,
"address": address,
"id_server": id_server,
},
desc="remove_user_bound_threepid",
)

def get_id_servers_user_bound(self, user_id, medium, address):
"""Get the list of identity servers that the server proxied bind
requests to for given user and threepid

Args:
user_id (str)
medium (str)
address (str)

Returns:
Deferred[list[str]]: Resolves to a list of identity servers
"""
return self._simple_select_onecol(
table="user_threepid_id_server",
keyvalues={
"user_id": user_id,
"medium": medium,
"address": address,
},
retcol="id_server",
desc="get_id_servers_user_bound",
)


class RegistrationStore(RegistrationWorkerStore,
background_updates.BackgroundUpdateStore):
Expand Down Expand Up @@ -356,6 +433,10 @@ def __init__(self, db_conn, hs):
# clear the background update.
self.register_noop_background_update("refresh_tokens_device_index")

self.register_background_update_handler(
"user_threepids_grandfather", self._bg_user_threepids_grandfather,
)

@defer.inlineCallbacks
def add_access_token_to_user(self, user_id, token, device_id=None):
"""Adds an access token for the given user.
Expand Down Expand Up @@ -738,3 +819,34 @@ def get_user_pending_deactivation(self):
allow_none=True,
desc="get_users_pending_deactivation",
)

@defer.inlineCallbacks
def _bg_user_threepids_grandfather(self, progress, batch_size):
"""We now track which identity servers a user binds their 3PID to, so
we need to handle the case of existing bindings where we didn't track
this.

We do this by grandfathering in existing user threepids assuming that
they used one of the server configured trusted identity servers.
Copy link
Member

Choose a reason for hiding this comment

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

one of, or all of? (surely it should be one of, but the code does all of)

Copy link
Member Author

Choose a reason for hiding this comment

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

We assume that they used "one of", but we don't know which one so we have to include all the id servers.

"""

id_servers = set(self.config.trusted_third_party_id_servers)

def _bg_user_threepids_grandfather_txn(txn):
sql = """
INSERT INTO user_threepid_id_server
(user_id, medium, address, id_server)
SELECT user_id, medium, address, ?
FROM user_threepids
"""

txn.executemany(sql, [(id_server,) for id_server in id_servers])

if id_servers:
yield self.runInteraction(
"_bg_user_threepids_grandfather", _bg_user_threepids_grandfather_txn,
)

yield self._end_background_update("user_threepids_grandfather")

defer.returnValue(1)
29 changes: 29 additions & 0 deletions synapse/storage/schema/delta/53/user_threepid_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

-- Tracks which identity server a user bound their threepid via.
CREATE TABLE user_threepid_id_server (
user_id TEXT NOT NULL,
medium TEXT NOT NULL,
address TEXT NOT NULL,
id_server TEXT NOT NULL
);

CREATE UNIQUE INDEX user_threepid_id_server_idx ON user_threepid_id_server(
user_id, medium, address, id_server
);

INSERT INTO background_updates (update_name, progress_json) VALUES
('user_threepids_grandfather', '{}');