From 286c749992af25ba4a878b21dc8e397725232cc4 Mon Sep 17 00:00:00 2001 From: John Tordoff <> Date: Fri, 6 Oct 2023 22:37:38 +0800 Subject: [PATCH] update gdpr to delete draftregistrations and draftnode --- osf/models/user.py | 57 ++++++++++++++++++++++++++++++++++++++---- osf_tests/test_user.py | 42 ++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/osf/models/user.py b/osf/models/user.py index ba8a6fb59f98..ba60c7d33a94 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -1905,7 +1905,12 @@ def gdpr_delete(self): https://openscience.atlassian.net/wiki/spaces/PRODUC/pages/482803755/GDPR-Related+protocols """ - from osf.models import Preprint, AbstractNode + from osf.models import ( + Preprint, + AbstractNode, + DraftRegistration, + DraftNode + ) user_nodes = self.nodes.exclude(is_deleted=True) # Validates the user isn't trying to delete things they deliberately made public. @@ -1924,7 +1929,7 @@ def gdpr_delete(self): ) shared_nodes = user_nodes.exclude(id__in=personal_nodes.values_list('id')) - for node in shared_nodes.exclude(type__in=['osf.quickfilesnode', 'osf.draftnode']): + for node in shared_nodes: alternate_admins = OSFUser.objects.filter(groups__name=node.format_group(ADMIN)).filter(is_active=True).exclude(id=self.id) if not alternate_admins: raise UserStateError( @@ -1937,21 +1942,63 @@ def gdpr_delete(self): 'have an external account for {} attached to Node {}, ' 'which has other contributors.'.format(addon.short_name, node._id)) + # Validates that the user isn't trying to delete things DraftRegistration they are the only admin on. + personal_draft_registrations = ( + DraftRegistration.objects.annotate(contrib_count=Count('_contributors')) + .filter(contrib_count__lte=1) + .filter(_contributors=self) + .filter(deleted__isnull=True) + ) + shared_draft_registrations = self.draft_registrations.exclude(id__in=personal_draft_registrations.values_list('id')) + for node in shared_draft_registrations: + alternate_admins = OSFUser.objects.filter( + groups__name=node.format_group(ADMIN) + ).filter( + is_active=True + ).exclude( + id=self.id + ) + if not alternate_admins: + raise UserStateError( + f'You cannot delete DraftRegistration {node._id} because it would be a node with contributors, but with no ' + f'admin.' + ) + for addon in node.get_addons(): + if addon.short_name not in ('osfstorage', 'wiki') and \ + addon.user_settings and \ + addon.user_settings.owner.id == self.id: + raise UserStateError( + f'You cannot delete this user because they have an external account for {addon.short_name}' + f' attached to DraftRegistration {node._id}, which has other contributors.' + ) + for group in self.osf_groups: if not group.managers.exclude(id=self.id).filter(is_registered=True).exists() and group.members.exclude(id=self.id).exists(): raise UserStateError('You cannot delete this user because they are the only registered manager of OSFGroup {} that contains other members.'.format(group._id)) for node in shared_nodes.all(): - logger.info('Removing {self._id} as a contributor to node (pk:{node_id})...'.format(self=self, node_id=node.pk)) + logger.info(f'Removing {self._id} as a contributor to node (pk:{node.pk})...') node.remove_contributor(self, auth=Auth(self), log=False) + for draft_registration in shared_draft_registrations.all(): + logger.info(f'Removing {self._id} as a contributor to node (pk:{node.pk})...') + draft_registration.remove_contributor(self, auth=Auth(self), log=False) + # This is doesn't to remove identifying info, but ensures other users can't see the deleted user's profile etc. self.deactivate_account() # delete all personal nodes (one contributor), bookmarks, quickfiles etc. for node in personal_nodes.all(): - logger.info('Soft-deleting node (pk: {node_id})...'.format(node_id=node.pk)) - node.remove_node(auth=Auth(self)) + if isinstance(node, DraftNode): + logger.info(f'Hard-deleting draftnode (pk: {node.pk})...') + node.delete() + else: + logger.info(f'Soft-deleting node (pk: {node.pk})...') + node.remove_node(auth=Auth(self)) + + for draft_registration in personal_draft_registrations.all(): + logger.info(f'Soft-deleting draft registrations (pk: {draft_registration.pk})...') + draft_registration.delete() for group in self.osf_groups: if len(group.managers) == 1 and group.managers[0] == self: diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py index 0c67bc4ed43d..baf24287ea02 100644 --- a/osf_tests/test_user.py +++ b/osf_tests/test_user.py @@ -37,6 +37,8 @@ NotableDomain, PreprintContributor, DraftRegistrationContributor, + DraftRegistration, + DraftNode, UserSessionMap, ) from osf.models.institution_affiliation import get_user_by_institution_identity @@ -66,7 +68,8 @@ UnregUserFactory, UserFactory, RegistrationFactory, - PreprintFactory + PreprintFactory, + DraftNodeFactory ) from tests.base import OsfTestCase from tests.utils import run_celery_tasks @@ -2387,6 +2390,12 @@ def registration(self, user): registration.save() return registration + @pytest.fixture() + def registration_with_draft_node(self, user, registration): + registration.branched_from = DraftNodeFactory(creator=user) + registration.save() + return registration + @pytest.fixture() def project(self, user): project = ProjectFactory(creator=user) @@ -2433,11 +2442,42 @@ def test_can_gdpr_delete_personal_nodes(self, user): user.gdpr_delete() assert user.nodes.exclude(is_deleted=True).count() == 0 + def test_can_gdpr_delete_personal_registrations(self, user, registration_with_draft_node): + assert DraftRegistration.objects.all().count() == 1 + assert DraftNode.objects.all().count() == 1 + + with pytest.raises(UserStateError) as exc_info: + user.gdpr_delete() + + assert exc_info.value.args[0] == 'You cannot delete this user because they have one or more registrations.' + assert DraftRegistration.objects.all().count() == 1 + assert DraftNode.objects.all().count() == 1 + + registration_with_draft_node.remove_node(Auth(user)) + assert DraftRegistration.objects.all().count() == 1 + assert DraftNode.objects.all().count() == 1 + user.gdpr_delete() + + assert user.nodes.exclude(is_deleted=True).count() == 0 + assert DraftRegistration.objects.all().count() == 0 + assert DraftNode.objects.all().count() == 0 + def test_can_gdpr_delete_shared_nodes_with_multiple_admins(self, user, project_with_two_admins): user.gdpr_delete() assert user.nodes.all().count() == 0 + def test_can_gdpr_delete_shared_draft_registration_with_multiple_admins(self, user, registration): + other_admin = AuthUserFactory() + draft_registrations = user.draft_registrations.get() + draft_registrations.add_contributor(other_admin, permissions='admin') + assert draft_registrations.contributors.all().count() == 2 + registration.delete_registration_tree(save=True) + + user.gdpr_delete() + assert draft_registrations.contributors.get() == other_admin + assert user.nodes.filter(deleted__isnull=True).count() == 0 + def test_cant_gdpr_delete_registrations(self, user, registration): with pytest.raises(UserStateError) as exc_info: