permission-node: wrap request and response arrays in object

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2022-01-06 12:18:01 +00:00
parent ef291ff985
commit 1fb2e0e0b4
5 changed files with 196 additions and 151 deletions
@@ -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 },
],
}),
);
},
);
@@ -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<ApplyConditionsResponse> {
): Promise<ApplyConditionsResponseEntry[]> {
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;
}
}
+6 -2
View File
@@ -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<DefinitivePolicyDecision>;
@@ -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')
@@ -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<DefinitivePolicyDecision>;
*
* @public
*/
export type ApplyConditionsResponse = ApplyConditionsResponseEntry[];
export type ApplyConditionsResponse = {
items: ApplyConditionsResponseEntry[];
};
const applyConditions = <TResource>(
criteria: PermissionCriteria<PermissionCondition>,
@@ -165,7 +171,9 @@ export const createPermissionIntegrationRouter = <TResource>(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 = <TResource>(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 = <TResource>(options: {
return acc;
}, {} as Record<string, TResource | undefined>);
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 = <TResource>(options: {
? AuthorizeResult.ALLOW
: AuthorizeResult.DENY,
})),
);
});
},
);