diff --git a/.changeset/clean-planes-join.md b/.changeset/clean-planes-join.md new file mode 100644 index 0000000000..2ad009c133 --- /dev/null +++ b/.changeset/clean-planes-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Changed the createPermissionIntegrationRouter API to allow getResources to be optional diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 3f712fe362..3019812749 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -124,7 +124,7 @@ export const createPermissionIntegrationRouter: < NoInfer, PermissionRuleParams >[]; - getResources: (resourceRefs: string[]) => Promise<(TResource | undefined)[]>; + getResources?: GetResourcesFn | undefined; }) => express.Router; // @public @@ -137,6 +137,11 @@ export const createPermissionRule: < rule: PermissionRule, ) => PermissionRule; +// @public +export type GetResourcesFn = ( + resourceRefs: string[], +) => Promise>; + // @alpha export const isAndCriteria: ( criteria: PermissionCriteria, diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 32d814465f..33b12f4fc2 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -19,18 +19,15 @@ import { createPermission, Permission, } from '@backstage/plugin-permission-common'; -import express, { Express, Router } from 'express'; +import express from 'express'; import request, { Response } from 'supertest'; import { z } from 'zod'; -import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter'; +import { + createPermissionIntegrationRouter, + GetResourcesFn, +} from './createPermissionIntegrationRouter'; import { createPermissionRule } from './createPermissionRule'; -const mockGetResources: jest.MockedFunction< - Parameters[0]['getResources'] -> = jest.fn(async resourceRefs => - resourceRefs.map(resourceRef => ({ id: resourceRef })), -); - const testPermission: Permission = createPermission({ name: 'test.permission', attributes: {}, @@ -56,29 +53,30 @@ const testRule2 = createPermissionRule({ toQuery: () => ({}), }); -describe('createPermissionIntegrationRouter', () => { - let app: Express; - let router: Router; +const defaultMockedGetResources: GetResourcesFn<{ id: string }> = jest.fn( + async resourceRefs => resourceRefs.map(resourceRef => ({ id: resourceRef })), +); - beforeAll(() => { - router = createPermissionIntegrationRouter({ - resourceType: 'test-resource', - permissions: [testPermission], - getResources: mockGetResources, - rules: [testRule1, testRule2], - }); - - app = express().use(router); +const createApp = ( + mockedGetResources: + | typeof defaultMockedGetResources + | null = defaultMockedGetResources, +) => { + const router = createPermissionIntegrationRouter({ + resourceType: 'test-resource', + permissions: [testPermission], + getResources: mockedGetResources || undefined, + rules: [testRule1, testRule2], }); + return express().use(router); +}; + +describe('createPermissionIntegrationRouter', () => { afterEach(() => { jest.clearAllMocks(); }); - it('works', async () => { - expect(router).toBeDefined(); - }); - describe('POST /.well-known/backstage/permissions/apply-conditions', () => { it.each([ { @@ -150,7 +148,7 @@ describe('createPermissionIntegrationRouter', () => { ], }, ])('returns 200/ALLOW when criteria match (case %#)', async conditions => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -238,7 +236,7 @@ describe('createPermissionIntegrationRouter', () => { ])( 'returns 200/DENY when criteria do not match (case %#)', async conditions => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -262,7 +260,7 @@ describe('createPermissionIntegrationRouter', () => { let response: Response; beforeEach(async () => { - response = await request(app) + response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -353,7 +351,7 @@ describe('createPermissionIntegrationRouter', () => { }); it('calls getResources for all required resources at once', () => { - expect(mockGetResources).toHaveBeenCalledWith([ + expect(defaultMockedGetResources).toHaveBeenCalledWith([ 'default:test/resource-1', 'default:test/resource-2', 'default:test/resource-3', @@ -363,7 +361,7 @@ describe('createPermissionIntegrationRouter', () => { }); it('returns 400 when called with incorrect resource type', async () => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -413,11 +411,11 @@ describe('createPermissionIntegrationRouter', () => { }); it('returns 200/DENY when resource is not found', async () => { - mockGetResources.mockImplementationOnce(async resourceRefs => - resourceRefs.map(() => undefined), + const mockedGetResources: GetResourcesFn<{ id: string }> = jest.fn( + async resourceRefs => resourceRefs.map(() => undefined), ); - const response = await request(app) + const response = await request(createApp(mockedGetResources)) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -446,15 +444,16 @@ describe('createPermissionIntegrationRouter', () => { }); it('interleaves responses for present and missing resources', async () => { - mockGetResources.mockImplementationOnce(async resourceRefs => - resourceRefs.map(resourceRef => - resourceRef === 'default:test/missing-resource' - ? undefined - : { id: resourceRef }, - ), + const mockedGetResources: GetResourcesFn<{ id: string }> = jest.fn( + async resourceRefs => + resourceRefs.map(resourceRef => + resourceRef === 'default:test/missing-resource' + ? undefined + : { id: resourceRef }, + ), ); - const response = await request(app) + const response = await request(createApp(mockedGetResources)) .post('/.well-known/backstage/permissions/apply-conditions') .send({ items: [ @@ -548,18 +547,31 @@ describe('createPermissionIntegrationRouter', () => { ], }, ])(`returns 400 for invalid input %#`, async input => { - const response = await request(app) + const response = await request(createApp()) .post('/.well-known/backstage/permissions/apply-conditions') .send(input); expect(response.status).toEqual(400); expect(response.error && response.error.text).toMatch(/invalid/i); }); + + it('returns 400 with no getResources implementation', async () => { + const response = await request(createApp(null)) + .post('/.well-known/backstage/permissions/apply-conditions') + .send({ + items: [], + }); + + expect(response.status).toEqual(400); + expect(response.body.error.message).toEqual( + 'This plugin does not support the apply-conditions API.', + ); + }); }); 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( + const response = await request(createApp()).get( '/.well-known/backstage/permissions/metadata', ); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 23b59c9c43..49a89f0394 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -125,6 +125,16 @@ export type MetadataResponse = { rules: MetadataResponseSerializedRule[]; }; +/** + * Function type for returning an array of resources + * matching the given resourceRefs. + * + * @public + */ +export type GetResourcesFn = ( + resourceRefs: string[], +) => Promise>; + const applyConditions = ( criteria: PermissionCriteria>, resource: TResource | undefined, @@ -205,9 +215,7 @@ export const createPermissionIntegrationRouter = < // consider any rules whose resource type does not match // to be an error. rules: PermissionRule>[]; - getResources: ( - resourceRefs: string[], - ) => Promise>; + getResources?: GetResourcesFn; }): express.Router => { const { resourceType, permissions, rules, getResources } = options; const router = Router(); @@ -250,6 +258,12 @@ export const createPermissionIntegrationRouter = < router.post( '/.well-known/backstage/permissions/apply-conditions', async (req, res: Response) => { + if (!getResources) { + throw new InputError( + 'This plugin does not support the apply-conditions API.', + ); + } + const parseResult = applyConditionsRequestSchema.safeParse(req.body); if (!parseResult.success) {