permission-backend: wrap authorize request and response batches in envelope
Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-permission-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Wrap batched requests and responses to /authorize in an envelope object. The latest version of the PermissionClient in @backstage/permission-common uses the new format - as long as the permission-backend is consumed using this client, no other changes are necessary.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-permission-common': minor
|
||||
---
|
||||
|
||||
**BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/permission-backend.
|
||||
@@ -103,22 +103,24 @@ describe('createRouter', () => {
|
||||
it('calls the permission policy', async () => {
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission1',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission1',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission2',
|
||||
attributes: {},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission2',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
@@ -141,10 +143,12 @@ describe('createRouter', () => {
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(response.body).toEqual([
|
||||
{ id: '123', result: AuthorizeResult.DENY },
|
||||
{ id: '234', result: AuthorizeResult.DENY },
|
||||
]);
|
||||
expect(response.body).toEqual({
|
||||
items: [
|
||||
{ id: '123', result: AuthorizeResult.DENY },
|
||||
{ id: '234', result: AuthorizeResult.DENY },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves identity from the Authorization header', async () => {
|
||||
@@ -152,15 +156,17 @@ describe('createRouter', () => {
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(policy.handle).toHaveBeenCalledWith(
|
||||
@@ -172,9 +178,9 @@ describe('createRouter', () => {
|
||||
},
|
||||
{ id: 'test-user', token: 'test-token' },
|
||||
);
|
||||
expect(response.body).toEqual([
|
||||
{ id: '123', result: AuthorizeResult.ALLOW },
|
||||
]);
|
||||
expect(response.body).toEqual({
|
||||
items: [{ id: '123', result: AuthorizeResult.ALLOW }],
|
||||
});
|
||||
});
|
||||
|
||||
describe('conditional policy result', () => {
|
||||
@@ -188,27 +194,31 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{
|
||||
id: '123',
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
pluginId: 'test-plugin',
|
||||
resourceType: 'test-resource-1',
|
||||
conditions: { rule: 'test-rule', params: ['abc'] },
|
||||
},
|
||||
]);
|
||||
expect(response.body).toEqual({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
pluginId: 'test-plugin',
|
||||
resourceType: 'test-resource-1',
|
||||
conditions: { rule: 'test-rule', params: ['abc'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('makes separate batched requests to multiple plugin backends', async () => {
|
||||
@@ -241,44 +251,46 @@ describe('createRouter', () => {
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:1',
|
||||
},
|
||||
resourceRef: 'resource:1',
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:2',
|
||||
},
|
||||
resourceRef: 'resource:2',
|
||||
},
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:3',
|
||||
},
|
||||
resourceRef: 'resource:3',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:4',
|
||||
},
|
||||
resourceRef: 'resource:4',
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-1',
|
||||
@@ -319,12 +331,14 @@ describe('createRouter', () => {
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{ id: '123', result: AuthorizeResult.ALLOW },
|
||||
{ id: '234', result: AuthorizeResult.ALLOW },
|
||||
{ id: '345', result: AuthorizeResult.DENY },
|
||||
{ id: '456', result: AuthorizeResult.DENY },
|
||||
]);
|
||||
expect(response.body).toEqual({
|
||||
items: [
|
||||
{ id: '123', result: AuthorizeResult.ALLOW },
|
||||
{ id: '234', result: AuthorizeResult.ALLOW },
|
||||
{ id: '345', result: AuthorizeResult.DENY },
|
||||
{ id: '456', result: AuthorizeResult.DENY },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves definitive results unchanged', async () => {
|
||||
@@ -363,60 +377,62 @@ describe('createRouter', () => {
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:1',
|
||||
},
|
||||
resourceRef: 'resource:1',
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:2',
|
||||
},
|
||||
resourceRef: 'resource:2',
|
||||
},
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:3',
|
||||
},
|
||||
resourceRef: 'resource:3',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:4',
|
||||
},
|
||||
resourceRef: 'resource:4',
|
||||
},
|
||||
{
|
||||
id: '567',
|
||||
permission: {
|
||||
name: 'test.permission.5',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
{
|
||||
id: '567',
|
||||
permission: {
|
||||
name: 'test.permission.5',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:5',
|
||||
},
|
||||
resourceRef: 'resource:5',
|
||||
},
|
||||
{
|
||||
id: '678',
|
||||
permission: {
|
||||
name: 'test.permission.6',
|
||||
attributes: {},
|
||||
{
|
||||
id: '678',
|
||||
permission: {
|
||||
name: 'test.permission.6',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-1',
|
||||
@@ -457,14 +473,16 @@ describe('createRouter', () => {
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{ id: '123', result: AuthorizeResult.DENY },
|
||||
{ id: '234', result: AuthorizeResult.DENY },
|
||||
{ id: '345', result: AuthorizeResult.ALLOW },
|
||||
{ id: '456', result: AuthorizeResult.ALLOW },
|
||||
{ id: '567', result: AuthorizeResult.ALLOW },
|
||||
{ id: '678', result: AuthorizeResult.DENY },
|
||||
]);
|
||||
expect(response.body).toEqual({
|
||||
items: [
|
||||
{ id: '123', result: AuthorizeResult.DENY },
|
||||
{ id: '234', result: AuthorizeResult.DENY },
|
||||
{ id: '345', result: AuthorizeResult.ALLOW },
|
||||
{ id: '456', result: AuthorizeResult.ALLOW },
|
||||
{ id: '567', result: AuthorizeResult.ALLOW },
|
||||
{ id: '678', result: AuthorizeResult.DENY },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves conditional results without resourceRefs unchanged', async () => {
|
||||
@@ -494,43 +512,45 @@ describe('createRouter', () => {
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:1',
|
||||
},
|
||||
resourceRef: 'resource:1',
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:2',
|
||||
},
|
||||
resourceRef: 'resource:2',
|
||||
},
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
resourceRef: 'resource:3',
|
||||
},
|
||||
resourceRef: 'resource:3',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-1',
|
||||
@@ -559,18 +579,20 @@ describe('createRouter', () => {
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{ id: '123', result: AuthorizeResult.ALLOW },
|
||||
{ id: '234', result: AuthorizeResult.ALLOW },
|
||||
{ id: '345', result: AuthorizeResult.ALLOW },
|
||||
{
|
||||
id: '456',
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
pluginId: 'plugin-1',
|
||||
resourceType: 'test-resource-1',
|
||||
conditions: { rule: 'test-rule', params: ['abc'] },
|
||||
},
|
||||
]);
|
||||
expect(response.body).toEqual({
|
||||
items: [
|
||||
{ id: '123', result: AuthorizeResult.ALLOW },
|
||||
{ id: '234', result: AuthorizeResult.ALLOW },
|
||||
{ id: '345', result: AuthorizeResult.ALLOW },
|
||||
{
|
||||
id: '456',
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
pluginId: 'plugin-1',
|
||||
resourceType: 'test-resource-1',
|
||||
conditions: { rule: 'test-rule', params: ['abc'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[ApplyConditionsResponseEntry['result'], string]>([
|
||||
@@ -600,26 +622,28 @@ describe('createRouter', () => {
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'test/resource',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'test/resource',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
resourceRef: 'test/resource',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
{
|
||||
id: '234',
|
||||
resourceRef: 'test/resource',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'test-plugin',
|
||||
@@ -641,16 +665,18 @@ describe('createRouter', () => {
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{
|
||||
id: '123',
|
||||
result,
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
result,
|
||||
},
|
||||
]);
|
||||
expect(response.body).toEqual({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
result,
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
result,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -660,9 +686,14 @@ describe('createRouter', () => {
|
||||
'',
|
||||
{},
|
||||
[{ permission: { name: 'test.permission', attributes: {} } }],
|
||||
[{ id: '123' }],
|
||||
[{ id: '123', permission: { name: 'test.permission' } }],
|
||||
[{ id: '123', permission: { attributes: { invalid: 'attribute' } } }],
|
||||
{ items: [{ permission: { name: 'test.permission', attributes: {} } }] },
|
||||
{ items: [{ id: '123' }] },
|
||||
{ items: [{ id: '123', permission: { name: 'test.permission' } }] },
|
||||
{
|
||||
items: [
|
||||
{ id: '123', permission: { attributes: { invalid: 'attribute' } } },
|
||||
],
|
||||
},
|
||||
])('returns a 400 error for invalid request %#', async requestBody => {
|
||||
const response = await request(app).post('/authorize').send(requestBody);
|
||||
|
||||
@@ -686,16 +717,18 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.send([
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(500);
|
||||
expect(response.body).toEqual(
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
AuthorizeResponse,
|
||||
AuthorizeRequest,
|
||||
Identified,
|
||||
AuthorizeRequestEnvelope,
|
||||
AuthorizeResponseEnvelope,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
ApplyConditionsRequestEntry,
|
||||
@@ -42,26 +44,28 @@ import { PermissionIntegrationClient } from './PermissionIntegrationClient';
|
||||
import { memoize } from 'lodash';
|
||||
import DataLoader from 'dataloader';
|
||||
|
||||
const requestSchema: z.ZodSchema<Identified<AuthorizeRequest>[]> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
resourceRef: z.string().optional(),
|
||||
permission: z.object({
|
||||
name: z.string(),
|
||||
resourceType: z.string().optional(),
|
||||
attributes: z.object({
|
||||
action: z
|
||||
.union([
|
||||
z.literal('create'),
|
||||
z.literal('read'),
|
||||
z.literal('update'),
|
||||
z.literal('delete'),
|
||||
])
|
||||
.optional(),
|
||||
const requestSchema: z.ZodSchema<AuthorizeRequestEnvelope> = z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
resourceRef: z.string().optional(),
|
||||
permission: z.object({
|
||||
name: z.string(),
|
||||
resourceType: z.string().optional(),
|
||||
attributes: z.object({
|
||||
action: z
|
||||
.union([
|
||||
z.literal('create'),
|
||||
z.literal('read'),
|
||||
z.literal('update'),
|
||||
z.literal('delete'),
|
||||
])
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Options required when constructing a new {@link express#Router} using
|
||||
@@ -150,8 +154,8 @@ export async function createRouter(
|
||||
router.post(
|
||||
'/authorize',
|
||||
async (
|
||||
req: Request<Identified<AuthorizeRequest>[]>,
|
||||
res: Response<Identified<AuthorizeResponse>[]>,
|
||||
req: Request<AuthorizeRequestEnvelope>,
|
||||
res: Response<AuthorizeResponseEnvelope>,
|
||||
) => {
|
||||
const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
const user = token ? await identity.authenticate(token) : undefined;
|
||||
@@ -164,15 +168,15 @@ export async function createRouter(
|
||||
|
||||
const body = parseResult.data;
|
||||
|
||||
res.json(
|
||||
await handleRequest(
|
||||
body,
|
||||
res.json({
|
||||
items: await handleRequest(
|
||||
body.items,
|
||||
user,
|
||||
policy,
|
||||
permissionIntegrationClient,
|
||||
req.header('authorization'),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ export type AuthorizeRequest = {
|
||||
resourceRef?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthorizeRequestEnvelope = {
|
||||
items: Identified<AuthorizeRequest>[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthorizeRequestOptions = {
|
||||
token?: string;
|
||||
@@ -26,6 +31,11 @@ export type AuthorizeResponse =
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthorizeResponseEnvelope = {
|
||||
items: Identified<AuthorizeResponse>[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export enum AuthorizeResult {
|
||||
ALLOW = 'ALLOW',
|
||||
|
||||
@@ -54,12 +54,14 @@ describe('PermissionClient', () => {
|
||||
|
||||
describe('authorize', () => {
|
||||
const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => {
|
||||
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
}));
|
||||
const responses = req.body.items.map(
|
||||
(a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
}),
|
||||
);
|
||||
|
||||
return res(json(responses));
|
||||
return res(json({ items: responses }));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -79,12 +81,15 @@ describe('PermissionClient', () => {
|
||||
await client.authorize([mockAuthorizeRequest]);
|
||||
|
||||
const request = mockAuthorizeHandler.mock.calls[0][0];
|
||||
expect(request.body[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
permission: mockPermission,
|
||||
resourceRef: 'foo',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(request.body).toEqual({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
permission: mockPermission,
|
||||
resourceRef: 'foo',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the response from the fetch request', async () => {
|
||||
@@ -122,7 +127,11 @@ describe('PermissionClient', () => {
|
||||
it('should reject responses with missing ids', async () => {
|
||||
mockAuthorizeHandler.mockImplementationOnce(
|
||||
(_req, res, { json }: RestContext) => {
|
||||
return res(json([{ id: 'wrong-id', result: AuthorizeResult.ALLOW }]));
|
||||
return res(
|
||||
json({
|
||||
items: [{ id: 'wrong-id', result: AuthorizeResult.ALLOW }],
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
@@ -133,12 +142,14 @@ describe('PermissionClient', () => {
|
||||
it('should reject invalid responses', async () => {
|
||||
mockAuthorizeHandler.mockImplementationOnce(
|
||||
(req, res, { json }: RestContext) => {
|
||||
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
outcome: AuthorizeResult.ALLOW,
|
||||
}));
|
||||
const responses = req.body.items.map(
|
||||
(a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
outcome: AuthorizeResult.ALLOW,
|
||||
}),
|
||||
);
|
||||
|
||||
return res(json(responses));
|
||||
return res(json({ items: responses }));
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
@@ -151,10 +162,10 @@ describe('PermissionClient', () => {
|
||||
(req, res, { json }: RestContext) => {
|
||||
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
outcome: AuthorizeResult.DENY,
|
||||
result: AuthorizeResult.DENY,
|
||||
}));
|
||||
|
||||
return res(json(responses));
|
||||
return res(json({ items: responses }));
|
||||
},
|
||||
);
|
||||
const disabled = new PermissionClient({
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
Identified,
|
||||
PermissionCriteria,
|
||||
PermissionCondition,
|
||||
AuthorizeResponseEnvelope,
|
||||
AuthorizeRequestEnvelope,
|
||||
} from './types/api';
|
||||
import { DiscoveryApi } from './types/discovery';
|
||||
import {
|
||||
@@ -46,22 +48,24 @@ const permissionCriteriaSchema: z.ZodSchema<
|
||||
.or(z.object({ not: permissionCriteriaSchema })),
|
||||
);
|
||||
|
||||
const responseSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
result: z
|
||||
.literal(AuthorizeResult.ALLOW)
|
||||
.or(z.literal(AuthorizeResult.DENY)),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
const responseSchema = z.object({
|
||||
items: z.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
result: z.literal(AuthorizeResult.CONDITIONAL),
|
||||
conditions: permissionCriteriaSchema,
|
||||
}),
|
||||
),
|
||||
);
|
||||
result: z
|
||||
.literal(AuthorizeResult.ALLOW)
|
||||
.or(z.literal(AuthorizeResult.DENY)),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
result: z.literal(AuthorizeResult.CONDITIONAL),
|
||||
conditions: permissionCriteriaSchema,
|
||||
}),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* An isomorphic client for requesting authorization for Backstage permissions.
|
||||
@@ -106,17 +110,17 @@ export class PermissionClient implements PermissionAuthorizer {
|
||||
return requests.map(_ => ({ result: AuthorizeResult.ALLOW }));
|
||||
}
|
||||
|
||||
const identifiedRequests: Identified<AuthorizeRequest>[] = requests.map(
|
||||
request => ({
|
||||
const requestEnvelope: AuthorizeRequestEnvelope = {
|
||||
items: requests.map(request => ({
|
||||
id: uuid.v4(),
|
||||
...request,
|
||||
}),
|
||||
);
|
||||
})),
|
||||
};
|
||||
|
||||
const permissionApi = await this.discovery.getBaseUrl('permission');
|
||||
const response = await fetch(`${permissionApi}/authorize`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(identifiedRequests),
|
||||
body: JSON.stringify(requestEnvelope),
|
||||
headers: {
|
||||
...this.getAuthorizationHeader(options?.token),
|
||||
'content-type': 'application/json',
|
||||
@@ -126,15 +130,15 @@ export class PermissionClient implements PermissionAuthorizer {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
const identifiedResponses = await response.json();
|
||||
this.assertValidResponses(identifiedRequests, identifiedResponses);
|
||||
const responseEnvelope = await response.json();
|
||||
this.assertValidResponses(requestEnvelope, responseEnvelope);
|
||||
|
||||
const responsesById = identifiedResponses.reduce((acc, r) => {
|
||||
const responsesById = responseEnvelope.items.reduce((acc, r) => {
|
||||
acc[r.id] = r;
|
||||
return acc;
|
||||
}, {} as Record<string, Identified<AuthorizeResponse>>);
|
||||
|
||||
return identifiedRequests.map(request => responsesById[request.id]);
|
||||
return requestEnvelope.items.map(request => responsesById[request.id]);
|
||||
}
|
||||
|
||||
private getAuthorizationHeader(token?: string): Record<string, string> {
|
||||
@@ -142,12 +146,14 @@ export class PermissionClient implements PermissionAuthorizer {
|
||||
}
|
||||
|
||||
private assertValidResponses(
|
||||
requests: Identified<AuthorizeRequest>[],
|
||||
requestEnvelope: AuthorizeRequestEnvelope,
|
||||
json: any,
|
||||
): asserts json is Identified<AuthorizeResponse>[] {
|
||||
): asserts json is AuthorizeResponseEnvelope {
|
||||
const authorizedResponses = responseSchema.parse(json);
|
||||
const responseIds = authorizedResponses.map(r => r.id);
|
||||
const hasAllRequestIds = requests.every(r => responseIds.includes(r.id));
|
||||
const responseIds = authorizedResponses.items.map(r => r.id);
|
||||
const hasAllRequestIds = requestEnvelope.items.every(r =>
|
||||
responseIds.includes(r.id),
|
||||
);
|
||||
if (!hasAllRequestIds) {
|
||||
throw new Error(
|
||||
'Unexpected authorization response from permission-backend',
|
||||
|
||||
@@ -43,7 +43,7 @@ export enum AuthorizeResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* An authorization request for {@link PermissionClient#authorize}.
|
||||
* An individual authorization request for {@link PermissionClient#authorize}.
|
||||
* @public
|
||||
*/
|
||||
export type AuthorizeRequest = {
|
||||
@@ -51,6 +51,14 @@ export type AuthorizeRequest = {
|
||||
resourceRef?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A batch of authorization requests from {@link PermissionClient#authorize}.
|
||||
* @public
|
||||
*/
|
||||
export type AuthorizeRequestEnvelope = {
|
||||
items: Identified<AuthorizeRequest>[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A condition returned with a CONDITIONAL authorization response.
|
||||
*
|
||||
@@ -75,7 +83,7 @@ export type PermissionCriteria<TQuery> =
|
||||
| TQuery;
|
||||
|
||||
/**
|
||||
* An authorization response from {@link PermissionClient#authorize}.
|
||||
* An individual authorization response from {@link PermissionClient#authorize}.
|
||||
* @public
|
||||
*/
|
||||
export type AuthorizeResponse =
|
||||
@@ -84,3 +92,11 @@ export type AuthorizeResponse =
|
||||
result: AuthorizeResult.CONDITIONAL;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A batch of authorization responses from {@link PermissionClient#authorize}.
|
||||
* @public
|
||||
*/
|
||||
export type AuthorizeResponseEnvelope = {
|
||||
items: Identified<AuthorizeResponse>[];
|
||||
};
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
export { AuthorizeResult } from './api';
|
||||
export type {
|
||||
AuthorizeRequest,
|
||||
AuthorizeRequestEnvelope,
|
||||
AuthorizeResponse,
|
||||
AuthorizeResponseEnvelope,
|
||||
Identified,
|
||||
PermissionCondition,
|
||||
PermissionCriteria,
|
||||
|
||||
@@ -32,12 +32,12 @@ import { RestContext, rest } from 'msw';
|
||||
|
||||
const server = setupServer();
|
||||
const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => {
|
||||
const responses = req.body.map((r: Identified<AuthorizeRequest>) => ({
|
||||
const responses = req.body.items.map((r: Identified<AuthorizeRequest>) => ({
|
||||
id: r.id,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
}));
|
||||
|
||||
return res(json(responses));
|
||||
return res(json({ items: responses }));
|
||||
});
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discovery: PluginEndpointDiscovery = {
|
||||
|
||||
Reference in New Issue
Block a user