From d504478c1e81cbba96d66bd7cbb2f24ee5b5de43 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 12 Mar 2026 10:01:42 +0100 Subject: [PATCH] docs: add permissions documentation for actions registry Signed-off-by: benjdlambert --- .changeset/actions-permissions-api.md | 25 +++++++++++++ .changeset/actions-permissions-defaults.md | 2 +- .../core-services/actions-registry.md | 36 +++++++++++++++++++ docs/backend-system/core-services/actions.md | 4 +++ .../DefaultActionsRegistryService.ts | 9 +++-- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/.changeset/actions-permissions-api.md b/.changeset/actions-permissions-api.md index e18a2f2469..193e84b7a7 100644 --- a/.changeset/actions-permissions-api.md +++ b/.changeset/actions-permissions-api.md @@ -3,3 +3,28 @@ --- Added optional `permission` field to `ActionsRegistryActionOptions`, allowing actions to declare a `BasicPermission` that controls visibility and access. + +```typescript +import { createPermission } from '@backstage/plugin-permission-common'; + +const myPermission = createPermission({ + name: 'myPlugin.myAction.use', + attributes: {}, +}); + +actionsRegistry.register({ + name: 'my-action', + title: 'My Action', + description: 'An action that requires permission', + permission: myPermission, + schema: { + input: z => z.object({ name: z.string() }), + output: z => z.object({ ok: z.boolean() }), + }, + action: async ({ input }) => { + return { output: { ok: true } }; + }, +}); +``` + +Actions without a `permission` field continue to work as before. diff --git a/.changeset/actions-permissions-defaults.md b/.changeset/actions-permissions-defaults.md index 1f5c9a916e..3c471398c0 100644 --- a/.changeset/actions-permissions-defaults.md +++ b/.changeset/actions-permissions-defaults.md @@ -2,4 +2,4 @@ '@backstage/backend-defaults': patch --- -Added permissions integration to the actions registry. Actions with a `permission` field are now checked against the permissions framework when listing and invoking. Denied actions are filtered from listings and return 404 on invocation. +Added permissions integration to the actions registry. Actions registered with a `permission` field are now checked against the permissions framework when listing and invoking. Denied actions are filtered from list results, and invoking a denied action returns a `404 Not Found` as if the action does not exist. diff --git a/docs/backend-system/core-services/actions-registry.md b/docs/backend-system/core-services/actions-registry.md index 9e493e6bb2..97db476707 100644 --- a/docs/backend-system/core-services/actions-registry.md +++ b/docs/backend-system/core-services/actions-registry.md @@ -25,6 +25,7 @@ Each action registered with the service must conform to the `ActionsRegistryActi ### Optional Properties +- **`permission`:** A `BasicPermission` that controls visibility and access to the action through the permissions framework. See [Permissions](#permissions) below. - **`attributes`:** Object containing behavioral flags: - **`destructive`:** Boolean indicating if the action modifies or deletes data - **`idempotent`:** Boolean indicating if running the action multiple times produces the same result @@ -157,6 +158,41 @@ export const myPlugin = createBackendPlugin({ }); ``` +## Permissions + +Actions can optionally declare a `permission` to control visibility and access through the Backstage permissions framework. When a permission is set, the action is only visible in listings and accessible by users who are authorized. + +Actions that are denied by the permission policy are filtered from `list()` results and return a `404 Not Found` on `invoke()`, as if they don't exist. + +### Adding a Permission to an Action + +```typescript +import { createPermission } from '@backstage/plugin-permission-common'; + +// Define a permission for your action +const deleteEntityPermission = createPermission({ + name: 'catalog.entity.delete', + attributes: { action: 'delete' }, +}); + +actionsRegistry.register({ + name: 'delete-entity', + title: 'Delete Entity', + description: 'Removes an entity from the catalog', + permission: deleteEntityPermission, + schema: { + input: z => z.object({ entityRef: z.string() }), + output: z => z.object({ deleted: z.boolean() }), + }, + action: async ({ input }) => { + // action logic + return { output: { deleted: true } }; + }, +}); +``` + +Actions without a `permission` field remain visible and accessible by all callers, preserving backwards compatibility. + ## Best Practices ### Naming Conventions diff --git a/docs/backend-system/core-services/actions.md b/docs/backend-system/core-services/actions.md index 02f8b65591..d1680b2380 100644 --- a/docs/backend-system/core-services/actions.md +++ b/docs/backend-system/core-services/actions.md @@ -65,6 +65,10 @@ backend: - 'scaffolder.internal.*' ``` +### Permissions + +Actions registered with a `permission` field are automatically checked against the permissions framework. When listing actions, any actions denied by the active permission policy are filtered out of the results. When invoking a denied action, a `404 Not Found` error is returned. See the [Actions Registry Permissions](./actions-registry.md#permissions) documentation for how to configure permissions on actions. + ## Using the Service ### Listing Available Actions diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts index a324389055..33e59afbe4 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -16,6 +16,7 @@ import { AuthService, + BackstageCredentials, HttpAuthService, LoggerService, PermissionsService, @@ -32,6 +33,8 @@ import { import { InputError, NotAllowedError, NotFoundError } from '@backstage/errors'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; +type ActionEntry = [string, ActionsRegistryActionOptions]; + export class DefaultActionsRegistryService implements ActionsRegistryService { private actions: Map> = new Map(); @@ -196,9 +199,9 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { } private async filterByPermissions( - entries: [string, ActionsRegistryActionOptions][], - credentials: Parameters[1]['credentials'], - ): Promise<[string, ActionsRegistryActionOptions][]> { + entries: ActionEntry[], + credentials: BackstageCredentials, + ): Promise { const permissionedEntries = entries.filter( ([_, action]) => action.permission, );