From 74967cf50d48f19bb9520df9659bd3209bf34f62 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Thu, 13 Jan 2022 13:04:47 +0000 Subject: [PATCH] catalog-backend: update permission integration to support batching Signed-off-by: MT Lewis --- .../src/service/NextCatalogBuilder.ts | 30 ++++--- .../src/service/NextRouter.test.ts | 87 +++++++++++++++++++ 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 391c89d83d..be75b4dfe7 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -25,6 +25,7 @@ import { NoForeignRootFieldsEntityPolicy, parseEntityRef, SchemaValidEntityPolicy, + stringifyEntityRef, Validators, } from '@backstage/catalog-model'; import { @@ -34,7 +35,7 @@ import { } from '@backstage/integration'; import { createHash } from 'crypto'; import { Router } from 'express'; -import lodash from 'lodash'; +import lodash, { keyBy } from 'lodash'; import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog'; import { DatabaseLocationsCatalog, @@ -415,18 +416,27 @@ export class NextCatalogBuilder { ); const permissionIntegrationRouter = createPermissionIntegrationRouter({ resourceType: RESOURCE_TYPE_CATALOG_ENTITY, - getResource: async (resourceRef: string) => { - const parsed = parseEntityRef(resourceRef); + getResources: async (resourceRefs: string[]) => { + const { entities } = await entitiesCatalog.entities({ + filter: { + anyOf: resourceRefs.map(resourceRef => { + const { kind, namespace, name } = parseEntityRef(resourceRef); - const { entities } = await unauthorizedEntitiesCatalog.entities({ - filter: basicEntityFilter({ - kind: parsed.kind, - 'metadata.namespace': parsed.namespace, - 'metadata.name': parsed.name, - }), + return basicEntityFilter({ + kind, + 'metadata.namespace': namespace, + 'metadata.name': name, + }); + }), + }, }); - return entities[0]; + const entitiesByRef = keyBy(entities, stringifyEntityRef); + + return resourceRefs.map( + resourceRef => + entitiesByRef[stringifyEntityRef(parseEntityRef(resourceRef))], + ); }, rules: this.permissionRules, }); diff --git a/plugins/catalog-backend/src/service/NextRouter.test.ts b/plugins/catalog-backend/src/service/NextRouter.test.ts index 84580d0ca4..b79fba4a56 100644 --- a/plugins/catalog-backend/src/service/NextRouter.test.ts +++ b/plugins/catalog-backend/src/service/NextRouter.test.ts @@ -24,6 +24,9 @@ import { EntitiesCatalog } from '../catalog'; import { LocationService, RefreshService } from './types'; import { basicEntityFilter } from './request'; import { createNextRouter } from './NextRouter'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; describe('createNextRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked; @@ -433,3 +436,87 @@ describe('createNextRouter readonly enabled', () => { }); }); }); + +describe('NextRouter permissioning', () => { + let entitiesCatalog: jest.Mocked; + let locationService: jest.Mocked; + let app: express.Express; + let refreshService: RefreshService; + + const fakeRule = { + name: 'FAKE_RULE', + description: 'fake rule', + apply: () => true, + toQuery: () => ({ key: '', values: [] }), + }; + + beforeAll(async () => { + entitiesCatalog = { + entities: jest.fn(), + removeEntityByUid: jest.fn(), + batchAddOrUpdateEntities: jest.fn(), + entityAncestry: jest.fn(), + }; + locationService = { + getLocation: jest.fn(), + createLocation: jest.fn(), + listLocations: jest.fn(), + deleteLocation: jest.fn(), + }; + refreshService = { refresh: jest.fn() }; + const router = await createNextRouter({ + entitiesCatalog, + locationService, + logger: getVoidLogger(), + refreshService, + config: new ConfigReader(undefined), + permissionIntegrationRouter: createPermissionIntegrationRouter({ + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + rules: [fakeRule], + getResources: jest.fn((resourceRefs: string[]) => + Promise.resolve( + resourceRefs.map(resourceRef => ({ id: resourceRef })), + ), + ), + }), + }); + app = express().use(router); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('accepts and evaluates conditions at the apply-conditions endpoint', async () => { + const spideySense: Entity = { + apiVersion: 'a', + kind: 'component', + metadata: { + name: 'spidey-sense', + }, + }; + entitiesCatalog.entities.mockResolvedValueOnce({ + entities: [spideySense], + pageInfo: { hasNextPage: false }, + }); + + const requestBody = { + items: [ + { + id: '123', + resourceType: 'catalog-entity', + resourceRef: 'component:default/spidey-sense', + conditions: { rule: 'FAKE_RULE', params: ['user:default/spiderman'] }, + }, + ], + }; + const response = await request(app) + .post('/.well-known/backstage/permissions/apply-conditions') + .send(requestBody); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + items: [{ id: '123', result: AuthorizeResult.ALLOW }], + }); + }); +});