From 1ffe31cf694ea3ae507937e47da107fb7cda4e29 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 9 Apr 2025 22:26:14 +0200 Subject: [PATCH] permission-backend: accept batched permissions Signed-off-by: Vincenzo Scamporlino --- .../permission-backend/src/service/router.ts | 130 +++++++++++------- 1 file changed, 83 insertions(+), 47 deletions(-) diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index 0759f6df3a..f89a83ed51 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -21,13 +21,12 @@ import { InputError } from '@backstage/errors'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizeResult, - EvaluatePermissionRequest, - EvaluatePermissionRequestBatch, EvaluatePermissionResponse, - EvaluatePermissionResponseBatch, IdentifiedPermissionMessage, isResourcePermission, PermissionAttributes, + PermissionMessageBatch, + PolicyDecision, } from '@backstage/plugin-permission-common'; import { ApplyConditionsRequestEntry, @@ -74,25 +73,30 @@ const resourcePermissionSchema = z.object({ resourceType: z.string(), }); -const evaluatePermissionRequestSchema: z.ZodSchema< - IdentifiedPermissionMessage -> = z.union([ +const evaluatePermissionRequestSchema = z.union([ z.object({ id: z.string(), resourceRef: z.undefined().optional(), + resourceRefs: z.undefined().optional(), permission: basicPermissionSchema, }), z.object({ id: z.string(), resourceRef: z.string().optional(), + resourceRefs: z.undefined().optional(), + permission: resourcePermissionSchema, + }), + z.object({ + id: z.string(), + resourceRef: z.undefined().optional(), + resourceRefs: z.array(z.string()), permission: resourcePermissionSchema, }), ]); -const evaluatePermissionRequestBatchSchema: z.ZodSchema = - z.object({ - items: z.array(evaluatePermissionRequestSchema), - }); +const evaluatePermissionRequestBatchSchema = z.object({ + items: z.array(evaluatePermissionRequestSchema), +}); /** * Options required when constructing a new {@link express#Router} using @@ -112,7 +116,7 @@ export interface RouterOptions { } const handleRequest = async ( - requests: IdentifiedPermissionMessage[], + requests: z.infer['items'], policy: PermissionPolicy, permissionIntegrationClient: PermissionIntegrationClient, credentials: BackstageCredentials< @@ -120,7 +124,11 @@ const handleRequest = async ( >, auth: AuthService, userInfo: UserInfoService, -): Promise[]> => { +): Promise< + IdentifiedPermissionMessage< + EvaluatePermissionResponse | BulkDefinitivePolicyDecision + >[] +> => { const applyConditionsLoaderFor = memoize((pluginId: string) => { return new DataLoader< ApplyConditionsRequestEntry, @@ -150,40 +158,59 @@ const handleRequest = async ( } return Promise.all( - requests.map(({ id, resourceRef, ...request }) => - policy.handle(request, user).then(decision => { - if (decision.result !== AuthorizeResult.CONDITIONAL) { - return { - id, + requests.map(request => + policy + .handle({ permission: request.permission }, user) + .then(async decision => { + if (decision.result !== AuthorizeResult.CONDITIONAL) { + return { + id: request.id, + ...decision, + }; + } + + if (!isResourcePermission(request.permission)) { + throw new Error( + `Conditional decision returned from permission policy for non-resource permission ${request.permission.name}`, + ); + } + + if (decision.resourceType !== request.permission.resourceType) { + throw new Error( + `Invalid resource conditions returned from permission policy for permission ${request.permission.name}`, + ); + } + + if (request.resourceRefs) { + const results = await Promise.all( + request.resourceRefs.map(resourceRef => + applyConditionsLoaderFor(decision.pluginId).load({ + id: request.id, + resourceRef, + ...decision, + }), + ), + ); + + return { + id: request.id, + result: results.map(({ result }) => result), + }; + } + + if (!request.resourceRef) { + return { + id: request.id, + ...decision, + }; + } + + return applyConditionsLoaderFor(decision.pluginId).load({ + id: request.id, + resourceRef: request.resourceRef, ...decision, - }; - } - - if (!isResourcePermission(request.permission)) { - throw new Error( - `Conditional decision returned from permission policy for non-resource permission ${request.permission.name}`, - ); - } - - if (decision.resourceType !== request.permission.resourceType) { - throw new Error( - `Invalid resource conditions returned from permission policy for permission ${request.permission.name}`, - ); - } - - if (!resourceRef) { - return { - id, - ...decision, - }; - } - - return applyConditionsLoaderFor(decision.pluginId).load({ - id, - resourceRef, - ...decision, - }); - }), + }); + }), ), ); }; @@ -226,8 +253,10 @@ export async function createRouter( router.post( '/authorize', async ( - req: Request, - res: Response, + req: Request, + res: Response< + PermissionMessageBatch + >, ) => { const credentials = await httpAuth.credentials(req, { allow: ['user', 'none'], @@ -274,3 +303,10 @@ export async function createRouter( return router; } + +/** + * @internal + */ +type BulkDefinitivePolicyDecision = { + result: Array; +};