@backstage/plugin-permission-common
Version:
Isomorphic types and client for Backstage permissions and authorization
197 lines (190 loc) • 6.09 kB
JavaScript
;
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