@cerbos/core
Version:
Common types used by the Cerbos client libraries
1,382 lines (1,294 loc) • 37.7 kB
text/typescript
import {
accessLogEntryFromProtobuf,
checkResourcesResponseFromProtobuf,
decisionLogEntryFromProtobuf,
deleteSchemasResponseFromProtobuf,
disablePoliciesResponseFromProtobuf,
enablePoliciesResponseFromProtobuf,
getPoliciesResponseFromProtobuf,
getSchemasResponseFromProtobuf,
healthCheckResponseFromProtobuf,
inspectPoliciesResponseFromProtobuf,
listPoliciesResponseFromProtobuf,
listSchemasResponseFromProtobuf,
planResourcesResponseFromProtobuf,
} from "./convert/fromProtobuf";
import {
addOrUpdatePoliciesRequestToProtobuf,
addOrUpdateSchemasRequestToProtobuf,
checkResourcesRequestToProtobuf,
deleteSchemasRequestToProtobuf,
disablePoliciesRequestToProtobuf,
enablePoliciesRequestToProtobuf,
getPoliciesRequestToProtobuf,
getSchemasRequestToProtobuf,
healthCheckRequestToProtobuf,
inspectPoliciesRequestToProtobuf,
listAccessLogEntriesRequestToProtobuf,
listDecisionLogEntriesRequestToProtobuf,
listPoliciesRequestToProtobuf,
planResourcesRequestToProtobuf,
} from "./convert/toProtobuf";
import { NotOK, ValidationFailed } from "./errors";
import { ListAuditLogEntriesRequest_Kind } from "./protobuf/cerbos/request/v1/request";
import type { _Method, _Request, _Response, _Service } from "./rpcs";
import type {
AccessLogEntry,
AddOrUpdatePoliciesRequest,
AddOrUpdateSchemasRequest,
AuxData,
CheckResourceRequest,
CheckResourcesRequest,
CheckResourcesResponse,
CheckResourcesResult,
DecisionLogEntry,
DeleteSchemasRequest,
DeleteSchemasResponse,
DisablePoliciesRequest,
DisablePoliciesResponse,
EnablePoliciesRequest,
EnablePoliciesResponse,
GetPoliciesRequest,
GetPoliciesResponse,
GetSchemasRequest,
GetSchemasResponse,
HealthCheckRequest,
HealthCheckResponse,
InspectPoliciesRequest,
InspectPoliciesResponse,
IsAllowedRequest,
ListAccessLogEntriesRequest,
ListDecisionLogEntriesRequest,
ListPoliciesRequest,
ListPoliciesResponse,
ListSchemasResponse,
PlanResourcesRequest,
PlanResourcesResponse,
Policy,
Principal,
ReloadStoreRequest,
Schema,
ServerInfo,
ValidationError,
ValidationFailedCallback,
} from "./types/external";
import { Service, ServiceStatus, Status } from "./types/external";
/** @internal */
export class _AbortHandler {
public constructor(public readonly signal: AbortSignal | undefined) {}
public throwIfAborted(): void {
if (this.signal?.aborted) {
throw this.error();
}
}
public onAbort(listener: (error: NotOK) => void): void {
this.signal?.addEventListener(
"abort",
() => {
listener(this.error());
},
{ once: true },
);
}
public error(): NotOK {
const reason = this.signal?.reason as unknown;
return new NotOK(
Status.CANCELLED,
reason instanceof Error ? `Aborted: ${reason.message}` : "Aborted",
{ cause: reason },
);
}
}
/** @internal */
export interface _Transport {
unary<Service extends _Service, Method extends _Method<Service, "unary">>(
service: Service,
method: Method,
request: _Request<Service, "unary", Method>,
headers: Headers,
abortHandler: _AbortHandler,
): Promise<_Response<Service, "unary", Method>>;
serverStream<
Service extends _Service,
Method extends _Method<Service, "serverStream">,
>(
service: Service,
method: Method,
request: _Request<Service, "serverStream", Method>,
headers: Headers,
abortHandler: _AbortHandler,
): AsyncGenerator<
_Response<Service, "serverStream", Method>,
void,
undefined
>;
}
/** @internal */
export type _Instrumenter = (transport: _Transport) => _Transport;
const instrumenters = new Set<_Instrumenter>();
/** @internal */
export function _addInstrumenter(instrumenter: _Instrumenter): void {
instrumenters.add(instrumenter);
}
/** @internal */
export function _removeInstrumenter(instrumenter: _Instrumenter): void {
instrumenters.delete(instrumenter);
}
/**
* HTTP headers from which to construct a {@link https://developer.mozilla.org/en-US/docs/Web/API/Headers | Headers} object.
*
* @public
*/
export type HeadersInit = [string, string][] | Record<string, string> | Headers;
/**
* Options for creating a new {@link Client}.
*
* @public
*/
export interface Options {
/**
* Credentials for the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API}.
*
* @defaultValue `undefined`
*/
adminCredentials?: AdminCredentials | undefined;
/**
* Headers to add to every request to the policy decision point.
*
* @remarks
* Headers can be included in the policy decision point's audit logs by setting the `includeMetadataKeys` or `excludeMetadataKeys` fields in the
* `audit` {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit | configuration block}.
*
* The `User-Agent` header is set using {@link Options.userAgent}.
*
* @defaultValue `undefined`
*/
headers?:
| HeadersInit
| (() => HeadersInit | Promise<HeadersInit>)
| undefined;
/**
* Action to take when input fails schema validation.
*
* @remarks
* Possible values are
*
* - `"throw"`, to throw a {@link ValidationFailed} error;
*
* - a {@link ValidationFailedCallback} function; or
*
* - `undefined`, to return the validation errors in the response.
*
* @defaultValue `undefined`
*/
onValidationError?: "throw" | ValidationFailedCallback | undefined;
/**
* Identifier of the playground instance to use when prototyping against the hosted demo policy decision point.
*
* @defaultValue `undefined`
*/
playgroundInstance?: string | undefined;
/**
* Custom user agent to prepend to the built-in value.
*
* @defaultValue `undefined`
*/
userAgent?: string | undefined;
}
/**
* Credentials for the {@link https://docs.cerbos.dev/cerbos/latest/api/admin_api | admin API}.
*
* @public
*/
export interface AdminCredentials {
/**
* Username for authenticating to the admin API.
*/
username: string;
/**
* Password for authenticating to the admin API.
*/
password: string;
}
/**
* Options for sending a request to the policy decision point.
*
* @public
*/
export interface RequestOptions {
/**
* Headers to add to the request.
*
* @remarks
* Headers can be included in the policy decision point's audit logs by setting the `includeMetadataKeys` or `excludeMetadataKeys` fields
* in the `audit` {@link https://docs.cerbos.dev/cerbos/latest/configuration/audit | configuration block}.
*
* The `User-Agent` header is set using {@link Options.userAgent}.
*
* @defaultValue `undefined`
*/
headers?: HeadersInit | undefined;
/**
* A {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | signal} to abort the request.
*
* @defaultValue `undefined`
*/
signal?: AbortSignal | undefined;
}
/**
* Base implementation of a client for interacting with the Cerbos policy decision point server.
*
* @public
*/
export abstract class Client {
/** @internal */
protected constructor(
private readonly transport: _Transport,
private readonly 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 });
* ```
*/
public async addOrUpdatePolicies(
request: AddOrUpdatePoliciesRequest,
options?: RequestOptions,
): Promise<void> {
await this.unary(
"admin",
"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}:
*
* ```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 });
* ```
*/
public async addOrUpdateSchemas(
request: AddOrUpdateSchemasRequest,
options?: RequestOptions,
): Promise<void> {
await this.unary(
"admin",
"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 });
* ```
*/
public async checkHealth(
request: HealthCheckRequest = {},
options?: RequestOptions,
): Promise<HealthCheckResponse> {
try {
return healthCheckResponseFromProtobuf(
await this.unary(
"health",
"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
* ```
*/
public async checkResource(
request: CheckResourceRequest,
options?: RequestOptions,
): Promise<CheckResourcesResult> {
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
* ```
*/
public async checkResources(
request: CheckResourcesRequest,
options?: RequestOptions,
): Promise<CheckResourcesResponse> {
const response = checkResourcesResponseFromProtobuf(
await this.unary(
"cerbos",
"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");
* ```
*/
public async deleteSchema(
id: string,
options?: RequestOptions,
): Promise<boolean> {
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"],
* });
* ```
*/
public async deleteSchemas(
request: DeleteSchemasRequest,
options?: RequestOptions,
): Promise<DeleteSchemasResponse> {
return deleteSchemasResponseFromProtobuf(
await this.unary(
"admin",
"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"],
* });
* ```
*/
public async disablePolicies(
request: DisablePoliciesRequest,
options?: RequestOptions,
): Promise<DisablePoliciesResponse> {
return disablePoliciesResponseFromProtobuf(
await this.unary(
"admin",
"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");
* ```
*/
public async disablePolicy(
id: string,
options?: RequestOptions,
): Promise<boolean> {
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"],
* });
* ```
*/
public async enablePolicies(
request: EnablePoliciesRequest,
options?: RequestOptions,
): Promise<EnablePoliciesResponse> {
return enablePoliciesResponseFromProtobuf(
await this.unary(
"admin",
"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");
* ```
*/
public async enablePolicy(
id: string,
options?: RequestOptions,
): Promise<boolean> {
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");
* ```
*/
public async getAccessLogEntry(
callId: string,
options?: RequestOptions,
): Promise<AccessLogEntry | undefined> {
for await (const entry of this.serverStream(
"admin",
"listAuditLogEntries",
{
kind: ListAuditLogEntriesRequest_Kind.KIND_ACCESS,
filter: { $case: "lookup", lookup: 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");
* ```
*/
public async getDecisionLogEntry(
callId: string,
options?: RequestOptions,
): Promise<DecisionLogEntry | undefined> {
for await (const entry of this.serverStream(
"admin",
"listAuditLogEntries",
{
kind: ListAuditLogEntriesRequest_Kind.KIND_DECISION,
filter: { $case: "lookup", lookup: 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"],
* });
* ```
*/
public async getPolicies(
request: GetPoliciesRequest,
options?: RequestOptions,
): Promise<GetPoliciesResponse> {
return getPoliciesResponseFromProtobuf(
await this.unary(
"admin",
"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");
* ```
*/
public async getPolicy(
id: string,
options?: RequestOptions,
): Promise<Policy | undefined> {
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");
* ```
*/
public async getSchema(
id: string,
options?: RequestOptions,
): Promise<Schema | undefined> {
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"],
* });
* ```
*/
public async getSchemas(
request: GetSchemasRequest,
options?: RequestOptions,
): Promise<GetSchemasResponse> {
return getSchemasResponseFromProtobuf(
await this.unary(
"admin",
"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();
* ```
*/
public async inspectPolicies(
request: InspectPoliciesRequest = {},
options?: RequestOptions,
): Promise<InspectPoliciesResponse> {
return inspectPoliciesResponseFromProtobuf(
await this.unary(
"admin",
"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
* ```
*/
public async isAllowed(
request: IsAllowedRequest,
options?: RequestOptions,
): Promise<boolean> {
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);
* }
* ```
*/
public async *listAccessLogEntries(
request: ListAccessLogEntriesRequest,
options?: RequestOptions,
): AsyncGenerator<AccessLogEntry, void, undefined> {
for await (const entry of this.serverStream(
"admin",
"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);
* }
* ```
*/
public async *listDecisionLogEntries(
request: ListDecisionLogEntriesRequest,
options?: RequestOptions,
): AsyncGenerator<DecisionLogEntry, void, undefined> {
for await (const entry of this.serverStream(
"admin",
"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();
* ```
*/
public async listPolicies(
request: ListPoliciesRequest = {},
options?: RequestOptions,
): Promise<ListPoliciesResponse> {
return listPoliciesResponseFromProtobuf(
await this.unary(
"admin",
"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();
* ```
*/
public async listSchemas(
options?: RequestOptions,
): Promise<ListSchemasResponse> {
return 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",
* });
* ```
*/
public async planResources(
request: PlanResourcesRequest,
options?: RequestOptions,
): Promise<PlanResourcesResponse> {
const response = planResourcesResponseFromProtobuf(
await this.unary(
"cerbos",
"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 });
* ```
*/
public async reloadStore(
request: ReloadStoreRequest,
options?: RequestOptions,
): Promise<void> {
await this.unary("admin", "reloadStore", request, options);
}
/**
* Retrieve information about the Cerbos policy decision point server.
*/
public async serverInfo(options?: RequestOptions): Promise<ServerInfo> {
return await this.unary("cerbos", "serverInfo", {}, options);
}
/**
* Create a client instance with a pre-specified principal.
*/
public withPrincipal(
principal: Principal,
auxData: Pick<AuxData, "jwt"> = {},
): ClientWithPrincipal<this> {
return new ClientWithPrincipal(this, principal, auxData);
}
private async unary<
Service extends _Service,
Method extends _Method<Service, "unary">,
>(
service: Service,
method: Method,
request: _Request<Service, "unary", Method>,
{ headers, signal }: RequestOptions = {},
): Promise<_Response<Service, "unary", Method>> {
return await this.transport.unary(
service,
method,
request,
await this.mergeHeaders(headers, service),
new _AbortHandler(signal),
);
}
private async *serverStream<
Service extends _Service,
Method extends _Method<Service, "serverStream">,
>(
service: Service,
method: Method,
request: _Request<Service, "serverStream", Method>,
{ headers, signal }: RequestOptions = {},
): AsyncGenerator<
_Response<Service, "serverStream", Method>,
void,
undefined
> {
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();
}
}
private async mergeHeaders(
override: HeadersInit | undefined,
service: _Service,
): Promise<Headers> {
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;
}
private handleValidationErrors({
validationErrors,
}: {
validationErrors: ValidationError[];
}): void {
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.
*
* @public
*/
export class ClientWithPrincipal<ClientType extends Client = Client> {
/** @internal */
public constructor(
/**
* The client from which this instance was created.
*/
public readonly client: ClientType,
/**
* The principal for whom this instance was created.
*/
public readonly principal: Principal,
/**
* Auxiliary data related to the principal for whom this instance was created.
*
* @defaultValue `{}`
*/
public readonly auxData: Pick<AuxData, "jwt"> = {},
) {}
/**
* Check the principal's permissions on a resource.
* See {@link Client.checkResource} for details.
*/
public async checkResource(
request: Omit<CheckResourceRequest, "principal">,
options?: RequestOptions,
): Promise<CheckResourcesResult> {
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.
*/
public async checkResources(
request: Omit<CheckResourcesRequest, "principal">,
options?: RequestOptions,
): Promise<CheckResourcesResponse> {
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.
*/
public async isAllowed(
request: Omit<IsAllowedRequest, "principal">,
options?: RequestOptions,
): Promise<boolean> {
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.
*/
public async planResources(
request: Omit<PlanResourcesRequest, "principal">,
options?: RequestOptions,
): Promise<PlanResourcesResponse> {
return await this.client.planResources(this.merge(request), options);
}
private merge<
Request extends { principal: Principal; auxData?: AuxData | undefined },
>({ auxData = {}, ...rest }: Omit<Request, "principal">): Request {
return {
principal: this.principal,
auxData: {
...this.auxData,
...auxData,
},
...rest,
} as Request;
}
}