Skip to main content

Core overview

How duck-iam folds RBAC roles and ABAC policies into a single evaluation pipeline, plus the vocabulary and the map of the core pages

duck-iam is one access-control engine with two authoring models. Roles (RBAC) are concise for "who can do what"; policies (ABAC) express "under which conditions". They are written differently but evaluated identically: role permissions are compiled into a synthetic policy, that policy joins your hand-written ones, and every check walks the same evaluator. This page fixes the vocabulary and routes you to the page that documents each part.

One evaluator, two authoring models

The two models converge before anything is evaluated, so a role grant and a policy rule are the same kind of object by the time a decision is made.

Loading diagram...

rolesToPolicy() turns every permission of every role into one allow rule guarded by a subject.roles contains <roleId> condition, and, when the permission or the role that declared it sets a scope that is neither undefined nor '*', a scope condition. The result is a policy with id: '__rbac__' and algorithm: 'allow-overrides'. That policy is prepended to the policies your adapter returns, and from there nothing in the evaluator knows or cares which grant came from a role.

The vocabulary

Use these words, and these exported type names. Every page in the duck-iam docs uses them the same way.

TermExported typeWhat it is
subjectIamRequest.ISubjectWho is asking. Id, effective roles, scoped roles, attributes.
actionplain stringThe operation, e.g. read, posts:publish. No verbs are built in.
resourceIamRequest.IResourceWhat is being acted on. Type, optional id, attributes.
scopeplain stringMulti-tenant namespace on the request, on a scoped role assignment, on a role, or on a permission.
environmentIamRequest.IEnvironmentRequest-time context: ip, user agent, timestamp, now, custom fields.
requestIamRequest.IAccessRequestThe five above assembled into one object.
roleAccessControl.IRoleNamed set of permissions, optionally inheriting from other roles.
permissionAccessControl.IPermissionOne action + resource (+ optional scope, conditions) inside a role.
policyAccessControl.IPolicyNamed set of rules with a combining algorithm and optional targets.
ruleAccessControl.IRuleOne statement: effect, actions, resources, priority, conditions.
targetIPolicy['targets']Coarse gate on a whole policy: actions, resources, roles.
conditionAccessControl.IConditionOne field / operator / value comparison.
condition groupAccessControl.IConditionGroupall / any / none tree of conditions.
effectAccessControl.Effect'allow' or 'deny'.
combining algorithmAccessControl.CombiningAlgorithmHow rules inside one policy are folded.
cross-policy combineAccessControl.PolicyCombineHow per-policy verdicts are merged.
decisionAccessControl.IDecisionThe result object in development mode.
modeAccessControl.Mode'development' or 'production'; defaults to 'production'.
explain traceExplain.IResultThe read-only debugging trace from engine.explain().

Every one of these is re-exported from @gentleduck/iam and @gentleduck/iam/core.

import type { AccessControl, IamPrimitives, IamRequest } from '@gentleduck/iam'

Which page do I need

The core section splits along the shape of the question you are asking.

Loading diagram...

PageCovers
primitivesEvery exported type in the request, policy, and decision vocabulary, field by field.
evaluation pipelineSubject resolution, scoped-role enrichment, policy-set assembly, per-policy evaluation, the decision.
rule matchingAction and resource patterns, condition groups, field resolution, every operator's edge semantics.
cross-policy combinationNotApplicable semantics, the three policyCombine modes, defaultEffect, defense in depth.
rolesDefining roles, inheritance, scoped roles, conditional permissions, the rolesToPolicy output.
policiesThe policy builder, rules, targets, conditions, nesting, combining algorithms.

Why hybrid

RBAC alone expresses "editors update posts" but not "editors update posts they own, during business hours, from an allowed region". ABAC alone makes every ordinary grant a hand-written rule, which is verbose for the eighty percent case.

duck-iam lets you keep both:

  • Common grants stay roles. Concise, easy to audit, easy to hand to an admin UI through engine.admin.
  • Contextual restrictions stay policies. Time windows, ownership, geo-fencing, feature flags, break-glass.
  • Both contribute to one decision through the configured cross-policy combine, which defaults to strict AND.

When to use / When not to use

Use the core engine directly when you need a decision inside your own code path: a resolver, a job runner, a service-to-service call. Use it through a server integration when the decision guards an HTTP route, so subject and scope extraction is done for you.

Do not reach for a policy when a role would do. A policy exists to say something a role cannot: a condition on request context, a deny, or a restriction that must apply regardless of who is asking. A policy set of three to five well-named concerns is far easier to audit than twenty overlapping ones.

Do not use engine.explain() on a hot path. It is development-mode only, allocates a full trace, and throws when the engine is in production mode.

Gotchas

Are roles just shorthand for policies?

At evaluation time, yes: rolesToPolicy() materialises them into the __rbac__ policy so roles and ABAC rules run through the same evaluator. You still model them separately, because roles are the better authoring surface for ordinary grants and policies are the better surface for contextual logic.

The __rbac__ policy is omitted when it has no rules

The engine merges [__rbac__, ...adapterPolicies] only when the generated policy has at least one rule. Roles that exist but grant nothing produce an empty rule list, and the policy is dropped rather than joining the combine as a silent participant.

What happens when nothing matches?

defaultEffect decides, and it is 'deny' unless you change it. A policy whose targets do not match is skipped entirely rather than folded in as a default vote - see NotApplicable semantics. Setting defaultEffect: 'allow' additionally requires allowFailOpen: true; the engine constructor throws without it and logs a startup warning even with it.

The cross-policy combine is configurable

Since 2.0.0 the engine takes policyCombine ('and' by default, plus 'allow-overrides' and 'first-applicable'). Older documentation described the AND as fixed engine behaviour; it is not. See cross-policy combination.

scope, environment, or resource attribute?

Put a tenant identifier in scope when it should activate scoped role assignments and scope-restricted permissions - that is the only one of the three the subject resolver reads. Put it in environment or resource.attributes when it is only extra context for conditions and must not change which roles a subject holds.

See also