Skip to content
Merged
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
4 changes: 4 additions & 0 deletions app/lib/admin/actions/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import 'moderation_case_list.dart';
import 'moderation_case_resolve.dart';
import 'moderation_case_update.dart';
import 'moderation_transparency_metrics.dart';
import 'package_delete.dart';
import 'package_discontinue.dart';
import 'package_info.dart';
import 'package_latest_update.dart';
import 'package_reservation_create.dart';
import 'package_reservation_delete.dart';
import 'package_reservation_list.dart';
import 'package_version_delete.dart';
import 'package_version_info.dart';
import 'package_version_retraction.dart';
import 'publisher_create.dart';
Expand Down Expand Up @@ -105,12 +107,14 @@ final class AdminAction {
moderationCaseResolve,
moderationCaseUpdate,
moderationTransparencyMetrics,
packageDelete,
packageDiscontinue,
packageInfo,
packageLatestUpdate,
packageReservationCreate,
packageReservationDelete,
packageReservationList,
packageVersionDelete,
packageVersionInfo,
packageVersionRetraction,
publisherCreate,
Expand Down
57 changes: 57 additions & 0 deletions app/lib/admin/actions/package_delete.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import '../../account/backend.dart';
import '../../shared/configuration.dart';
import '../backend.dart';
import 'actions.dart';

final packageDelete = AdminAction(
name: 'package-delete',
options: {
'package': 'name of package to delete',
},
summary: 'Deletes package <package>.',
description: '''
Deletes package <package>.

Deletes all associated resources:

* PackageVersions
* Likes
* AuditLogRecords
* PackageVersionAsset
* replacedBy references
* archives (might be retrievable from backup)

The package will be "tombstoned" and no package with the same name can be
published later.
''',
invoke: (args) async {
final packageName = args['package'];
if (packageName == null) {
throw InvalidInputException('Missing `package` argument');
}

await requireAuthenticatedAdmin(AdminPermission.removePackage);
final result = await adminBackend.removePackage(packageName);

return {
'message': '''
Package and all associated resources deleted.

A tombstone has been created

'NOTICE: Redis caches referencing the package will expire given time.'
''',
'package': packageName,
'deletedPackages': result.deletedPackages,
'deletedPackageVersions': result.deletedPackageVersions,
'deletedPackageVersionInfos': result.deletedPackageVersionInfos,
'deletedPackageVersionAssets': result.deletedPackageVersionAssets,
'deletedLikes': result.deletedLikes,
'deletedAuditLogs': result.deletedAuditLogs,
'replacedByFixes': result.replacedByFixes,
};
});
50 changes: 50 additions & 0 deletions app/lib/admin/actions/package_version_delete.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import '../../account/backend.dart';
import '../../shared/configuration.dart';
import '../backend.dart';
import 'actions.dart';

final packageVersionDelete = AdminAction(
name: 'package-version-delete',
options: {
'package': 'name of package to delete',
'version': 'version of package',
},
summary: 'Deletes package <package> version <version>.',
description: '''
Deletes package <package> version <version>.

Deletes all associated resources:

* PackageVersions
* PackageVersionAsset
* archives (might be retrievable from backup)

The package version will be "tombstoned" and same version cannot be published
later.
''',
invoke: (args) async {
await requireAuthenticatedAdmin(AdminPermission.removePackage);
final packageName = args['package'];
if (packageName == null) {
throw InvalidInputException('Missing `package` argument');
}
final version = args['version'];
if (version == null) {
throw InvalidInputException('Missing `version` argument');
}
final result =
await adminBackend.removePackageVersion(packageName, version);

return {
'message': 'Package version and all associated resources deleted.',
'package': packageName,
'version': version,
'deletedPackageVersions': result.deletedPackageVersions,
'deletedPackageVersionInfos': result.deletedPackageVersionInfos,
'deletedPackageVersionAssets': result.deletedPackageVersionAssets,
};
});
85 changes: 46 additions & 39 deletions app/lib/admin/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -294,24 +294,16 @@ class AdminBackend {
/// Creates a [ModeratedPackage] instance (if not already present) in
/// Datastore representing the removed package. No new package with the same
/// name can be published.
///
/// Verifies the current authenticated user for admin permissions.
Future<void> removePackage(String packageName) async {
final caller =
await requireAuthenticatedAdmin(AdminPermission.removePackage);
_logger.info('${caller.displayId}) initiated the delete '
'of package $packageName');
await _removePackage(packageName);
}

/// Removes the package from the Datastore and updates other related
/// entities. It is safe to call [removePackage] on an already removed
/// package, as the call is idempotent.
///
/// Creates a [ModeratedPackage] instance (if not already present) in
/// Datastore representing the removed package. No new package with the same
/// name can be published.
Future<void> _removePackage(
Future<
({
int deletedPackages,
int deletedPackageVersions,
int deletedPackageVersionInfos,
int deletedPackageVersionAssets,
int deletedLikes,
int deletedAuditLogs,
int replacedByFixes,
})> removePackage(
String packageName, {
DateTime? moderated,
}) async {
Expand All @@ -336,6 +328,7 @@ class AdminBackend {
_logger.info('Removing package from Package.replacedBy...');
final replacedByQuery = _db.query<Package>()
..filter('replacedBy =', packageName);
var replacedByFixes = 0;
await for (final pkg in replacedByQuery.run()) {
await withRetryTransaction(_db, (tx) async {
final p = await tx.lookupOrNull<Package>(pkg.key);
Expand All @@ -346,26 +339,28 @@ class AdminBackend {
p.replacedBy = null;
tx.insert(p);
}
replacedByFixes++;
});
}

_logger.info('Removing package from PackageVersionInfo ...');
await _db.deleteWithQuery(
final deletedPackageVersionInfos = await _db.deleteWithQuery(
_db.query<PackageVersionInfo>()..filter('package =', packageName));

_logger.info('Removing package from PackageVersionAsset ...');
await _db.deleteWithQuery(
final deletedPackageVersionAssets = await _db.deleteWithQuery(
_db.query<PackageVersionAsset>()..filter('package =', packageName));

_logger.info('Removing package from Like ...');
await _db.deleteWithQuery(
final deletedLikes = await _db.deleteWithQuery(
_db.query<Like>()..filter('packageName =', packageName));

_logger.info('Removing package from AuditLogRecord...');
await _db.deleteWithQuery(
final deletedAuditLogRecords = await _db.deleteWithQuery(
_db.query<AuditLogRecord>()..filter('packages =', packageName));

_logger.info('Removing Package from Datastore...');
var deletedPackages = 0;
await withRetryTransaction(_db, (tx) async {
final package = await tx.lookupOrNull<Package>(packageKey);
if (package == null) {
Expand All @@ -376,7 +371,7 @@ class AdminBackend {
return;
}
tx.delete(packageKey);

deletedPackages = 1;
final moderatedPkgKey =
_db.emptyKey.append(ModeratedPackage, id: packageName);
final moderatedPkg =
Expand Down Expand Up @@ -409,8 +404,15 @@ class AdminBackend {
.deleteWithQuery(_db.query<PackageVersion>(ancestorKey: packageKey));

_logger.info('Package "$packageName" got successfully removed.');
_logger.info(
'NOTICE: Redis caches referencing the package will expire given time.');
return (
deletedPackages: deletedPackages,
deletedPackageVersions: versions.length,
deletedPackageVersionInfos: deletedPackageVersionInfos.deleted,
deletedPackageVersionAssets: deletedPackageVersionAssets.deleted,
deletedLikes: deletedLikes.deleted,
deletedAuditLogs: deletedAuditLogRecords.deleted,
replacedByFixes: replacedByFixes
);
}

/// Updates the options (e.g. retraction) of the specific package version and
Expand Down Expand Up @@ -454,13 +456,13 @@ class AdminBackend {
/// Removes the specific package version from the Datastore and updates other
/// related entities. It is safe to call [removePackageVersion] on an already
/// removed version, as the call is idempotent.
Future<void> removePackageVersion(String packageName, String version) async {
final caller =
await requireAuthenticatedAdmin(AdminPermission.removePackage);

_logger.info('${caller.displayId}) initiated the delete '
'of package $packageName $version');

Future<
({
int deletedPackageVersions,
int deletedPackageVersionInfos,
int deletedPackageVersionAssets,
})> removePackageVersion(String packageName, String version) async {
var deletedPackageVersions = 0;
final currentDartSdk = await getCachedDartSdkVersion(
lastKnownStable: toolStableDartSdkVersion);
final currentFlutterSdk = await getCachedFlutterSdkVersion(
Expand All @@ -469,8 +471,7 @@ class AdminBackend {
final packageKey = _db.emptyKey.append(Package, id: packageName);
final package = await tx.lookupOrNull<Package>(packageKey);
if (package == null) {
throw Exception(
'Package "$packageName" does not exists. Use full package removal without the version qualifier.');
throw Exception('Package "$packageName" does not exists.');
}

final versionsQuery = tx.query<PackageVersion>(packageKey);
Expand All @@ -479,8 +480,9 @@ class AdminBackend {
if (versionNames.contains(version)) {
tx.delete(packageKey.append(PackageVersion, id: version));
package.updated = clock.now().toUtc();
deletedPackageVersions = 1;
} else {
print('Package $packageName does not have a version $version.');
_logger.info('Package $packageName does not have a version $version.');
}

if (versionNames.length == 1 && versionNames.single == version) {
Expand All @@ -501,15 +503,15 @@ class AdminBackend {
tx.insert(package);
});

print('Removing GCS objects ...');
_logger.info('Removing GCS objects ...');
await packageBackend.removePackageTarball(packageName, version);

await _db.deleteWithQuery(
final deletedPackageVersionInfos = await _db.deleteWithQuery(
_db.query<PackageVersionInfo>()..filter('package =', packageName),
where: (PackageVersionInfo info) => info.version == version,
);

await _db.deleteWithQuery(
final deletedPackageVersionAssets = await _db.deleteWithQuery(
_db.query<PackageVersionAsset>()..filter('package =', packageName),
where: (PackageVersionAsset asset) => asset.version == version,
);
Expand All @@ -518,6 +520,11 @@ class AdminBackend {
await purgeScorecardData(packageName, version, isLatest: true);
// trigger (eventual) re-analysis
await taskBackend.trackPackage(packageName);
return (
deletedPackageVersions: deletedPackageVersions,
deletedPackageVersionInfos: deletedPackageVersionInfos.deleted,
deletedPackageVersionAssets: deletedPackageVersionAssets.deleted,
);
}

/// Handles `GET '/api/admin/packages/<package>/assigned-tags'`.
Expand Down Expand Up @@ -793,7 +800,7 @@ class AdminBackend {
}

_logger.info('Deleting moderated package: ${package.name}');
await _removePackage(
await removePackage(
package.name!,
moderated: package.moderatedAt,
);
Expand Down
17 changes: 0 additions & 17 deletions app/lib/frontend/handlers/pubapi.client.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 0 additions & 13 deletions app/lib/frontend/handlers/pubapi.dart
Original file line number Diff line number Diff line change
Expand Up @@ -524,19 +524,6 @@ class PubApi {
return jsonResponse({'status': 'OK'});
}

@EndPoint.delete('/api/admin/packages/<package>')
Future<Response> adminRemovePackage(Request request, String package) async {
await adminBackend.removePackage(package);
return jsonResponse({'status': 'OK'});
}

@EndPoint.delete('/api/admin/packages/<package>/versions/<version>')
Future<Response> adminRemovePackageVersion(
Request request, String package, String version) async {
await adminBackend.removePackageVersion(package, version);
return jsonResponse({'status': 'OK'});
}

@EndPoint.put('/api/admin/packages/<package>/versions/<version>/options')
Future<VersionOptions> adminUpdateVersionOptions(Request request,
String package, String version, VersionOptions options) async {
Expand Down
Loading