UNPKG

@cerbos/core

Version:
854 lines 30.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientWithPrincipal = exports.Client = exports._AbortHandler = void 0; exports._addInstrumenter = _addInstrumenter; exports._removeInstrumenter = _removeInstrumenter; const fromProtobuf_1 = require("./convert/fromProtobuf"); const toProtobuf_1 = require("./convert/toProtobuf"); const errors_1 = require("./errors"); const request_1 = require("./protobuf/cerbos/request/v1/request"); const external_1 = require("./types/external"); /** @internal */ class _AbortHandler { signal; constructor(signal) { this.signal = signal; } throwIfAborted() { if (this.signal?.aborted) { throw this.error(); } } onAbort(listener) { this.signal?.addEventListener("abort", () => { listener(this.error()); }, { once: true }); } error() { const reason = this.signal?.reason; return new errors_1.NotOK(external_1.Status.CANCELLED, reason instanceof Error ? `Aborted: ${reason.message}` : "Aborted", { cause: reason }); } } exports._AbortHandler = _AbortHandler; const instrumenters = new Set(); /** @internal */ function _addInstrumenter(instrumenter) { instrumenters.add(instrumenter); } /** @internal */ function _removeInstrumenter(instrumenter) { instrumenters.delete(instrumenter); } /** * Base implementation of a client for interacting with the Cerbos policy decision point server. * * @public */ class Client { transport; options; /** @internal */ constructor(transport, options) { this.transport = transport; this.options = options; for (const instrumenter of instrumenters) { this.transport = instrumenter(this.transport); } } /** * Add policies, or update existing policies. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * @example * Create a policy in code: * * ```typescript * await cerbos.addOrUpdatePolicies({ * policies: [{ * resourcePolicy: { * resource: "document", * version: "1", * rules: [{ * actions: ["*"], * effect: Effect.ALLOW, * roles: ["ADMIN"], * }], * }, * }], * }); * ``` * * @example * Load a policy from a YAML or JSON file with {@link @cerbos/files#readPolicy}: * * ```typescript * import { readPolicy } from "@cerbos/files"; * * await cerbos.addOrUpdatePolicies({ * policies: [await readPolicy("path/to/policy.yaml")], * }); * ``` * * @example * Load policies and schemas from a directory with {@link @cerbos/files#readDirectory}: * * ```typescript * import { readDirectory } from "@cerbos/files"; * * const { policies, schemas } = await readDirectory("path/to/directory"); * * await cerbos.addOrUpdateSchemas({ schemas }); * await cerbos.addOrUpdatePolicies({ policies }); * ``` */ async addOrUpdatePolicies(request, options) { await this.unary("admin", "addOrUpdatePolicy", (0, toProtobuf_1.addOrUpdatePoliciesRequestToProtobuf)(request), options); } /** * Add schemas to be used for validating principal or resource attributes, or update existing schemas. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * @example * Create a schema in code: * * ```typescript * * await cerbos.addOrUpdateSchemas({ * schemas: [{ * id: "document.json", * definition: { * type: "object", * properties: { * owner: { type: "string" } * } * }, * }], * }); * ``` * * @example * Load a schema from a JSON file with {@link @cerbos/files#readSchema}: * * ```typescript * import { readSchema } from "@cerbos/files"; * * await cerbos.addOrUpdateSchemas({ * schemas: [await readSchema("_schemas/path/to/schema.json")], * }); * ``` * * @example * Load policies and schemas from a directory with {@link @cerbos/files#readDirectory}: * * ```typescript * import { readDirectory } from "@cerbos/files"; * * const { policies, schemas } = await readDirectory("path/to/directory"); * * await cerbos.addOrUpdateSchemas({ schemas }); * await cerbos.addOrUpdatePolicies({ policies }); * ``` */ async addOrUpdateSchemas(request, options) { await this.unary("admin", "addOrUpdateSchema", (0, toProtobuf_1.addOrUpdateSchemasRequestToProtobuf)(request), options); } /** * Checks the health of services provided by the policy decision point server. * * @example * ```typescript * const { status } = await cerbos.checkHealth(); * ``` * * @example * ```typescript * const { status } = await cerbos.checkHealth({ service: Service.ADMIN }); * ``` */ async checkHealth(request = {}, options) { try { return (0, fromProtobuf_1.healthCheckResponseFromProtobuf)(await this.unary("health", "check", (0, toProtobuf_1.healthCheckRequestToProtobuf)(request), options)); } catch (error) { if (request.service === external_1.Service.ADMIN && error instanceof errors_1.NotOK && error.code === external_1.Status.NOT_FOUND) { return { status: external_1.ServiceStatus.DISABLED }; } throw error; } } /** * Check a principal's permissions on a resource. * * @example * ```typescript * const decision = await cerbos.checkResource({ * principal: { * id: "user@example.com", * roles: ["USER"], * attr: { tier: "PREMIUM" }, * }, * resource: { * kind: "document", * id: "1", * attr: { owner: "user@example.com" }, * }, * actions: ["view", "edit"], * }); * * decision.isAllowed("view"); // => true * ``` */ async checkResource(request, options) { const { resource, actions, ...rest } = request; const response = await this.checkResources({ resources: [{ resource, actions }], ...rest }, options); const result = response.findResult(resource); if (!result) { throw new Error("No decision returned for resource"); } return result; } /** * Check a principal's permissions on a set of resources. * * @example * ```typescript * const decision = await cerbos.checkResources({ * principal: { * id: "user@example.com", * roles: ["USER"], * attr: { tier: "PREMIUM" }, * }, * resources: [ * { * resource: { * kind: "document", * id: "1", * attr: { owner: "user@example.com" }, * }, * actions: ["view", "edit"], * }, * { * resource: { * kind: "image", * id: "1", * attr: { owner: "user@example.com" }, * }, * actions: ["delete"], * }, * ], * }); * * decision.isAllowed({ * resource: { kind: "document", id: "1" }, * action: "view", * }); // => true * ``` */ async checkResources(request, options) { const response = (0, fromProtobuf_1.checkResourcesResponseFromProtobuf)(await this.unary("cerbos", "checkResources", (0, toProtobuf_1.checkResourcesRequestToProtobuf)(request), options)); this.handleValidationErrors(response); return response; } /** * Delete a schema. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point (PDP) server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * The way this method handles failure depends on the version of the connected PDP server. * When the server is running Cerbos v0.25 or later, it returns `true` if the schema was deleted and `false` if the schema was not found. * With earlier versions of Cerbos, it throws an error if the schema was not found, and returns successfully if the schema was deleted; the returned value should be ignored. * * @example * ```typescript * const deleted = await cerbos.deleteSchema("document.json"); * ``` */ async deleteSchema(id, options) { const { deletedSchemas } = await this.deleteSchemas({ ids: [id] }, options); return deletedSchemas === 1; } /** * Delete multiple schemas. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point (PDP) server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * The way this method handles failure depends on the version of the connected PDP server. * When the server is running Cerbos v0.25 or later, it returns a {@link DeleteSchemasResponse} that includes the number of schemas that were deleted. * With earlier versions of Cerbos, it throws an error if no schemas were found, and returns successfully if at least one schema was deleted; the returned value should be ignored. * * @example * ```typescript * const result = await cerbos.deleteSchemas({ * ids: ["document.json", "image.json"], * }); * ``` */ async deleteSchemas(request, options) { return (0, fromProtobuf_1.deleteSchemasResponseFromProtobuf)(await this.unary("admin", "deleteSchema", (0, toProtobuf_1.deleteSchemasRequestToProtobuf)(request), options)); } /** * Disable multiple policies. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point server to be at least v0.25 and configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * @example * ```typescript * const result = await cerbos.disablePolicies({ * ids: ["resource.document.v1", "resource.image.v1"], * }); * ``` */ async disablePolicies(request, options) { return (0, fromProtobuf_1.disablePoliciesResponseFromProtobuf)(await this.unary("admin", "disablePolicy", (0, toProtobuf_1.disablePoliciesRequestToProtobuf)(request), options)); } /** * Disable a policy. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point server to be at least v0.25 and configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * @example * ```typescript * const disabled = await cerbos.disablePolicy("resource.document.v1"); * ``` */ async disablePolicy(id, options) { const { disabledPolicies } = await this.disablePolicies({ ids: [id] }, options); return disabledPolicies === 1; } /** * Enable multiple policies. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point server to be at least v0.26 and configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * @example * ```typescript * const result = await cerbos.enablePolicies({ * ids: ["resource.document.v1", "resource.image.v1"], * }); * ``` */ async enablePolicies(request, options) { return (0, fromProtobuf_1.enablePoliciesResponseFromProtobuf)(await this.unary("admin", "enablePolicy", (0, toProtobuf_1.enablePoliciesRequestToProtobuf)(request), options)); } /** * Enable a policy. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point server to be at least v0.26 and configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled, and * * - a dynamic {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * @example * ```typescript * const enabled = await cerbos.enablePolicy("resource.document.v1"); * ``` */ async enablePolicy(id, options) { const { enabledPolicies } = await this.enablePolicies({ ids: [id] }, options); return enabledPolicies === 1; } /** * Fetch an access log entry by call ID from the policy decision point server's audit log. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}; and * * - the Cerbos policy decision point server to be configured with * * - the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled * * - the {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit#local | local audit logging backend}, and * * - {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit | access logs} enabled. * * @example * ```typescript * const entry = await cerbos.getAccessLogEntry("01F9VS1N77S83MTSBBX44GYSJ6"); * ``` */ async getAccessLogEntry(callId, options) { for await (const entry of this.serverStream("admin", "listAuditLogEntries", { kind: request_1.ListAuditLogEntriesRequest_Kind.KIND_ACCESS, filter: { $case: "lookup", lookup: callId }, }, options)) { return (0, fromProtobuf_1.accessLogEntryFromProtobuf)(entry); } return undefined; } /** * Fetch a decision log entry by call ID from the policy decision point server's audit log. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}; and * * - the Cerbos policy decision point server to be at least v0.18 and configured with * * - the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled * * - the {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit#local | local audit logging backend}, and * * - {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit | decision logs} enabled. * * @example * ```typescript * const entry = await cerbos.getDecisionLogEntry("01F9VS1N77S83MTSBBX44GYSJ6"); * ``` */ async getDecisionLogEntry(callId, options) { for await (const entry of this.serverStream("admin", "listAuditLogEntries", { kind: request_1.ListAuditLogEntriesRequest_Kind.KIND_DECISION, filter: { $case: "lookup", lookup: callId }, }, options)) { return (0, fromProtobuf_1.decisionLogEntryFromProtobuf)(entry); } return undefined; } /** * Fetch multiple policies by ID. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, and * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled. * * @example * ```typescript * const policies = await cerbos.getPolicies({ * ids: ["resource.document.v1", "resource.image.v1"], * }); * ``` */ async getPolicies(request, options) { return (0, fromProtobuf_1.getPoliciesResponseFromProtobuf)(await this.unary("admin", "getPolicy", (0, toProtobuf_1.getPoliciesRequestToProtobuf)(request), options)); } /** * Fetch a policy by ID. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, and * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled. * * @example * ```typescript * const policy = await cerbos.getPolicy("resource.document.v1"); * ``` */ async getPolicy(id, options) { const { policies: [policy], } = await this.getPolicies({ ids: [id] }, options); return policy; } /** * Fetch a schema by ID. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, and * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled. * * @example * ```typescript * const schema = await cerbos.getSchema("document.json"); * ``` */ async getSchema(id, options) { const { schemas: [schema], } = await this.getSchemas({ ids: [id] }, options); return schema; } /** * Fetch multiple schemas by ID. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, and * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled. * * @example * ```typescript * const schemas = await cerbos.getSchemas({ * ids: ["document.json", "image.json"], * }); * ``` */ async getSchemas(request, options) { return (0, fromProtobuf_1.getSchemasResponseFromProtobuf)(await this.unary("admin", "getSchema", (0, toProtobuf_1.getSchemasRequestToProtobuf)(request), options)); } /** * Inspect policies in the store. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}; and * * - the Cerbos policy decision point server to be at least v0.35 and configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled. * * @example * ```typescript * const { policies } = await cerbos.inspectPolicies(); * ``` */ async inspectPolicies(request = {}, options) { return (0, fromProtobuf_1.inspectPoliciesResponseFromProtobuf)(await this.unary("admin", "inspectPolicies", (0, toProtobuf_1.inspectPoliciesRequestToProtobuf)(request), options)); } /** * Check if a principal is allowed to perform an action on a resource. * * @example * ```typescript * await cerbos.isAllowed({ * principal: { * id: "user@example.com", * roles: ["USER"], * attr: { tier: "PREMIUM" }, * }, * resource: { * kind: "document", * id: "1", * attr: { owner: "user@example.com" }, * }, * action: "view", * }); // => true * ``` */ async isAllowed(request, options) { const { action, ...rest } = request; const result = await this.checkResource({ actions: [action], ...rest }, options); const allowed = result.isAllowed(action); if (allowed === undefined) { throw new Error("No decision returned for action"); } return allowed; } /** * List access log entries from the policy decision point server's audit log. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}; and * * - the Cerbos policy decision point server to be configured with * * - the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled * * - the {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit#local | local audit logging backend}, and * * - {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit | access logs} enabled. * * @example * ```typescript * for await (const entry of cerbos.listAccessLogEntries({ filter: { tail: 5 } })) { * console.log(entry); * } * ``` */ async *listAccessLogEntries(request, options) { for await (const entry of this.serverStream("admin", "listAuditLogEntries", (0, toProtobuf_1.listAccessLogEntriesRequestToProtobuf)(request), options)) { yield (0, fromProtobuf_1.accessLogEntryFromProtobuf)(entry); } } /** * List decision log entries from the policy decision point server's audit log. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}; and * * - the Cerbos policy decision point server to be configured with * * - the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled * * - the {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit#local | local audit logging backend}, and * * - {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit | decision logs} enabled. * * @example * ```typescript * for await (const entry of cerbos.listDecisionLogEntries({ filter: { tail: 5 } })) { * console.log(entry); * } * ``` */ async *listDecisionLogEntries(request, options) { for await (const entry of this.serverStream("admin", "listAuditLogEntries", (0, toProtobuf_1.listDecisionLogEntriesRequestToProtobuf)(request), options)) { yield (0, fromProtobuf_1.decisionLogEntryFromProtobuf)(entry); } } /** * List policies. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, and * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled. * * @example * ```typescript * const { ids } = await cerbos.listPolicies(); * ``` */ async listPolicies(request = {}, options) { return (0, fromProtobuf_1.listPoliciesResponseFromProtobuf)(await this.unary("admin", "listPolicies", (0, toProtobuf_1.listPoliciesRequestToProtobuf)(request), options)); } /** * List schemas. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, and * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API} enabled. * * @example * ```typescript * const { ids } = await cerbos.listSchemas(); * ``` */ async listSchemas(options) { return (0, fromProtobuf_1.listSchemasResponseFromProtobuf)(await this.unary("admin", "listSchemas", {}, options)); } /** * Produce a query plan that can be used to obtain a list of resources on which a principal is allowed to perform a particular action. * * @example * ```typescript * const plan = await cerbos.planResources({ * principal: { * id: "user@example.com", * roles: ["USER"], * attr: { tier: "PREMIUM" }, * }, * resource: { kind: "document" }, * action: "view", * }); * ``` */ async planResources(request, options) { const response = (0, fromProtobuf_1.planResourcesResponseFromProtobuf)(await this.unary("cerbos", "planResources", (0, toProtobuf_1.planResourcesRequestToProtobuf)(request), options)); this.handleValidationErrors(response); return response; } /** * Reload the store. * * @remarks * Requires * * - the client to be configured with {@link Options.adminCredentials}, * * - the Cerbos policy decision point server to be configured with the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API}, and * * - a reloadable {@link https://docs.cerbos.dev/cerbos/latest/configuration/storage | storage backend}. * * @example * ```typescript * await cerbos.reloadStore({ wait: true }); * ``` */ async reloadStore(request, options) { await this.unary("admin", "reloadStore", request, options); } /** * Retrieve information about the Cerbos policy decision point server. */ async serverInfo(options) { return await this.unary("cerbos", "serverInfo", {}, options); } /** * Create a client instance with a pre-specified principal. */ withPrincipal(principal, auxData = {}) { return new ClientWithPrincipal(this, principal, auxData); } async unary(service, method, request, { headers, signal } = {}) { return await this.transport.unary(service, method, request, await this.mergeHeaders(headers, service), new _AbortHandler(signal)); } async *serverStream(service, method, request, { headers, signal } = {}) { const abortController = new AbortController(); signal?.addEventListener("abort", () => { abortController.abort(signal.reason); }, { once: true }); if (signal?.aborted) { abortController.abort(signal.reason); } try { yield* this.transport.serverStream(service, method, request, await this.mergeHeaders(headers, service), new _AbortHandler(abortController.signal)); } finally { abortController.abort(); } } async mergeHeaders(override, service) { const init = this.options.headers; const headers = new Headers(typeof init === "function" ? await init() : init); if (service === "admin" && this.options.adminCredentials) { const { username, password } = this.options.adminCredentials; headers.set("Authorization", `Basic ${btoa(`${username}:${password}`)}`); } if (this.options.playgroundInstance) { headers.set("Playground-Instance", this.options.playgroundInstance); } if (override) { for (const [name, value] of new Headers(override)) { headers.set(name, value); } } return headers; } handleValidationErrors({ validationErrors, }) { const { onValidationError } = this.options; if (onValidationError) { if (validationErrors.length > 0) { if (onValidationError === "throw") { throw new errors_1.ValidationFailed(validationErrors); } onValidationError(validationErrors); } } } } exports.Client = Client; /** * A client instance with a pre-specified principal. * * @public */ class ClientWithPrincipal { client; principal; auxData; /** @internal */ constructor( /** * The client from which this instance was created. */ client, /** * The principal for whom this instance was created. */ principal, /** * Auxiliary data related to the principal for whom this instance was created. * * @defaultValue `{}` */ auxData = {}) { this.client = client; this.principal = principal; this.auxData = auxData; } /** * Check the principal's permissions on a resource. * See {@link Client.checkResource} for details. */ async checkResource(request, options) { return await this.client.checkResource(this.merge(request), options); } /** * Check the principal's permissions on a set of resources. * See {@link Client.checkResources} for details. */ async checkResources(request, options) { return await this.client.checkResources(this.merge(request), options); } /** * Check if the principal is allowed to perform an action on a resource. * See {@link Client.isAllowed} for details. */ async isAllowed(request, options) { return await this.client.isAllowed(this.merge(request), options); } /** * Produce a query plan that can be used to obtain a list of resources on which the principal is allowed to perform a particular action. * See {@link Client.planResources} for details. */ async planResources(request, options) { return await this.client.planResources(this.merge(request), options); } merge({ auxData = {}, ...rest }) { return { principal: this.principal, auxData: { ...this.auxData, ...auxData, }, ...rest, }; } } exports.ClientWithPrincipal = ClientWithPrincipal; //# sourceMappingURL=client.js.map