forked from MirrorHub/synapse
Delete server-side backup keys when deactivating an account. (#15181)
This commit is contained in:
parent
6d103373e2
commit
6b23d74ad1
6 changed files with 291 additions and 3 deletions
1
changelog.d/15181.bugfix
Normal file
1
changelog.d/15181.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Delete server-side backup keys when deactivating an account.
|
|
@ -59,6 +59,7 @@ from synapse.storage.databases.main.account_data import AccountDataWorkerStore
|
||||||
from synapse.storage.databases.main.client_ips import ClientIpBackgroundUpdateStore
|
from synapse.storage.databases.main.client_ips import ClientIpBackgroundUpdateStore
|
||||||
from synapse.storage.databases.main.deviceinbox import DeviceInboxBackgroundUpdateStore
|
from synapse.storage.databases.main.deviceinbox import DeviceInboxBackgroundUpdateStore
|
||||||
from synapse.storage.databases.main.devices import DeviceBackgroundUpdateStore
|
from synapse.storage.databases.main.devices import DeviceBackgroundUpdateStore
|
||||||
|
from synapse.storage.databases.main.e2e_room_keys import EndToEndRoomKeyBackgroundStore
|
||||||
from synapse.storage.databases.main.end_to_end_keys import EndToEndKeyBackgroundStore
|
from synapse.storage.databases.main.end_to_end_keys import EndToEndKeyBackgroundStore
|
||||||
from synapse.storage.databases.main.event_push_actions import EventPushActionsStore
|
from synapse.storage.databases.main.event_push_actions import EventPushActionsStore
|
||||||
from synapse.storage.databases.main.events_bg_updates import (
|
from synapse.storage.databases.main.events_bg_updates import (
|
||||||
|
@ -225,6 +226,7 @@ class Store(
|
||||||
MainStateBackgroundUpdateStore,
|
MainStateBackgroundUpdateStore,
|
||||||
UserDirectoryBackgroundUpdateStore,
|
UserDirectoryBackgroundUpdateStore,
|
||||||
EndToEndKeyBackgroundStore,
|
EndToEndKeyBackgroundStore,
|
||||||
|
EndToEndRoomKeyBackgroundStore,
|
||||||
StatsStore,
|
StatsStore,
|
||||||
AccountDataWorkerStore,
|
AccountDataWorkerStore,
|
||||||
PushRuleStore,
|
PushRuleStore,
|
||||||
|
|
|
@ -176,6 +176,9 @@ class DeactivateAccountHandler:
|
||||||
# Remove account data (including ignored users and push rules).
|
# Remove account data (including ignored users and push rules).
|
||||||
await self.store.purge_account_data_for_user(user_id)
|
await self.store.purge_account_data_for_user(user_id)
|
||||||
|
|
||||||
|
# Delete any server-side backup keys
|
||||||
|
await self.store.bulk_delete_backup_keys_and_versions_for_user(user_id)
|
||||||
|
|
||||||
# Let modules know the user has been deactivated.
|
# Let modules know the user has been deactivated.
|
||||||
await self._third_party_rules.on_user_deactivation_status_changed(
|
await self._third_party_rules.on_user_deactivation_status_changed(
|
||||||
user_id,
|
user_id,
|
||||||
|
|
|
@ -13,17 +13,24 @@
|
||||||
# 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.
|
||||||
|
|
||||||
from typing import Dict, Iterable, Mapping, Optional, Tuple, cast
|
from typing import TYPE_CHECKING, Dict, Iterable, Mapping, Optional, Tuple, cast
|
||||||
|
|
||||||
from typing_extensions import Literal, TypedDict
|
from typing_extensions import Literal, TypedDict
|
||||||
|
|
||||||
from synapse.api.errors import StoreError
|
from synapse.api.errors import StoreError
|
||||||
from synapse.logging.opentracing import log_kv, trace
|
from synapse.logging.opentracing import log_kv, trace
|
||||||
from synapse.storage._base import SQLBaseStore, db_to_json
|
from synapse.storage._base import SQLBaseStore, db_to_json
|
||||||
from synapse.storage.database import LoggingTransaction
|
from synapse.storage.database import (
|
||||||
|
DatabasePool,
|
||||||
|
LoggingDatabaseConnection,
|
||||||
|
LoggingTransaction,
|
||||||
|
)
|
||||||
from synapse.types import JsonDict, JsonSerializable, StreamKeyType
|
from synapse.types import JsonDict, JsonSerializable, StreamKeyType
|
||||||
from synapse.util import json_encoder
|
from synapse.util import json_encoder
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
|
||||||
class RoomKey(TypedDict):
|
class RoomKey(TypedDict):
|
||||||
"""`KeyBackupData` in the Matrix spec.
|
"""`KeyBackupData` in the Matrix spec.
|
||||||
|
@ -37,7 +44,82 @@ class RoomKey(TypedDict):
|
||||||
session_data: JsonSerializable
|
session_data: JsonSerializable
|
||||||
|
|
||||||
|
|
||||||
class EndToEndRoomKeyStore(SQLBaseStore):
|
class EndToEndRoomKeyBackgroundStore(SQLBaseStore):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
database: DatabasePool,
|
||||||
|
db_conn: LoggingDatabaseConnection,
|
||||||
|
hs: "HomeServer",
|
||||||
|
):
|
||||||
|
super().__init__(database, db_conn, hs)
|
||||||
|
|
||||||
|
self.db_pool.updates.register_background_update_handler(
|
||||||
|
"delete_e2e_backup_keys_for_deactivated_users",
|
||||||
|
self._delete_e2e_backup_keys_for_deactivated_users,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _delete_keys_txn(self, txn: LoggingTransaction, user_id: str) -> None:
|
||||||
|
self.db_pool.simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="e2e_room_keys",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db_pool.simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="e2e_room_keys_versions",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _delete_e2e_backup_keys_for_deactivated_users(
|
||||||
|
self, progress: JsonDict, batch_size: int
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Retroactively purges account data for users that have already been deactivated.
|
||||||
|
Gets run as a background update caused by a schema delta.
|
||||||
|
"""
|
||||||
|
|
||||||
|
last_user: str = progress.get("last_user", "")
|
||||||
|
|
||||||
|
def _delete_backup_keys_for_deactivated_users_txn(
|
||||||
|
txn: LoggingTransaction,
|
||||||
|
) -> int:
|
||||||
|
sql = """
|
||||||
|
SELECT name FROM users
|
||||||
|
WHERE deactivated = ? and name > ?
|
||||||
|
ORDER BY name ASC
|
||||||
|
LIMIT ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
txn.execute(sql, (1, last_user, batch_size))
|
||||||
|
users = [row[0] for row in txn]
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
self._delete_keys_txn(txn, user)
|
||||||
|
|
||||||
|
if users:
|
||||||
|
self.db_pool.updates._background_update_progress_txn(
|
||||||
|
txn,
|
||||||
|
"delete_e2e_backup_keys_for_deactivated_users",
|
||||||
|
{"last_user": users[-1]},
|
||||||
|
)
|
||||||
|
|
||||||
|
return len(users)
|
||||||
|
|
||||||
|
number_deleted = await self.db_pool.runInteraction(
|
||||||
|
"_delete_backup_keys_for_deactivated_users",
|
||||||
|
_delete_backup_keys_for_deactivated_users_txn,
|
||||||
|
)
|
||||||
|
|
||||||
|
if number_deleted < batch_size:
|
||||||
|
await self.db_pool.updates._end_background_update(
|
||||||
|
"delete_e2e_backup_keys_for_deactivated_users"
|
||||||
|
)
|
||||||
|
|
||||||
|
return number_deleted
|
||||||
|
|
||||||
|
|
||||||
|
class EndToEndRoomKeyStore(EndToEndRoomKeyBackgroundStore):
|
||||||
"""The store for end to end room key backups.
|
"""The store for end to end room key backups.
|
||||||
|
|
||||||
See https://spec.matrix.org/v1.1/client-server-api/#server-side-key-backups
|
See https://spec.matrix.org/v1.1/client-server-api/#server-side-key-backups
|
||||||
|
@ -550,3 +632,29 @@ class EndToEndRoomKeyStore(SQLBaseStore):
|
||||||
await self.db_pool.runInteraction(
|
await self.db_pool.runInteraction(
|
||||||
"delete_e2e_room_keys_version", _delete_e2e_room_keys_version_txn
|
"delete_e2e_room_keys_version", _delete_e2e_room_keys_version_txn
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def bulk_delete_backup_keys_and_versions_for_user(self, user_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Bulk deletes all backup room keys and versions for a given user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user whose backup keys and versions we're deleting
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _delete_all_e2e_room_keys_and_versions_txn(txn: LoggingTransaction) -> None:
|
||||||
|
self.db_pool.simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="e2e_room_keys",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db_pool.simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="e2e_room_keys_versions",
|
||||||
|
keyvalues={"user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.db_pool.runInteraction(
|
||||||
|
"delete_all_e2e_room_keys_and_versions",
|
||||||
|
_delete_all_e2e_room_keys_and_versions_txn,
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* Copyright 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
|
||||||
|
(7404, 'delete_e2e_backup_keys_for_deactivated_users', '{}');
|
|
@ -474,6 +474,163 @@ class DeactivateTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertEqual(len(memberships), 1, memberships)
|
self.assertEqual(len(memberships), 1, memberships)
|
||||||
self.assertEqual(memberships[0].room_id, room_id, memberships)
|
self.assertEqual(memberships[0].room_id, room_id, memberships)
|
||||||
|
|
||||||
|
def test_deactivate_account_deletes_server_side_backup_keys(self) -> None:
|
||||||
|
key_handler = self.hs.get_e2e_room_keys_handler()
|
||||||
|
room_keys = {
|
||||||
|
"rooms": {
|
||||||
|
"!abc:matrix.org": {
|
||||||
|
"sessions": {
|
||||||
|
"c0ff33": {
|
||||||
|
"first_message_index": 1,
|
||||||
|
"forwarded_count": 1,
|
||||||
|
"is_verified": False,
|
||||||
|
"session_data": "SSBBTSBBIEZJU0gK",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id = self.register_user("missPiggy", "test")
|
||||||
|
tok = self.login("missPiggy", "test")
|
||||||
|
|
||||||
|
# add some backup keys/versions
|
||||||
|
version = self.get_success(
|
||||||
|
key_handler.create_version(
|
||||||
|
user_id,
|
||||||
|
{
|
||||||
|
"algorithm": "m.megolm_backup.v1",
|
||||||
|
"auth_data": "first_version_auth_data",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_success(key_handler.upload_room_keys(user_id, version, room_keys))
|
||||||
|
|
||||||
|
version2 = self.get_success(
|
||||||
|
key_handler.create_version(
|
||||||
|
user_id,
|
||||||
|
{
|
||||||
|
"algorithm": "m.megolm_backup.v1",
|
||||||
|
"auth_data": "second_version_auth_data",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_success(key_handler.upload_room_keys(user_id, version2, room_keys))
|
||||||
|
|
||||||
|
self.deactivate(user_id, tok)
|
||||||
|
store = self.hs.get_datastores().main
|
||||||
|
|
||||||
|
# Check that the user has been marked as deactivated.
|
||||||
|
self.assertTrue(self.get_success(store.get_user_deactivated_status(user_id)))
|
||||||
|
|
||||||
|
# Check that there are no entries in 'e2e_room_keys` and `e2e_room_keys_versions`
|
||||||
|
res = self.get_success(
|
||||||
|
self.hs.get_datastores().main.db_pool.simple_select_list(
|
||||||
|
"e2e_room_keys", {"user_id": user_id}, "*", "simple_select"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(len(res), 0)
|
||||||
|
|
||||||
|
res2 = self.get_success(
|
||||||
|
self.hs.get_datastores().main.db_pool.simple_select_list(
|
||||||
|
"e2e_room_keys_versions", {"user_id": user_id}, "*", "simple_select"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(len(res2), 0)
|
||||||
|
|
||||||
|
def test_background_update_deletes_deactivated_users_server_side_backup_keys(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
key_handler = self.hs.get_e2e_room_keys_handler()
|
||||||
|
room_keys = {
|
||||||
|
"rooms": {
|
||||||
|
"!abc:matrix.org": {
|
||||||
|
"sessions": {
|
||||||
|
"c0ff33": {
|
||||||
|
"first_message_index": 1,
|
||||||
|
"forwarded_count": 1,
|
||||||
|
"is_verified": False,
|
||||||
|
"session_data": "SSBBTSBBIEZJU0gK",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.store = self.hs.get_datastores().main
|
||||||
|
|
||||||
|
# create a bunch of users and add keys for them
|
||||||
|
users = []
|
||||||
|
for i in range(0, 20):
|
||||||
|
user_id = self.register_user("missPiggy" + str(i), "test")
|
||||||
|
users.append((user_id,))
|
||||||
|
|
||||||
|
# add some backup keys/versions
|
||||||
|
version = self.get_success(
|
||||||
|
key_handler.create_version(
|
||||||
|
user_id,
|
||||||
|
{
|
||||||
|
"algorithm": "m.megolm_backup.v1",
|
||||||
|
"auth_data": str(i) + "_version_auth_data",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_success(key_handler.upload_room_keys(user_id, version, room_keys))
|
||||||
|
|
||||||
|
version2 = self.get_success(
|
||||||
|
key_handler.create_version(
|
||||||
|
user_id,
|
||||||
|
{
|
||||||
|
"algorithm": "m.megolm_backup.v1",
|
||||||
|
"auth_data": str(i) + "_version_auth_data",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_success(key_handler.upload_room_keys(user_id, version2, room_keys))
|
||||||
|
|
||||||
|
# deactivate most of the users by editing DB
|
||||||
|
self.get_success(
|
||||||
|
self.store.db_pool.simple_update_many(
|
||||||
|
table="users",
|
||||||
|
key_names=("name",),
|
||||||
|
key_values=users[0:18],
|
||||||
|
value_names=("deactivated",),
|
||||||
|
value_values=[(1,) for i in range(1, 19)],
|
||||||
|
desc="",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# run background update
|
||||||
|
self.get_success(
|
||||||
|
self.store.db_pool.simple_insert(
|
||||||
|
"background_updates",
|
||||||
|
{
|
||||||
|
"update_name": "delete_e2e_backup_keys_for_deactivated_users",
|
||||||
|
"progress_json": "{}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.store.db_pool.updates._all_done = False
|
||||||
|
self.wait_for_background_updates()
|
||||||
|
|
||||||
|
# check that keys are deleted for the deactivated users but not the others
|
||||||
|
res = self.get_success(
|
||||||
|
self.hs.get_datastores().main.db_pool.simple_select_list(
|
||||||
|
"e2e_room_keys", None, ("user_id",), "simple_select"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(len(res), 4)
|
||||||
|
|
||||||
|
res2 = self.get_success(
|
||||||
|
self.hs.get_datastores().main.db_pool.simple_select_list(
|
||||||
|
"e2e_room_keys_versions", None, ("user_id",), "simple_select"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(len(res2), 4)
|
||||||
|
|
||||||
def deactivate(self, user_id: str, tok: str) -> None:
|
def deactivate(self, user_id: str, tok: str) -> None:
|
||||||
request_data = {
|
request_data = {
|
||||||
"auth": {
|
"auth": {
|
||||||
|
|
Loading…
Reference in a new issue