Changed the createPermissionIntegrationRouter API to allow getResources to be optional

Signed-off-by: Harry Hogg <hhogg@spotify.com>
This commit is contained in:
Harry Hogg
2023-02-14 10:23:58 +00:00
parent 55df43ac8a
commit 27a103ca07
4 changed files with 81 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-permission-node': patch
---
Changed the createPermissionIntegrationRouter API to allow getResources to be optional
+6 -1
View File
@@ -124,7 +124,7 @@ export const createPermissionIntegrationRouter: <
NoInfer<TResourceType>,
PermissionRuleParams
>[];
getResources: (resourceRefs: string[]) => Promise<(TResource | undefined)[]>;
getResources?: GetResourcesFn<TResource> | undefined;
}) => express.Router;
// @public
@@ -137,6 +137,11 @@ export const createPermissionRule: <
rule: PermissionRule<TResource, TQuery, TResourceType, TParams>,
) => PermissionRule<TResource, TQuery, TResourceType, TParams>;
// @public
export type GetResourcesFn<TResource> = (
resourceRefs: string[],
) => Promise<Array<TResource | undefined>>;
// @alpha
export const isAndCriteria: <T>(
criteria: PermissionCriteria<T>,
@@ -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<typeof createPermissionIntegrationRouter>[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',
);
@@ -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<TResource> = (
resourceRefs: string[],
) => Promise<Array<TResource | undefined>>;
const applyConditions = <TResourceType extends string, TResource>(
criteria: PermissionCriteria<PermissionCondition<TResourceType>>,
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<TResource, any, NoInfer<TResourceType>>[];
getResources: (
resourceRefs: string[],
) => Promise<Array<TResource | undefined>>;
getResources?: GetResourcesFn<TResource>;
}): 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<ApplyConditionsResponse | string>) => {
if (!getResources) {
throw new InputError(
'This plugin does not support the apply-conditions API.',
);
}
const parseResult = applyConditionsRequestSchema.safeParse(req.body);
if (!parseResult.success) {