def test_unit__anonymize_user__ok__explicit_name(self, session, app_config) -> None: api = UserApi(current_user=None, session=session, config=app_config) u = api.create_minimal_user("bob@bob") assert u.display_name == "bob" assert u.email == "bob@bob" cleanup_lib = CleanupLib(app_config=app_config, session=session) cleanup_lib.anonymize_user(u, anonymized_user_display_name="anonymous") assert u.display_name == "anonymous" assert u.email.endswith("@anonymous.local")
def test_safe_update__ok__nominal_case(self, session, app_config, admin_user) -> None: assert session.query(Workspace).all() == [] cleanup_lib = CleanupLib(app_config=app_config, session=session, dry_run_mode=False) test_workspace = Workspace() test_workspace.owner_id = admin_user.user_id cleanup_lib.safe_update(test_workspace) transaction.commit() assert session.query(Workspace).one() == test_workspace
def test_safe_update__ok__dry_run(self, session, app_config, admin_user) -> None: assert session.query(Workspace).all() == [] cleanup_lib = CleanupLib(app_config=app_config, session=session, dry_run_mode=True) test_workspace = Workspace() test_workspace.owner_id = admin_user.user_id cleanup_lib.safe_update(test_workspace) transaction.commit() with pytest.raises(NoResultFound): assert session.query(Workspace).one() == test_workspace
def test_unit__anonymize_user__ok__nominal_case(self, session, app_config) -> None: api = UserApi(current_user=None, session=session, config=app_config) u = api.create_minimal_user(email="bob@bob", username="******") assert u.display_name == "bob" assert u.username == "bob" assert u.email == "bob@bob" cleanup_lib = CleanupLib(app_config=app_config, session=session) cleanup_lib.anonymize_user(u) assert u.display_name == "Deleted user" assert u.email.endswith("@anonymous.local") assert u.username is None
def test_safe_delete__ok__dry_run(self, session, app_config, admin_user) -> None: assert session.query(Workspace).all() == [] cleanup_lib = CleanupLib(app_config=app_config, session=session, dry_run_mode=True) test_workspace = Workspace() test_workspace.owner_id = admin_user.user_id session.add(test_workspace) transaction.commit() cleanup_lib.safe_delete(test_workspace) transaction.commit() assert session.query(Workspace).one() == test_workspace
def test_safe_delete_dir__ok__dry_run(self, session, app_config, admin_user) -> None: assert session.query(Workspace).all() == [] cleanup_lib = CleanupLib(app_config=app_config, session=session, dry_run_mode=True) dir_path = tempfile.mkdtemp() file_path = "{}/my_file.txt".format(dir_path) Path(file_path).touch() assert Path(dir_path).is_dir() assert Path(file_path).is_file() cleanup_lib.safe_delete_dir(dir_path) assert Path(dir_path).is_dir() assert Path(file_path).is_file()
def take_app_action(self, parsed_args: argparse.Namespace, app_context: AppEnvironment) -> None: self._session = app_context["request"].dbsession self._app_config = app_context["registry"].settings["CFG"] if parsed_args.dry_run_mode: print("(!) Running in dry-run mode, not change will be applied.") app_context["request"].tm.doom() with unprotected_content_revision(self._session) as session: uapi = UserApi( config=self._app_config, session=session, current_user=None, show_deleted=True, show_deactivated=True, ) user_list = [] # type: typing.List[User] for login in parsed_args.logins: try: user = uapi.get_one_by_login(login) user_list.append(user) except UserDoesNotExist as exc: print('ERROR: user with email "{}" does not exist'.format( login)) raise exc for user in user_list: print("~~~~~~~~~~") cleanup_lib = CleanupLib(session, self._app_config, dry_run_mode=parsed_args.dry_run_mode) print("anonymize user {}.".format(user.user_id)) cleanup_lib.anonymize_user( user, anonymized_user_display_name=parsed_args.anonymize_name) self._session.flush() print('user {} anonymized to "{} <{}/{}>".'.format( user.user_id, user.display_name, user.email, user.username)) print("~~~~~~~~~~")
def should_anonymize(self, user: User, owned_workspaces_will_be_deleted: bool, cleanup_lib: CleanupLib) -> UserNeedAnonymization: # INFO - G.M - 2019-12-20 - check user revisions that need to be deleted for consistent database # if we do not want to anonymize user but delete him should_anonymize = cleanup_lib.should_anonymize( user, owned_workspace_will_be_deleted=owned_workspaces_will_be_deleted) if should_anonymize.blocking_revisions: print( '{} revision of user "{}" in sharespaces found, deleting them, can cause inconsistent' " database.".format(len(should_anonymize.blocking_revisions), user.user_id)) if should_anonymize.blocking_workspaces: print( '{} workspace(s) of user "{}" found, cannot delete user without deleting/changing ownership' .format(len(should_anonymize.blocking_workspaces), user.user_id)) return should_anonymize
def test_unit__delete_content__ok__nominal_case( self, session, app_config, content_type_list, content_api_factory, workspace_api_factory, share_lib_factory, ) -> None: content_api = content_api_factory.get(show_deleted=True, show_active=True, show_archived=True) workspace_api = workspace_api_factory.get() test_workspace = workspace_api.create_workspace("test_workspace") folder = content_api.create( label="test-folder", content_type_slug=content_type_list.Folder.slug, workspace=test_workspace, do_save=True, do_notify=False, ) folder_id = folder.content_id file_ = content_api.create( content_type_slug=content_type_list.File.slug, workspace=test_workspace, parent=folder, label="Test file", do_save=True, do_notify=False, ) file_id = file_.content_id comment = content_api.create_comment(workspace=test_workspace, parent=file_, content="Toto", do_save=True, do_notify=False) comment_id = comment.content_id share_api = share_lib_factory.get() shares = share_api.share_content(file_, emails=["*****@*****.**"]) share_id = shares[0].share_id session.flush() transaction.commit() assert content_api.get_one(folder_id, content_type=content_type_list.Any_SLUG) assert content_api.get_one(file_id, content_type=content_type_list.Any_SLUG) assert content_api.get_one(comment_id, content_type=content_type_list.Any_SLUG) assert session.query(ContentShare).filter( ContentShare.share_id == share_id).one() with unprotected_content_revision(session) as unprotected_session: cleanup_lib = CleanupLib(app_config=app_config, session=unprotected_session) cleanup_lib.delete_content(folder) session.flush() transaction.commit() with pytest.raises(ContentNotFound): content_api.get_one(folder_id, content_type=content_type_list.Any_SLUG) with pytest.raises(ContentNotFound): content_api.get_one(file_id, content_type=content_type_list.Any_SLUG) with pytest.raises(ContentNotFound): content_api.get_one(comment_id, content_type=content_type_list.Any_SLUG) with pytest.raises(NoResultFound): session.query(ContentShare).filter( ContentShare.share_id == share_id).one()
def test_unit__delete_revision__ok__delete_older_revision( self, admin_user, session, app_config, content_type_list, content_api_factory, workspace_api_factory, share_lib_factory, upload_permission_lib_factory, ) -> None: content_api = content_api_factory.get(show_deleted=True, show_active=True, show_archived=True) workspace_api = workspace_api_factory.get() test_workspace = workspace_api.create_workspace("test_workspace") session.add(test_workspace) folder = content_api.create( label="test-folder", content_type_slug=content_type_list.Folder.slug, workspace=test_workspace, do_save=True, do_notify=False, ) file_ = content_api.create( content_type_slug=content_type_list.File.slug, workspace=test_workspace, parent=folder, label="Test file", do_save=True, do_notify=False, ) with new_revision(session=session, tm=transaction.manager, content=file_): content_api.update_file_data(file_, "Test_file.txt", new_mimetype="plain/text", new_content=b"Test file") content_api.mark_read(file_) file_id = file_.content_id revisions = file_.revisions session.flush() transaction.commit() assert len(revisions) == 2 first_revision_id = revisions[0].revision_id second_revision_id = revisions[1].revision_id content = content_api.get_one(file_id, content_type=content_type_list.Any_SLUG) assert content assert content.revision.revision_id == second_revision_id assert (session.query(ContentRevisionRO).filter( ContentRevisionRO.revision_id == first_revision_id).one()) assert (session.query(ContentRevisionRO).filter( ContentRevisionRO.revision_id == second_revision_id).one()) assert (session.query(RevisionReadStatus).filter( RevisionReadStatus.revision_id == first_revision_id).one()) assert (session.query(RevisionReadStatus).filter( RevisionReadStatus.revision_id == second_revision_id).one()) with unprotected_content_revision(session) as unprotected_session: cleanup_lib = CleanupLib(app_config=app_config, session=unprotected_session) cleanup_lib.delete_revision(revision=revisions[0]) session.flush() transaction.commit() assert content assert content.revision.revision_id == second_revision_id assert (session.query(ContentRevisionRO).filter( ContentRevisionRO.revision_id == second_revision_id).one()) with pytest.raises(NoResultFound): assert (session.query(ContentRevisionRO).filter( ContentRevisionRO.revision_id == first_revision_id).one()) assert (session.query(RevisionReadStatus).filter( RevisionReadStatus.revision_id == second_revision_id).one()) with pytest.raises(NoResultFound): assert (session.query(RevisionReadStatus).filter( RevisionReadStatus.revision_id == first_revision_id).one())
def test_unit__delete_user_associated_data__ok__nominal_case( self, admin_user, session, app_config, content_type_list, content_api_factory, workspace_api_factory, share_lib_factory, upload_permission_lib_factory, ) -> None: content_api = content_api_factory.get(show_deleted=True, show_active=True, show_archived=True) workspace_api = workspace_api_factory.get() test_workspace = workspace_api.create_workspace("test_workspace") session.add(test_workspace) session.flush() workspace_id = test_workspace.workspace_id folder = content_api.create( label="test-folder", content_type_slug=content_type_list.Folder.slug, workspace=test_workspace, do_save=True, do_notify=False, ) folder_id = folder.content_id folder2 = content_api.create( label="test-folder2", content_type_slug=content_type_list.Folder.slug, workspace=test_workspace, do_save=True, do_notify=False, ) folder2_id = folder2.content_id file_ = content_api.create( content_type_slug=content_type_list.File.slug, workspace=test_workspace, parent=folder, label="Test file", do_save=True, do_notify=False, ) file_id = file_.content_id comment = content_api.create_comment(workspace=test_workspace, parent=file_, content="Toto", do_save=True, do_notify=False) comment_id = comment.content_id share_api = share_lib_factory.get() shares = share_api.share_content(file_, emails=["*****@*****.**"]) share_id = shares[0].share_id upload_permission_lib = upload_permission_lib_factory.get() upload_permissions = upload_permission_lib.add_permission_to_workspace( workspace=test_workspace, emails=["*****@*****.**"]) upload_permission_id = upload_permissions[0].upload_permission_id session.flush() transaction.commit() assert content_api.get_one(folder_id, content_type=content_type_list.Any_SLUG) assert content_api.get_one(file_id, content_type=content_type_list.Any_SLUG) assert content_api.get_one(comment_id, content_type=content_type_list.Any_SLUG) assert session.query(ContentShare).filter( ContentShare.share_id == share_id).one() assert (session.query(UploadPermission).filter( UploadPermission.upload_permission_id == upload_permission_id).one()) session.query(Workspace).filter( Workspace.workspace_id == workspace_id).one() with unprotected_content_revision(session) as unprotected_session: cleanup_lib = CleanupLib(app_config=app_config, session=unprotected_session) cleanup_lib.delete_user_associated_data(admin_user) session.flush() transaction.commit() # INFO - G.M - 2019-12-20 - workspace is not deleted by this method session.query(Workspace).filter( Workspace.workspace_id == workspace_id).one() with pytest.raises(NoResultFound): session.query(UserRoleInWorkspace).filter( UserRoleInWorkspace.workspace_id == workspace_id).one() with pytest.raises(NoResultFound): session.query(UploadPermission).filter( UploadPermission.workspace_id == workspace_id).one() with pytest.raises(ContentNotFound): content_api.get_one(folder2_id, content_type=content_type_list.Any_SLUG) with pytest.raises(ContentNotFound): content_api.get_one(folder_id, content_type=content_type_list.Any_SLUG) with pytest.raises(ContentNotFound): content_api.get_one(file_id, content_type=content_type_list.Any_SLUG) with pytest.raises(ContentNotFound): content_api.get_one(comment_id, content_type=content_type_list.Any_SLUG) with pytest.raises(NoResultFound): session.query(ContentShare).filter( ContentShare.share_id == share_id).one() with pytest.raises(NoResultFound): session.query(UploadPermission).filter( UploadPermission.upload_permission_id == upload_permission_id).one()
def _delete_user_database_info( self, user: User, cleanup_lib: CleanupLib, delete_owned_workspaces: bool = False, force_delete_all_user_revisions: bool = False, anonymize_if_required: bool = False, anonymized_user_display_name: typing.Optional[str] = None, ): print('trying to delete user {}: "{}"\n'.format( user.user_id, user.login)) deleted_workspace_ids = [] deleted_user_id = user.user_id should_anonymize = self.should_anonymize( user, owned_workspaces_will_be_deleted=delete_owned_workspaces, cleanup_lib=cleanup_lib) force_delete_all_associated_data = (force_delete_all_user_revisions and delete_owned_workspaces) revision_conflict_for_deleting_user = ( should_anonymize.blocking_revisions and not force_delete_all_associated_data) workspace_conflict_for_deleting_user = ( should_anonymize.blocking_workspaces and not delete_owned_workspaces) if (revision_conflict_for_deleting_user or workspace_conflict_for_deleting_user ) and not anonymize_if_required: raise UserCannotBeDeleted( 'user "{}" has revisions or workspaces left, cannot delete it'. format(user.user_id)) if delete_owned_workspaces: deleted_workspace_ids = cleanup_lib.delete_user_owned_workspace( user) print('owned workspace for user "{}" deleted'.format(user.user_id)) if force_delete_all_user_revisions: cleanup_lib.delete_user_revisions(user) print('all user "{}" revisions deleted'.format(user.user_id)) if should_anonymize.need_anonymization and not force_delete_all_associated_data: # NOTE S.G. 2020-10-14 - Need to load the user config now as loading it after # delete_user_associated_data() in dry-run mode doesn't work user_config = user.config cleanup_lib.delete_user_associated_data(user) cleanup_lib.safe_delete(user_config) cleanup_lib.anonymize_user( user, anonymized_user_display_name=anonymized_user_display_name) print('user {} anonymized to "{} <{}/{}>".'.format( user.user_id, user.display_name, user.email, user.username)) else: print('delete user "{}"'.format(user.user_id)) # NOTE S.G. 2020-10-14 - Need to load the user config now as loading it after # delete_user_associated_data() in dry-run mode doesn't work user_config = user.config cleanup_lib.delete_user_associated_data(user) cleanup_lib.safe_delete(user_config) cleanup_lib.safe_delete(user) print('user "{}" deleted'.format(user.user_id)) self._session.flush() return DeleteResultIds(deleted_user_id, deleted_workspace_ids)
def take_app_action(self, parsed_args: argparse.Namespace, app_context: AppEnvironment) -> None: self._session = app_context["request"].dbsession self._app_config = app_context["registry"].settings["CFG"] delete_user_revision = parsed_args.force or parsed_args.delete_revisions delete_owned_sharespaces = (parsed_args.force or parsed_args.best_effort or parsed_args.delete_sharespaces) anonymize_if_required = parsed_args.best_effort or parsed_args.anonymize_if_required if parsed_args.dry_run_mode: print("(!) Running in dry-run mode, no changes will be applied.") app_context["request"].tm.doom() if parsed_args.force: print("(!) Running in force mode") if parsed_args.best_effort: print("(!) Running in best-effort mode") if delete_user_revision: print( "/!\\ Delete all user revisions, database created may be broken /!\\." ) if delete_owned_sharespaces: print("(!) User owned sharespaces will be deleted too.") if anonymize_if_required: print("(!) Will anonymize user if not possible to delete it") if parsed_args.anonymize_name: print('(!) Custom anonymize name choosen is: "{}"'.format( parsed_args.anonymize_name)) print("") deleted_user_ids = set() # typing.Set[int] deleted_workspace_ids = set() # typing.Set[int] with unprotected_content_revision(self._session) as session: uapi = UserApi( config=self._app_config, session=session, current_user=None, show_deleted=True, show_deactivated=True, ) user_list = [] # type: typing.List[User] for login in parsed_args.logins: try: user = uapi.get_one_by_login(login) user_list.append(user) except UserDoesNotExist as exc: print( 'ERROR: user with email/username "{}" does not exist'. format(login)) raise exc print("~~~~") print("Deletion of user from Database") print("~~~~\n") print("~~~~") for user in user_list: cleanup_lib = CleanupLib(session, self._app_config, dry_run_mode=parsed_args.dry_run_mode) deleted_user_ids_result = self._delete_user_database_info( user, force_delete_all_user_revisions=delete_user_revision, anonymize_if_required=anonymize_if_required, delete_owned_workspaces=delete_owned_sharespaces, anonymized_user_display_name=parsed_args.anonymize_name, cleanup_lib=cleanup_lib, ) deleted_user_ids.add(deleted_user_ids_result.user_id) deleted_workspace_ids.update( deleted_user_ids_result.workspace_ids) print("~~~~") print( "deletion of user(s) from database process almost finished, change will be applied at end " "of this script.\n") print("~~~~") print("Deletion of Caldav Agenda\n") app_lib = ApplicationApi(app_list=app_list) if app_lib.exist(AGENDA__APP_SLUG): # INFO - G.M - 2019-12-13 - cleanup agenda at end of process if deleted_workspace_ids: deleted_workspace_ids_str = [ '"{}"'.format(workspace_id) for workspace_id in deleted_workspace_ids ] print("delete agenda of workspaces {}".format( ", ".join(deleted_workspace_ids_str))) for workspace_id in deleted_workspace_ids: try: cleanup_lib.delete_workspace_agenda(workspace_id) except AgendaNotFoundError: print( 'Warning: Cannot delete agenda for workspace "{}", agenda not found. Agenda path may be incorrect or agenda not created' .format(workspace_id)) print(traceback.format_exc()) if deleted_user_ids: deleted_user_ids_str = [ '"{}"'.format(user_id) for user_id in deleted_user_ids ] print("delete agenda of users {}".format( ", ".join(deleted_user_ids_str))) for user_id in deleted_user_ids: try: cleanup_lib.delete_user_agenda(user_id) except AgendaNotFoundError: print( 'Warning: Cannot delete agenda for user "{}", agenda not found. Agenda path may be incorrect or agenda not created' .format(user_id)) print(traceback.format_exc()) else: print( "Warning ! Agenda app not enabled, agenda will not be deleted." ) print("~~~~") print("deletion of Agenda process finished") print("~~~~") if parsed_args.dry_run_mode: print("Finished (dry-run mode, no change applied)") else: print("Finished")