permission-node: wrap request and response arrays in object
Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
+118
-99
@@ -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,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user