diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts index 814ccd3f38..4be1b55379 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -39,7 +39,9 @@ describe('PermissionIntegrationClient', () => { const mockApplyConditionsHandler = jest.fn( (_req, res, { json }: RestContext) => { - return res(json([{ id: '123', result: AuthorizeResult.ALLOW }])); + return res( + json({ items: [{ id: '123', result: AuthorizeResult.ALLOW }] }), + ); }, ); @@ -101,14 +103,16 @@ describe('PermissionIntegrationClient', () => { expect(mockApplyConditionsHandler).toHaveBeenCalledWith( expect.objectContaining({ - body: [ - { - id: '123', - resourceRef: 'testResource1', - resourceType: 'test-resource', - conditions: mockConditions, - }, - ], + body: { + items: [ + { + id: '123', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }, + ], + }, }), expect.anything(), expect.anything(), @@ -184,7 +188,9 @@ describe('PermissionIntegrationClient', () => { it('should reject invalid responses', async () => { mockApplyConditionsHandler.mockImplementationOnce( (_req, res, { json }: RestContext) => { - return res(json([{ id: '123', outcome: AuthorizeResult.ALLOW }])); + return res( + json({ items: [{ id: '123', outcome: AuthorizeResult.ALLOW }] }), + ); }, ); @@ -204,11 +210,13 @@ describe('PermissionIntegrationClient', () => { mockApplyConditionsHandler.mockImplementationOnce( (_req, res, { json }: RestContext) => { return res( - json([ - { id: '123', result: AuthorizeResult.ALLOW }, - { id: '456', result: AuthorizeResult.DENY }, - { id: '789', result: AuthorizeResult.ALLOW }, - ]), + json({ + items: [ + { id: '123', result: AuthorizeResult.ALLOW }, + { id: '456', result: AuthorizeResult.DENY }, + { id: '789', result: AuthorizeResult.ALLOW }, + ], + }), ); }, ); diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts index f4fdb6cbfc..2b2161ee71 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts @@ -20,18 +20,20 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ApplyConditionsRequestEntry, - ApplyConditionsResponse, + ApplyConditionsResponseEntry, ConditionalPolicyDecision, } from '@backstage/plugin-permission-node'; -const responseSchema = z.array( - z.object({ - id: z.string(), - result: z - .literal(AuthorizeResult.ALLOW) - .or(z.literal(AuthorizeResult.DENY)), - }), -); +const responseSchema = z.object({ + items: z.array( + z.object({ + id: z.string(), + result: z + .literal(AuthorizeResult.ALLOW) + .or(z.literal(AuthorizeResult.DENY)), + }), + ), +}); export type ResourcePolicyDecision = ConditionalPolicyDecision & { resourceRef: string; @@ -48,21 +50,23 @@ export class PermissionIntegrationClient { pluginId: string, decisions: readonly ApplyConditionsRequestEntry[], authHeader?: string, - ): Promise { + ): Promise { const endpoint = `${await this.discovery.getBaseUrl( pluginId, )}/.well-known/backstage/permissions/apply-conditions`; const response = await fetch(endpoint, { method: 'POST', - body: JSON.stringify( - decisions.map(({ id, resourceRef, resourceType, conditions }) => ({ - id, - resourceRef, - resourceType, - conditions, - })), - ), + body: JSON.stringify({ + items: decisions.map( + ({ id, resourceRef, resourceType, conditions }) => ({ + id, + resourceRef, + resourceType, + conditions, + }), + ), + }), headers: { ...(authHeader ? { authorization: authHeader } : {}), 'content-type': 'application/json', @@ -75,6 +79,8 @@ export class PermissionIntegrationClient { ); } - return responseSchema.parse(await response.json()); + const result = responseSchema.parse(await response.json()); + + return result.items; } } diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 7023369306..d9ea64483e 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -18,7 +18,9 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { TokenManager } from '@backstage/backend-common'; // @public -export type ApplyConditionsRequest = ApplyConditionsRequestEntry[]; +export type ApplyConditionsRequest = { + items: ApplyConditionsRequestEntry[]; +}; // @public export type ApplyConditionsRequestEntry = Identified<{ @@ -28,7 +30,9 @@ export type ApplyConditionsRequestEntry = Identified<{ }>; // @public -export type ApplyConditionsResponse = ApplyConditionsResponseEntry[]; +export type ApplyConditionsResponse = { + items: ApplyConditionsResponseEntry[]; +}; // @public export type ApplyConditionsResponseEntry = Identified; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 2e911b7cae..79bfcb3b0b 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -96,22 +96,26 @@ describe('createPermissionIntegrationRouter', () => { ])('returns 200/ALLOW when criteria match (case %#)', async conditions => { const response = await request(app) .post('/.well-known/backstage/permissions/apply-conditions') - .send([ - { - id: '123', - resourceRef: 'default:test/resource', - resourceType: 'test-resource', - conditions, - }, - ]); + .send({ + items: [ + { + id: '123', + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions, + }, + ], + }); expect(response.status).toEqual(200); - expect(response.body).toEqual([ - { - id: '123', - result: AuthorizeResult.ALLOW, - }, - ]); + expect(response.body).toEqual({ + items: [ + { + id: '123', + result: AuthorizeResult.ALLOW, + }, + ], + }); }); it.each([ @@ -145,19 +149,21 @@ describe('createPermissionIntegrationRouter', () => { async conditions => { const response = await request(app) .post('/.well-known/backstage/permissions/apply-conditions') - .send([ - { - id: '123', - resourceRef: 'default:test/resource', - resourceType: 'test-resource', - conditions, - }, - ]); + .send({ + items: [ + { + id: '123', + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions, + }, + ], + }); expect(response.status).toEqual(200); - expect(response.body).toEqual([ - { id: '123', result: AuthorizeResult.DENY }, - ]); + expect(response.body).toEqual({ + items: [{ id: '123', result: AuthorizeResult.DENY }], + }); }, ); @@ -167,54 +173,58 @@ describe('createPermissionIntegrationRouter', () => { beforeEach(async () => { response = await request(app) .post('/.well-known/backstage/permissions/apply-conditions') - .send([ - { - id: '123', - resourceRef: 'default:test/resource-1', - resourceType: 'test-resource', - conditions: { rule: 'test-rule-1', params: [] }, - }, - { - id: '234', - resourceRef: 'default:test/resource-1', - resourceType: 'test-resource', - conditions: { rule: 'test-rule-2', params: [] }, - }, - { - id: '345', - resourceRef: 'default:test/resource-2', - resourceType: 'test-resource', - conditions: { not: { rule: 'test-rule-1', params: [] } }, - }, - { - id: '456', - resourceRef: 'default:test/resource-3', - resourceType: 'test-resource', - conditions: { not: { rule: 'test-rule-2', params: [] } }, - }, - { - id: '567', - resourceRef: 'default:test/resource-4', - resourceType: 'test-resource', - conditions: { - anyOf: [ - { rule: 'test-rule-1', params: [] }, - { rule: 'test-rule-2', params: [] }, - ], + .send({ + items: [ + { + id: '123', + resourceRef: 'default:test/resource-1', + resourceType: 'test-resource', + conditions: { rule: 'test-rule-1', params: [] }, }, - }, - ]); + { + id: '234', + resourceRef: 'default:test/resource-1', + resourceType: 'test-resource', + conditions: { rule: 'test-rule-2', params: [] }, + }, + { + id: '345', + resourceRef: 'default:test/resource-2', + resourceType: 'test-resource', + conditions: { not: { rule: 'test-rule-1', params: [] } }, + }, + { + id: '456', + resourceRef: 'default:test/resource-3', + resourceType: 'test-resource', + conditions: { not: { rule: 'test-rule-2', params: [] } }, + }, + { + id: '567', + resourceRef: 'default:test/resource-4', + resourceType: 'test-resource', + conditions: { + anyOf: [ + { rule: 'test-rule-1', params: [] }, + { rule: 'test-rule-2', params: [] }, + ], + }, + }, + ], + }); }); it('processes batched requests', () => { expect(response.status).toEqual(200); - expect(response.body).toEqual([ - { id: '123', result: AuthorizeResult.ALLOW }, - { id: '234', result: AuthorizeResult.DENY }, - { id: '345', result: AuthorizeResult.DENY }, - { id: '456', result: AuthorizeResult.ALLOW }, - { id: '567', result: AuthorizeResult.ALLOW }, - ]); + expect(response.body).toEqual({ + items: [ + { id: '123', result: AuthorizeResult.ALLOW }, + { id: '234', result: AuthorizeResult.DENY }, + { id: '345', result: AuthorizeResult.DENY }, + { id: '456', result: AuthorizeResult.ALLOW }, + { id: '567', result: AuthorizeResult.ALLOW }, + ], + }); }); it('calls getResources for all required resources at once', () => { @@ -230,16 +240,18 @@ describe('createPermissionIntegrationRouter', () => { it('returns 400 when called with incorrect resource type', async () => { const response = await request(app) .post('/.well-known/backstage/permissions/apply-conditions') - .send([ - { - id: '123', - resourceRef: 'default:test/resource', - resourceType: 'test-incorrect-resource', - conditions: { - anyOf: [], + .send({ + items: [ + { + id: '123', + resourceRef: 'default:test/resource', + resourceType: 'test-incorrect-resource', + conditions: { + anyOf: [], + }, }, - }, - ]); + ], + }); expect(response.status).toEqual(400); expect(response.error && response.error.text).toMatch( @@ -254,22 +266,26 @@ describe('createPermissionIntegrationRouter', () => { const response = await request(app) .post('/.well-known/backstage/permissions/apply-conditions') - .send([ - { - id: '123', - resourceRef: 'default:test/resource', - resourceType: 'test-resource', - conditions: { rule: 'testRule1', params: [] }, - }, - ]); + .send({ + items: [ + { + id: '123', + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions: { rule: 'test-rule-1', params: [] }, + }, + ], + }); expect(response.status).toEqual(200); - expect(response.body).toEqual([ - { - id: '123', - result: AuthorizeResult.DENY, - }, - ]); + expect(response.body).toEqual({ + items: [ + { + id: '123', + result: AuthorizeResult.DENY, + }, + ], + }); }); it.each([ @@ -278,14 +294,17 @@ describe('createPermissionIntegrationRouter', () => { {}, { resourceType: 'test-resource-type' }, [{ resourceType: 'test-resource-type' }], - [{ resourceRef: 'test/resource-ref' }], - [ - { - resourceType: 'test-resource-type', - resourceRef: 'test/resource-ref', - }, - ], - [{ conditions: { anyOf: [] } }], + { items: [{ resourceType: 'test-resource-type' }] }, + { items: [{ resourceRef: 'test/resource-ref' }] }, + { + items: [ + { + resourceType: 'test-resource-type', + resourceRef: 'test/resource-ref', + }, + ], + }, + { items: [{ conditions: { anyOf: [] } }] }, ])(`returns 400 for invalid input %#`, async input => { const response = await request(app) .post('/.well-known/backstage/permissions/apply-conditions') diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 747e47b87d..4968db6100 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -48,14 +48,16 @@ const permissionCriteriaSchema: z.ZodSchema< ]), ); -const applyConditionsRequestSchema = z.array( - z.object({ - id: z.string(), - resourceRef: z.string(), - resourceType: z.string(), - conditions: permissionCriteriaSchema, - }), -); +const applyConditionsRequestSchema = z.object({ + items: z.array( + z.object({ + id: z.string(), + resourceRef: z.string(), + resourceType: z.string(), + conditions: permissionCriteriaSchema, + }), + ), +}); /** * A request to load the referenced resource and apply conditions in order to @@ -74,7 +76,9 @@ export type ApplyConditionsRequestEntry = Identified<{ * * @public */ -export type ApplyConditionsRequest = ApplyConditionsRequestEntry[]; +export type ApplyConditionsRequest = { + items: ApplyConditionsRequestEntry[]; +}; /** * The result of applying the conditions, expressed as a definitive authorize @@ -89,7 +93,9 @@ export type ApplyConditionsResponseEntry = Identified; * * @public */ -export type ApplyConditionsResponse = ApplyConditionsResponseEntry[]; +export type ApplyConditionsResponse = { + items: ApplyConditionsResponseEntry[]; +}; const applyConditions = ( criteria: PermissionCriteria, @@ -165,7 +171,9 @@ export const createPermissionIntegrationRouter = (options: { const getRule = createGetRule(rules); - const assertValidResourceTypes = (requests: ApplyConditionsRequest) => { + const assertValidResourceTypes = ( + requests: ApplyConditionsRequestEntry[], + ) => { const invalidResourceType = requests.find( request => request.resourceType !== resourceType, )?.resourceType; @@ -188,10 +196,10 @@ export const createPermissionIntegrationRouter = (options: { const body = parseResult.data; - assertValidResourceTypes(body); + assertValidResourceTypes(body.items); const resourceRefs = Array.from( - new Set(body.map(({ resourceRef }) => resourceRef)), + new Set(body.items.map(({ resourceRef }) => resourceRef)), ); const resourceArray = await getResources(resourceRefs); const resources = resourceRefs.reduce((acc, resourceRef, index) => { @@ -200,8 +208,8 @@ export const createPermissionIntegrationRouter = (options: { return acc; }, {} as Record); - return res.status(200).json( - body.map(request => ({ + return res.status(200).json({ + items: body.items.map(request => ({ id: request.id, result: applyConditions( request.conditions, @@ -211,7 +219,7 @@ export const createPermissionIntegrationRouter = (options: { ? AuthorizeResult.ALLOW : AuthorizeResult.DENY, })), - ); + }); }, );