UNPKG

@backstage/plugin-permission-node

Version:

Common permission and authorization utilities for backend plugins

233 lines (227 loc) • 8.46 kB
'use strict'; var express = require('express'); var Router = require('express-promise-router'); var zod = require('zod'); var zodToJsonSchema = require('zod-to-json-schema'); var errors = require('@backstage/errors'); var pluginPermissionCommon = require('@backstage/plugin-permission-common'); var util = require('./util.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var express__default = /*#__PURE__*/_interopDefaultCompat(express); var Router__default = /*#__PURE__*/_interopDefaultCompat(Router); var zodToJsonSchema__default = /*#__PURE__*/_interopDefaultCompat(zodToJsonSchema); const permissionCriteriaSchema = zod.z.lazy( () => zod.z.union([ zod.z.object({ anyOf: zod.z.array(permissionCriteriaSchema).nonempty() }), zod.z.object({ allOf: zod.z.array(permissionCriteriaSchema).nonempty() }), zod.z.object({ not: permissionCriteriaSchema }), zod.z.object({ rule: zod.z.string(), resourceType: zod.z.string(), params: zod.z.record(zod.z.any()).optional() }) ]) ); const applyConditionsRequestSchema = zod.z.object({ items: zod.z.array( zod.z.object({ id: zod.z.string(), resourceRef: zod.z.union([zod.z.string(), zod.z.array(zod.z.string()).nonempty()]), resourceType: zod.z.string(), conditions: permissionCriteriaSchema }) ) }); const applyConditions = (criteria, resource, getRule) => { if (resource === void 0) { return false; } if (util.isAndCriteria(criteria)) { return criteria.allOf.every( (child) => applyConditions(child, resource, getRule) ); } else if (util.isOrCriteria(criteria)) { return criteria.anyOf.some( (child) => applyConditions(child, resource, getRule) ); } else if (util.isNotCriteria(criteria)) { return !applyConditions(criteria.not, resource, getRule); } const rule = getRule(criteria.rule); const result = rule.paramsSchema?.safeParse(criteria.params); if (result && !result.success) { throw new errors.InputError(`Parameters to rule are invalid`, result.error); } return rule.apply(resource, criteria.params ?? {}); }; function authorizeResult(criteria, resource, getRule) { return applyConditions(criteria, resource, getRule) ? pluginPermissionCommon.AuthorizeResult.ALLOW : pluginPermissionCommon.AuthorizeResult.DENY; } function createConditionAuthorizer(rules) { const getRule = "getRuleByName" in rules ? (n) => rules.getRuleByName(n) : util.createGetRule(rules); return (decision, resource) => { if (decision.result === pluginPermissionCommon.AuthorizeResult.CONDITIONAL) { return applyConditions(decision.conditions, resource, getRule); } return decision.result === pluginPermissionCommon.AuthorizeResult.ALLOW; }; } class PermissionIntegrationMetadataStore { #rulesByTypeByName = /* @__PURE__ */ new Map(); #permissionsByName = /* @__PURE__ */ new Map(); #resourcesByType = /* @__PURE__ */ new Map(); #serializedRules = new Array(); getSerializedMetadata() { return { permissions: Array.from(this.#permissionsByName.values()), rules: this.#serializedRules }; } hasResourceType(type) { return this.#resourcesByType.has(type); } async getResources(resourceType, refs) { const resource = this.#resourcesByType.get(resourceType); if (!resource?.getResources) { throw new errors.NotImplementedError( `This plugin does not expose any permission rule or can't evaluate the conditions request for ${resourceType}` ); } const uniqueRefs = Array.from(new Set(refs)); const resources = await resource.getResources(uniqueRefs); return Object.fromEntries( uniqueRefs.map((ref, index) => [ref, resources[index]]) ); } getRuleMapper(resourceType) { return (name) => { const rule = this.#rulesByTypeByName.get(resourceType)?.get(name); if (!rule) { throw new Error( `Permission rule '${name}' does not exist for resource type '${resourceType}'` ); } return rule; }; } addPermissions(permissions) { for (const permission of permissions) { this.#permissionsByName.set(permission.name, permission); } } addPermissionRules(rules) { for (const rule of rules) { const rulesByName = this.#rulesByTypeByName.get(rule.resourceType) ?? /* @__PURE__ */ new Map(); this.#rulesByTypeByName.set(rule.resourceType, rulesByName); if (rulesByName.has(rule.name)) { throw new Error( `Refused to add permission rule for type '${rule.resourceType}' with name '${rule.name}' because it already exists` ); } rulesByName.set(rule.name, rule); this.#serializedRules.push({ name: rule.name, description: rule.description, resourceType: rule.resourceType, paramsSchema: zodToJsonSchema__default.default(rule.paramsSchema ?? zod.z.object({})) }); } } addResourceType(resource) { const { resourceType } = resource; if (this.#resourcesByType.has(resourceType)) { throw new Error( `Refused to add permission resource with type '${resourceType}' because it already exists` ); } this.#resourcesByType.set(resourceType, resource); if (resource.rules) { this.addPermissionRules(resource.rules); } if (resource.permissions) { this.addPermissions(resource.permissions); } } } function createPermissionIntegrationRouter(options) { const store = new PermissionIntegrationMetadataStore(); if (options) { if ("resources" in options) { if ("permissions" in options) { store.addPermissions(options.permissions); } for (const resource of options.resources) { store.addResourceType(resource); } } else if ("resourceType" in options) { store.addResourceType(options); } else { store.addPermissions(options.permissions); } } const router = Router__default.default(); router.use("/.well-known/backstage/permissions/", express__default.default.json()); router.get("/.well-known/backstage/permissions/metadata", (_, res) => { res.json(store.getSerializedMetadata()); }); router.post( "/.well-known/backstage/permissions/apply-conditions", async (req, res) => { const parseResult = applyConditionsRequestSchema.safeParse(req.body); if (!parseResult.success) { throw new errors.InputError(parseResult.error.toString()); } const { items: requests } = parseResult.data; const invalidResourceTypes = requests.filter( (i) => !store.hasResourceType(i.resourceType) ); if (invalidResourceTypes.length) { throw new errors.InputError( `Unexpected resource types: ${invalidResourceTypes.map((i) => i.resourceType).join(", ")}.` ); } const resourcesByType = {}; for (const requestedType of new Set(requests.map((i) => i.resourceType))) { resourcesByType[requestedType] = await store.getResources( requestedType, requests.filter((r) => r.resourceType === requestedType).map((i) => i.resourceRef).flat() ); } res.json({ items: requests.map((request) => ({ id: request.id, result: Array.isArray(request.resourceRef) ? request.resourceRef.map( (resourceRef) => authorizeResult( request.conditions, resourcesByType[request.resourceType][resourceRef], store.getRuleMapper(request.resourceType) ) ) : authorizeResult( request.conditions, resourcesByType[request.resourceType][request.resourceRef], store.getRuleMapper(request.resourceType) ) })) }); } ); return Object.assign(router, { addPermissions(permissions) { store.addPermissions(permissions); }, addPermissionRules(rules) { store.addPermissionRules(rules); }, addResourceType(resource) { store.addResourceType(resource); }, getPermissionRuleset(resourceRef) { return { getRuleByName: store.getRuleMapper(resourceRef.resourceType) }; } }); } exports.createConditionAuthorizer = createConditionAuthorizer; exports.createPermissionIntegrationRouter = createPermissionIntegrationRouter; //# sourceMappingURL=createPermissionIntegrationRouter.cjs.js.map