From 58426f9c0fbea6ec5c028781431dc7b8d9f3f7e2 Mon Sep 17 00:00:00 2001 From: Joon Park Date: Fri, 10 Jun 2022 11:32:28 +0100 Subject: [PATCH] Create permission aggregation endpoints (#11695) * Create permission aggregation endpoints Signed-off-by: Joon Park * Spelling Signed-off-by: Joon Park * Refactor permission metadata aggregation into one endpoint Signed-off-by: Joe Porpeglia * Change parameter field shape Signed-off-by: Joon Park Co-authored-by: Joe Porpeglia --- .changeset/giant-birds-wink.md | 7 +++ .github/vale/Vocab/Backstage/accept.txt | 1 + plugins/permission-node/api-report.md | 1 + .../createPermissionIntegrationRouter.test.ts | 49 ++++++++++++++++--- .../createPermissionIntegrationRouter.ts | 22 +++++++-- 5 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 .changeset/giant-birds-wink.md diff --git a/.changeset/giant-birds-wink.md b/.changeset/giant-birds-wink.md new file mode 100644 index 0000000000..c3dc2446b2 --- /dev/null +++ b/.changeset/giant-birds-wink.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Added a new endpoint for aggregating permission metadata from a plugin backend: `/.well-known/backstage/permissions/metadata` + +By default, the metadata endpoint will return information about the permission rules supported by the plugin. Plugin authors can also provide an optional `permissions` parameter to `createPermissionIntegrationRouter`. If provided, these `Permission` objects will be included in the metadata returned by this endpoint. The `permissions` parameter will eventually be required in a future breaking change. diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 7631640199..198a16e566 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -275,6 +275,7 @@ scrollbar seb semlas semver +serializable Serverless shoutout siloed diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index b2e4ff45b8..702d363137 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -112,6 +112,7 @@ export const createPermissionIntegrationRouter: < TResource, >(options: { resourceType: TResourceType; + permissions?: Permission[] | undefined; rules: PermissionRule, unknown[]>[]; getResources: (resourceRefs: string[]) => Promise<(TResource | undefined)[]>; }) => express.Router; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 65c43c7f30..ac4005c487 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { + AuthorizeResult, + createPermission, + Permission, +} from '@backstage/plugin-permission-common'; import express, { Express, Router } from 'express'; import request, { Response } from 'supertest'; import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter'; @@ -26,22 +30,25 @@ const mockGetResources: jest.MockedFunction< resourceRefs.map(resourceRef => ({ id: resourceRef })), ); +const testPermission: Permission = createPermission({ + name: 'test.permission', + attributes: {}, +}); + const testRule1 = createPermissionRule({ name: 'test-rule-1', description: 'Test rule 1', resourceType: 'test-resource', - apply: jest.fn( - (_resource: any, _firstParam: string, _secondParam: number) => true, - ), - toQuery: jest.fn(), + apply: (_resource: any, _firstParam: string, _secondParam: number) => true, + toQuery: (_firstParam: string, _secondParam: number) => ({}), }); const testRule2 = createPermissionRule({ name: 'test-rule-2', description: 'Test rule 2', resourceType: 'test-resource', - apply: jest.fn((_resource: any, _firstParam: object) => false), - toQuery: jest.fn(), + apply: (_resource: any, _firstParam: object) => false, + toQuery: (_firstParam: object) => ({}), }); describe('createPermissionIntegrationRouter', () => { @@ -51,6 +58,7 @@ describe('createPermissionIntegrationRouter', () => { beforeAll(() => { router = createPermissionIntegrationRouter({ resourceType: 'test-resource', + permissions: [testPermission], getResources: mockGetResources, rules: [testRule1, testRule2], }); @@ -501,4 +509,31 @@ describe('createPermissionIntegrationRouter', () => { expect(response.error && response.error.text).toMatch(/invalid/i); }); }); + + describe('GET /.well-known/backstage/permissions/metadata', () => { + it('returns a list of permissions and rules used by a given backend', async () => { + const response = await request(app).get( + '/.well-known/backstage/permissions/metadata', + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + permissions: [testPermission], + rules: [ + { + name: testRule1.name, + description: testRule1.description, + resourceType: testRule1.resourceType, + parameters: { count: 2 }, + }, + { + name: testRule2.name, + description: testRule2.description, + resourceType: testRule2.resourceType, + parameters: { count: 1 }, + }, + ], + }); + }); + }); }); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 4bdb4397dd..28abc2f48d 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -23,6 +23,7 @@ import { AuthorizeResult, DefinitivePolicyDecision, IdentifiedPermissionMessage, + Permission, PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; @@ -174,6 +175,7 @@ export const createPermissionIntegrationRouter = < TResource, >(options: { resourceType: TResourceType; + permissions?: Array; // Do not infer value of TResourceType from supplied rules. // instead only consider the resourceType parameter, and // consider any rules whose resource type does not match @@ -183,8 +185,22 @@ export const createPermissionIntegrationRouter = < resourceRefs: string[], ) => Promise>; }): express.Router => { - const { resourceType, rules, getResources } = options; + const { resourceType, permissions, rules, getResources } = options; const router = Router(); + router.use(express.json()); + + router.get('/.well-known/backstage/permissions/metadata', (_, res) => { + const serializableRules = rules.map(rule => ({ + name: rule.name, + description: rule.description, + resourceType: rule.resourceType, + parameters: { + count: rule.toQuery.length, + }, + })); + + return res.json({ permissions, rules: serializableRules }); + }); const getRule = createGetRule(rules); @@ -202,8 +218,6 @@ export const createPermissionIntegrationRouter = < } }; - router.use(express.json()); - router.post( '/.well-known/backstage/permissions/apply-conditions', async (req, res: Response) => { @@ -227,7 +241,7 @@ export const createPermissionIntegrationRouter = < return acc; }, {} as Record); - return res.status(200).json({ + return res.json({ items: body.items.map(request => ({ id: request.id, result: applyConditions(