UNPKG

@backstage/plugin-permission-common

Version:

Isomorphic types and client for Backstage permissions and authorization

172 lines (169 loc) • 5.16 kB
import { ResponseError } from '@backstage/errors'; import fetch from 'cross-fetch'; import * as uuid from 'uuid'; import { z } from 'zod'; import { AuthorizeResult } from './types/api.esm.js'; import { isResourcePermission } from './permissions/util.esm.js'; const permissionCriteriaSchema = z.lazy( () => z.object({ rule: z.string(), resourceType: z.string(), params: z.record(z.any()).optional() }).or(z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() })).or(z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() })).or(z.object({ not: permissionCriteriaSchema })) ); const authorizePermissionResponseSchema = z.object({ result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY)) }); const authorizePermissionResponseBatchSchema = z.object({ result: z.array( z.union([ z.literal(AuthorizeResult.ALLOW), z.literal(AuthorizeResult.DENY) ]) ) }).or(authorizePermissionResponseSchema); const queryPermissionResponseSchema = z.union([ z.object({ result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY)) }), z.object({ result: z.literal(AuthorizeResult.CONDITIONAL), pluginId: z.string(), resourceType: z.string(), conditions: permissionCriteriaSchema }) ]); const responseSchema = (itemSchema, ids) => z.object({ items: z.array( z.intersection( z.object({ id: z.string() }), itemSchema ) ).refine( (items) => items.length === ids.size && items.every(({ id }) => ids.has(id)), { message: "Items in response do not match request" } ) }); class PermissionClient { enabled; discovery; enableBatchedRequests; constructor(options) { this.discovery = options.discovery; this.enabled = options.config.getOptionalBoolean("permission.enabled") ?? false; this.enableBatchedRequests = options.config.getOptionalBoolean( "permission.EXPERIMENTAL_enableBatchedRequests" ) ?? false; } /** * {@inheritdoc PermissionEvaluator.authorize} */ async authorize(requests, options) { if (!this.enabled) { return requests.map((_) => ({ result: AuthorizeResult.ALLOW })); } if (this.enableBatchedRequests) { return this.makeBatchedRequest(requests, options); } return this.makeRequest( requests, authorizePermissionResponseSchema, options ); } /** * {@inheritdoc PermissionEvaluator.authorizeConditional} */ async authorizeConditional(queries, options) { if (!this.enabled) { return queries.map((_) => ({ result: AuthorizeResult.ALLOW })); } return this.makeRequest(queries, queryPermissionResponseSchema, options); } async makeRequest(queries, itemSchema, options) { const request = { items: queries.map((query) => ({ id: uuid.v4(), ...query })) }; const parsedResponse = await this.makeRawRequest( request, itemSchema, options ); const responsesById = parsedResponse.items.reduce((acc, r) => { acc[r.id] = r; return acc; }, {}); return request.items.map((query) => responsesById[query.id]); } async makeBatchedRequest(queries, options) { const request = {}; for (const query of queries) { const { permission, resourceRef } = query; if (isResourcePermission(permission)) { request[permission.name] ||= { permission, resourceRef: [], id: uuid.v4() }; if (resourceRef) { request[permission.name].resourceRef?.push(resourceRef); } } else { request[permission.name] ||= { permission, id: uuid.v4() }; } } const parsedResponse = await this.makeRawRequest( { items: Object.values(request) }, authorizePermissionResponseBatchSchema, options ); const responsesById = parsedResponse.items.reduce((acc, r) => { acc[r.id] = r; return acc; }, {}); return queries.map((query) => { const { id } = request[query.permission.name]; const item = responsesById[id]; if (Array.isArray(item.result)) { return { result: query.resourceRef ? item.result.shift() : item.result[0] }; } return { result: item.result }; }); } async makeRawRequest(request, itemSchema, options) { const permissionApi = await this.discovery.getBaseUrl("permission"); const response = await fetch(`${permissionApi}/authorize`, { method: "POST", body: JSON.stringify(request), headers: { ...this.getAuthorizationHeader(options?.token), "content-type": "application/json" } }); if (!response.ok) { throw await ResponseError.fromResponse(response); } const responseBody = await response.json(); return responseSchema( itemSchema, new Set(request.items.map(({ id }) => id)) ).parse(responseBody); } getAuthorizationHeader(token) { return token ? { Authorization: `Bearer ${token}` } : {}; } } export { PermissionClient }; //# sourceMappingURL=PermissionClient.esm.js.map