Skip to content

Commit e7571b2

Browse files
antkrytihorsokhanexoft
authored andcommitted
[ENG-9002] Unable to GDPR delete users with Registrations or Preprints (CenterForOpenScience#11568)
* update gdpr delete logic * fix test; better error handling
1 parent 88a4ee1 commit e7571b2

3 files changed

Lines changed: 85 additions & 63 deletions

File tree

admin/users/views.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,17 +216,25 @@ def post(self, request, *args, **kwargs):
216216
user = self.get_object()
217217
user.gdpr_delete()
218218
user.save()
219-
messages.success(request, f'User {user._id} was successfully GDPR deleted')
220-
update_admin_log(
221-
user_id=self.request.user.id,
222-
object_id=user.pk,
223-
object_repr='User',
224-
message=f'User {user._id} was successfully GDPR deleted',
225-
action_flag=USER_GDPR_DELETED
226-
)
227219
except UserStateError as e:
228220
messages.warning(request, str(e))
229221

222+
messages.success(request, f'User {user._id} was successfully GDPR deleted')
223+
224+
# Update SHARE for all public resources
225+
for node in user.nodes.filter(is_public=True, is_deleted=False):
226+
node.update_search()
227+
for preprint in user.preprints.filter(is_public=True, deleted__isnull=True):
228+
preprint.update_search()
229+
230+
update_admin_log(
231+
user_id=self.request.user.id,
232+
object_id=user.pk,
233+
object_repr='User',
234+
message=f'User {user._id} was successfully GDPR deleted',
235+
action_flag=USER_GDPR_DELETED
236+
)
237+
230238
return redirect(self.get_success_url())
231239

232240

osf/models/user.py

Lines changed: 22 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
1919
from django.contrib.auth.hashers import check_password
2020
from django.contrib.auth.models import PermissionsMixin
21+
from django.core.exceptions import FieldDoesNotExist
2122
from django.dispatch import receiver
2223
from django.db import models
2324
from django.db.models import Count, Exists, OuterRef
@@ -2020,14 +2021,12 @@ def gdpr_delete(self):
20202021
"""
20212022
Complies with GDPR guidelines by disabling the account and removing identifying information.
20222023
"""
2023-
2024-
# Check if user has something intentionally public, like preprints or registrations
2025-
self._validate_no_public_entities()
2026-
2027-
# Check if user has any non-registration AbstractNodes or DraftRegistrations that they might still share with
2028-
# other contributors
20292024
self._validate_and_remove_resource_for_gdpr_delete(
2030-
self.nodes.exclude(type='osf.registration'), # Includes DraftNodes and other typed nodes
2025+
self.nodes.all(),
2026+
hard_delete=False
2027+
)
2028+
self._validate_and_remove_resource_for_gdpr_delete(
2029+
self.preprints.all(),
20312030
hard_delete=False
20322031
)
20332032
self._validate_and_remove_resource_for_gdpr_delete(
@@ -2038,39 +2037,6 @@ def gdpr_delete(self):
20382037
# Finally delete the user's info.
20392038
self._clear_identifying_information()
20402039

2041-
def _validate_no_public_entities(self):
2042-
"""
2043-
Ensure that the user doesn't have any public facing resources like Registrations or Preprints
2044-
that would be left with other contributors after this deletion.
2045-
2046-
Allow GDPR deletion if the user is the sole contributor on a public Registration or Preprint.
2047-
"""
2048-
from osf.models import Preprint, AbstractNode
2049-
2050-
registrations_with_others = AbstractNode.objects.annotate(
2051-
contrib_count=Count('_contributors', distinct=True),
2052-
).filter(
2053-
_contributors=self,
2054-
deleted__isnull=True,
2055-
type='osf.registration',
2056-
contrib_count__gt=1
2057-
).exists()
2058-
2059-
if registrations_with_others:
2060-
raise UserStateError('You cannot delete this user because they have one or more registrations.')
2061-
2062-
preprints_with_others = Preprint.objects.annotate(
2063-
contrib_count=Count('_contributors', distinct=True),
2064-
).filter(
2065-
_contributors=self,
2066-
ever_public=True,
2067-
deleted__isnull=True,
2068-
contrib_count__gt=1
2069-
).exists()
2070-
2071-
if preprints_with_others:
2072-
raise UserStateError('You cannot delete this user because they have one or more preprints.')
2073-
20742040
def _validate_and_remove_resource_for_gdpr_delete(self, resources, hard_delete):
20752041
"""
20762042
This method ensures a user's resources are properly deleted of using during GDPR delete request.
@@ -2095,26 +2061,35 @@ def _validate_and_remove_resource_for_gdpr_delete(self, resources, hard_delete):
20952061
)
20962062

20972063
shared_resources = resources.exclude(id__in=personal_resources.values_list('id'))
2098-
for node in shared_resources:
2099-
self._validate_admin_status_for_gdpr_delete(node)
2100-
self._validate_addons_for_gdpr_delete(node)
2064+
for resource in shared_resources:
2065+
self._validate_admin_status_for_gdpr_delete(resource)
2066+
self._validate_addons_for_gdpr_delete(resource)
21012067

21022068
for resource in shared_resources.all():
21032069
logger.info(f'Removing {self._id} as a contributor to {resource.__class__.__name__} (pk:{resource.pk})...')
21042070
resource.remove_contributor(self, auth=Auth(self), log=False)
2071+
if getattr(resource, 'is_public', False) and hasattr(resource, 'update_search'):
2072+
resource.update_search()
21052073

2106-
# Delete all personal entities (excluding public registrations)
2074+
# Delete all personal non-public entities
21072075
personal_to_delete = personal_resources
2108-
if hasattr(model, 'is_public') and hasattr(model, 'type'):
2109-
personal_to_delete = personal_to_delete.exclude(is_public=True, type='osf.registration')
2076+
try:
2077+
if model._meta.get_field('is_public'):
2078+
personal_to_delete = personal_to_delete.exclude(is_public=True)
2079+
except FieldDoesNotExist:
2080+
pass
21102081

21112082
for entity in personal_to_delete.all():
21122083
if hard_delete:
21132084
logger.info(f'Hard-deleting {entity.__class__.__name__} (pk: {entity.pk})...')
21142085
entity.delete()
21152086
else:
21162087
logger.info(f'Soft-deleting {entity.__class__.__name__} (pk: {entity.pk})...')
2117-
entity.remove_node(auth=Auth(self))
2088+
if hasattr(entity, 'remove_node'):
2089+
entity.remove_node(auth=Auth(self))
2090+
else:
2091+
entity.is_deleted = True
2092+
entity.save()
21182093

21192094
def _clear_identifying_information(self):
21202095
'''

osf_tests/test_user.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,6 +2143,37 @@ def project_user_is_only_admin(self, user):
21432143
project.save()
21442144
return project
21452145

2146+
@mock.patch('osf.models.node.AbstractNode.update_search')
2147+
def test_gdpr_delete_triggers_share_update_for_public_shared_nodes(
2148+
self, mock_update_search, user, project_with_two_admins):
2149+
project_with_two_admins.is_public = True
2150+
project_with_two_admins.save()
2151+
2152+
user.gdpr_delete()
2153+
2154+
assert mock_update_search.called
2155+
2156+
@mock.patch('osf.models.node.AbstractNode.update_search')
2157+
def test_gdpr_delete_does_not_trigger_share_update_for_non_public_shared_nodes(
2158+
self, mock_update_search, user, project_with_two_admins):
2159+
assert project_with_two_admins.is_public is False
2160+
2161+
user.gdpr_delete()
2162+
2163+
assert not mock_update_search.called
2164+
2165+
@mock.patch('osf.models.preprint.Preprint.update_search')
2166+
def test_gdpr_delete_triggers_share_update_for_public_shared_preprints(
2167+
self, mock_update_search, user, preprint):
2168+
other_user = AuthUserFactory()
2169+
preprint.add_contributor(other_user, auth=Auth(user), permissions='admin')
2170+
preprint.save()
2171+
assert preprint.is_public is True
2172+
2173+
user.gdpr_delete()
2174+
2175+
assert mock_update_search.called
2176+
21462177
def test_can_gdpr_delete(self, user):
21472178
user.social = ['fake social']
21482179
user.schools = ['fake schools']
@@ -2184,28 +2215,36 @@ def test_can_gdpr_delete_shared_draft_registration_with_multiple_admins(self, us
21842215
assert draft_registrations.contributors.get() == other_admin
21852216
assert user.nodes.filter(deleted__isnull=True).count() == 0
21862217

2187-
def test_cant_gdpr_delete_multiple_contributors_registrations(self, user, registration):
2218+
def test_gdpr_delete_removes_user_from_shared_registrations(self, user, registration):
21882219
registration.is_public = True
21892220
other_user = AuthUserFactory()
21902221
registration.add_contributor(other_user, auth=Auth(user), permissions='admin')
21912222
registration.save()
21922223

21932224
assert registration.contributors.count() == 2
21942225

2195-
with pytest.raises(UserStateError) as exc_info:
2196-
user.gdpr_delete()
2226+
user.gdpr_delete()
2227+
registration.reload()
21972228

2198-
assert exc_info.value.args[0] == 'You cannot delete this user because they have one or more registrations.'
2229+
assert registration.contributors.count() == 1
2230+
assert registration.contributors.first() == other_user
2231+
assert not registration.is_deleted
2232+
assert user.deleted is not None
21992233

2200-
def test_cant_gdpr_delete_multiple_contributors_preprints(self, user, preprint):
2234+
def test_gdpr_delete_removes_user_from_shared_preprints(self, user, preprint):
22012235
other_user = AuthUserFactory()
22022236
preprint.add_contributor(other_user, auth=Auth(user), permissions='admin')
22032237
preprint.save()
22042238

2205-
with pytest.raises(UserStateError) as exc_info:
2206-
user.gdpr_delete()
2239+
assert preprint.contributors.count() == 2
2240+
2241+
user.gdpr_delete()
2242+
preprint.reload()
22072243

2208-
assert exc_info.value.args[0] == 'You cannot delete this user because they have one or more preprints.'
2244+
assert preprint.contributors.count() == 1
2245+
assert preprint.contributors.first() == other_user
2246+
assert not preprint.is_deleted
2247+
assert user.deleted is not None
22092248

22102249
def test_can_gdpr_delete_sole_contributor_registration(self, user):
22112250
registration = RegistrationFactory(creator=user)

0 commit comments

Comments
 (0)