mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-28 02:54:50 +00:00
Don't delete the dehydrated device when MAS syncs the device list
The modern MAS device-sync endpoint (_synapse/mas/sync_devices) reconciled the user's device list against the set of devices MAS knows about, but the dehydrated device (MSC3814) has no MAS session and so was never in that set. It was therefore treated as an extra device and deleted on every sync — in particular when a user logged out their last device — which broke offline key delivery via the dehydrated device. Exclude the dehydrated device from the reconciliation, mirroring the admin devices API (which flags it) and MAS's own legacy sync path (which filters it out).
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Fix a bug where a user's dehydrated device (MSC3814) was deleted when their device list was synced from MAS (e.g. on logging out their last device), breaking offline key delivery.
|
||||
@@ -196,6 +196,16 @@ class MasSyncDevicesResource(MasBaseResource):
|
||||
current_devices_list = set(current_devices.keys())
|
||||
target_device_list = set(body.devices)
|
||||
|
||||
# Exclude the dehydrated device (MSC3814): it has no MAS session, so MAS
|
||||
# never lists it in the target set and the reconciliation below would
|
||||
# otherwise treat it as extra and delete it. This mirrors the admin
|
||||
# devices API and MAS's own legacy device-sync path, which both skip it.
|
||||
dehydrated_device = await self.device_handler.get_dehydrated_device(
|
||||
user_id=str(user_id)
|
||||
)
|
||||
if dehydrated_device is not None:
|
||||
current_devices_list.discard(dehydrated_device[0])
|
||||
|
||||
to_add = target_device_list - current_devices_list
|
||||
to_delete = current_devices_list - target_device_list
|
||||
|
||||
|
||||
@@ -546,6 +546,48 @@ class MasSyncDevicesResource(BaseTestCase):
|
||||
devices = self.get_success(store.get_devices_by_user(str(self.alice_user_id)))
|
||||
self.assertEqual(set(devices.keys()), {"DEVICE1"})
|
||||
|
||||
def test_sync_devices_preserves_dehydrated_device(self) -> None:
|
||||
# Store a dehydrated device (MSC3814). It has no MAS session, so it is
|
||||
# never part of the target device list, but it must survive a sync.
|
||||
device_handler = self.hs.get_device_handler()
|
||||
dehydrated_device_id = self.get_success(
|
||||
device_handler.store_dehydrated_device(
|
||||
user_id=str(self.alice_user_id),
|
||||
device_id=None,
|
||||
device_data={"device_data": {"foo": "bar"}},
|
||||
initial_device_display_name="dehydrated device",
|
||||
keys_for_device={},
|
||||
)
|
||||
)
|
||||
|
||||
# Sync down to a single real device. The dehydrated device is not in the
|
||||
# target list, but must not be deleted.
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/_synapse/mas/sync_devices",
|
||||
shorthand=False,
|
||||
access_token=self.SHARED_SECRET,
|
||||
content={
|
||||
"localpart": "alice",
|
||||
"devices": ["DEVICE1"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
self.assertEqual(channel.json_body, {})
|
||||
|
||||
# The real devices are reconciled, but the dehydrated device survives.
|
||||
store = self.hs.get_datastores().main
|
||||
devices = self.get_success(store.get_devices_by_user(str(self.alice_user_id)))
|
||||
self.assertEqual(set(devices.keys()), {"DEVICE1", dehydrated_device_id})
|
||||
|
||||
# And it is still registered as the dehydrated device.
|
||||
dehydrated = self.get_success(
|
||||
device_handler.get_dehydrated_device(str(self.alice_user_id))
|
||||
)
|
||||
assert dehydrated is not None
|
||||
self.assertEqual(dehydrated[0], dehydrated_device_id)
|
||||
|
||||
def test_sync_devices_add_and_delete(self) -> None:
|
||||
# Sync with a mix of additions and deletions
|
||||
channel = self.make_request(
|
||||
|
||||
Reference in New Issue
Block a user