Create permission aggregation endpoints (#11695)

* Create permission aggregation endpoints

Signed-off-by: Joon Park <joonp@spotify.com>

* Spelling

Signed-off-by: Joon Park <joonp@spotify.com>

* Refactor permission metadata aggregation into one endpoint

Signed-off-by: Joe Porpeglia <josephp@spotify.com>

* Change parameter field shape

Signed-off-by: Joon Park <joonp@spotify.com>

Co-authored-by: Joe Porpeglia <josephp@spotify.com>
This commit is contained in:
Joon Park
2022-06-10 11:32:28 +01:00
committed by GitHub
parent 0991717d6e
commit 58426f9c0f
5 changed files with 69 additions and 11 deletions
+7
View File
@@ -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.
+1
View File
@@ -275,6 +275,7 @@ scrollbar
seb
semlas
semver
serializable
Serverless
shoutout
siloed
+1
View File
@@ -112,6 +112,7 @@ export const createPermissionIntegrationRouter: <
TResource,
>(options: {
resourceType: TResourceType;
permissions?: Permission[] | undefined;
rules: PermissionRule<TResource, any, NoInfer<TResourceType>, unknown[]>[];
getResources: (resourceRefs: string[]) => Promise<(TResource | undefined)[]>;
}) => express.Router;
@@ -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 },
},
],
});
});
});
});
@@ -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<Permission>;
// 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<Array<TResource | undefined>>;
}): 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<ApplyConditionsResponse | string>) => {
@@ -227,7 +241,7 @@ export const createPermissionIntegrationRouter = <
return acc;
}, {} as Record<string, TResource | undefined>);
return res.status(200).json({
return res.json({
items: body.items.map(request => ({
id: request.id,
result: applyConditions(