@backstage/plugin-permission-node
Version:
Common permission and authorization utilities for backend plugins
233 lines (227 loc) • 8.46 kB
JavaScript
;
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