UNPKG

@backstage/plugin-permission-common

Version:

Isomorphic types and client for Backstage permissions and authorization

197 lines (190 loc) • 6.09 kB
'use strict'; var errors = require('@backstage/errors'); var fetch = require('cross-fetch'); var uuid = require('uuid'); var zod = require('zod'); var api = require('./types/api.cjs.js'); var util = require('./permissions/util.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } function _interopNamespaceCompat(e) { if (e && typeof e === 'object' && 'default' in e) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch); var uuid__namespace = /*#__PURE__*/_interopNamespaceCompat(uuid); const permissionCriteriaSchema = zod.z.lazy( () => zod.z.object({ rule: zod.z.string(), resourceType: zod.z.string(), params: zod.z.record(zod.z.any()).optional() }).or(zod.z.object({ anyOf: zod.z.array(permissionCriteriaSchema).nonempty() })).or(zod.z.object({ allOf: zod.z.array(permissionCriteriaSchema).nonempty() })).or(zod.z.object({ not: permissionCriteriaSchema })) ); const authorizePermissionResponseSchema = zod.z.object({ result: zod.z.literal(api.AuthorizeResult.ALLOW).or(zod.z.literal(api.AuthorizeResult.DENY)) }); const authorizePermissionResponseBatchSchema = zod.z.object({ result: zod.z.array( zod.z.union([ zod.z.literal(api.AuthorizeResult.ALLOW), zod.z.literal(api.AuthorizeResult.DENY) ]) ) }).or(authorizePermissionResponseSchema); const queryPermissionResponseSchema = zod.z.union([ zod.z.object({ result: zod.z.literal(api.AuthorizeResult.ALLOW).or(zod.z.literal(api.AuthorizeResult.DENY)) }), zod.z.object({ result: zod.z.literal(api.AuthorizeResult.CONDITIONAL), pluginId: zod.z.string(), resourceType: zod.z.string(), conditions: permissionCriteriaSchema }) ]); const responseSchema = (itemSchema, ids) => zod.z.object({ items: zod.z.array( zod.z.intersection( zod.z.object({ id: zod.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: api.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: api.AuthorizeResult.ALLOW })); } return this.makeRequest(queries, queryPermissionResponseSchema, options); } async makeRequest(queries, itemSchema, options) { const request = { items: queries.map((query) => ({ id: uuid__namespace.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 (util.isResourcePermission(permission)) { request[permission.name] ||= { permission, resourceRef: [], id: uuid__namespace.v4() }; if (resourceRef) { request[permission.name].resourceRef?.push(resourceRef); } } else { request[permission.name] ||= { permission, id: uuid__namespace.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__default.default(`${permissionApi}/authorize`, { method: "POST", body: JSON.stringify(request), headers: { ...this.getAuthorizationHeader(options?.token), "content-type": "application/json" } }); if (!response.ok) { throw await errors.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}` } : {}; } } exports.PermissionClient = PermissionClient; //# sourceMappingURL=PermissionClient.cjs.js.map