permission: batch apply conditions payload

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2025-04-11 10:34:21 +02:00
parent ec42d827c2
commit 4d4ec58d6b
5 changed files with 202 additions and 61 deletions
@@ -34,9 +34,16 @@ const responseSchema = z.object({
items: z.array(
z.object({
id: z.string(),
result: z
.literal(AuthorizeResult.ALLOW)
.or(z.literal(AuthorizeResult.DENY)),
result: z.union([
z.literal(AuthorizeResult.ALLOW),
z.literal(AuthorizeResult.DENY),
z.array(
z.union([
z.literal(AuthorizeResult.ALLOW),
z.literal(AuthorizeResult.DENY),
]),
),
]),
}),
),
});
@@ -45,6 +52,9 @@ export type ResourcePolicyDecision = ConditionalPolicyDecision & {
resourceRef: string;
};
/**
* @internal
*/
export class PermissionIntegrationClient {
private readonly discovery: DiscoveryService;
private readonly auth: AuthService;
@@ -74,14 +84,7 @@ export class PermissionIntegrationClient {
const response = await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({
items: decisions.map(
({ id, resourceRef, resourceType, conditions }) => ({
id,
resourceRef,
resourceType,
conditions,
}),
),
items: decisions,
}),
headers: {
...(token ? { authorization: `Bearer ${token}` } : {}),
@@ -165,11 +165,7 @@ describe('createRouter', () => {
mockApplyConditions.mockResolvedValueOnce([
{
id: '123',
result: AuthorizeResult.ALLOW,
},
{
id: '123',
result: AuthorizeResult.DENY,
result: [AuthorizeResult.ALLOW, AuthorizeResult.DENY],
},
]);
@@ -198,6 +194,21 @@ describe('createRouter', () => {
],
});
expect(mockApplyConditions).toHaveBeenCalledWith(
'test-plugin',
expect.any(Object),
[
{
conditions: { params: ['abc'], rule: 'test-rule' },
id: '123',
pluginId: 'test-plugin',
resourceRefs: ['resource:1', 'resource:2'],
resourceType: 'test-resource-1',
result: 'CONDITIONAL',
},
],
);
expect(response.status).toEqual(200);
expect(policy.handle).toHaveBeenCalledWith(
@@ -26,7 +26,6 @@ import {
isResourcePermission,
PermissionAttributes,
PermissionMessageBatch,
PolicyDecision,
} from '@backstage/plugin-permission-common';
import {
ApplyConditionsRequestEntry,
@@ -125,9 +124,7 @@ const handleRequest = async (
auth: AuthService,
userInfo: UserInfoService,
): Promise<
IdentifiedPermissionMessage<
EvaluatePermissionResponse | BulkDefinitivePolicyDecision
>[]
IdentifiedPermissionMessage<InternalEvaluatePermissionResponse>[]
> => {
const applyConditionsLoaderFor = memoize((pluginId: string) => {
return new DataLoader<
@@ -182,20 +179,11 @@ const handleRequest = async (
}
if (request.resourceRefs) {
const results = await Promise.all(
request.resourceRefs.map(resourceRef =>
applyConditionsLoaderFor(decision.pluginId).load({
id: request.id,
resourceRef,
...decision,
}),
),
);
return {
return applyConditionsLoaderFor(decision.pluginId).load({
id: request.id,
result: results.map(({ result }) => result),
};
resourceRefs: request.resourceRefs,
...decision,
});
}
if (!request.resourceRef) {
@@ -254,9 +242,7 @@ export async function createRouter(
'/authorize',
async (
req: Request,
res: Response<
PermissionMessageBatch<PolicyDecision | BulkDefinitivePolicyDecision>
>,
res: Response<PermissionMessageBatch<InternalEvaluatePermissionResponse>>,
) => {
const credentials = await httpAuth.credentials(req, {
allow: ['user', 'none'],
@@ -307,6 +293,8 @@ export async function createRouter(
/**
* @internal
*/
type BulkDefinitivePolicyDecision = {
result: Array<AuthorizeResult.ALLOW | AuthorizeResult.DENY>;
};
type InternalEvaluatePermissionResponse =
| EvaluatePermissionResponse
| {
result: Array<AuthorizeResult.ALLOW | AuthorizeResult.DENY>;
};