diff --git a/plugins/permission-common/.eslintrc.js b/plugins/permission-common/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/permission-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/permission-common/README.md b/plugins/permission-common/README.md new file mode 100644 index 0000000000..7ba998bed5 --- /dev/null +++ b/plugins/permission-common/README.md @@ -0,0 +1,7 @@ +# @backstage/plugin-permission-common + +> NOTE: THIS PACKAGE IS EXPERIMENTAL, HERE BE DRAGONS + +Isomorphic types and client for Backstage permissions and authorization. For +more information, see the [authorization +PRFC](https://github.com/backstage/backstage/pull/7761). diff --git a/plugins/permission-common/api-report.md b/plugins/permission-common/api-report.md new file mode 100644 index 0000000000..cbf1877c75 --- /dev/null +++ b/plugins/permission-common/api-report.md @@ -0,0 +1,95 @@ +## API Report File for "@backstage/plugin-permission-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export type AuthorizeRequest = { + permission: Permission; + resourceRef?: string; +}; + +// @public +export type AuthorizeRequestOptions = { + token?: string; +}; + +// @public +export type AuthorizeResponse = + | { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; + } + | { + result: AuthorizeResult.CONDITIONAL; + conditions: PermissionCriteria; + }; + +// @public +export enum AuthorizeResult { + ALLOW = 'ALLOW', + CONDITIONAL = 'CONDITIONAL', + DENY = 'DENY', +} + +// @public +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; + +// @public +export type Identified = T & { + id: string; +}; + +// @public +export function isCreatePermission(permission: Permission): boolean; + +// @public +export function isDeletePermission(permission: Permission): boolean; + +// @public +export function isReadPermission(permission: Permission): boolean; + +// @public +export function isUpdatePermission(permission: Permission): boolean; + +// @public +export type Permission = { + name: string; + attributes: PermissionAttributes; + resourceType?: string; +}; + +// @public +export type PermissionAttributes = { + action?: 'create' | 'read' | 'update' | 'delete'; +}; + +// @public +export class PermissionClient { + constructor(options: { discoveryApi: DiscoveryApi; enabled?: boolean }); + authorize( + requests: AuthorizeRequest[], + options?: AuthorizeRequestOptions, + ): Promise; +} + +// @public +export type PermissionCondition = { + rule: string; + params: TParams; +}; + +// @public +export type PermissionCriteria = + | { + allOf: PermissionCriteria[]; + } + | { + anyOf: PermissionCriteria[]; + } + | { + not: PermissionCriteria; + } + | TQuery; +``` diff --git a/plugins/permission-common/config.d.ts b/plugins/permission-common/config.d.ts new file mode 100644 index 0000000000..2378fa4a71 --- /dev/null +++ b/plugins/permission-common/config.d.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** Configuration options for Backstage permissions and authorization */ + permission?: { + /** + * Whether authorization is enabled in Backstage. Defaults to false, which means authorization + * requests will be automatically allowed without invoking the authorization policy. + * @visibility frontend + */ + enabled?: boolean; + }; +} diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json new file mode 100644 index 0000000000..5e79c1ba7b --- /dev/null +++ b/plugins/permission-common/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-permission-common", + "description": "Isomorphic types and client for Backstage permissions and authorization", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/permission-common" + }, + "keywords": [ + "backstage", + "permissions" + ], + "license": "Apache-2.0", + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts", + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/errors": "^0.1.2", + "cross-fetch": "^3.0.6", + "uuid": "^8.0.0", + "zod": "^3.11.6" + }, + "devDependencies": { + "@backstage/cli": "^0.8.0", + "@types/jest": "^26.0.7", + "msw": "^0.35.0" + } +} diff --git a/plugins/permission-common/src/PermissionClient.test.ts b/plugins/permission-common/src/PermissionClient.test.ts new file mode 100644 index 0000000000..06e2ae0561 --- /dev/null +++ b/plugins/permission-common/src/PermissionClient.test.ts @@ -0,0 +1,167 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RestContext, rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { PermissionClient } from './PermissionClient'; +import { AuthorizeRequest, AuthorizeResult, Identified } from './types/api'; +import { DiscoveryApi } from './types/discovery'; +import { Permission } from './types/permission'; + +const server = setupServer(); +const token = 'fake-token'; + +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discoveryApi: DiscoveryApi = { + async getBaseUrl() { + return mockBaseUrl; + }, +}; +const client: PermissionClient = new PermissionClient({ + discoveryApi, + enabled: true, +}); + +const mockPermission: Permission = { + name: 'test.permission', + attributes: {}, + resourceType: 'test-resource', +}; + +const mockAuthorizeRequest = { + permission: mockPermission, + resourceRef: 'foo', +}; + +describe('PermissionClient', () => { + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); + afterAll(() => server.close()); + afterEach(() => server.resetHandlers()); + + describe('authorize', () => { + const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { + const responses = req.body.map((a: Identified) => ({ + id: a.id, + result: AuthorizeResult.ALLOW, + })); + + return res(json(responses)); + }); + + beforeEach(() => { + server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch entities from correct endpoint', async () => { + await client.authorize([mockAuthorizeRequest]); + expect(mockAuthorizeHandler).toHaveBeenCalled(); + }); + + it('should include a request body', async () => { + await client.authorize([mockAuthorizeRequest]); + + const request = mockAuthorizeHandler.mock.calls[0][0]; + expect(request.body[0]).toEqual( + expect.objectContaining({ + permission: mockPermission, + resourceRef: 'foo', + }), + ); + }); + + it('should return the response from the fetch request', async () => { + const response = await client.authorize([mockAuthorizeRequest]); + expect(response[0]).toEqual( + expect.objectContaining({ result: AuthorizeResult.ALLOW }), + ); + }); + + it('should not include authorization headers if no token is supplied', async () => { + await client.authorize([mockAuthorizeRequest]); + + const request = mockAuthorizeHandler.mock.calls[0][0]; + expect(request.headers.has('authorization')).toEqual(false); + }); + + it('should include correctly-constructed authorization header if token is supplied', async () => { + await client.authorize([mockAuthorizeRequest], { token }); + + const request = mockAuthorizeHandler.mock.calls[0][0]; + expect(request.headers.get('authorization')).toEqual('Bearer fake-token'); + }); + + it('should forward response errors', async () => { + mockAuthorizeHandler.mockImplementationOnce( + (_req, res, { status }: RestContext) => { + return res(status(401)); + }, + ); + await expect( + client.authorize([mockAuthorizeRequest], { token }), + ).rejects.toThrowError(/request failed with 401/i); + }); + + it('should reject responses with missing ids', async () => { + mockAuthorizeHandler.mockImplementationOnce( + (_req, res, { json }: RestContext) => { + return res(json([{ id: 'wrong-id', result: AuthorizeResult.ALLOW }])); + }, + ); + await expect( + client.authorize([mockAuthorizeRequest], { token }), + ).rejects.toThrowError(/Unexpected authorization response/i); + }); + + it('should reject invalid responses', async () => { + mockAuthorizeHandler.mockImplementationOnce( + (req, res, { json }: RestContext) => { + const responses = req.body.map((a: Identified) => ({ + id: a.id, + outcome: AuthorizeResult.ALLOW, + })); + + return res(json(responses)); + }, + ); + await expect( + client.authorize([mockAuthorizeRequest], { token }), + ).rejects.toThrowError(/invalid input/i); + }); + + it('should allow all when authorization is not enabled', async () => { + mockAuthorizeHandler.mockImplementationOnce( + (req, res, { json }: RestContext) => { + const responses = req.body.map((a: Identified) => ({ + id: a.id, + outcome: AuthorizeResult.DENY, + })); + + return res(json(responses)); + }, + ); + const disabled = new PermissionClient({ discoveryApi, enabled: false }); + const response = await disabled.authorize([mockAuthorizeRequest]); + expect(response[0]).toEqual( + expect.objectContaining({ result: AuthorizeResult.ALLOW }), + ); + expect(mockAuthorizeHandler).not.toBeCalled(); + }); + }); +}); diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts new file mode 100644 index 0000000000..5b17ccd333 --- /dev/null +++ b/plugins/permission-common/src/PermissionClient.ts @@ -0,0 +1,159 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ResponseError } from '@backstage/errors'; +import fetch from 'cross-fetch'; +import * as uuid from 'uuid'; +import { z } from 'zod'; +import { + AuthorizeResult, + AuthorizeRequest, + AuthorizeResponse, + Identified, + PermissionCriteria, + PermissionCondition, +} from './types/api'; +import { DiscoveryApi } from './types/discovery'; + +const permissionCriteriaSchema: z.ZodSchema< + PermissionCriteria +> = z.lazy(() => + z + .object({ + rule: z.string(), + params: z.array(z.unknown()), + }) + .or(z.object({ anyOf: z.array(permissionCriteriaSchema) })) + .or(z.object({ allOf: z.array(permissionCriteriaSchema) })) + .or(z.object({ not: permissionCriteriaSchema })), +); + +const responseSchema = z.array( + z + .object({ + id: z.string(), + result: z + .literal(AuthorizeResult.ALLOW) + .or(z.literal(AuthorizeResult.DENY)), + }) + .or( + z.object({ + id: z.string(), + result: z.literal(AuthorizeResult.CONDITIONAL), + conditions: permissionCriteriaSchema, + }), + ), +); + +/** + * Options for authorization requests; currently only an optional auth token. + * @public + */ +export type AuthorizeRequestOptions = { + token?: string; +}; + +/** + * An isomorphic client for requesting authorization for Backstage permissions. + * @public + */ +export class PermissionClient { + private readonly enabled: boolean; + private readonly discoveryApi: DiscoveryApi; + + constructor(options: { discoveryApi: DiscoveryApi; enabled?: boolean }) { + this.discoveryApi = options.discoveryApi; + this.enabled = options.enabled ?? false; + } + + /** + * Request authorization from the permission-backend for the given set of permissions. + * + * Authorization requests check that a given Backstage user can perform a protected operation, + * potentially for a specific resource (such as a catalog entity). The Backstage identity token + * should be included in the `options` if available. + * + * Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`. + * + * The response will be either ALLOW or DENY when either the permission has no resourceType, or a + * resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be + * returned if no resourceRef is provided in the request. Conditional responses are intended only + * for backends which have access to the data source for permissioned resources, so that filters + * can be applied when loading collections of resources. + * @public + */ + async authorize( + requests: AuthorizeRequest[], + options?: AuthorizeRequestOptions, + ): Promise { + // TODO(permissions): it would be great to provide some kind of typing guarantee that + // conditional responses will only ever be returned for requests containing a resourceType + // but no resourceRef. That way clients who aren't prepared to handle filtering according + // to conditions can be guaranteed that they won't unexpectedly get a CONDITIONAL response. + + if (!this.enabled) { + return requests.map(_ => ({ result: AuthorizeResult.ALLOW })); + } + + const identifiedRequests: Identified[] = requests.map( + request => ({ + id: uuid.v4(), + ...request, + }), + ); + + const permissionApi = await this.discoveryApi.getBaseUrl('permission'); + const response = await fetch(`${permissionApi}/authorize`, { + method: 'POST', + body: JSON.stringify(identifiedRequests), + headers: { + ...this.getAuthorizationHeader(options?.token), + 'content-type': 'application/json', + }, + }); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + const identifiedResponses = await response.json(); + this.assertValidResponses(identifiedRequests, identifiedResponses); + + const responsesById = identifiedResponses.reduce((acc, r) => { + acc[r.id] = r; + return acc; + }, {} as Record>); + + return identifiedRequests.map(request => responsesById[request.id]); + } + + private getAuthorizationHeader(token?: string): Record { + return token ? { Authorization: `Bearer ${token}` } : {}; + } + + private assertValidResponses( + requests: Identified[], + json: any, + ): asserts json is Identified[] { + const authorizedResponses = responseSchema.parse(json); + const responseIds = authorizedResponses.map(r => r.id); + const hasAllRequestIds = requests.every(r => responseIds.includes(r.id)); + if (!hasAllRequestIds) { + throw new Error( + 'Unexpected authorization response from permission-backend', + ); + } + } +} diff --git a/plugins/permission-common/src/index.ts b/plugins/permission-common/src/index.ts new file mode 100644 index 0000000000..7c3f6dd613 --- /dev/null +++ b/plugins/permission-common/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Isomorphic types and client for Backstage permissions and authorization + * + * @packageDocumentation + */ +export * from './types'; +export * from './permissions'; +export * from './PermissionClient'; diff --git a/plugins/permission-common/src/permissions/index.ts b/plugins/permission-common/src/permissions/index.ts new file mode 100644 index 0000000000..736d898539 --- /dev/null +++ b/plugins/permission-common/src/permissions/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './util'; diff --git a/plugins/permission-common/src/permissions/util.ts b/plugins/permission-common/src/permissions/util.ts new file mode 100644 index 0000000000..188609f4ae --- /dev/null +++ b/plugins/permission-common/src/permissions/util.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Permission } from '../types'; + +/** + * Check if a given permission is related to a create action. + * @public + */ +export function isCreatePermission(permission: Permission) { + return permission.attributes.action === 'create'; +} + +/** + * Check if a given permission is related to a read action. + * @public + */ +export function isReadPermission(permission: Permission) { + return permission.attributes.action === 'read'; +} + +/** + * Check if a given permission is related to an update action. + * @public + */ +export function isUpdatePermission(permission: Permission) { + return permission.attributes.action === 'update'; +} + +/** + * Check if a given permission is related to a delete action. + * @public + */ +export function isDeletePermission(permission: Permission) { + return permission.attributes.action === 'delete'; +} diff --git a/plugins/permission-common/src/types/api.ts b/plugins/permission-common/src/types/api.ts new file mode 100644 index 0000000000..7f4cd0730b --- /dev/null +++ b/plugins/permission-common/src/types/api.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Permission } from './permission'; + +/** + * A request with a UUID identifier, so that batched responses can be matched up with the original + * requests. + * @public + */ +export type Identified = T & { id: string }; + +/** + * The result of an authorization request. + * @public + */ +export enum AuthorizeResult { + /** + * The authorization request is denied. + */ + DENY = 'DENY', + /** + * The authorization request is allowed. + */ + ALLOW = 'ALLOW', + /** + * The authorization request is allowed if the provided conditions are met. + */ + CONDITIONAL = 'CONDITIONAL', +} + +/** + * An authorization request for {@link PermissionClient#authorize}. + * @public + */ +export type AuthorizeRequest = { + permission: Permission; + resourceRef?: string; +}; + +/** + * A condition returned with a CONDITIONAL authorization response. + * + * Conditions are a reference to a rule defined by a plugin, and parameters to apply the rule. For + * example, a rule might be `isOwner` from the catalog-backend, and params may be a list of entity + * claims from a identity token. + * @public + */ +export type PermissionCondition = { + rule: string; + params: TParams; +}; + +/** + * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure. + * @public + */ +export type PermissionCriteria = + | { allOf: PermissionCriteria[] } + | { anyOf: PermissionCriteria[] } + | { not: PermissionCriteria } + | TQuery; + +/** + * An authorization response from {@link PermissionClient#authorize}. + * @public + */ +export type AuthorizeResponse = + | { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY } + | { + result: AuthorizeResult.CONDITIONAL; + conditions: PermissionCriteria; + }; diff --git a/plugins/permission-common/src/types/discovery.ts b/plugins/permission-common/src/types/discovery.ts new file mode 100644 index 0000000000..19ee5ed19c --- /dev/null +++ b/plugins/permission-common/src/types/discovery.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This is a copy of the core DiscoveryApi, to avoid importing core. + * + * @public + */ +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; diff --git a/plugins/permission-common/src/types/index.ts b/plugins/permission-common/src/types/index.ts new file mode 100644 index 0000000000..e21fa19e8b --- /dev/null +++ b/plugins/permission-common/src/types/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { AuthorizeResult } from './api'; +export type { + AuthorizeRequest, + AuthorizeResponse, + Identified, + PermissionCondition, + PermissionCriteria, +} from './api'; +export type { DiscoveryApi } from './discovery'; +export type { PermissionAttributes, Permission } from './permission'; diff --git a/plugins/permission-common/src/types/permission.ts b/plugins/permission-common/src/types/permission.ts new file mode 100644 index 0000000000..181b36065f --- /dev/null +++ b/plugins/permission-common/src/types/permission.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The attributes related to a given permission; these should be generic and widely applicable to + * all permissions in the system. + * @public + */ +export type PermissionAttributes = { + action?: 'create' | 'read' | 'update' | 'delete'; +}; + +/** + * A permission that can be checked through authorization. + * + * Permissions are the "what" part of authorization, the action to be performed. This may be reading + * an entity from the catalog, executing a software template, or any other action a plugin author + * may wish to protect. + * + * To evaluate authorization, a permission is paired with a Backstage identity (the "who") and + * evaluated using an authorization policy. + * @public + */ +export type Permission = { + name: string; + attributes: PermissionAttributes; + resourceType?: string; +}; diff --git a/yarn.lock b/yarn.lock index 2a984d11fa..debcf79d8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29630,7 +29630,7 @@ zip-stream@^4.1.0: compress-commons "^4.1.0" readable-stream "^3.6.0" -zod@^3.9.5: +zod@^3.11.6, zod@^3.9.5: version "3.11.6" resolved "https://registry.npmjs.org/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483" integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==