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

Commit

Permalink
Fix registering a device on an account with lots of devices (#15348)
Browse files Browse the repository at this point in the history
Fixes up #15183
  • Loading branch information
erikjohnston authored Mar 29, 2023
1 parent 5350b5d commit f0d8f66
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 3 deletions.
1 change: 1 addition & 0 deletions changelog.d/15348.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prune user's old devices on login if they have too many.
2 changes: 2 additions & 0 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,8 @@ async def _maybe_prune_too_many_devices(self, user_id: str) -> None:
if not device_ids:
return

logger.info("Pruning %d stale devices for %s", len(device_ids), user_id)

# Now spawn a background loop that deletes said devices.
async def _prune_too_many_devices_loop() -> None:
if user_id in self._currently_pruning_devices_for_users:
Expand Down
9 changes: 6 additions & 3 deletions synapse/storage/databases/main/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1638,19 +1638,22 @@ async def check_too_many_devices_for_user(self, user_id: str) -> List[str]:
"""

rows = await self.db_pool.execute(
"check_too_many_devices_for_user_last_seen", None, sql, (user_id,)
"check_too_many_devices_for_user_last_seen",
None,
sql,
user_id,
)
if rows:
max_last_seen = max(rows[0][0], max_last_seen)

# Fetch the devices to delete.
sql = """
SELECT DISTINCT device_id FROM devices
SELECT device_id FROM devices
LEFT JOIN e2e_device_keys_json USING (user_id, device_id)
WHERE
user_id = ?
AND NOT hidden
AND last_seen < ?
AND last_seen <= ?
AND key_json IS NULL
ORDER BY last_seen
"""
Expand Down
47 changes: 47 additions & 0 deletions tests/rest/client/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,53 @@ def test_require_approval(self) -> None:
ApprovalNoticeMedium.NONE, channel.json_body["approval_notice_medium"]
)

def test_check_stale_devices_get_pruned(self) -> None:
"""Check that if a user has some stale devices we log them out when they
log in a new device."""

# Register some devices, but not too many that we go over the threshold
# where we prune more aggressively.
user_id = self.register_user("user", "pass")
for _ in range(0, 50):
self.login(user_id, "pass")

store = self.hs.get_datastores().main

res = self.get_success(store.get_devices_by_user(user_id))
self.assertEqual(len(res), 50)

# Advance time so that the above devices are considered "old".
self.reactor.advance(30 * 24 * 60 * 60 * 1000)

self.login(user_id, "pass")

self.reactor.pump([60] * 10) # Ensure background job runs

# We expect all old devices to have been logged out
res = self.get_success(store.get_devices_by_user(user_id))
self.assertEqual(len(res), 1)

def test_check_recent_devices_get_pruned(self) -> None:
"""Check that if a user has many devices we log out the last oldest
ones.
Note: this is similar to above, except if we lots of devices we prune
devices even if they're not old.
"""

# Register a lot of devices in a short amount of time
user_id = self.register_user("user", "pass")
for _ in range(0, 100):
self.login(user_id, "pass")
self.reactor.advance(100)

store = self.hs.get_datastores().main

# We keep up to 50 devices that have been used in the last week, plus
# the device that was last logged in.
res = self.get_success(store.get_devices_by_user(user_id))
self.assertEqual(len(res), 51)


class AccountValidityTestCase(unittest.HomeserverTestCase):
servlets = [
Expand Down

0 comments on commit f0d8f66

Please sign in to comment.