Roles overview
What a role is in duck-iam, the IRole and IPermission shapes, how roles become a synthetic ABAC policy, and which roles page answers which question
A role is a named bag of action/resource permissions with optional inheritance and an optional scope. duck-iam has no separate RBAC engine: rolesToPolicy() converts every role into ABAC rules that run through the same pipeline as hand-written policies, so roles and policies compose. This page is the map of the roles section and the reference for the two data shapes every other page builds on.
What a role is
import { defineRole } from '@gentleduck/iam'
const viewer = defineRole('viewer')
.name('Viewer')
.desc('Read-only access to published content')
.grant('read', 'post')
.grant('read', 'comment')
.build()
defineRole(id) returns a RoleBuilder. .build() validates and returns a plain AccessControl.IRole - a JSON record with no methods, no engine reference, and nothing to serialise around. Store it with engine.admin.saveRole(role) or write it straight into an adapter.
When to use / When not to use
Use a role when the answer to "may this subject do this?" is a property of who the subject is. Use a policy when it is a property of the request: time, network, feature flag, or a relationship between two attributes that no role can encode.
- Role - "editors may update posts", "auditors may read everything", "org admins manage billing in their org".
- Role with a condition - "authors may update posts they own". The ownership check belongs to the role's meaning, so it goes on the role via
grantWhen(). - Policy - "nobody writes during maintenance mode", "deny requests from untrusted IPs", "block this specific subject". These span roles and have their own lifecycle.
Roles are additive by construction: the generated policy uses allow-overrides, so adding a role can only grant more. Nothing in the role model can take a permission away. To subtract, write a deny rule in a separate policy - see combining algorithms and cross-policy combining.
The two shapes
Copied from src/core/types/access-control.ts:
interface AccessControl.IRole<
TAction extends string = string,
TResource extends string = string,
TId extends string = string,
TScope extends string = string,
> {
readonly id: TId
readonly name: string
readonly description?: string
readonly permissions: readonly IPermission<TAction, TResource, TScope>[]
/** Parent role IDs to inherit permissions from (resolved recursively). */
readonly inherits?: readonly string[]
/** Default scope applied to all permissions in this role. */
readonly scope?: TScope
readonly metadata?: Readonly<IamPrimitives.Attributes>
}
interface AccessControl.IPermission<
TAction extends string = string,
TResource extends string = string,
TScope extends string = string,
> {
readonly action: TAction | '*'
readonly resource: TResource | '*'
readonly scope?: TScope | '*'
readonly conditions?: IConditionGroup
}
| Field | Type | Default | Meaning |
|---|---|---|---|
id | string | none, required | Unique role ID. Referenced by inherits, by assignments, and by the subject.roles contains "<id>" condition on every generated rule. |
name | string | the id | Display name. Also the prefix of each generated rule's description. |
description | string | undefined | undefined | Documentation only; never read during evaluation. |
permissions | IPermission[] | [] | The grants. An empty list is valid and grants nothing. |
inherits | string[] | undefined | undefined | Parent role IDs. undefined rather than [] when no parents were declared. |
scope | string | undefined | undefined | Default scope for every permission in the role. |
metadata | Attributes | undefined | undefined | Arbitrary bookkeeping; never read during evaluation. |
IamPrimitives.Attributes is Record<string, AttributeValue>, where AttributeValue is a scalar, an array of scalars, or a flat record of scalars. See primitives and types and namespaces.
How a role reaches a decision
Roles never bypass the policy engine. The diagram traces one engine.can() call from stored role rows to a decision.
BandCare the conversion documented on the rolesToPolicy conversion. Every permission becomes one allow rule gated bysubject.roles contains "<roleId>".Ecloses the assigned roles overinherits, which is what inheritance covers.Hmerges scoped assignments whose scope matches the request; see scoped roles.IandJare the shared pipeline described in evaluation and cross-policy combining. A deny from any applicable policy still wins over every allow__rbac__produced.
Both the role list and the converted __rbac__ policy are cached in the engine and rebuilt when a role changes. See engine caching.
Which page do I need
| Page | Covers |
|---|---|
| Defining roles | Every RoleBuilder method with its signature, what build() validates and throws, metadata, empty roles |
| Role inheritance | inherits(), multi-parent and diamond graphs, cycle handling, the depth-32 traversal bound and where getEffectiveRoles() and can() diverge past it |
| Type-safe roles | createIam(), how TAction / TResource / TRole / TScope / TContext flow, per-resource attribute narrowing |
| Scoped roles | Role-level scope, permission-level scope, scoped assignments, and how a scope is matched at request time |
| Conditional permissions | grantWhen(), how its conditions merge into the generated rule, and when a standalone policy is the better tool |
| The rolesToPolicy conversion | The conversion algorithm with a real dumped __rbac__ policy, rule id stability, cache invalidation |
Gotchas
rolesToPolicy() only emits allow rules and the generated policy uses allow-overrides. There is no "negative permission" and inherited permissions cannot be removed. Restriction is a job for a deny rule in a separate policy.defineRole('x').build()throws if the role failsvalidateRole(). Structural problems that only appear across a set of roles (duplicate IDs, danglinginherits, cycles, chains past the depth-32 traversal bound) are found byvalidateRoles(roles), which you call yourself. See validation.role.metadatais inert. Nothing in the evaluator reads it; onlypermissions,inherits, andscopeaffect decisions.- Role IDs appear verbatim inside generated rule conditions. Renaming a role means re-saving every role that inherits it and re-issuing every assignment that names it.
See also
- Policies overview - the ABAC half of the model
- Rule matching - how
'*','posts:*', and'dashboard.*'patterns match - Evaluation pipeline - the full request lifecycle
- Engine admin API -
saveRole,assignRole,revokeRole,export/import