UNPKG

@cerbos/core

Version:
821 lines 30.2 kB
import { ListAuditLogEntriesRequest_Kind } from "@cerbos/api/cerbos/request/v1/request_pb"; import { CerbosAdminService as admin, CerbosService as cerbos, } from "@cerbos/api/cerbos/svc/v1/svc_pb"; import { Health as health } from "@cerbos/api/grpc/health/v1/health_pb"; import { accessLogEntryFromProtobuf, checkResourcesResponseFromProtobuf, decisionLogEntryFromProtobuf, deleteSchemasResponseFromProtobuf, disablePoliciesResponseFromProtobuf, enablePoliciesResponseFromProtobuf, getPoliciesResponseFromProtobuf, getSchemasResponseFromProtobuf, healthCheckResponseFromProtobuf, inspectPoliciesResponseFromProtobuf, listPoliciesResponseFromProtobuf, listSchemasResponseFromProtobuf, planResourcesResponseFromProtobuf, serverInfoFromProtobuf, } from "./convert/fromProtobuf.js"; import { addOrUpdatePoliciesRequestToProtobuf, addOrUpdateSchemasRequestToProtobuf, checkResourcesRequestToProtobuf, deleteSchemasRequestToProtobuf, disablePoliciesRequestToProtobuf, enablePoliciesRequestToProtobuf, getPoliciesRequestToProtobuf, getSchemasRequestToProtobuf, healthCheckRequestToProtobuf, inspectPoliciesRequestToProtobuf, listAccessLogEntriesRequestToProtobuf, listDecisionLogEntriesRequestToProtobuf, listPoliciesRequestToProtobuf, planResourcesRequestToProtobuf, reloadStoreRequestToProtobuf, } from "./convert/toProtobuf.js"; import { NotOK, ValidationFailed } from "./errors.js"; import { AbortHandler, instrument } from "./transport.js"; import { Service, ServiceStatus, Status } from "./types/external.js"; /** * Base implementation of a client for interacting with the Cerbos policy decision point server. */ export class Client { transport; options; /** @internal */ constructor(transport, options) { this.transport = transport; this.options = options; this.transport = instrument(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 | 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 | 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.method.addOrUpdatePolicy, 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 | 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 | 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.method.addOrUpdateSchema, 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 healthCheckResponseFromProtobuf(await this.unary(health.method.check, healthCheckRequestToProtobuf(request), options)); } catch (error) { if (request.service === Service.ADMIN && error instanceof NotOK && error.code === Status.NOT_FOUND) { return { status: 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 = checkResourcesResponseFromProtobuf(await this.unary(cerbos.method.checkResources, 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 deleteSchemasResponseFromProtobuf(await this.unary(admin.method.deleteSchema, 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 disablePoliciesResponseFromProtobuf(await this.unary(admin.method.disablePolicy, 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 enablePoliciesResponseFromProtobuf(await this.unary(admin.method.enablePolicy, 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.method.listAuditLogEntries, { $typeName: "cerbos.request.v1.ListAuditLogEntriesRequest", kind: ListAuditLogEntriesRequest_Kind.ACCESS, filter: { case: "lookup", value: callId }, }, options)) { return 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.method.listAuditLogEntries, { $typeName: "cerbos.request.v1.ListAuditLogEntriesRequest", kind: ListAuditLogEntriesRequest_Kind.DECISION, filter: { case: "lookup", value: callId }, }, options)) { return 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 getPoliciesResponseFromProtobuf(await this.unary(admin.method.getPolicy, 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 getSchemasResponseFromProtobuf(await this.unary(admin.method.getSchema, 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 inspectPoliciesResponseFromProtobuf(await this.unary(admin.method.inspectPolicies, 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.method.listAuditLogEntries, listAccessLogEntriesRequestToProtobuf(request), options)) { yield 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.method.listAuditLogEntries, listDecisionLogEntriesRequestToProtobuf(request), options)) { yield 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 listPoliciesResponseFromProtobuf(await this.unary(admin.method.listPolicies, 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 listSchemasResponseFromProtobuf(await this.unary(admin.method.listSchemas, { $typeName: "cerbos.request.v1.ListSchemasRequest" }, 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 = planResourcesResponseFromProtobuf(await this.unary(cerbos.method.planResources, 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.method.reloadStore, reloadStoreRequestToProtobuf(request), options); } /** * Retrieve information about the Cerbos policy decision point server. */ async serverInfo(options) { return serverInfoFromProtobuf(await this.unary(cerbos.method.serverInfo, { $typeName: "cerbos.request.v1.ServerInfoRequest" }, options)); } /** * Create a client instance with a pre-specified principal. */ withPrincipal(principal, auxData = {}) { return new ClientWithPrincipal(this, principal, auxData); } /** @internal */ get ["~updateSignal"]() { return undefined; } async unary(method, request, { headers, signal } = {}) { return await this.transport.unary(method, request, await this.mergeHeaders(headers, method), new AbortHandler(signal)); } async *serverStream(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(method, request, await this.mergeHeaders(headers, method), new AbortHandler(abortController.signal)); } finally { abortController.abort(); } } async mergeHeaders(override, method) { const init = this.options.headers; const headers = new Headers(typeof init === "function" ? await init() : init); if (method.parent.typeName === admin.typeName && 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 ValidationFailed(validationErrors); } onValidationError(validationErrors); } } } } /** * A client instance with a pre-specified principal. * * @typeParam ClientType - Specific client instance type. */ export 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, }; } } //# sourceMappingURL=client.js.map