ReBAC (Relations)
Kerberos supports relationship-based access control (ReBAC) — "Google Drive-style" authorization where access flows through relationships (viewer of the parent folder, member of the team that owns the document) instead of attributes alone. The design is heavily inspired by SpiceDB (the mature open-source implementation of Google's Zanzibar), adapted to the Kerberos philosophy: in-process, zero-infra, static data blazing fast, dynamic data through the same read-only cache fallback used for policies.
It comes in two layers:
- The
relationsengine option — a delegation contract likelogger/cache/codec. ANY object with acheckmethod works, including a resolver backed by the join tables your database already has. - The built-in "Zanzibar-lite" resolver — the
RelationResolverclass from the@alexify/kerberos/relationssubpath (kept out of the main entry so non-ReBAC browser bundles do not grow).
Relation-backed derived roles
A derived-role definition may declare a relation instead of a condition. When the engine resolves derived roles for a resource policy (its only async phase), it asks the configured relations resolver whether the principal holds that relation/permission on the request's resource — and the role activates like any other derived role:
const derivedRoles = {
name: 'doc_roles',
definitions: [
// Classic (condition-backed) definitions still work unchanged:
{ name: 'OWNER', parentRoles: ['USER'], condition: { match: ({ P, R }) => R.attr.ownerId === P.id } },
// Relation-backed: activates when relations.check grants `view`.
// `parentRoles` and `condition` become optional synchronous gates.
{ name: 'DOC_VIEWER', relation: 'view' },
],
};
const policy = {
resourcePolicy: {
version: 'default',
resource: 'document',
importDerivedRoles: ['doc_roles'],
rules: [{ actions: ['view'], effect: 'EFFECT_ALLOW', derivedRoles: ['DOC_VIEWER', 'OWNER'] }],
},
};The delegation contract (bring your own resolver — e.g. SQL joins over your own tables):
const kerberos = new Kerberos([policy], [derivedRoles], {
relations: {
// Required. `memo` is a request-scoped Map shared across a whole
// checkResources batch — use it to share subproblems if you want.
async check({ principal, resource, relation }, { memo }) {
return db.hasRelation(principal.id, resource.kind, resource.id, relation);
},
// Optional batch fast path — called first when present.
async list({ principal, resource, relations }, { memo }) {
return db.grantedRelations(principal.id, resource.kind, resource.id, relations); // Set<string>
},
},
});Resolver failures follow the onError semantics, per-resource isolation in checkResources applies as usual, and with includeMeta every relation resolution is visible in meta.resolution as { source: 'relations', name, relation, matched }.