@backstage/plugin-permission-common
Version:
Isomorphic types and client for Backstage permissions and authorization
177 lines (171 loc) • 5.53 kB
JavaScript
;
var errors = require('@backstage/errors');
var fetch = require('cross-fetch');
var v3 = require('zod/v3');
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 }; }
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
const permissionCriteriaSchema = v3.z.lazy(
() => v3.z.object({
rule: v3.z.string(),
resourceType: v3.z.string(),
params: v3.z.record(v3.z.any()).optional()
}).or(v3.z.object({ anyOf: v3.z.array(permissionCriteriaSchema).nonempty() })).or(v3.z.object({ allOf: v3.z.array(permissionCriteriaSchema).nonempty() })).or(v3.z.object({ not: permissionCriteriaSchema }))
);
const authorizePermissionResponseSchema = v3.z.object({
result: v3.z.literal(api.AuthorizeResult.ALLOW).or(v3.z.literal(api.AuthorizeResult.DENY))
});
const authorizePermissionResponseBatchSchema = v3.z.object({
result: v3.z.array(
v3.z.union([
v3.z.literal(api.AuthorizeResult.ALLOW),
v3.z.literal(api.AuthorizeResult.DENY)
])
)
}).or(authorizePermissionResponseSchema);
const queryPermissionResponseSchema = v3.z.union([
v3.z.object({
result: v3.z.literal(api.AuthorizeResult.ALLOW).or(v3.z.literal(api.AuthorizeResult.DENY))
}),
v3.z.object({
result: v3.z.literal(api.AuthorizeResult.CONDITIONAL),
pluginId: v3.z.string(),
resourceType: v3.z.string(),
conditions: permissionCriteriaSchema
})
]);
const responseSchema = (itemSchema, ids) => v3.z.object({
items: v3.z.array(
v3.z.intersection(
v3.z.object({
id: v3.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: globalThis.crypto.randomUUID(),
...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: globalThis.crypto.randomUUID()
};
if (resourceRef) {
request[permission.name].resourceRef?.push(resourceRef);
}
} else {
request[permission.name] ||= {
permission,
id: globalThis.crypto.randomUUID()
};
}
}
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