@backstage/plugin-permission-common
Version:
Isomorphic types and client for Backstage permissions and authorization
172 lines (169 loc) • 5.16 kB
JavaScript
import { ResponseError } from '@backstage/errors';
import fetch from 'cross-fetch';
import * as uuid from 'uuid';
import { z } from 'zod';
import { AuthorizeResult } from './types/api.esm.js';
import { isResourcePermission } from './permissions/util.esm.js';
const permissionCriteriaSchema = z.lazy(
() => z.object({
rule: z.string(),
resourceType: z.string(),
params: z.record(z.any()).optional()
}).or(z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() })).or(z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() })).or(z.object({ not: permissionCriteriaSchema }))
);
const authorizePermissionResponseSchema = z.object({
result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY))
});
const authorizePermissionResponseBatchSchema = z.object({
result: z.array(
z.union([
z.literal(AuthorizeResult.ALLOW),
z.literal(AuthorizeResult.DENY)
])
)
}).or(authorizePermissionResponseSchema);
const queryPermissionResponseSchema = z.union([
z.object({
result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY))
}),
z.object({
result: z.literal(AuthorizeResult.CONDITIONAL),
pluginId: z.string(),
resourceType: z.string(),
conditions: permissionCriteriaSchema
})
]);
const responseSchema = (itemSchema, ids) => z.object({
items: z.array(
z.intersection(
z.object({
id: 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: 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: AuthorizeResult.ALLOW }));
}
return this.makeRequest(queries, queryPermissionResponseSchema, options);
}
async makeRequest(queries, itemSchema, options) {
const request = {
items: queries.map((query) => ({
id: uuid.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 (isResourcePermission(permission)) {
request[permission.name] ||= {
permission,
resourceRef: [],
id: uuid.v4()
};
if (resourceRef) {
request[permission.name].resourceRef?.push(resourceRef);
}
} else {
request[permission.name] ||= {
permission,
id: uuid.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(`${permissionApi}/authorize`, {
method: "POST",
body: JSON.stringify(request),
headers: {
...this.getAuthorizationHeader(options?.token),
"content-type": "application/json"
}
});
if (!response.ok) {
throw await 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}` } : {};
}
}
export { PermissionClient };
//# sourceMappingURL=PermissionClient.esm.js.map