-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Remove account data (including client config, push rules and ignored users) upon user deactivation. #11621
Remove account data (including client config, push rules and ignored users) upon user deactivation. #11621
Changes from 23 commits
68b663d
13fec0d
c1fed49
4da869e
b132bba
ab97003
5d4c6ca
6d7e9ac
d73be6c
e58ec5d
00f9033
2123435
a1a8c68
3edab88
249b05b
37dc2e1
81a2863
87473f4
e13780e
cbe543b
75f1e2f
1b28e2f
a68607c
97ceb01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Remove account data (including client config, push rules and ignored users) upon user deactivation. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
# Copyright 2021 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. | ||
from http import HTTPStatus | ||
from typing import Any, Dict | ||
|
||
from twisted.test.proto_helpers import MemoryReactor | ||
|
||
from synapse.api.constants import AccountDataTypes | ||
from synapse.push.rulekinds import PRIORITY_CLASS_MAP | ||
from synapse.rest import admin | ||
from synapse.rest.client import account, login | ||
from synapse.server import HomeServer | ||
from synapse.util import Clock | ||
|
||
from tests.unittest import HomeserverTestCase | ||
|
||
|
||
class DeactivateAccountTestCase(HomeserverTestCase): | ||
servlets = [ | ||
login.register_servlets, | ||
admin.register_servlets, | ||
account.register_servlets, | ||
] | ||
|
||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: | ||
self._store = hs.get_datastore() | ||
|
||
self.user = self.register_user("user", "pass") | ||
self.token = self.login("user", "pass") | ||
|
||
def _deactivate_my_account(self): | ||
""" | ||
Deactivates the account `self.user` using `self.token` and asserts | ||
that it returns a 200 success code. | ||
""" | ||
req = self.get_success( | ||
self.make_request( | ||
"POST", | ||
"account/deactivate", | ||
{ | ||
"auth": { | ||
"type": "m.login.password", | ||
"user": self.user, | ||
"password": "pass", | ||
}, | ||
"erase": True, | ||
}, | ||
access_token=self.token, | ||
) | ||
) | ||
self.assertEqual(req.code, HTTPStatus.OK, req) | ||
|
||
def test_global_account_data_deleted_upon_deactivation(self) -> None: | ||
""" | ||
Tests that global account data is removed upon deactivation. | ||
""" | ||
# Add some account data | ||
self.get_success( | ||
self._store.add_account_data_for_user( | ||
self.user, | ||
AccountDataTypes.DIRECT, | ||
{"@someone:remote": ["!somewhere:remote"]}, | ||
) | ||
) | ||
|
||
# Check that we actually added some. | ||
self.assertIsNotNone( | ||
self.get_success( | ||
self._store.get_global_account_data_by_type_for_user( | ||
self.user, AccountDataTypes.DIRECT | ||
) | ||
), | ||
) | ||
|
||
# Request the deactivation of our account | ||
self._deactivate_my_account() | ||
|
||
# Check that the account data does not persist. | ||
self.assertIsNone( | ||
self.get_success( | ||
self._store.get_global_account_data_by_type_for_user( | ||
reivilibre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.user, AccountDataTypes.DIRECT | ||
) | ||
), | ||
) | ||
|
||
def test_room_account_data_deleted_upon_deactivation(self) -> None: | ||
""" | ||
Tests that room account data is removed upon deactivation. | ||
""" | ||
room_id = "!room:test" | ||
|
||
# Add some room account data | ||
self.get_success( | ||
self._store.add_account_data_to_room( | ||
self.user, | ||
room_id, | ||
"m.fully_read", | ||
{"event_id": "$aaaa:test"}, | ||
) | ||
) | ||
|
||
# Check that we actually added some. | ||
self.assertIsNotNone( | ||
self.get_success( | ||
self._store.get_account_data_for_room_and_type( | ||
self.user, room_id, "m.fully_read" | ||
) | ||
), | ||
) | ||
|
||
# Request the deactivation of our account | ||
self._deactivate_my_account() | ||
|
||
# Check that the account data does not persist. | ||
self.assertIsNone( | ||
self.get_success( | ||
self._store.get_account_data_for_room_and_type( | ||
self.user, room_id, "m.fully_read" | ||
) | ||
), | ||
) | ||
|
||
def _is_custom_rule(self, push_rule: Dict[str, Any]) -> bool: | ||
""" | ||
Default rules start with a dot: such as .m.rule and .im.vector. | ||
This function returns true iff a rule is custom (not default). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. iff -> if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. iff means "if and only if", it isn't a typo. (Although I'm not a big fan of including abbreviations like this.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, thanks! TIL! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It comes up somewhat frequently which is why I usually try to spell it out! Sorry I missed that in review! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries. I learned something new, which is always great. Thank you for taking your time to explain it to me, even when this is already closed! <3 |
||
""" | ||
return "/." not in push_rule["rule_id"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why didn't you check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Was explained to me now, thanks a lot) |
||
|
||
def test_push_rules_deleted_upon_account_deactivation(self) -> None: | ||
""" | ||
Push rules are a special case of account data. | ||
They are stored separately but get sent to the client as account data in /sync. | ||
This tests that deactivating a user deletes push rules along with the rest | ||
of their account data. | ||
""" | ||
|
||
# Add a push rule | ||
self.get_success( | ||
self._store.add_push_rule( | ||
self.user, | ||
"personal.override.rule1", | ||
PRIORITY_CLASS_MAP["override"], | ||
[], | ||
[], | ||
) | ||
) | ||
|
||
# Test the rule exists | ||
push_rules = self.get_success(self._store.get_push_rules_for_user(self.user)) | ||
# Filter out default rules; we don't care | ||
push_rules = list(filter(self._is_custom_rule, push_rules)) | ||
# Check our rule made it | ||
self.assertEqual( | ||
push_rules, | ||
[ | ||
{ | ||
"user_name": "@user:test", | ||
"rule_id": "personal.override.rule1", | ||
"priority_class": 5, | ||
"priority": 0, | ||
"conditions": [], | ||
"actions": [], | ||
"default": False, | ||
} | ||
], | ||
push_rules, | ||
) | ||
|
||
# Request the deactivation of our account | ||
self._deactivate_my_account() | ||
|
||
push_rules = self.get_success(self._store.get_push_rules_for_user(self.user)) | ||
# Filter out default rules; we don't care | ||
push_rules = list(filter(self._is_custom_rule, push_rules)) | ||
# Check our rule no longer exists | ||
self.assertEqual(push_rules, [], push_rules) | ||
|
||
def test_ignored_users_deleted_upon_deactivation(self) -> None: | ||
""" | ||
Ignored users are a special case of account data. | ||
They get denormalised into the `ignored_users` table upon being stored as | ||
account data. | ||
Test that a user's list of ignored users is deleted upon deactivation. | ||
""" | ||
|
||
# Add an ignored user | ||
self.get_success( | ||
self._store.add_account_data_for_user( | ||
self.user, | ||
AccountDataTypes.IGNORED_USER_LIST, | ||
{"ignored_users": {"@sheltie:test": {}}}, | ||
) | ||
) | ||
|
||
# Test the user is ignored | ||
self.assertEqual( | ||
self.get_success(self._store.ignored_by("@sheltie:test")), {self.user} | ||
) | ||
|
||
# Request the deactivation of our account | ||
self._deactivate_my_account() | ||
|
||
# Test the user is no longer ignored by the user that was deactivated | ||
self.assertEqual( | ||
self.get_success(self._store.ignored_by("@sheltie:test")), set() | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I agree -- the other users still have this user ignored and might e.g. pull stale data with messages from this user. Those cached entries are still valid (and we're not modifying them). The goal is to purge useless cached data from the user being deactivated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignored_by
is a confusing name, I suspect that might be tripping you up.A is in ignored_by(B) iff A ignores B.
Because we're deleting the fact that A ignores B, we have to invalidate
ignored_by(B)
for all users B that A ignores. (It's easier in practice to invalidate the whole cache, I think — or to leave it alone, but we've said we're trying not to be too lazy with this).Note: during the deactivation of A, we only delete the 'A ignores B' facts but not 'B ignores A' facts — if there are still stale messages from A, users ignoring A will continue to do so.
Maybe
ignored_by
should beget_ignorers_of
, but even that sounds a little bit clunky