Merge pull request #33303 from backstage/blam/actions-permissions
`feat(actions)`: Add support for defining permissions at the action level
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': minor
|
||||
---
|
||||
|
||||
Added optional `visibilityPermission` 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',
|
||||
visibilityPermission: 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 `visibilityPermission` field continue to work as before.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Added permissions integration to the actions registry. Actions registered with a `visibilityPermission` 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. Permissions are automatically registered with the `PermissionsRegistryService` so they appear in the permission policy system.
|
||||
@@ -25,6 +25,7 @@ Each action registered with the service must conform to the `ActionsRegistryActi
|
||||
|
||||
### Optional Properties
|
||||
|
||||
- **`visibilityPermission`:** 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,43 @@ export const myPlugin = createBackendPlugin({
|
||||
});
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
Actions can optionally declare a `visibilityPermission` to control visibility and access through the Backstage permissions framework. The `visibilityPermission` must be a `BasicPermission` (not a resource permission). When set, the action is only visible in listings and accessible by callers who are authorized.
|
||||
|
||||
When accessed via the Actions Service or the `/.backstage/actions/v1/...` HTTP endpoints, actions that are denied by the permission policy are filtered from list results and return a `404 Not Found` on invocation, as if they don't exist.
|
||||
|
||||
Permissions declared on actions are automatically registered with the `PermissionsRegistryService` so they appear in the permission policy system.
|
||||
|
||||
### Adding a Permission to an Action
|
||||
|
||||
```typescript
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
// Define a permission for your action
|
||||
const myDeletePermission = createPermission({
|
||||
name: 'my-plugin.actions.deleteEntity',
|
||||
attributes: { action: 'delete' },
|
||||
});
|
||||
|
||||
actionsRegistry.register({
|
||||
name: 'delete-entity',
|
||||
title: 'Delete Entity',
|
||||
description: 'Removes an entity from the catalog',
|
||||
visibilityPermission: myDeletePermission,
|
||||
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 `visibilityPermission` field remain visible and accessible by all callers, preserving backwards compatibility.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
@@ -65,6 +65,10 @@ backend:
|
||||
- 'scaffolder.internal.*'
|
||||
```
|
||||
|
||||
### Permissions
|
||||
|
||||
Actions registered with a `visibilityPermission` 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
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
"@backstage/integration-aws-node": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
"@backstage/plugin-permission-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@google-cloud/storage": "^7.0.0",
|
||||
|
||||
+81
-4
@@ -16,8 +16,11 @@
|
||||
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
HttpAuthService,
|
||||
LoggerService,
|
||||
PermissionsRegistryService,
|
||||
PermissionsService,
|
||||
PluginMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import PromiseRouter from 'express-promise-router';
|
||||
@@ -29,6 +32,9 @@ import {
|
||||
ActionsRegistryService,
|
||||
} from '@backstage/backend-plugin-api/alpha';
|
||||
import { InputError, NotAllowedError, NotFoundError } from '@backstage/errors';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
|
||||
type ActionEntry = [string, ActionsRegistryActionOptions<any, any>];
|
||||
|
||||
export class DefaultActionsRegistryService implements ActionsRegistryService {
|
||||
private actions: Map<string, ActionsRegistryActionOptions<any, any>> =
|
||||
@@ -38,17 +44,23 @@ export class DefaultActionsRegistryService implements ActionsRegistryService {
|
||||
private readonly httpAuth: HttpAuthService;
|
||||
private readonly auth: AuthService;
|
||||
private readonly metadata: PluginMetadataService;
|
||||
private readonly permissions: PermissionsService;
|
||||
private readonly permissionsRegistry: PermissionsRegistryService;
|
||||
|
||||
private constructor(
|
||||
logger: LoggerService,
|
||||
httpAuth: HttpAuthService,
|
||||
auth: AuthService,
|
||||
metadata: PluginMetadataService,
|
||||
permissions: PermissionsService,
|
||||
permissionsRegistry: PermissionsRegistryService,
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.httpAuth = httpAuth;
|
||||
this.auth = auth;
|
||||
this.metadata = metadata;
|
||||
this.permissions = permissions;
|
||||
this.permissionsRegistry = permissionsRegistry;
|
||||
}
|
||||
|
||||
static create({
|
||||
@@ -56,25 +68,46 @@ export class DefaultActionsRegistryService implements ActionsRegistryService {
|
||||
logger,
|
||||
auth,
|
||||
metadata,
|
||||
permissions,
|
||||
permissionsRegistry,
|
||||
}: {
|
||||
httpAuth: HttpAuthService;
|
||||
logger: LoggerService;
|
||||
auth: AuthService;
|
||||
metadata: PluginMetadataService;
|
||||
permissions: PermissionsService;
|
||||
permissionsRegistry: PermissionsRegistryService;
|
||||
}): DefaultActionsRegistryService {
|
||||
return new DefaultActionsRegistryService(logger, httpAuth, auth, metadata);
|
||||
return new DefaultActionsRegistryService(
|
||||
logger,
|
||||
httpAuth,
|
||||
auth,
|
||||
metadata,
|
||||
permissions,
|
||||
permissionsRegistry,
|
||||
);
|
||||
}
|
||||
|
||||
createRouter(): Router {
|
||||
const router = PromiseRouter();
|
||||
router.use(json());
|
||||
|
||||
router.get('/.backstage/actions/v1/actions', (_, res) => {
|
||||
router.get('/.backstage/actions/v1/actions', async (req, res) => {
|
||||
const credentials = await this.httpAuth.credentials(req);
|
||||
const entries = Array.from(this.actions.entries());
|
||||
|
||||
const allowedActions = await this.filterByPermissions(
|
||||
entries,
|
||||
credentials,
|
||||
);
|
||||
|
||||
return res.json({
|
||||
actions: Array.from(this.actions.entries()).map(([id, action]) => ({
|
||||
actions: allowedActions.map(([id, action]) => ({
|
||||
id,
|
||||
name: action.name,
|
||||
title: action.title,
|
||||
description: action.description,
|
||||
pluginId: this.metadata.getId(),
|
||||
...action,
|
||||
attributes: {
|
||||
// Inspired by the @modelcontextprotocol/sdk defaults for the hints.
|
||||
// https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812
|
||||
@@ -116,6 +149,18 @@ export class DefaultActionsRegistryService implements ActionsRegistryService {
|
||||
throw new NotFoundError(`Action "${req.params.actionId}" not found`);
|
||||
}
|
||||
|
||||
if (action.visibilityPermission) {
|
||||
const [decision] = await this.permissions.authorize(
|
||||
[{ permission: action.visibilityPermission }],
|
||||
{ credentials },
|
||||
);
|
||||
if (decision.result !== AuthorizeResult.ALLOW) {
|
||||
throw new NotFoundError(
|
||||
`Action "${req.params.actionId}" not found`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const input = action.schema?.input
|
||||
? action.schema.input(z).safeParse(req.body)
|
||||
: ({ success: true, data: undefined } as const);
|
||||
@@ -160,6 +205,38 @@ export class DefaultActionsRegistryService implements ActionsRegistryService {
|
||||
throw new Error(`Action with id "${id}" is already registered`);
|
||||
}
|
||||
|
||||
if (options.visibilityPermission) {
|
||||
this.permissionsRegistry.addPermissions([options.visibilityPermission]);
|
||||
}
|
||||
|
||||
this.actions.set(id, options);
|
||||
}
|
||||
|
||||
private async filterByPermissions(
|
||||
entries: ActionEntry[],
|
||||
credentials: BackstageCredentials,
|
||||
): Promise<ActionEntry[]> {
|
||||
const permissionedEntries = entries.filter(
|
||||
([_, action]) => action.visibilityPermission,
|
||||
);
|
||||
|
||||
if (permissionedEntries.length === 0) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const decisions = await this.permissions.authorize(
|
||||
permissionedEntries.map(([_, action]) => ({
|
||||
permission: action.visibilityPermission!,
|
||||
})),
|
||||
{ credentials },
|
||||
);
|
||||
|
||||
const deniedIds = new Set(
|
||||
permissionedEntries
|
||||
.filter((_, index) => decisions[index].result !== AuthorizeResult.ALLOW)
|
||||
.map(([id]) => id),
|
||||
);
|
||||
|
||||
return entries.filter(([id]) => !deniedIds.has(id));
|
||||
}
|
||||
}
|
||||
|
||||
+311
@@ -24,6 +24,10 @@ import request from 'supertest';
|
||||
import { actionsRegistryServiceFactory } from './actionsRegistryServiceFactory';
|
||||
import { InputError, NotFoundError } from '@backstage/errors';
|
||||
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
createPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
|
||||
describe('actionsRegistryServiceFactory', () => {
|
||||
const defaultServices = [
|
||||
@@ -558,4 +562,311 @@ describe('actionsRegistryServiceFactory', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('permissions', () => {
|
||||
const testPermission = createPermission({
|
||||
name: 'test.action.use',
|
||||
attributes: {},
|
||||
});
|
||||
|
||||
it('should filter out actions with denied permissions when listing', async () => {
|
||||
const pluginSubject = createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actionsRegistry }) {
|
||||
actionsRegistry.register({
|
||||
name: 'public-action',
|
||||
title: 'Public Action',
|
||||
description: 'No permission required',
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
actionsRegistry.register({
|
||||
name: 'protected-action',
|
||||
title: 'Protected Action',
|
||||
description: 'Permission required',
|
||||
visibilityPermission: testPermission,
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
pluginSubject,
|
||||
actionsRegistryServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.permissions.factory({
|
||||
result: AuthorizeResult.DENY,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { body, status } = await request(server).get(
|
||||
'/api/my-plugin/.backstage/actions/v1/actions',
|
||||
);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.actions).toHaveLength(1);
|
||||
expect(body.actions[0].name).toBe('public-action');
|
||||
});
|
||||
|
||||
it('should include actions with allowed permissions when listing', async () => {
|
||||
const pluginSubject = createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actionsRegistry }) {
|
||||
actionsRegistry.register({
|
||||
name: 'protected-action',
|
||||
title: 'Protected Action',
|
||||
description: 'Permission required',
|
||||
visibilityPermission: testPermission,
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
pluginSubject,
|
||||
actionsRegistryServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.permissions.factory({
|
||||
result: AuthorizeResult.ALLOW,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { body, status } = await request(server).get(
|
||||
'/api/my-plugin/.backstage/actions/v1/actions',
|
||||
);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.actions).toHaveLength(1);
|
||||
expect(body.actions[0].name).toBe('protected-action');
|
||||
});
|
||||
|
||||
it('should return 404 when invoking an action with denied permission', async () => {
|
||||
const pluginSubject = createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actionsRegistry }) {
|
||||
actionsRegistry.register({
|
||||
name: 'protected-action',
|
||||
title: 'Protected Action',
|
||||
description: 'Permission required',
|
||||
visibilityPermission: testPermission,
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
pluginSubject,
|
||||
actionsRegistryServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.permissions.factory({
|
||||
result: AuthorizeResult.DENY,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { body, status } = await request(server).post(
|
||||
'/api/my-plugin/.backstage/actions/v1/actions/my-plugin:protected-action/invoke',
|
||||
);
|
||||
|
||||
expect(status).toBe(404);
|
||||
expect(body).toMatchObject({
|
||||
error: {
|
||||
message: 'Action "my-plugin:protected-action" not found',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow invoking an action when permission is granted', async () => {
|
||||
const mockAction = jest.fn().mockResolvedValue({ output: { ok: true } });
|
||||
|
||||
const pluginSubject = createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actionsRegistry }) {
|
||||
actionsRegistry.register({
|
||||
name: 'protected-action',
|
||||
title: 'Protected Action',
|
||||
description: 'Permission required',
|
||||
visibilityPermission: testPermission,
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({ ok: z.boolean() }),
|
||||
},
|
||||
action: mockAction,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
pluginSubject,
|
||||
actionsRegistryServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.permissions.factory({
|
||||
result: AuthorizeResult.ALLOW,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { body, status } = await request(server).post(
|
||||
'/api/my-plugin/.backstage/actions/v1/actions/my-plugin:protected-action/invoke',
|
||||
);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ output: { ok: true } });
|
||||
expect(mockAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass the correct permission to the authorize call', async () => {
|
||||
const permissionsMock = mockServices.permissions.mock({
|
||||
authorize: async () => [{ result: AuthorizeResult.ALLOW }],
|
||||
});
|
||||
|
||||
const pluginSubject = createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actionsRegistry }) {
|
||||
actionsRegistry.register({
|
||||
name: 'protected-action',
|
||||
title: 'Protected Action',
|
||||
description: 'Permission required',
|
||||
visibilityPermission: testPermission,
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
pluginSubject,
|
||||
actionsRegistryServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
permissionsMock.factory,
|
||||
],
|
||||
});
|
||||
|
||||
await request(server).get('/api/my-plugin/.backstage/actions/v1/actions');
|
||||
|
||||
expect(permissionsMock.authorize).toHaveBeenCalledWith(
|
||||
[{ permission: testPermission }],
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should register the permission with the permissions registry', async () => {
|
||||
const permissionsRegistryMock = mockServices.permissionsRegistry.mock();
|
||||
|
||||
const pluginSubject = createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
actionsRegistry: actionsRegistryServiceRef,
|
||||
},
|
||||
async init({ actionsRegistry }) {
|
||||
actionsRegistry.register({
|
||||
name: 'protected-action',
|
||||
title: 'Protected Action',
|
||||
description: 'Permission required',
|
||||
visibilityPermission: testPermission,
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
features: [
|
||||
pluginSubject,
|
||||
actionsRegistryServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
permissionsRegistryMock.factory,
|
||||
],
|
||||
});
|
||||
|
||||
expect(permissionsRegistryMock.addPermissions).toHaveBeenCalledWith([
|
||||
testPermission,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+13
-1
@@ -32,13 +32,25 @@ export const actionsRegistryServiceFactory = createServiceFactory({
|
||||
httpAuth: coreServices.httpAuth,
|
||||
logger: coreServices.logger,
|
||||
auth: coreServices.auth,
|
||||
permissions: coreServices.permissions,
|
||||
permissionsRegistry: coreServices.permissionsRegistry,
|
||||
},
|
||||
factory: ({ metadata, httpRouter, httpAuth, logger, auth }) => {
|
||||
factory: ({
|
||||
metadata,
|
||||
httpRouter,
|
||||
httpAuth,
|
||||
logger,
|
||||
auth,
|
||||
permissions,
|
||||
permissionsRegistry,
|
||||
}) => {
|
||||
const actionsRegistryService = DefaultActionsRegistryService.create({
|
||||
httpAuth,
|
||||
logger,
|
||||
auth,
|
||||
metadata,
|
||||
permissions,
|
||||
permissionsRegistry,
|
||||
});
|
||||
|
||||
httpRouter.use(actionsRegistryService.createRouter());
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
```ts
|
||||
import { AnyZodObject } from 'zod';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { BasicPermission } from '@backstage/plugin-permission-common';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
@@ -31,6 +32,7 @@ export type ActionsRegistryActionOptions<
|
||||
input: (zod: typeof z) => TInputSchema;
|
||||
output: (zod: typeof z) => TOutputSchema;
|
||||
};
|
||||
visibilityPermission?: BasicPermission;
|
||||
attributes?: {
|
||||
destructive?: boolean;
|
||||
idempotent?: boolean;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z, AnyZodObject } from 'zod';
|
||||
import { BasicPermission } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
LoggerService,
|
||||
BackstageCredentials,
|
||||
@@ -42,6 +43,7 @@ export type ActionsRegistryActionOptions<
|
||||
input: (zod: typeof z) => TInputSchema;
|
||||
output: (zod: typeof z) => TOutputSchema;
|
||||
};
|
||||
visibilityPermission?: BasicPermission;
|
||||
attributes?: {
|
||||
destructive?: boolean;
|
||||
idempotent?: boolean;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z, AnyZodObject } from 'zod';
|
||||
import { BasicPermission } from '@backstage/plugin-permission-common';
|
||||
import { LoggerService } from './LoggerService';
|
||||
import { BackstageCredentials } from './AuthService';
|
||||
|
||||
@@ -40,6 +41,7 @@ export type ActionsRegistryActionOptions<
|
||||
input: (zod: typeof z) => TInputSchema;
|
||||
output: (zod: typeof z) => TOutputSchema;
|
||||
};
|
||||
visibilityPermission?: BasicPermission;
|
||||
attributes?: {
|
||||
destructive?: boolean;
|
||||
idempotent?: boolean;
|
||||
|
||||
@@ -2524,6 +2524,7 @@ __metadata:
|
||||
"@backstage/integration-aws-node": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
"@backstage/plugin-permission-node": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@google-cloud/cloud-sql-connector": "npm:^1.4.0"
|
||||
|
||||
Reference in New Issue
Block a user