Kaizen
Browse modulesAccessaccess/serverClasses

Class: AccessService

Defined in: server/access/access-service.ts:45

The consumer-facing authorization service. Compiles a per-request PermissionSnapshot, evaluates checks through the shared deny-by-default kernel, derives a minimized CapabilitySnapshot for the client, pushes indexable list filters into a Prisma where, and writes leveled decision audit off the hot path.

Extends

Constructors

Constructor

new AccessService(deps: AccessFeatureDeps): AccessService;

Defined in: server/access/base-access-service.ts:36

Parameters

ParameterType
depsAccessFeatureDeps

Returns

AccessService

Inherited from

BaseAccessService.constructor

Properties

deps

protected readonly deps: AccessFeatureDeps;

Defined in: server/access/base-access-service.ts:34

Inherited from

BaseAccessService.deps

Accessors

actorId

Get Signature

get protected actorId(): string | null;

Defined in: server/access/base-access-service.ts:40

Returns

string | null

Inherited from

BaseAccessService.actorId

Methods

authorizedWhere()

authorizedWhere(
   resourceType: string, 
   subject: Subject, 
   action: `${string}.${string}`, 
   options?: {
  context?: AccessContext;
  scope?: GrantScope;
}
): Promise<AuthorizedWhereResult>;

Defined in: server/access/access-service.ts:180

Compile a SQL where for "list resources I can edit". SELECTORS ONLY — the result is honest: complete when SQL fully decides, partial (with a reason and postFilterRequired) when a non-indexable condition means the caller must post-filter with filterAuthorized, and none when no grant applies. Uses the defineResourceAuthz registry to know which selector fields are indexable.

Parameters

ParameterType
resourceTypestring
subjectSubject
action`${string}.${string}`
options?{ context?: AccessContext; scope?: GrantScope; }
options.context?AccessContext
options.scope?GrantScope

Returns

Promise<AuthorizedWhereResult>


can()

can(
   subject: Subject, 
   action: `${string}.${string}`, 
   resource?: object, 
   context?: AccessContext, 
   options?: {
  consistency?: "eventual" | "strong";
  resourceType?: string;
  scope?: GrantScope;
}
): Promise<Decision>;

Defined in: server/access/access-service.ts:51

Resolve can(subject, action, resource?, context?). Deny-by-default, fully explainable. Every call compiles afresh; consistency: "strong" does not change transaction isolation or lock authority.

Parameters

ParameterTypeDescription
subjectSubject-
action`${string}.${string}`-
resource?object-
context?AccessContext-
options?{ consistency?: "eventual" | "strong"; resourceType?: string; scope?: GrantScope; }-
options.consistency?"eventual" | "strong"-
options.resourceType?stringResource type, enforced against any typed grant selector (D).
options.scope?GrantScope-

Returns

Promise<Decision>


canAll()

canAll(
   subject: Subject, 
   checks: readonly {
  action: `${string}.${string}`;
  context?: AccessContext;
  resource?: object;
  resourceType?: string;
}[], 
   options?: {
  scope?: GrantScope;
}
): Promise<Decision[]>;

Defined in: server/access/access-service.ts:106

Batch variant. H2: each check's scope is resolved from ITS OWN context, identically to can() — not just checks[0]'s — so a mixed-scope batch can never let one check's org grants authorize another check in a different scope. Snapshots are still compiled once per distinct scope CHAIN (cached by the joined chain keys), so the common same-scope batch pays for exactly one compile, same as before.

Resolved sequentially, not via Promise.all: compileSnapshot runs inside deps.repos.transaction, which uses TransactionManager's AsyncLocalStorage to nest into any ambient transaction. Firing them concurrently would issue overlapping queries against the same transaction/connection (this is exactly how it breaks inside a test's wrapping transaction, and would do the same inside a consumer's own outer transaction).

Parameters

ParameterType
subjectSubject
checksreadonly { action: `${string}.${string}`; context?: AccessContext; resource?: object; resourceType?: string; }[]
options?{ scope?: GrantScope; }
options.scope?GrantScope

Returns

Promise<Decision[]>


coarsePermissionsForActor()

protected coarsePermissionsForActor(
   actor: {
  id: string;
  type: SubjectType;
}, 
   scope: GrantScope, 
   now?: Date
): Promise<CoarseActorAuthority>;

Defined in: server/access/base-access-service.ts:117

H3: derive an actor's UNCONDITIONALLY-held authority for a target scope — the delegable set for both GrantService.create's CreateGrantGuard and RoleService.createVersion's CreateVersionGuard. Only grants the actor holds with NO selector and NO condition contribute, mirroring hasCoarsePermission: a narrowly-held permission must not be re-delegable (as a grant) or re-addable (to a role) unconstrained. Effective-date validity is honored. Shared here (not duplicated per service) so the two escalation guards can never drift on what "coarsely held" means.

Derived over scopeChain(scope), so a platform admin can delegate inside an organization without mode: "system" (which would skip every escalation guard). The chain is derived here, never accepted as a parameter — see scopeChain.

scopeKeys reports PROVENANCE: which scopes actually backed the actor. That is what GrantService's self-org cap reads instead of a caller-asserted actorOrganizationId, making it server-derived and unspoofable.

Parameters

ParameterType
actor{ id: string; type: SubjectType; }
actor.idstring
actor.typeSubjectType
scopeGrantScope
nowDate

Returns

Promise<CoarseActorAuthority>

Inherited from

BaseAccessService.coarsePermissionsForActor


compileSnapshot()

compileSnapshot(
   subject: Subject, 
   scopes: GrantScope[], 
   _options?: {
  consistency?: "eventual" | "strong";
}
): Promise<PermissionSnapshot>;

Defined in: server/access/access-service.ts:342

Load the subject's (and their groups') active grants → roles → flat permission sets into a PermissionSnapshot. In v1 this ALWAYS compiles fresh from the DB — the versioned snapshot cache and per-subject grant-version counter are deferred to v2 (see docs/rbac/design.md). Because every compile reads the current transaction-visible state, revocation needs no cache invalidation and consistency: "strong" is an accepted NO-OP. The signature (and the consistency option on can/etc.) is preserved so a v2 cache can be reintroduced behind it without touching call sites.

Parameters

ParameterType
subjectSubject
scopesGrantScope[]
_options?{ consistency?: "eventual" | "strong"; }
_options.consistency?"eventual" | "strong"

Returns

Promise<PermissionSnapshot>


filterAuthorized()

filterAuthorized<T extends object>(
   subject: Subject, 
   resources: readonly T[], 
   action: `${string}.${string}`, 
   options?: {
  context?: AccessContext;
  resourceType?: string;
  scope?: GrantScope;
}
): Promise<T[]>;

Defined in: server/access/access-service.ts:147

Filter an in-memory list to the resources the subject is authorized for.

Type Parameters

Type Parameter
T extends object

Parameters

ParameterTypeDescription
subjectSubject-
resourcesreadonly T[]-
action`${string}.${string}`-
options?{ context?: AccessContext; resourceType?: string; scope?: GrantScope; }-
options.context?AccessContext-
options.resourceType?stringResource type, enforced against any typed grant selector (D).
options.scope?GrantScope-

Returns

Promise<T[]>


getCapabilitySnapshot()

getCapabilitySnapshot(
   subject: Subject, 
   actions: readonly `${string}.${string}`[], 
   options?: {
  context?: AccessContext;
  scope?: GrantScope;
}
): Promise<CapabilitySnapshot>;

Defined in: server/access/access-service.ts:286

Compile a snapshot to the minimized, client-safe CapabilitySnapshot. Only coarse global capabilities (unconstrained grants) are exposed — never the raw grant/condition graph. actions narrows the map to the consumer's known permission set.

Parameters

ParameterType
subjectSubject
actionsreadonly `${string}.${string}`[]
options?{ context?: AccessContext; scope?: GrantScope; }
options.context?AccessContext
options.scope?GrantScope

Returns

Promise<CapabilitySnapshot>


grantsForSubject()

grantsForSubject(subject: Subject, scope: GrantScope): Promise<CompiledGrant[]>;

Defined in: server/access/access-service.ts:315

"What roles/grants does subject X hold IN THIS SCOPE?"

Deliberately EXACT — the only read on this service that does NOT walk the scope chain. The chain answers "may this subject act here?"; this answers "does this subject hold something here specifically?", and provisioners depend on the difference. A consumer that grants an org-scoped admin role only when it is absent would, under a unioned read, see the subject's PLATFORM admin grant, conclude the role is present, and silently never create the org-scoped grant. Do not "fix" the inconsistency.

Parameters

ParameterType
subjectSubject
scopeGrantScope

Returns

Promise<CompiledGrant[]>


loadActiveGrants()

protected loadActiveGrants(
   repos: AccessRepositories, 
   subject: Subject, 
   scopes: GrantScope[]
): Promise<{
  conditionLogic: JsonValue;
  createdAt: Date;
  createdBy: string | null;
  deletedAt: Date | null;
  deletedBy: string | null;
  effectiveEnd: Date | null;
  effectiveStart: Date | null;
  grantFingerprint: string;
  id: string;
  organizationId: string | null;
  roleId: string;
  scopeKey: string;
  scopeType: RbacScopeType;
  selector: JsonValue;
  subjectId: string;
  subjectType: RbacSubjectType;
  updatedAt: Date;
  updatedBy: string | null;
}[]>;

Defined in: server/access/base-access-service.ts:70

Security-critical lookup: load a subject's active grants across a set of scopes, expanding org-group membership for non-group subjects. The single source of truth for "which grants apply to this subject right now" — every caller (snapshot compilation, coarse-permission derivation) funnels here so the scopeKey derivation and group-expansion rule can never drift between call sites.

Queried one scope at a time rather than with a single scopeKey IN (…): group expansion is a PER-SCOPE rule, not a property of the subject. It runs only for an organization-scoped lookup AND a non-group subject — a group subject's grants are read directly (it has no "groups" of its own to expand), and platform scope has no org-group notion, so a group's platform-scoped grant must NOT reach its members through an org-scoped chain. Flattening the chain into one query with a unioned groupIds would silently widen exactly that.

Must be called inside a repos.transaction(...) so the membership reads and the grant reads share one transaction.

Parameters

ParameterType
reposAccessRepositories
subjectSubject
scopesGrantScope[]

Returns

Promise<{ conditionLogic: JsonValue; createdAt: Date; createdBy: string | null; deletedAt: Date | null; deletedBy: string | null; effectiveEnd: Date | null; effectiveStart: Date | null; grantFingerprint: string; id: string; organizationId: string | null; roleId: string; scopeKey: string; scopeType: RbacScopeType; selector: JsonValue; subjectId: string; subjectType: RbacSubjectType; updatedAt: Date; updatedBy: string | null; }[]>

Inherited from

BaseAccessService.loadActiveGrants


subjectsForRole()

subjectsForRole(roleId: string): Promise<{
  scopeKey: string;
  subjectId: string;
  subjectType: SubjectType;
}[]>;

Defined in: server/access/access-service.ts:324

"Which subjects hold role R?"

Parameters

ParameterType
roleIdstring

Returns

Promise<{ scopeKey: string; subjectId: string; subjectType: SubjectType; }[]>


transaction()

protected transaction<T>(fn: (deps: AccessFeatureDeps) => Promise<T>): Promise<T>;

Defined in: server/access/base-access-service.ts:44

Type Parameters

Type Parameter
T

Parameters

ParameterType
fn(deps: AccessFeatureDeps) => Promise<T>

Returns

Promise<T>

Inherited from

BaseAccessService.transaction

On this page