Skip to content

Commit 93e0497

Browse files
Avoid a M_FORBIDDEN response when a user tries to erase their account and profile updates are disabled (#19398)
Currently synapse returns `M_FORBIDDEN` when trying to use the account deactivation API, if the server admin disabled displayname changes. This is undesirable, since it prevents GDPR erasure without admin interaction. The admin API seems to work fine though. This also only seems to affect the deactivate API, when the erase flag is true. Relevant endpoint: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3accountdeactivate This change only removes the checked for condition that the displayname and profile avatar are allowed to be changed per the configuration setting. If a user is deleting themselves, why is that denied? There did not seem to be a basic test for this endpoint that checks the `erase` usage, so that was added as well as checking the above mentioned behavior.
1 parent 613cb4d commit 93e0497

4 files changed

Lines changed: 134 additions & 30 deletions

File tree

changelog.d/19398.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow user requested erasure to succeed even if Synapse has disabled profile changes. Contributed by Famedly.

synapse/handlers/profile.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -406,28 +406,6 @@ async def delete_profile_upon_deactivation(
406406
# have it.
407407
raise AuthError(400, "Cannot remove another user's profile")
408408

409-
if not by_admin:
410-
current_profile = await self.store.get_profileinfo(target_user)
411-
if not self.hs.config.registration.enable_set_displayname:
412-
if current_profile.display_name:
413-
# SUSPICIOUS: It seems strange to block deactivation on this,
414-
# though this is preserving previous behaviour.
415-
raise SynapseError(
416-
400,
417-
"Changing display name is disabled on this server",
418-
Codes.FORBIDDEN,
419-
)
420-
421-
if not self.hs.config.registration.enable_set_avatar_url:
422-
if current_profile.avatar_url:
423-
# SUSPICIOUS: It seems strange to block deactivation on this,
424-
# though this is preserving previous behaviour.
425-
raise SynapseError(
426-
400,
427-
"Changing avatar is disabled on this server",
428-
Codes.FORBIDDEN,
429-
)
430-
431409
await self.store.delete_profile(target_user)
432410

433411
await self._third_party_rules.on_profile_update(

tests/handlers/test_profile.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from synapse.util.task_scheduler import TaskStatus
3939

4040
from tests import unittest
41+
from tests.unittest import override_config
4142

4243

4344
class ProfileTestCase(unittest.HomeserverTestCase):
@@ -314,9 +315,8 @@ async def potentially_slow_update_membership(
314315
membership[state_tuple].content["displayname"], "Frank Jr."
315316
)
316317

318+
@override_config({"enable_set_displayname": False})
317319
def test_set_my_name_if_disabled(self) -> None:
318-
self.hs.config.registration.enable_set_displayname = False
319-
320320
# Setting displayname for the first time is allowed
321321
self.get_success(self.store.set_profile_displayname(self.frank, "Frank"))
322322

@@ -435,9 +435,8 @@ def test_set_my_avatar(self) -> None:
435435
(self.get_success(self.store.get_profile_avatar_url(self.frank))),
436436
)
437437

438+
@override_config({"enable_set_avatar_url": False})
438439
def test_set_my_avatar_if_disabled(self) -> None:
439-
self.hs.config.registration.enable_set_avatar_url = False
440-
441440
# Setting displayname for the first time is allowed
442441
self.get_success(
443442
self.store.set_profile_avatar_url(self.frank, "http://my.server/me.png")

tests/rest/client/test_account.py

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@
3131

3232
import synapse.rest.admin
3333
from synapse.api.constants import LoginType, Membership
34-
from synapse.api.errors import Codes, HttpResponseException
34+
from synapse.api.errors import Codes, HttpResponseException, SynapseError
3535
from synapse.appservice import ApplicationService
3636
from synapse.rest import admin
3737
from synapse.rest.client import account, login, register, room
3838
from synapse.rest.synapse.client.password_reset import PasswordResetSubmitTokenResource
3939
from synapse.server import HomeServer
4040
from synapse.storage._base import db_to_json
41-
from synapse.types import JsonDict, UserID
41+
from synapse.types import JsonDict, UserID, create_requester
4242
from synapse.util.clock import Clock
4343

4444
from tests import unittest
@@ -500,6 +500,123 @@ def test_deactivate_account(self) -> None:
500500
channel = self.make_request("GET", "account/whoami", access_token=tok)
501501
self.assertEqual(channel.code, 401)
502502

503+
def test_deactivate_erase_account(self) -> None:
504+
"""
505+
Test that a user account can be signaled for erasure on the Matrix spec endpoint
506+
for client access, `/account/deactivate` and that profile data is erased as part
507+
of the process
508+
"""
509+
mxid = self.register_user("kermit", "test")
510+
user_id = UserID.from_string(mxid)
511+
tok = self.login("kermit", "test")
512+
513+
profile_handler = self.hs.get_profile_handler()
514+
515+
# Set some profile data that can be checked for after the user is erased
516+
self.get_success(
517+
profile_handler.set_displayname(
518+
user_id, create_requester(user_id), "Kermit the Frog"
519+
)
520+
)
521+
self.get_success(
522+
profile_handler.set_avatar_url(
523+
user_id, create_requester(user_id), "http://test/Kermit.jpg"
524+
)
525+
)
526+
# Verify it is set
527+
self.assertEqual(
528+
self.get_success(profile_handler.get_displayname(user_id)),
529+
"Kermit the Frog",
530+
)
531+
self.assertEqual(
532+
self.get_success(profile_handler.get_avatar_url(user_id)),
533+
"http://test/Kermit.jpg",
534+
)
535+
536+
# Deactivate!
537+
self.deactivate(mxid, tok, erase=True)
538+
539+
store = self.hs.get_datastores().main
540+
541+
# Check that the user has been marked as deactivated.
542+
self.assertTrue(self.get_success(store.get_user_deactivated_status(mxid)))
543+
544+
# On deactivation with 'erase', the entire database row is erased. Both of these
545+
# should raise a 404(Not Found) SynapseError
546+
display_name_failure = self.get_failure(
547+
profile_handler.get_displayname(user_id), SynapseError
548+
)
549+
assert display_name_failure.value.code == HTTPStatus.NOT_FOUND
550+
551+
avatar_url_failure = self.get_failure(
552+
profile_handler.get_avatar_url(user_id), SynapseError
553+
)
554+
assert avatar_url_failure.value.code == HTTPStatus.NOT_FOUND
555+
556+
# Check that this access token has been invalidated.
557+
channel = self.make_request("GET", "account/whoami", access_token=tok)
558+
self.assertEqual(channel.code, 401)
559+
560+
@override_config({"enable_set_displayname": False, "enable_set_avatar_url": False})
561+
def test_deactivate_erase_account_with_disabled_profile_changes(self) -> None:
562+
"""
563+
Test that deactivating the user with the 'erase' option will remove existing
564+
profile data, even with the Synapse configuration to forbid profile changes
565+
"""
566+
mxid = self.register_user("kermit", "test")
567+
user_id = UserID.from_string(mxid)
568+
tok = self.login("kermit", "test")
569+
570+
profile_handler = self.hs.get_profile_handler()
571+
572+
# Can not use the profile handler to set a display name when it is disabled. Use
573+
# the database directly
574+
store = self.hs.get_datastores().main
575+
self.get_success(store.set_profile_displayname(user_id, "Kermit the Frog"))
576+
self.get_success(
577+
store.set_profile_avatar_url(user_id, "http://test/Kermit.jpg")
578+
)
579+
580+
# Verify it is set
581+
self.assertEqual(
582+
(self.get_success(store.get_profile_displayname(user_id))),
583+
"Kermit the Frog",
584+
)
585+
self.assertEqual(
586+
self.get_success(profile_handler.get_displayname(user_id)),
587+
"Kermit the Frog",
588+
)
589+
self.assertEqual(
590+
(self.get_success(store.get_profile_avatar_url(user_id))),
591+
"http://test/Kermit.jpg",
592+
)
593+
self.assertEqual(
594+
self.get_success(profile_handler.get_avatar_url(user_id)),
595+
"http://test/Kermit.jpg",
596+
)
597+
598+
# Deactivate!
599+
self.deactivate(mxid, tok, erase=True)
600+
601+
# Check that the user has been marked as deactivated.
602+
self.assertTrue(self.get_success(store.get_user_deactivated_status(mxid)))
603+
604+
# On deactivation with 'erase', the entire database row is erased. Both of these
605+
# should raise a 404(Not Found) SynapseError
606+
display_name_failure = self.get_failure(
607+
profile_handler.get_displayname(user_id), SynapseError
608+
)
609+
assert display_name_failure.value.code == HTTPStatus.NOT_FOUND
610+
611+
avatar_url_failure = self.get_failure(
612+
profile_handler.get_avatar_url(user_id), SynapseError
613+
)
614+
assert avatar_url_failure.value.code == HTTPStatus.NOT_FOUND
615+
616+
# Check that this access token has been invalidated.
617+
channel = self.make_request("GET", "account/whoami", access_token=tok)
618+
self.assertEqual(channel.code, 401)
619+
503620
def test_pending_invites(self) -> None:
504621
"""Tests that deactivating a user rejects every pending invite for them."""
505622
store = self.hs.get_datastores().main
@@ -698,14 +815,23 @@ def test_background_update_deletes_deactivated_users_server_side_backup_keys(
698815
)
699816
self.assertEqual(len(res2), 4)
700817

701-
def deactivate(self, user_id: str, tok: str) -> None:
818+
def deactivate(self, user_id: str, tok: str, erase: bool = False) -> None:
819+
"""
820+
Helper to deactivate a user using the /account/deactivate endpoint, optionally
821+
with erasure
822+
823+
Args:
824+
user_id: the string formatted mxid(not a UserID)
825+
tok: the user's access token
826+
erase: bool of if this should be a full erasure request
827+
"""
702828
request_data = {
703829
"auth": {
704830
"type": "m.login.password",
705831
"user": user_id,
706832
"password": "test",
707833
},
708-
"erase": False,
834+
"erase": erase,
709835
}
710836
channel = self.make_request(
711837
"POST", "account/deactivate", request_data, access_token=tok

0 commit comments

Comments
 (0)