Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deletion of moderated publishers and users (after three years) #8060

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions app/lib/admin/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,21 @@ class AdminBackend {

/// Removes user from the Datastore and updates the packages and other
/// entities they may have controlled.
///
/// Verifies the current authenticated user for admin permissions.
Future<void> removeUser(String userId) async {
final caller = await requireAuthenticatedAdmin(AdminPermission.removeUsers);
final user = await accountBackend.lookupUserById(userId);
if (user == null) return;
if (user.isDeleted) return;

_logger.info('${caller.displayId}) initiated the delete '
'of ${user.userId} (${user.email})');
await _removeUser(user);
}

/// Removes user from the Datastore and updates the packages and other
/// entities they may have controlled.
Future<void> _removeUser(User user) async {
// Package.uploaders
final pool = Pool(10);
final futures = <Future>[];
Expand Down Expand Up @@ -874,7 +880,57 @@ class AdminBackend {
'Deleted moderated package version: ${version.qualifiedVersionKey}');
}

// TODO: delete publisher instances
// TODO: mark user instances deleted
// delete publishers
final publisherQuery = _db.query<Publisher>()
..filter('moderatedAt <', before)
..order('moderatedAt');
await for (final publisher in publisherQuery.run()) {
// sanity check
if (!publisher.isModerated) {
continue;
}

_logger.info('Deleting moderated publisher: ${publisher.publisherId}');

// removes packages of this publisher, no uploaders will be set, marks discontinued
final pkgQuery = _db.query<Package>()
..filter('publisherId =', publisher.publisherId);
await for (final pkg in pkgQuery.run()) {
await withRetryTransaction(_db, (tx) async {
final p = await tx.lookupOrNull<Package>(pkg.key);
if (p == null) return;
if (p.publisherId != publisher.publisherId) return;

p.publisherId = null;
p.updated = clock.now().toUtc();
p.isDiscontinued = true;
tx.insert(p);
});
}

// removes publisher members
await _db.deleteWithQuery(
_db.query<PublisherMember>(ancestorKey: publisher.key));

// removes publisher entity
await _db.commit(deletes: [publisher.key]);

_logger.info('Deleted moderated publisher: ${publisher.publisherId}');
}

// mark user instances deleted
final userQuery = _db.query<User>()
..filter('moderatedAt <', before)
..order('moderatedAt');
await for (final user in userQuery.run()) {
// sanity check
if (!user.isModerated || user.isDeleted) {
continue;
}

_logger.info('Deleting moderated user: ${user.userId}');
await _removeUser(user);
_logger.info('Deleting moderated user: ${user.userId}');
}
}
}
26 changes: 26 additions & 0 deletions app/test/admin/moderate_publisher_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import 'package:_pub_shared/data/account_api.dart';
import 'package:_pub_shared/data/admin_api.dart';
import 'package:_pub_shared/data/publisher_api.dart';
import 'package:clock/clock.dart';
import 'package:pub_dev/admin/backend.dart';
import 'package:pub_dev/admin/models.dart';
import 'package:pub_dev/fake/backend/fake_auth_provider.dart';
import 'package:pub_dev/package/backend.dart';
import 'package:pub_dev/publisher/backend.dart';
import 'package:pub_dev/search/backend.dart';
import 'package:pub_dev/shared/datastore.dart';
Expand Down Expand Up @@ -204,5 +206,29 @@ void main() {
message: 'ModerationCase.status ("no-action") != "pending".',
);
});

testWithProfile('cleanup deletes datastore entities and abandons packages',
fn: () async {
// moderate and cleanup
await _moderate('example.com', state: true, caseId: 'none');
await adminBackend.deleteModeratedSubjects(before: clock.now().toUtc());

// no publisher or member
expect(await publisherBackend.getPublisher('example.com'), isNull);
expect(
await publisherBackend.listPublisherMembers('example.com'),
isEmpty,
);

// publisher package has no publisher or uploader
final pkg = await packageBackend.lookupPackage('neon');
expect(pkg!.publisherId, isNull);
expect(pkg.uploaders, isEmpty);

// other packages are not affected
final other = await packageBackend.lookupPackage('oxygen');
expect(other!.isDiscontinued, false);
expect(other.uploaders, isNotEmpty);
});
});
}
25 changes: 25 additions & 0 deletions app/test/admin/moderate_user_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import 'package:_pub_shared/data/account_api.dart' as account_api;
import 'package:_pub_shared/data/admin_api.dart';
import 'package:_pub_shared/data/package_api.dart';
import 'package:_pub_shared/data/publisher_api.dart';
import 'package:clock/clock.dart';
import 'package:pub_dev/account/auth_provider.dart';
import 'package:pub_dev/account/backend.dart';
import 'package:pub_dev/account/models.dart';
import 'package:pub_dev/admin/backend.dart';
import 'package:pub_dev/admin/models.dart';
import 'package:pub_dev/fake/backend/fake_auth_provider.dart';
import 'package:pub_dev/package/backend.dart';
import 'package:pub_dev/publisher/backend.dart';
import 'package:pub_dev/shared/configuration.dart';
import 'package:pub_dev/shared/datastore.dart';
import 'package:test/test.dart';
Expand Down Expand Up @@ -282,5 +284,28 @@ void main() {
expect(p2!.publisherId, isNotEmpty);
expect(p2.isDiscontinued, true);
});

testWithProfile('cleanup deletes datastore entities', fn: () async {
// moderate and cleanup
final origUser = await accountBackend.lookupUserByEmail('admin@pub.dev');
await _moderate('admin@pub.dev', state: true, reason: 'policy-violation');
await adminBackend.deleteModeratedSubjects(before: clock.now().toUtc());

// entity is marked as deleted
final user = await accountBackend.lookupUserById(origUser.userId);
expect(user!.isDeleted, true);

// package has no uploader
final pkg = await packageBackend.lookupPackage('oxygen');
expect(pkg!.uploaders, isEmpty);
expect(pkg.isDiscontinued, true);

// publisher has no members
final publisher = await publisherBackend.getPublisher('example.com');
expect(publisher!.isAbandoned, true);
final members =
await publisherBackend.listPublisherMembers('example.com');
expect(members, isEmpty);
});
});
}
Loading