Policy Types
Kerberos.js supports three policy types:
resourcePolicy: selected byresource.kind,resource.policyVersion, andresource.scopeprincipalPolicy: selected byprincipal.id,principal.policyVersion, andprincipal.scoperolePolicy: selected by eachprincipal.roles[], and — like resource policies —resource.policyVersionandresource.scope
You can pass either type on its own or mix them in the same constructor call:
const kerberos = new Kerberos(
[
expenseResourcePolicy,
sallyPrincipalPolicy,
userRolePolicy,
],
[commonRoles]
);ResourcePolicy
ResourcePolicy is the workhorse policy type, selected by resource.kind. Rules are matched by action, then by roles or derivedRoles, and may also use conditions, variables, constants, outputs, versions, and scopes — see the Quick Start for a complete example.
Conflict resolution
Conflicts are resolved per principal role, matching Cerbos: EFFECT_DENY overrides EFFECT_ALLOW within a role, and an EFFECT_ALLOW from any role wins across roles. Rule order never decides the outcome.
This is deliberate anti-lockout behaviour — picking up an extra, less privileged role can never take away access another role grants:
rules: [
{ actions: ['close'], effect: Effect.Allow, roles: ['SUPPORT'] },
{ actions: ['close'], effect: Effect.Deny, roles: ['AUDITOR'] },
];
// principal roles ['SUPPORT', 'AUDITOR'] -> EFFECT_ALLOWA deny that is meant to hold regardless has to cover the role carrying the allow — either with the '*' wildcard or by naming it:
{ actions: ['close'], effect: Effect.Deny, roles: ['*'] } // always denies
{ actions: ['close'], effect: Effect.Deny, roles: ['SUPPORT', 'AUDITOR'] } // denies both rolesDerived roles do not form a dimension of their own: a rule reached through derivedRoles counts for the principal roles listed in that definition's parentRoles.
PrincipalPolicy
PrincipalPolicy follows the Cerbos-style model for principal-specific overrides. It is bound to a single principal and targets resource + action directly instead of roles / derivedRoles.
const sallyPrincipalPolicy = {
principalPolicy: {
principal: 'sally',
version: 'default',
scope: 'acme.corp',
constants: {
restrictedVendor: 'Flux Water Gear',
},
variables: {
isRestrictedVendor: ({ R, C }) => R.attr.vendor === C.restrictedVendor,
},
rules: [
{
resource: 'expense',
actions: [
{
name: 'deny-restricted-vendor-view',
action: 'view',
effect: Effect.Deny,
condition: {
match: ({ V }) => V.isRestrictedVendor,
},
},
{
name: 'allow-delete-override',
action: 'delete',
effect: Effect.Allow,
},
],
},
],
},
};RolePolicy
RolePolicy follows the Cerbos-style role-centric model. It is bound to a single role, targets resource + allowActions, and acts as a narrowing filter over the ResourcePolicy — it never grants on its own. Three consequences worth internalising:
- A role policy cannot allow what the resource policy withholds. The resource policy is always what grants; a role policy only takes away. With no matching
ResourcePolicyat all, nothing is allowed. - Multiple role policies union. A principal may do what any of its roles permits. Holding an extra role can widen access, never narrow it.
- A role with no applicable role policy is unrestricted. If any of the principal's roles has no role policy targeting this resource kind, the filter does not apply at all.
// resourcePolicy `report` allows view + edit + delete for roles: ['*']
rolePolicy READER: allowActions: ['view']
rolePolicy WRITER: allowActions: ['edit']
roles: ['READER'] -> view (filtered to the allowlist)
roles: ['READER', 'WRITER'] -> view, edit (union, not intersection)
roles: ['READER', 'PLAIN'] -> view, edit, delete (PLAIN is unconstrained)const userRolePolicy = {
rolePolicy: {
role: 'USER',
version: 'default',
scope: 'acme.corp',
constants: {
restrictedVendor: 'Flux Water Gear',
},
variables: {
isRestrictedVendor: ({ R, C }) => R.attr.vendor === C.restrictedVendor,
},
rules: [
{
resource: 'expense',
allowActions: ['create'],
},
{
resource: 'expense',
allowActions: ['view'],
condition: {
match: ({ V }) => V.isRestrictedVendor === false,
},
},
],
},
};RolePolicy also supports parentRoles. Within a single role, the child keeps only actions that are also allowed by each locally defined parent role policy (intersection along the inheritance chain — distinct from the union across the principal's roles). Missing parent role policies are treated as external IdP roles and do not impose extra constraints inside Kerberos.
Mixed Policy Evaluation
When mixed policy types are present, Kerberos resolves each action in this order:
- Find the matching
PrincipalPolicyfor the request principal. - If it returns an explicit
EFFECT_ALLOWorEFFECT_DENY, use that result — role policies do not narrow a principal-policy override. - Otherwise, evaluate the matching
ResourcePolicy. Before its rules are matched, the imported derived roles are resolved: condition-backed definitions evaluate synchronously, and relation-backed definitions (therelation:field) resolve through the configuredrelationsresolver (ReBAC) —list-first with parallelcheckfallback, one shared memo per request. The resultingeffectiveDerivedRolesthen participate in rule matching alongside plainroles. Conflicts resolve per principal role:EFFECT_DENYoverridesEFFECT_ALLOWwithin a role, anEFFECT_ALLOWfrom any role wins across roles. - Apply the
RolePolicylayer as a filter on that result: if every principal role is constrained by an applicable role policy, anEFFECT_ALLOWsurvives only when at least one of those roles allowlists the action (union across roles,parentRolesintersection within a role). - If nothing matches, return
EFFECT_DENY.
The decision is computed per action — different actions in the same request may be resolved by different policy layers. Each lookup (principal / role / resource) walks the scope search chain and policyVersion, and checks in-memory policies first, then the optional cache.
Within the role layer: the principal may do what any of its roles allowlists (union). A role with no applicable role policy is unrestricted, which disables the filter entirely. When a role declares
parentRoles, the child keeps only the actions that are also allowed by each locally defined parent role policy (intersection along the chain).
This keeps Kerberos.js aligned with the Cerbos-style principal override model described in the Cerbos principal policies documentation while extending the runtime with role-centric policy evaluation similar to Cerbos role policies.