diff --git a/app/config/dartlang-pub-dev.yaml b/app/config/dartlang-pub-dev.yaml index e5e0574b92..b217b18c57 100644 --- a/app/config/dartlang-pub-dev.yaml +++ b/app/config/dartlang-pub-dev.yaml @@ -39,6 +39,7 @@ admins: email: istvan.soos@gmail.com permissions: - executeTool + - invokeAction - listUsers - manageAssignedTags - managePackageOwnership @@ -49,6 +50,7 @@ admins: email: pub-moderation-admin@dartlang-pub-dev.iam.gserviceaccount.com permissions: - executeTool + - invokeAction - listUsers - manageAssignedTags - managePackageOwnership diff --git a/app/config/dartlang-pub.yaml b/app/config/dartlang-pub.yaml index 51b6e02b4a..377f1464bd 100644 --- a/app/config/dartlang-pub.yaml +++ b/app/config/dartlang-pub.yaml @@ -53,6 +53,7 @@ admins: email: pub-moderation-admin@dartlang-pub.iam.gserviceaccount.com permissions: - executeTool + - invokeAction - listUsers - manageAssignedTags - managePackageOwnership diff --git a/app/lib/admin/actions/actions.dart b/app/lib/admin/actions/actions.dart new file mode 100644 index 0000000000..187bd75bfb --- /dev/null +++ b/app/lib/admin/actions/actions.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2023, 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 'package:pub_dev/admin/actions/publisher_block.dart'; +import 'package:pub_dev/admin/actions/publisher_members_list.dart'; +import 'package:pub_dev/admin/actions/tool_execute.dart'; +import 'package:pub_dev/admin/actions/tool_list.dart'; + +import '../../shared/exceptions.dart'; +import 'task_bump_priority.dart'; + +export '../../shared/exceptions.dart'; + +final class AdminAction { + /// Name of the action is an identifier to be specified when the action is triggered. + final String name; + + /// Map from option name to description of the option. + final Map options; + + /// A one-liner summary of what this action does. + final String summary; + + /// A multi-line explanation of what this action does, written in markdown. + /// + /// This **must** explain what the action does? What the implications are? + /// What other actions could be useful to use in conjunction. + /// What are reasonable expectations around cache-time outs, etc. + /// + /// Do write detailed documentation and include examples. + final String description; + + /// Function to be called to invoke the action. + /// + /// This function is passed an `arguments` Map where keys match the keys in + /// [options]. + /// Returns a JSON response, a failed invocation should throw a + /// [ResponseException]. + /// Any other exception will be considered an internal error. + final Future> Function( + Map arguments, + ) invoke; + + AdminAction({ + required this.name, + required this.summary, + required this.description, + this.options = const {}, + required this.invoke, + }) { + // Check that name works as a command-line argument + if (!RegExp(r'^[a-z][a-z0-9-]{0,128}$').hasMatch(name)) { + throw ArgumentError.value(name, 'name'); + } + // Check that the keys for options works as command-line options + if (options.keys + .any((k) => !RegExp(r'^[a-z][a-z0-9-]{0,128}$').hasMatch(k))) { + throw ArgumentError.value(options, 'options'); + } + } + + static List actions = [ + publisherBlock, + publisherMembersList, + taskBumpPriority, + toolExecute, + toolList, + ]; +} diff --git a/app/lib/admin/actions/publisher_block.dart b/app/lib/admin/actions/publisher_block.dart new file mode 100644 index 0000000000..9a976ebe5c --- /dev/null +++ b/app/lib/admin/actions/publisher_block.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2020, 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 'package:pub_dev/account/backend.dart'; +import 'package:pub_dev/admin/actions/actions.dart'; +import 'package:pub_dev/publisher/backend.dart'; +import 'package:pub_dev/publisher/models.dart'; +import 'package:pub_dev/shared/datastore.dart'; + +final publisherBlock = AdminAction( + name: 'publisher-block', + summary: 'Block publisher and block all members', + description: ''' +Get information about publisher and list all members. +''', + options: { + 'publisher': 'Publisher to be blocked', + }, + invoke: (options) async { + final publisherId = options['publisher']!; + InvalidInputException.check( + publisherId.isNotEmpty, + 'publisher must be given', + ); + + final publisher = await publisherBackend.getPublisher(publisherId); + if (publisher == null) { + throw NotFoundException.resource(publisherId); + } + final members = await publisherBackend.listPublisherMembers(publisherId); + + for (final m in members) { + await accountBackend.updateBlockedFlag(m.userId, true); + } + + final publisherKey = dbService.emptyKey.append(Publisher, id: publisherId); + await withRetryTransaction(dbService, (tx) async { + final p = await tx.lookupValue(publisherKey); + p.markForBlocked(); + tx.insert(p); + }); + + return { + 'publisher': publisher.publisherId, + 'description': publisher.description, + 'website': publisher.websiteUrl, + 'contact': publisher.contactEmail, + 'created': publisher.created, + 'blocked': true, + 'members': members + .map((m) => { + 'email': m.email, + 'role': m.role, + 'userId': m.userId, + 'blocked': true, + }) + .toList(), + }; + }, +); diff --git a/app/lib/admin/actions/publisher_members_list.dart b/app/lib/admin/actions/publisher_members_list.dart new file mode 100644 index 0000000000..72c75c9072 --- /dev/null +++ b/app/lib/admin/actions/publisher_members_list.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2020, 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 'package:pub_dev/admin/actions/actions.dart'; +import 'package:pub_dev/publisher/backend.dart'; + +final publisherMembersList = AdminAction( + name: 'publisher-members-list', + summary: 'List all members a publisher', + description: ''' +Get information about publisher and list all members. +''', + options: { + 'publisher': 'Publisher for which to list members', + }, + invoke: (options) async { + final publisherId = options['publisher']!; + InvalidInputException.check( + publisherId.isNotEmpty, + 'publisher must be given', + ); + + final publisher = await publisherBackend.getPublisher(publisherId); + if (publisher == null) { + throw NotFoundException.resource(publisherId); + } + final members = await publisherBackend.listPublisherMembers(publisherId); + + return { + 'publisher': publisher.publisherId, + 'description': publisher.description, + 'website': publisher.websiteUrl, + 'contact': publisher.contactEmail, + 'created': publisher.created, + 'members': members + .map((m) => { + 'email': m.email, + 'role': m.role, + 'userId': m.userId, + }) + .toList(), + }; + }, +); diff --git a/app/lib/admin/actions/task_bump_priority.dart b/app/lib/admin/actions/task_bump_priority.dart new file mode 100644 index 0000000000..cb22a1593f --- /dev/null +++ b/app/lib/admin/actions/task_bump_priority.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2023, 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 'package:pub_dev/admin/actions/actions.dart'; +import 'package:pub_dev/task/backend.dart'; + +final taskBumpPriority = AdminAction( + name: 'task-bump-priority', + summary: 'Increase priority for task scheduling of specific package', + description: ''' +This action will lower the `PackageState.pendingAt` property for the given +package. This should cause an analysis task for the package to be scheduled +sooner. + +This will always set `pendingAt` to the same value, it will not trigger new +analysis, if none is pending, merely increase the priority. Calling it multiple +times will have no effect, it will always set `pendingAt` to the same value. + +This is intended for debugging, or solving one-off issues. +''', + options: { + 'package': 'Name of package whose priority should be bumped', + }, + invoke: (options) async { + final package = options['package']!; + InvalidInputException.checkPackageName(package); + + await taskBackend.adminBumpPriority(package); + + return {'message': 'Priority may have been bumped, good luck!'}; + }, +); diff --git a/app/lib/admin/actions/tool_execute.dart b/app/lib/admin/actions/tool_execute.dart new file mode 100644 index 0000000000..7aa816d10c --- /dev/null +++ b/app/lib/admin/actions/tool_execute.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2023, 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 'package:pub_dev/admin/actions/actions.dart'; +import 'package:pub_dev/admin/backend.dart'; + +final toolExecute = AdminAction( + name: 'tool-execute', + summary: 'Execute legacy tool', + description: ''' +Execute a legacy tool. Beware that args will be split on comma, hence, it's not +possible to call them with arguments that contain comma. +''', + options: { + 'tool': 'name of tool to execute', + 'args': 'comma separate list of arguments', + }, + invoke: (options) async { + final toolName = options['toolName']!; + final args = options['args']!.split(','); + InvalidInputException.check(toolName.isNotEmpty, 'tool must given'); + + final tool = availableTools[toolName]; + if (tool == null) { + throw NotFoundException.resource(toolName); + } + + final message = await tool(args); + + return {'message': message}; + }, +); diff --git a/app/lib/admin/actions/tool_list.dart b/app/lib/admin/actions/tool_list.dart new file mode 100644 index 0000000000..a38d6e87f0 --- /dev/null +++ b/app/lib/admin/actions/tool_list.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2023, 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 'package:pub_dev/admin/actions/actions.dart'; +import 'package:pub_dev/admin/backend.dart'; + +final toolList = AdminAction( + name: 'tool-list', + summary: 'List legacy tools', + description: ''' +Print a list of legacy tools, these can be invoked with the `tool-execute` +command. +''', + options: {}, + invoke: (options) async { + return { + 'tools': availableTools.keys + .map((k) => { + 'tool': k, + }) + .toList(), + }; + }, +); diff --git a/app/lib/admin/backend.dart b/app/lib/admin/backend.dart index a3fd018c72..79eca310b8 100644 --- a/app/lib/admin/backend.dart +++ b/app/lib/admin/backend.dart @@ -8,6 +8,7 @@ import 'package:_pub_shared/data/admin_api.dart' as api; import 'package:_pub_shared/data/package_api.dart'; import 'package:_pub_shared/search/tags.dart'; import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:convert/convert.dart'; import 'package:gcloud/service_scope.dart' as ss; import 'package:logging/logging.dart'; @@ -31,7 +32,7 @@ import '../shared/datastore.dart'; import '../shared/email.dart'; import '../shared/exceptions.dart'; import '../tool/utils/dart_sdk_version.dart'; -import 'tools/block_publisher_and_all_members.dart'; +import 'actions/actions.dart' show AdminAction; import 'tools/create_publisher.dart'; import 'tools/delete_all_staging.dart'; import 'tools/delete_publisher.dart'; @@ -71,7 +72,6 @@ final Map availableTools = { 'package-publisher': executeSetPackagePublisher, 'update-package-versions': executeUpdatePackageVersions, 'recent-uploaders': executeRecentUploaders, - 'block-publisher-and-all-members': executeBlockPublisherAndAllMembers, 'publisher-member': executePublisherMember, 'publisher-invite-member': executePublisherInviteMember, 'set-package-blocked': executeSetPackageBlocked, @@ -688,4 +688,49 @@ class AdminBackend { }); return await handleGetPackageUploaders(packageName); } + + Future listActions() async { + await requireAuthenticatedAdmin(AdminPermission.invokeAction); + + return api.AdminListActionsResponse( + actions: AdminAction.actions + .map( + (action) => api.AdminAction( + name: action.name, + summary: action.summary, + description: action.description, + options: action.options, + ), + ) + .toList(), + ); + } + + Future invokeAction( + String actionName, + Map args, + ) async { + await requireAuthenticatedAdmin(AdminPermission.invokeAction); + + final action = AdminAction.actions.firstWhereOrNull( + (a) => a.name == actionName, + ); + if (action == null) { + throw NotFoundException.resource(actionName); + } + + // Don't allow unknown arguments + final unknownArgs = + args.keys.toSet().difference(action.options.keys.toSet()); + InvalidInputException.check( + unknownArgs.isEmpty, + 'Unknown options: ${unknownArgs.join(',')}', + ); + + final result = await action.invoke({ + for (final k in action.options.keys) k: args[k] ?? '', + }); + + return api.AdminInvokeActionResponse(output: result); + } } diff --git a/app/lib/admin/tools/block_publisher_and_all_members.dart b/app/lib/admin/tools/block_publisher_and_all_members.dart deleted file mode 100644 index 753078913f..0000000000 --- a/app/lib/admin/tools/block_publisher_and_all_members.dart +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2020, 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 'dart:async'; - -import 'package:pub_dev/account/backend.dart'; -import 'package:pub_dev/publisher/backend.dart'; -import 'package:pub_dev/publisher/models.dart'; -import 'package:pub_dev/shared/datastore.dart'; - -Future executeBlockPublisherAndAllMembers(List args) async { - if (args.isEmpty || - args.length != 2 || - (args[0] != 'block' && args[0] != 'list')) { - return 'Remove publisher and blocks all members.\n' - ' list # list publisher data\n' - ' block # block publisher and all members\n'; - } - final command = args[0]; - final publisherId = args[1]; - - final publisher = (await publisherBackend.getPublisher(publisherId))!; - final members = await publisherBackend.listPublisherMembers(publisherId); - - final output = StringBuffer() - ..writeln('Publisher: ${publisher.publisherId}') - ..writeln('Description: ${publisher.description!.replaceAll('\n', ' ')}') - ..writeln('Website: ${publisher.websiteUrl}') - ..writeln('Contact: ${publisher.contactEmail}') - ..writeln('Created on: ${publisher.created}') - ..writeln('Members:'); - for (final m in members) { - output.writeln(' - ${m.role} ${m.email}'); - } - - if (command == 'list') { - return output.toString(); - } else if (command == 'block') { - for (final m in members) { - await accountBackend.updateBlockedFlag(m.userId, true); - } - - final publisherKey = dbService.emptyKey.append(Publisher, id: publisherId); - await withRetryTransaction(dbService, (tx) async { - final p = await tx.lookupValue(publisherKey); - p.markForBlocked(); - tx.insert(p); - }); - output.writeln('Blocked.'); - return output.toString(); - } else { - return 'Unknown command: $command.'; - } -} diff --git a/app/lib/frontend/handlers/pubapi.client.dart b/app/lib/frontend/handlers/pubapi.client.dart index 84b015d421..90381e7954 100644 --- a/app/lib/frontend/handlers/pubapi.client.dart +++ b/app/lib/frontend/handlers/pubapi.client.dart @@ -471,6 +471,24 @@ class PubApiClient { ); } + Future<_i7.AdminListActionsResponse> adminListActions() async { + return _i7.AdminListActionsResponse.fromJson(await _client.requestJson( + verb: 'get', + path: '/api/admin/actions', + )); + } + + Future<_i7.AdminInvokeActionResponse> adminInvokeAction( + String action, + _i7.AdminInvokeActionArguments payload, + ) async { + return _i7.AdminInvokeActionResponse.fromJson(await _client.requestJson( + verb: 'post', + path: '/api/admin/actions/$action', + body: payload.toJson(), + )); + } + Future<_i7.AdminListUsersResponse> adminListUsers({ String? email, String? ouid, diff --git a/app/lib/frontend/handlers/pubapi.dart b/app/lib/frontend/handlers/pubapi.dart index c0bcaf0eaa..b25ffc2e5b 100644 --- a/app/lib/frontend/handlers/pubapi.dart +++ b/app/lib/frontend/handlers/pubapi.dart @@ -459,6 +459,20 @@ class PubApi { return Response.ok(await adminBackend.executeTool(tool, parsedArgs)); } + @EndPoint.get('/api/admin/actions') + Future adminListActions(Request request) { + return adminBackend.listActions(); + } + + @EndPoint.post('/api/admin/actions/') + Future adminInvokeAction( + Request request, + String action, + AdminInvokeActionArguments args, + ) { + return adminBackend.invokeAction(action, args.arguments); + } + @EndPoint.get('/api/admin/users') Future adminListUsers( Request request, { diff --git a/app/lib/frontend/handlers/pubapi.g.dart b/app/lib/frontend/handlers/pubapi.g.dart index 875b4511c2..be3264551d 100644 --- a/app/lib/frontend/handlers/pubapi.g.dart +++ b/app/lib/frontend/handlers/pubapi.g.dart @@ -1066,6 +1066,44 @@ Router _$PubApiRouter(PubApi service) { } }, ); + router.add( + 'GET', + r'/api/admin/actions', + (Request request) async { + try { + final _$result = await service.adminListActions( + request, + ); + return $utilities.jsonResponse(_$result.toJson()); + } on ApiResponseException catch (e) { + return e.asApiResponse(); + } catch (e, st) { + return $utilities.unhandledError(e, st); + } + }, + ); + router.add( + 'POST', + r'/api/admin/actions/', + ( + Request request, + String action, + ) async { + try { + final _$result = await service.adminInvokeAction( + request, + action, + await $utilities.decodeJson( + request, (o) => AdminInvokeActionArguments.fromJson(o)), + ); + return $utilities.jsonResponse(_$result.toJson()); + } on ApiResponseException catch (e) { + return e.asApiResponse(); + } catch (e, st) { + return $utilities.unhandledError(e, st); + } + }, + ); router.add( 'GET', r'/api/admin/users', diff --git a/app/lib/shared/configuration.dart b/app/lib/shared/configuration.dart index 44f435698e..e359b90672 100644 --- a/app/lib/shared/configuration.dart +++ b/app/lib/shared/configuration.dart @@ -479,6 +479,9 @@ enum AdminPermission { /// Permission to execute a tool. executeTool, + /// Permission to invoke admin actions. + invokeAction, + /// Permission to list all users. listUsers, diff --git a/app/lib/shared/configuration.g.dart b/app/lib/shared/configuration.g.dart index a4e3f252e3..e00df43830 100644 --- a/app/lib/shared/configuration.g.dart +++ b/app/lib/shared/configuration.g.dart @@ -204,6 +204,7 @@ Map _$AdminIdToJson(AdminId instance) => { const _$AdminPermissionEnumMap = { AdminPermission.executeTool: 'executeTool', + AdminPermission.invokeAction: 'invokeAction', AdminPermission.listUsers: 'listUsers', AdminPermission.manageAssignedTags: 'manageAssignedTags', AdminPermission.managePackageOwnership: 'managePackageOwnership', diff --git a/app/test/admin/api_actions_test.dart b/app/test/admin/api_actions_test.dart new file mode 100644 index 0000000000..864407e4a6 --- /dev/null +++ b/app/test/admin/api_actions_test.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2023, 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 'package:_pub_shared/data/admin_api.dart'; +import 'package:test/test.dart'; + +import '../shared/test_models.dart'; +import '../shared/test_services.dart'; + +void main() { + group('list actions', () { + setupTestsWithAdminTokenIssues((api) async { + await api.adminListActions(); + }); + }); + + group('invoke action', () { + setupTestsWithAdminTokenIssues((api) async { + await api.adminInvokeAction( + 'tool-list', + AdminInvokeActionArguments(arguments: {}), + ); + }); + + testWithProfile('tool-list', fn: () async { + final api = createPubApiClient(authToken: siteAdminToken); + final result = await api.adminInvokeAction( + 'tool-list', + AdminInvokeActionArguments(arguments: {}), + ); + expect(result.output.containsKey('tools'), isTrue); + }); + }); +} diff --git a/pkg/_pub_shared/lib/data/admin_api.dart b/pkg/_pub_shared/lib/data/admin_api.dart index fe11c60a18..d018ca9f41 100644 --- a/pkg/_pub_shared/lib/data/admin_api.dart +++ b/pkg/_pub_shared/lib/data/admin_api.dart @@ -30,6 +30,76 @@ class AdminListUsersResponse { Map toJson() => _$AdminListUsersResponseToJson(this); } +/// Admin API response for listing all _admin actions_. +@JsonSerializable() +class AdminListActionsResponse { + /// List of admin actions. + final List actions; + + // json_serializable boiler-plate + AdminListActionsResponse({required this.actions}); + factory AdminListActionsResponse.fromJson(Map json) => + _$AdminListActionsResponseFromJson(json); + Map toJson() => _$AdminListActionsResponseToJson(this); +} + +@JsonSerializable() +class AdminAction { + /// Name of the action is an identifier to be specified when the action is + /// triggered. + final String name; + + /// Map from option name to description of the option. + /// + /// This are specified as querystring parameters when invoking the action. + final Map options; + + /// A one-liner summary of what this action does. + final String summary; + + /// A multi-line explanation of what this action does, written in markdown. + /// + /// This **must** explain what the action does? What the implications are? + /// What other actions could be useful to use in conjunction. + /// What are reasonable expectations around cache-time outs, etc. + /// + /// Do write detailed documentation and include examples. + final String description; + + // json_serializable boiler-plate + AdminAction({ + required this.name, + required this.options, + required this.summary, + required this.description, + }); + factory AdminAction.fromJson(Map json) => + _$AdminActionFromJson(json); + Map toJson() => _$AdminActionToJson(this); +} + +@JsonSerializable() +class AdminInvokeActionArguments { + /// Arguments for the [AdminAction.options] when invoking an admin action. + final Map arguments; + + AdminInvokeActionArguments({required this.arguments}); + factory AdminInvokeActionArguments.fromJson(Map json) => + _$AdminInvokeActionArgumentsFromJson(json); + Map toJson() => _$AdminInvokeActionArgumentsToJson(this); +} + +@JsonSerializable() +class AdminInvokeActionResponse { + /// Output from running the action. + final Map output; + + AdminInvokeActionResponse({required this.output}); + factory AdminInvokeActionResponse.fromJson(Map json) => + _$AdminInvokeActionResponseFromJson(json); + Map toJson() => _$AdminInvokeActionResponseToJson(this); +} + /// Entry in the [AdminListUsersResponse] structure. @JsonSerializable() class AdminUserEntry { diff --git a/pkg/_pub_shared/lib/data/admin_api.g.dart b/pkg/_pub_shared/lib/data/admin_api.g.dart index e84442eee7..8f702cbc7b 100644 --- a/pkg/_pub_shared/lib/data/admin_api.g.dart +++ b/pkg/_pub_shared/lib/data/admin_api.g.dart @@ -22,6 +22,59 @@ Map _$AdminListUsersResponseToJson( 'continuationToken': instance.continuationToken, }; +AdminListActionsResponse _$AdminListActionsResponseFromJson( + Map json) => + AdminListActionsResponse( + actions: (json['actions'] as List) + .map((e) => AdminAction.fromJson(e as Map)) + .toList(), + ); + +Map _$AdminListActionsResponseToJson( + AdminListActionsResponse instance) => + { + 'actions': instance.actions, + }; + +AdminAction _$AdminActionFromJson(Map json) => AdminAction( + name: json['name'] as String, + options: Map.from(json['options'] as Map), + summary: json['summary'] as String, + description: json['description'] as String, + ); + +Map _$AdminActionToJson(AdminAction instance) => + { + 'name': instance.name, + 'options': instance.options, + 'summary': instance.summary, + 'description': instance.description, + }; + +AdminInvokeActionArguments _$AdminInvokeActionArgumentsFromJson( + Map json) => + AdminInvokeActionArguments( + arguments: Map.from(json['arguments'] as Map), + ); + +Map _$AdminInvokeActionArgumentsToJson( + AdminInvokeActionArguments instance) => + { + 'arguments': instance.arguments, + }; + +AdminInvokeActionResponse _$AdminInvokeActionResponseFromJson( + Map json) => + AdminInvokeActionResponse( + output: json['output'] as Map, + ); + +Map _$AdminInvokeActionResponseToJson( + AdminInvokeActionResponse instance) => + { + 'output': instance.output, + }; + AdminUserEntry _$AdminUserEntryFromJson(Map json) => AdminUserEntry( userId: json['userId'] as String?, diff --git a/pkg/_pub_shared/lib/src/pubapi.client.dart b/pkg/_pub_shared/lib/src/pubapi.client.dart index 84b015d421..90381e7954 100644 --- a/pkg/_pub_shared/lib/src/pubapi.client.dart +++ b/pkg/_pub_shared/lib/src/pubapi.client.dart @@ -471,6 +471,24 @@ class PubApiClient { ); } + Future<_i7.AdminListActionsResponse> adminListActions() async { + return _i7.AdminListActionsResponse.fromJson(await _client.requestJson( + verb: 'get', + path: '/api/admin/actions', + )); + } + + Future<_i7.AdminInvokeActionResponse> adminInvokeAction( + String action, + _i7.AdminInvokeActionArguments payload, + ) async { + return _i7.AdminInvokeActionResponse.fromJson(await _client.requestJson( + verb: 'post', + path: '/api/admin/actions/$action', + body: payload.toJson(), + )); + } + Future<_i7.AdminListUsersResponse> adminListUsers({ String? email, String? ouid,