forked from MirrorHub/synapse
Add support for MSC2697: Dehydrated devices (#8380)
This allows a user to store an offline device on the server and then restore it at a subsequent login.
This commit is contained in:
parent
43c622885c
commit
4cb44a1585
9 changed files with 454 additions and 21 deletions
1
changelog.d/8380.feature
Normal file
1
changelog.d/8380.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add support for device dehydration ([MSC2697](https://github.com/matrix-org/matrix-doc/pull/2697)).
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2016 OpenMarket Ltd
|
# Copyright 2016 OpenMarket Ltd
|
||||||
# Copyright 2019 New Vector Ltd
|
# Copyright 2019 New Vector Ltd
|
||||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
# Copyright 2019,2020 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from synapse.api import errors
|
from synapse.api import errors
|
||||||
from synapse.api.constants import EventTypes
|
from synapse.api.constants import EventTypes
|
||||||
|
@ -29,6 +29,7 @@ from synapse.api.errors import (
|
||||||
from synapse.logging.opentracing import log_kv, set_tag, trace
|
from synapse.logging.opentracing import log_kv, set_tag, trace
|
||||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||||
from synapse.types import (
|
from synapse.types import (
|
||||||
|
JsonDict,
|
||||||
StreamToken,
|
StreamToken,
|
||||||
get_domain_from_id,
|
get_domain_from_id,
|
||||||
get_verify_key_from_cross_signing_key,
|
get_verify_key_from_cross_signing_key,
|
||||||
|
@ -505,6 +506,85 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||||
# receive device updates. Mark this in DB.
|
# receive device updates. Mark this in DB.
|
||||||
await self.store.mark_remote_user_device_list_as_unsubscribed(user_id)
|
await self.store.mark_remote_user_device_list_as_unsubscribed(user_id)
|
||||||
|
|
||||||
|
async def store_dehydrated_device(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
device_data: JsonDict,
|
||||||
|
initial_device_display_name: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Store a dehydrated device for a user. If the user had a previous
|
||||||
|
dehydrated device, it is removed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user that we are storing the device for
|
||||||
|
device_data: the dehydrated device information
|
||||||
|
initial_device_display_name: The display name to use for the device
|
||||||
|
Returns:
|
||||||
|
device id of the dehydrated device
|
||||||
|
"""
|
||||||
|
device_id = await self.check_device_registered(
|
||||||
|
user_id, None, initial_device_display_name,
|
||||||
|
)
|
||||||
|
old_device_id = await self.store.store_dehydrated_device(
|
||||||
|
user_id, device_id, device_data
|
||||||
|
)
|
||||||
|
if old_device_id is not None:
|
||||||
|
await self.delete_device(user_id, old_device_id)
|
||||||
|
return device_id
|
||||||
|
|
||||||
|
async def get_dehydrated_device(
|
||||||
|
self, user_id: str
|
||||||
|
) -> Optional[Tuple[str, JsonDict]]:
|
||||||
|
"""Retrieve the information for a dehydrated device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user whose dehydrated device we are looking for
|
||||||
|
Returns:
|
||||||
|
a tuple whose first item is the device ID, and the second item is
|
||||||
|
the dehydrated device information
|
||||||
|
"""
|
||||||
|
return await self.store.get_dehydrated_device(user_id)
|
||||||
|
|
||||||
|
async def rehydrate_device(
|
||||||
|
self, user_id: str, access_token: str, device_id: str
|
||||||
|
) -> dict:
|
||||||
|
"""Process a rehydration request from the user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user who is rehydrating the device
|
||||||
|
access_token: the access token used for the request
|
||||||
|
device_id: the ID of the device that will be rehydrated
|
||||||
|
Returns:
|
||||||
|
a dict containing {"success": True}
|
||||||
|
"""
|
||||||
|
success = await self.store.remove_dehydrated_device(user_id, device_id)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise errors.NotFoundError()
|
||||||
|
|
||||||
|
# If the dehydrated device was successfully deleted (the device ID
|
||||||
|
# matched the stored dehydrated device), then modify the access
|
||||||
|
# token to use the dehydrated device's ID and copy the old device
|
||||||
|
# display name to the dehydrated device, and destroy the old device
|
||||||
|
# ID
|
||||||
|
old_device_id = await self.store.set_device_for_access_token(
|
||||||
|
access_token, device_id
|
||||||
|
)
|
||||||
|
old_device = await self.store.get_device(user_id, old_device_id)
|
||||||
|
await self.store.update_device(user_id, device_id, old_device["display_name"])
|
||||||
|
# can't call self.delete_device because that will clobber the
|
||||||
|
# access token so call the storage layer directly
|
||||||
|
await self.store.delete_device(user_id, old_device_id)
|
||||||
|
await self.store.delete_e2e_keys_by_device(
|
||||||
|
user_id=user_id, device_id=old_device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# tell everyone that the old device is gone and that the dehydrated
|
||||||
|
# device has a new display name
|
||||||
|
await self.notify_device_update(user_id, [old_device_id, device_id])
|
||||||
|
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
def _update_device_from_client_ips(device, client_ips):
|
def _update_device_from_client_ips(device, client_ips):
|
||||||
ip = client_ips.get((device["user_id"], device["device_id"]), {})
|
ip = client_ips.get((device["user_id"], device["device_id"]), {})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
# Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -21,6 +22,7 @@ from synapse.http.servlet import (
|
||||||
assert_params_in_dict,
|
assert_params_in_dict,
|
||||||
parse_json_object_from_request,
|
parse_json_object_from_request,
|
||||||
)
|
)
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
|
|
||||||
from ._base import client_patterns, interactive_auth_handler
|
from ._base import client_patterns, interactive_auth_handler
|
||||||
|
|
||||||
|
@ -151,7 +153,139 @@ class DeviceRestServlet(RestServlet):
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
|
||||||
|
|
||||||
|
class DehydratedDeviceServlet(RestServlet):
|
||||||
|
"""Retrieve or store a dehydrated device.
|
||||||
|
|
||||||
|
GET /org.matrix.msc2697.v2/dehydrated_device
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_id": "dehydrated_device_id",
|
||||||
|
"device_data": {
|
||||||
|
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
|
||||||
|
"account": "dehydrated_device"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PUT /org.matrix.msc2697/dehydrated_device
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_data": {
|
||||||
|
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
|
||||||
|
"account": "dehydrated_device"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_id": "dehydrated_device_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = client_patterns("/org.matrix.msc2697.v2/dehydrated_device", releases=())
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super().__init__()
|
||||||
|
self.hs = hs
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.device_handler = hs.get_device_handler()
|
||||||
|
|
||||||
|
async def on_GET(self, request: SynapseRequest):
|
||||||
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
dehydrated_device = await self.device_handler.get_dehydrated_device(
|
||||||
|
requester.user.to_string()
|
||||||
|
)
|
||||||
|
if dehydrated_device is not None:
|
||||||
|
(device_id, device_data) = dehydrated_device
|
||||||
|
result = {"device_id": device_id, "device_data": device_data}
|
||||||
|
return (200, result)
|
||||||
|
else:
|
||||||
|
raise errors.NotFoundError("No dehydrated device available")
|
||||||
|
|
||||||
|
async def on_PUT(self, request: SynapseRequest):
|
||||||
|
submission = parse_json_object_from_request(request)
|
||||||
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
|
if "device_data" not in submission:
|
||||||
|
raise errors.SynapseError(
|
||||||
|
400, "device_data missing", errcode=errors.Codes.MISSING_PARAM,
|
||||||
|
)
|
||||||
|
elif not isinstance(submission["device_data"], dict):
|
||||||
|
raise errors.SynapseError(
|
||||||
|
400,
|
||||||
|
"device_data must be an object",
|
||||||
|
errcode=errors.Codes.INVALID_PARAM,
|
||||||
|
)
|
||||||
|
|
||||||
|
device_id = await self.device_handler.store_dehydrated_device(
|
||||||
|
requester.user.to_string(),
|
||||||
|
submission["device_data"],
|
||||||
|
submission.get("initial_device_display_name", None),
|
||||||
|
)
|
||||||
|
return 200, {"device_id": device_id}
|
||||||
|
|
||||||
|
|
||||||
|
class ClaimDehydratedDeviceServlet(RestServlet):
|
||||||
|
"""Claim a dehydrated device.
|
||||||
|
|
||||||
|
POST /org.matrix.msc2697.v2/dehydrated_device/claim
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_id": "dehydrated_device_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = client_patterns(
|
||||||
|
"/org.matrix.msc2697.v2/dehydrated_device/claim", releases=()
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super().__init__()
|
||||||
|
self.hs = hs
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.device_handler = hs.get_device_handler()
|
||||||
|
|
||||||
|
async def on_POST(self, request: SynapseRequest):
|
||||||
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
|
submission = parse_json_object_from_request(request)
|
||||||
|
|
||||||
|
if "device_id" not in submission:
|
||||||
|
raise errors.SynapseError(
|
||||||
|
400, "device_id missing", errcode=errors.Codes.MISSING_PARAM,
|
||||||
|
)
|
||||||
|
elif not isinstance(submission["device_id"], str):
|
||||||
|
raise errors.SynapseError(
|
||||||
|
400, "device_id must be a string", errcode=errors.Codes.INVALID_PARAM,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.device_handler.rehydrate_device(
|
||||||
|
requester.user.to_string(),
|
||||||
|
self.auth.get_access_token_from_request(request),
|
||||||
|
submission["device_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (200, result)
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs, http_server):
|
def register_servlets(hs, http_server):
|
||||||
DeleteDevicesRestServlet(hs).register(http_server)
|
DeleteDevicesRestServlet(hs).register(http_server)
|
||||||
DevicesRestServlet(hs).register(http_server)
|
DevicesRestServlet(hs).register(http_server)
|
||||||
DeviceRestServlet(hs).register(http_server)
|
DeviceRestServlet(hs).register(http_server)
|
||||||
|
DehydratedDeviceServlet(hs).register(http_server)
|
||||||
|
ClaimDehydratedDeviceServlet(hs).register(http_server)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
# Copyright 2019 New Vector Ltd
|
# Copyright 2019 New Vector Ltd
|
||||||
|
# Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -67,6 +68,7 @@ class KeyUploadServlet(RestServlet):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.e2e_keys_handler = hs.get_e2e_keys_handler()
|
self.e2e_keys_handler = hs.get_e2e_keys_handler()
|
||||||
|
self.device_handler = hs.get_device_handler()
|
||||||
|
|
||||||
@trace(opname="upload_keys")
|
@trace(opname="upload_keys")
|
||||||
async def on_POST(self, request, device_id):
|
async def on_POST(self, request, device_id):
|
||||||
|
@ -75,23 +77,28 @@ class KeyUploadServlet(RestServlet):
|
||||||
body = parse_json_object_from_request(request)
|
body = parse_json_object_from_request(request)
|
||||||
|
|
||||||
if device_id is not None:
|
if device_id is not None:
|
||||||
# passing the device_id here is deprecated; however, we allow it
|
# Providing the device_id should only be done for setting keys
|
||||||
# for now for compatibility with older clients.
|
# for dehydrated devices; however, we allow it for any device for
|
||||||
|
# compatibility with older clients.
|
||||||
if requester.device_id is not None and device_id != requester.device_id:
|
if requester.device_id is not None and device_id != requester.device_id:
|
||||||
set_tag("error", True)
|
dehydrated_device = await self.device_handler.get_dehydrated_device(
|
||||||
log_kv(
|
user_id
|
||||||
{
|
|
||||||
"message": "Client uploading keys for a different device",
|
|
||||||
"logged_in_id": requester.device_id,
|
|
||||||
"key_being_uploaded": device_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
logger.warning(
|
|
||||||
"Client uploading keys for a different device "
|
|
||||||
"(logged in as %s, uploading for %s)",
|
|
||||||
requester.device_id,
|
|
||||||
device_id,
|
|
||||||
)
|
)
|
||||||
|
if dehydrated_device is not None and device_id != dehydrated_device[0]:
|
||||||
|
set_tag("error", True)
|
||||||
|
log_kv(
|
||||||
|
{
|
||||||
|
"message": "Client uploading keys for a different device",
|
||||||
|
"logged_in_id": requester.device_id,
|
||||||
|
"key_being_uploaded": device_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
"Client uploading keys for a different device "
|
||||||
|
"(logged in as %s, uploading for %s)",
|
||||||
|
requester.device_id,
|
||||||
|
device_id,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
device_id = requester.device_id
|
device_id = requester.device_id
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2016 OpenMarket Ltd
|
# Copyright 2016 OpenMarket Ltd
|
||||||
# Copyright 2019 New Vector Ltd
|
# Copyright 2019 New Vector Ltd
|
||||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
# Copyright 2019,2020 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -33,7 +33,7 @@ from synapse.storage.database import (
|
||||||
make_tuple_comparison_clause,
|
make_tuple_comparison_clause,
|
||||||
)
|
)
|
||||||
from synapse.types import Collection, JsonDict, get_verify_key_from_cross_signing_key
|
from synapse.types import Collection, JsonDict, get_verify_key_from_cross_signing_key
|
||||||
from synapse.util import json_encoder
|
from synapse.util import json_decoder, json_encoder
|
||||||
from synapse.util.caches.descriptors import Cache, cached, cachedList
|
from synapse.util.caches.descriptors import Cache, cached, cachedList
|
||||||
from synapse.util.iterutils import batch_iter
|
from synapse.util.iterutils import batch_iter
|
||||||
from synapse.util.stringutils import shortstr
|
from synapse.util.stringutils import shortstr
|
||||||
|
@ -698,6 +698,80 @@ class DeviceWorkerStore(SQLBaseStore):
|
||||||
_mark_remote_user_device_list_as_unsubscribed_txn,
|
_mark_remote_user_device_list_as_unsubscribed_txn,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_dehydrated_device(
|
||||||
|
self, user_id: str
|
||||||
|
) -> Optional[Tuple[str, JsonDict]]:
|
||||||
|
"""Retrieve the information for a dehydrated device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user whose dehydrated device we are looking for
|
||||||
|
Returns:
|
||||||
|
a tuple whose first item is the device ID, and the second item is
|
||||||
|
the dehydrated device information
|
||||||
|
"""
|
||||||
|
# FIXME: make sure device ID still exists in devices table
|
||||||
|
row = await self.db_pool.simple_select_one(
|
||||||
|
table="dehydrated_devices",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
retcols=["device_id", "device_data"],
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
(row["device_id"], json_decoder.decode(row["device_data"])) if row else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_dehydrated_device_txn(
|
||||||
|
self, txn, user_id: str, device_id: str, device_data: str
|
||||||
|
) -> Optional[str]:
|
||||||
|
old_device_id = self.db_pool.simple_select_one_onecol_txn(
|
||||||
|
txn,
|
||||||
|
table="dehydrated_devices",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
retcol="device_id",
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
self.db_pool.simple_upsert_txn(
|
||||||
|
txn,
|
||||||
|
table="dehydrated_devices",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
values={"device_id": device_id, "device_data": device_data},
|
||||||
|
)
|
||||||
|
return old_device_id
|
||||||
|
|
||||||
|
async def store_dehydrated_device(
|
||||||
|
self, user_id: str, device_id: str, device_data: JsonDict
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Store a dehydrated device for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user that we are storing the device for
|
||||||
|
device_id: the ID of the dehydrated device
|
||||||
|
device_data: the dehydrated device information
|
||||||
|
Returns:
|
||||||
|
device id of the user's previous dehydrated device, if any
|
||||||
|
"""
|
||||||
|
return await self.db_pool.runInteraction(
|
||||||
|
"store_dehydrated_device_txn",
|
||||||
|
self._store_dehydrated_device_txn,
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
json_encoder.encode(device_data),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def remove_dehydrated_device(self, user_id: str, device_id: str) -> bool:
|
||||||
|
"""Remove a dehydrated device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user that the dehydrated device belongs to
|
||||||
|
device_id: the ID of the dehydrated device
|
||||||
|
"""
|
||||||
|
count = await self.db_pool.simple_delete(
|
||||||
|
"dehydrated_devices",
|
||||||
|
{"user_id": user_id, "device_id": device_id},
|
||||||
|
desc="remove_dehydrated_device",
|
||||||
|
)
|
||||||
|
return count >= 1
|
||||||
|
|
||||||
|
|
||||||
class DeviceBackgroundUpdateStore(SQLBaseStore):
|
class DeviceBackgroundUpdateStore(SQLBaseStore):
|
||||||
def __init__(self, database: DatabasePool, db_conn, hs):
|
def __init__(self, database: DatabasePool, db_conn, hs):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
# Copyright 2019 New Vector Ltd
|
# Copyright 2019 New Vector Ltd
|
||||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
# Copyright 2019,2020 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -844,6 +844,11 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore):
|
||||||
self._invalidate_cache_and_stream(
|
self._invalidate_cache_and_stream(
|
||||||
txn, self.count_e2e_one_time_keys, (user_id, device_id)
|
txn, self.count_e2e_one_time_keys, (user_id, device_id)
|
||||||
)
|
)
|
||||||
|
self.db_pool.simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="dehydrated_devices",
|
||||||
|
keyvalues={"user_id": user_id, "device_id": device_id},
|
||||||
|
)
|
||||||
self.db_pool.simple_delete_txn(
|
self.db_pool.simple_delete_txn(
|
||||||
txn,
|
txn,
|
||||||
table="e2e_fallback_keys_json",
|
table="e2e_fallback_keys_json",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
# Copyright 2017-2018 New Vector Ltd
|
# Copyright 2017-2018 New Vector Ltd
|
||||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
# Copyright 2019,2020 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -964,6 +964,36 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
||||||
desc="add_access_token_to_user",
|
desc="add_access_token_to_user",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _set_device_for_access_token_txn(self, txn, token: str, device_id: str) -> str:
|
||||||
|
old_device_id = self.db_pool.simple_select_one_onecol_txn(
|
||||||
|
txn, "access_tokens", {"token": token}, "device_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db_pool.simple_update_txn(
|
||||||
|
txn, "access_tokens", {"token": token}, {"device_id": device_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
self._invalidate_cache_and_stream(txn, self.get_user_by_access_token, (token,))
|
||||||
|
|
||||||
|
return old_device_id
|
||||||
|
|
||||||
|
async def set_device_for_access_token(self, token: str, device_id: str) -> str:
|
||||||
|
"""Sets the device ID associated with an access token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: The access token to modify.
|
||||||
|
device_id: The new device ID.
|
||||||
|
Returns:
|
||||||
|
The old device ID associated with the access token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return await self.db_pool.runInteraction(
|
||||||
|
"set_device_for_access_token",
|
||||||
|
self._set_device_for_access_token_txn,
|
||||||
|
token,
|
||||||
|
device_id,
|
||||||
|
)
|
||||||
|
|
||||||
async def register_user(
|
async def register_user(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* Copyright 2020 The Matrix.org Foundation C.I.C
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dehydrated_devices(
|
||||||
|
user_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
device_id TEXT NOT NULL,
|
||||||
|
device_data TEXT NOT NULL -- JSON-encoded client-defined data
|
||||||
|
);
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2016 OpenMarket Ltd
|
# Copyright 2016 OpenMarket Ltd
|
||||||
# Copyright 2018 New Vector Ltd
|
# Copyright 2018 New Vector Ltd
|
||||||
|
# Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -224,3 +225,84 @@ class DeviceTestCase(unittest.HomeserverTestCase):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.reactor.advance(1000)
|
self.reactor.advance(1000)
|
||||||
|
|
||||||
|
|
||||||
|
class DehydrationTestCase(unittest.HomeserverTestCase):
|
||||||
|
def make_homeserver(self, reactor, clock):
|
||||||
|
hs = self.setup_test_homeserver("server", http_client=None)
|
||||||
|
self.handler = hs.get_device_handler()
|
||||||
|
self.registration = hs.get_registration_handler()
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
return hs
|
||||||
|
|
||||||
|
def test_dehydrate_and_rehydrate_device(self):
|
||||||
|
user_id = "@boris:dehydration"
|
||||||
|
|
||||||
|
self.get_success(self.store.register_user(user_id, "foobar"))
|
||||||
|
|
||||||
|
# First check if we can store and fetch a dehydrated device
|
||||||
|
stored_dehydrated_device_id = self.get_success(
|
||||||
|
self.handler.store_dehydrated_device(
|
||||||
|
user_id=user_id,
|
||||||
|
device_data={"device_data": {"foo": "bar"}},
|
||||||
|
initial_device_display_name="dehydrated device",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
retrieved_device_id, device_data = self.get_success(
|
||||||
|
self.handler.get_dehydrated_device(user_id=user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(retrieved_device_id, stored_dehydrated_device_id)
|
||||||
|
self.assertEqual(device_data, {"device_data": {"foo": "bar"}})
|
||||||
|
|
||||||
|
# Create a new login for the user and dehydrated the device
|
||||||
|
device_id, access_token = self.get_success(
|
||||||
|
self.registration.register_device(
|
||||||
|
user_id=user_id, device_id=None, initial_display_name="new device",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trying to claim a nonexistent device should throw an error
|
||||||
|
self.get_failure(
|
||||||
|
self.handler.rehydrate_device(
|
||||||
|
user_id=user_id,
|
||||||
|
access_token=access_token,
|
||||||
|
device_id="not the right device ID",
|
||||||
|
),
|
||||||
|
synapse.api.errors.NotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
|
# dehydrating the right devices should succeed and change our device ID
|
||||||
|
# to the dehydrated device's ID
|
||||||
|
res = self.get_success(
|
||||||
|
self.handler.rehydrate_device(
|
||||||
|
user_id=user_id,
|
||||||
|
access_token=access_token,
|
||||||
|
device_id=retrieved_device_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(res, {"success": True})
|
||||||
|
|
||||||
|
# make sure that our device ID has changed
|
||||||
|
user_info = self.get_success(self.auth.get_user_by_access_token(access_token))
|
||||||
|
|
||||||
|
self.assertEqual(user_info["device_id"], retrieved_device_id)
|
||||||
|
|
||||||
|
# make sure the device has the display name that was set from the login
|
||||||
|
res = self.get_success(self.handler.get_device(user_id, retrieved_device_id))
|
||||||
|
|
||||||
|
self.assertEqual(res["display_name"], "new device")
|
||||||
|
|
||||||
|
# make sure that the device ID that we were initially assigned no longer exists
|
||||||
|
self.get_failure(
|
||||||
|
self.handler.get_device(user_id, device_id),
|
||||||
|
synapse.api.errors.NotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
|
# make sure that there's no device available for dehydrating now
|
||||||
|
ret = self.get_success(self.handler.get_dehydrated_device(user_id=user_id))
|
||||||
|
|
||||||
|
self.assertIsNone(ret)
|
||||||
|
|
Loading…
Reference in a new issue