Skip to content

Commit a87dc2a

Browse files
authored
Deletion of moderated publishers and users (after three years) (#8060)
1 parent 0dbb7eb commit a87dc2a

File tree

3 files changed

+110
-3
lines changed

3 files changed

+110
-3
lines changed

app/lib/admin/backend.dart

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,21 @@ class AdminBackend {
150150

151151
/// Removes user from the Datastore and updates the packages and other
152152
/// entities they may have controlled.
153+
///
154+
/// Verifies the current authenticated user for admin permissions.
153155
Future<void> removeUser(String userId) async {
154156
final caller = await requireAuthenticatedAdmin(AdminPermission.removeUsers);
155157
final user = await accountBackend.lookupUserById(userId);
156158
if (user == null) return;
157159
if (user.isDeleted) return;
158-
159160
_logger.info('${caller.displayId}) initiated the delete '
160161
'of ${user.userId} (${user.email})');
162+
await _removeUser(user);
163+
}
161164

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

877-
// TODO: delete publisher instances
878-
// TODO: mark user instances deleted
883+
// delete publishers
884+
final publisherQuery = _db.query<Publisher>()
885+
..filter('moderatedAt <', before)
886+
..order('moderatedAt');
887+
await for (final publisher in publisherQuery.run()) {
888+
// sanity check
889+
if (!publisher.isModerated) {
890+
continue;
891+
}
892+
893+
_logger.info('Deleting moderated publisher: ${publisher.publisherId}');
894+
895+
// removes packages of this publisher, no uploaders will be set, marks discontinued
896+
final pkgQuery = _db.query<Package>()
897+
..filter('publisherId =', publisher.publisherId);
898+
await for (final pkg in pkgQuery.run()) {
899+
await withRetryTransaction(_db, (tx) async {
900+
final p = await tx.lookupOrNull<Package>(pkg.key);
901+
if (p == null) return;
902+
if (p.publisherId != publisher.publisherId) return;
903+
904+
p.publisherId = null;
905+
p.updated = clock.now().toUtc();
906+
p.isDiscontinued = true;
907+
tx.insert(p);
908+
});
909+
}
910+
911+
// removes publisher members
912+
await _db.deleteWithQuery(
913+
_db.query<PublisherMember>(ancestorKey: publisher.key));
914+
915+
// removes publisher entity
916+
await _db.commit(deletes: [publisher.key]);
917+
918+
_logger.info('Deleted moderated publisher: ${publisher.publisherId}');
919+
}
920+
921+
// mark user instances deleted
922+
final userQuery = _db.query<User>()
923+
..filter('moderatedAt <', before)
924+
..order('moderatedAt');
925+
await for (final user in userQuery.run()) {
926+
// sanity check
927+
if (!user.isModerated || user.isDeleted) {
928+
continue;
929+
}
930+
931+
_logger.info('Deleting moderated user: ${user.userId}');
932+
await _removeUser(user);
933+
_logger.info('Deleting moderated user: ${user.userId}');
934+
}
879935
}
880936
}

app/test/admin/moderate_publisher_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import 'package:_pub_shared/data/account_api.dart';
66
import 'package:_pub_shared/data/admin_api.dart';
77
import 'package:_pub_shared/data/publisher_api.dart';
88
import 'package:clock/clock.dart';
9+
import 'package:pub_dev/admin/backend.dart';
910
import 'package:pub_dev/admin/models.dart';
1011
import 'package:pub_dev/fake/backend/fake_auth_provider.dart';
12+
import 'package:pub_dev/package/backend.dart';
1113
import 'package:pub_dev/publisher/backend.dart';
1214
import 'package:pub_dev/search/backend.dart';
1315
import 'package:pub_dev/shared/datastore.dart';
@@ -204,5 +206,29 @@ void main() {
204206
message: 'ModerationCase.status ("no-action") != "pending".',
205207
);
206208
});
209+
210+
testWithProfile('cleanup deletes datastore entities and abandons packages',
211+
fn: () async {
212+
// moderate and cleanup
213+
await _moderate('example.com', state: true, caseId: 'none');
214+
await adminBackend.deleteModeratedSubjects(before: clock.now().toUtc());
215+
216+
// no publisher or member
217+
expect(await publisherBackend.getPublisher('example.com'), isNull);
218+
expect(
219+
await publisherBackend.listPublisherMembers('example.com'),
220+
isEmpty,
221+
);
222+
223+
// publisher package has no publisher or uploader
224+
final pkg = await packageBackend.lookupPackage('neon');
225+
expect(pkg!.publisherId, isNull);
226+
expect(pkg.uploaders, isEmpty);
227+
228+
// other packages are not affected
229+
final other = await packageBackend.lookupPackage('oxygen');
230+
expect(other!.isDiscontinued, false);
231+
expect(other.uploaders, isNotEmpty);
232+
});
207233
});
208234
}

app/test/admin/moderate_user_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import 'package:_pub_shared/data/account_api.dart' as account_api;
66
import 'package:_pub_shared/data/admin_api.dart';
77
import 'package:_pub_shared/data/package_api.dart';
88
import 'package:_pub_shared/data/publisher_api.dart';
9+
import 'package:clock/clock.dart';
910
import 'package:pub_dev/account/auth_provider.dart';
1011
import 'package:pub_dev/account/backend.dart';
1112
import 'package:pub_dev/account/models.dart';
1213
import 'package:pub_dev/admin/backend.dart';
1314
import 'package:pub_dev/admin/models.dart';
1415
import 'package:pub_dev/fake/backend/fake_auth_provider.dart';
1516
import 'package:pub_dev/package/backend.dart';
17+
import 'package:pub_dev/publisher/backend.dart';
1618
import 'package:pub_dev/shared/configuration.dart';
1719
import 'package:pub_dev/shared/datastore.dart';
1820
import 'package:test/test.dart';
@@ -282,5 +284,28 @@ void main() {
282284
expect(p2!.publisherId, isNotEmpty);
283285
expect(p2.isDiscontinued, true);
284286
});
287+
288+
testWithProfile('cleanup deletes datastore entities', fn: () async {
289+
// moderate and cleanup
290+
final origUser = await accountBackend.lookupUserByEmail('[email protected]');
291+
await _moderate('[email protected]', state: true, reason: 'policy-violation');
292+
await adminBackend.deleteModeratedSubjects(before: clock.now().toUtc());
293+
294+
// entity is marked as deleted
295+
final user = await accountBackend.lookupUserById(origUser.userId);
296+
expect(user!.isDeleted, true);
297+
298+
// package has no uploader
299+
final pkg = await packageBackend.lookupPackage('oxygen');
300+
expect(pkg!.uploaders, isEmpty);
301+
expect(pkg.isDiscontinued, true);
302+
303+
// publisher has no members
304+
final publisher = await publisherBackend.getPublisher('example.com');
305+
expect(publisher!.isAbandoned, true);
306+
final members =
307+
await publisherBackend.listPublisherMembers('example.com');
308+
expect(members, isEmpty);
309+
});
285310
});
286311
}

0 commit comments

Comments
 (0)