DocDuck needs more than a viewer. An **editor** writes documents, an **admin** manages teams and users. Rather than repeating permissions, each role inherits the one below it - and then you validate the result before the app serves a request.

## Learning goals

* Build an inheritance chain with `.inherits()` and know how it resolves.
* Use `grantCRUD`, `grantAll`, `grantRead`, and `grantScoped`.
* Read the wildcard matching rules exactly, including what `'*'` does *not* match.
* Know the 32-level inheritance cap and how cycles are handled.
* Validate a role set with `validateRoles` and interpret every issue code.

## The hierarchy

delete document, archive document, manage team, manage user"]
  E["editorcreate document, update document, share document"]
  V["viewerread document, read team"]
  A --> |"inherits"| E
  E --> |"inherits"| V
  CAROL["carol"] -.-> |"assigned"| A
  BOB["bob"] -.-> |"assigned"| E
  ALICE["alice"] -.-> |"assigned"| V`}
/>

`editor` inherits `viewer`, `admin` inherits `editor`. Carol holds only `admin`, but the engine resolves her effective roles to `admin`, `editor`, `viewer` and collects the permissions of all three. Inheritance points *down* the diagram, from `admin` to `editor` to `viewer`; permissions travel back *up* the same edges.

## Building it

**Extend `src/roles.ts`**

```ts title="src/roles.ts"
import { defineRole } from '@gentleduck/iam'

export const viewer = defineRole('viewer')
  .name('Viewer')
  .desc('Read-only access to documents and teams')
  .grant('read', 'document')
  .grant('read', 'team')
  .build()

export const editor = defineRole('editor')
  .name('Editor')
  .desc('Writes documents')
  .inherits('viewer')
  .grant('create', 'document')
  .grant('update', 'document')
  .grant('share', 'document')
  .build()

export const admin = defineRole('admin')
  .name('Administrator')
  .desc('Manages teams and their members')
  .inherits('editor')
  .grant('delete', 'document')
  .grant('archive', 'document')
  .grant('manage', 'team')
  .grant('manage', 'user')
  .meta({ tier: 'staff' })
  .build()

export const roles = [viewer, editor, admin]
```

**Validate at startup**

`validateRoles` is not re-exported from the package root - it lives behind
`@gentleduck/iam/core/validate` so services that never validate do not pay for
the validator chunk.

```ts title="src/roles.ts"
import { validateRoles } from '@gentleduck/iam/core/validate'

const check = validateRoles(roles)
if (!check.valid) {
  throw new Error(check.issues.map((i) => `[${i.code}] ${i.message}`).join('; '))
}
for (const issue of check.issues) {
  if (issue.type === 'warning') console.warn(`[iam] ${issue.code}: ${issue.message}`)
}
```

**Assign the roles**

```ts title="src/access.ts"
import { IamEngine } from '@gentleduck/iam'
import { IamMemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { roles } from './roles'

export const adapter = new IamMemoryAdapter({
  roles,
  assignments: {
    alice: ['viewer'],
    bob: ['editor'],
    carol: ['admin'],
  },
})

export const engine = new IamEngine({ adapter, mode: 'development' })
```

**Check the inherited permissions**

```ts title="src/main.ts"
import { engine } from './access'

const doc = { type: 'document', id: 'doc-1', attributes: {} }

async function main() {
  console.log(await engine.can('carol', 'read', doc))    // true  (from viewer)
  console.log(await engine.can('carol', 'update', doc))  // true  (from editor)
  console.log(await engine.can('carol', 'delete', doc))  // true  (own)
  console.log(await engine.can('bob', 'delete', doc))    // false (editor stops short)

  console.log(await engine.getEffectiveRoles('carol'))
  // [ 'admin', 'editor', 'viewer' ]
}

void main()
```

`engine.getEffectiveRoles(subjectId, scope?)` returns exactly the role list the
engine resolved internally, through the same subject cache. It is the fastest way
to confirm an inheritance chain is wired the way you think it is.

## How inheritance resolves

Two separate walks happen, and they answer different questions.

gated on subject.roles contains admin"]
  end
  EFF --> PERM`}
/>

`resolveEffectiveRoles` fills `subject.roles`. `collectPermissions` runs inside `rolesToPolicy` and emits the rules. Both use a `visited` set, so a cycle - `a` inherits `b`, `b` inherits `a` - is skipped rather than recursed into.

`MAX_INHERITANCE_DEPTH` is 32. Anything past that depth is silently dropped at runtime, which is why `validateRoles` reports `INHERITANCE_TOO_DEEP` as an **error**: a chain that deep would lose permissions without any signal. In practice three to five levels is the readable limit.

### Multiple inheritance

A role can name several parents:

```ts
export const reviewer = defineRole('reviewer')
  .name('Reviewer')
  .inherits('viewer', 'commenter')
  .grant('archive', 'document')
  .build()
```

The reviewer gets everything from both parents plus its own grant. Diamonds are fine: if two parents both inherit `viewer`, the depth memo makes `viewer` contribute once.

A child cannot remove a permission its parent grants. To carve an exception out of an inherited grant, write an ABAC deny policy (chapter 3) - that is the layer designed to subtract.

## The RoleBuilder API

```ts
defineRole('editor')
  .name('Editor')                     // display name; defaults to the role ID
  .desc('Writes documents')           // description, documentation only
  .inherits('viewer')                 // one or more parent role IDs
  .scope('team-acme')                 // role-level scope (chapter 5)
  .meta({ tier: 'staff' })            // arbitrary metadata, never evaluated
  .grant('create', 'document')        // one permission
  .grantScoped('team-acme', 'update', 'document')
  .grantWhen('update', 'document', (w) => w.isOwner())
  .grantAll('comment')                // '*' action
  .grantRead('document', 'team')      // 'read' on several resources
  .grantCRUD('document')              // create + read + update + delete
  .build()
```

| Method | Signature | What it does |
| --- | --- | --- |
| `.name(n)` | `(n: string) => this` | Display name. Defaults to the role ID. |
| `.desc(d)` | `(d: string) => this` | Description. Never consulted at evaluation time. |
| `.inherits(...ids)` | `(...ids: string[]) => this` | Replaces the parent list. Calling it twice does not append. |
| `.scope(s)` | `(s: TScope) => this` | Adds `scope eq s` to every permission of the role. |
| `.meta(m)` | `(m: Attributes) => this` | Arbitrary metadata carried on the role record. |
| `.grant(a, r, scope?)` | `(action, resource, scope?) => this` | One permission. `'*'` is allowed for action and resource. |
| `.grantScoped(s, a, r)` | `(scope, action, resource) => this` | Same, with the scope first. Mixes scoped and global grants in one role. |
| `.grantWhen(a, r, fn)` | `(action, resource, fn) => this` | Permission guarded by a `When` condition group (chapter 3). |
| `.grantAll(r)` | `(resource) => this` | Shorthand for `.grant('*', resource)`. |
| `.grantRead(...rs)` | `(...resources) => this` | `.grant('read', r)` for each. |
| `.grantCRUD(r)` | `(resource) => this` | `create`, `read`, `update`, `delete` on one resource. |
| `.build()` | `() => AccessControl.IRole` | Validates, then returns the plain record. Throws on error-level issues. |

### What a permission and a role look like

```ts
interface IPermission {
  readonly action: string | '*'
  readonly resource: string | '*'
  readonly scope?: string | '*'
  readonly conditions?: AccessControl.IConditionGroup
}

interface IRole {
  readonly id: string
  readonly name: string
  readonly description?: string
  readonly permissions: readonly IPermission[]
  readonly inherits?: readonly string[]
  readonly scope?: string
  readonly metadata?: Readonly<IamPrimitives.Attributes>
}
```

Both are plain data. `.build()` sets `inherits` to `undefined` rather than `[]` when no parents were declared, so a round trip through JSON stays stable.

## Wildcards and matching

```ts
const superadmin = defineRole('superadmin').grant('*', '*').build()
const docAdmin = defineRole('doc-admin').grant('*', 'document').build()
const auditor = defineRole('auditor').grant('read', '*').build()
```

The matcher is deliberately small. There is no glob engine behind it.

| Pattern | Matches | Does not match |
| --- | --- | --- |
| `'*'` (action or resource) | anything | - |
| `'read'` | exactly `read` | `read:draft` |
| `'documents:*'` (action) | `documents:create`, `documents:read` | `documents`, `document:create` |
| `'document:*'` (resource) | `document:draft`, `document:draft:v2` | `document` |
| `'document.*'` (resource) | `document.draft`, `document.draft.v2` | `document` |
| `'document'` (resource) | exactly `document` | `document.draft`, `document:draft` |

`grant('read', 'document')` does **not** grant `read` on `document.draft`. Prefix matching only happens when the *pattern* ends in `:*` or `.*`, and the separator in the pattern must match the separator in the request. Actions recognise `:*` only; resources recognise both. Chapter 5 uses this for hierarchical resources.

## Validating the role set

```ts
import { validateRoles } from '@gentleduck/iam/core/validate'

const result = validateRoles(roles)
```

```ts
interface IResult {
  readonly valid: boolean            // false only when an error-level issue exists
  readonly issues: readonly IIssue[]
}

interface IIssue {
  readonly type: 'error' | 'warning'
  readonly code: IamValidate.ValidationCode
  readonly message: string
  readonly roleId?: string
  readonly path?: string
}
```

| Code | Severity | Fires when |
| --- | --- | --- |
| `DUPLICATE_ROLE_ID` | error | Two roles share an `id`. |
| `DANGLING_INHERIT` | error | A role inherits an ID that is not in the set. |
| `INHERITANCE_TOO_DEEP` | error | A chain is deeper than 32; the runtime would silently drop the tail. |
| `CIRCULAR_INHERIT` | warning | A cycle exists. The runtime skips it, so it is survivable but almost always a bug. |
| `EMPTY_ROLE` | warning | A role has no permissions and no parents. |

`valid` is `false` only for error-level issues. Treat warnings as build failures in CI anyway - both of them describe configuration that does nothing useful.

`RoleBuilder.build()` calls `validateRole` on the single role and throws the coded `IamError` `IAM_VALIDATION_FAILED` (`meta: { kind: 'role', issues }`). What `build()` cannot see is the *set*: duplicates, dangling parents, and cycles are cross-role facts, which is what `validateRoles` is for.

## What just happened

Carol's check for `delete` on `document` now walks a longer path than in chapter 1:

1. `resolveEffectiveRoles(['admin'], allRoles)` returns `['admin', 'editor', 'viewer']` and that array becomes `subject.roles`.
2. `rolesToPolicy` iterates the roles the adapter stores. For `admin` it collects the inherited permissions first (viewer's, then editor's) and then admin's own, emitting one allow rule for each - every one gated on `subject.roles contains 'admin'`.
3. The `delete` on `document` rule matches, the `allow-overrides` algorithm inside `__rbac__` returns allow, and there is no other policy to disagree with.

Bob's `delete` check fails at step 3: `editor` never emits a `delete` rule, so nothing in `__rbac__` matches and the decision falls through to `defaultEffect: 'deny'`. Roles only ever emit allow rules; a role cannot deny.

## Try it

1. Add `commenter` (`create` and `update` on `comment`) and make `editor` inherit both `viewer` and `commenter`. Confirm `getEffectiveRoles('bob')` lists four roles.
2. Introduce a cycle on purpose - `viewer` inherits `admin` - and run `validateRoles`. You get `CIRCULAR_INHERIT` as a warning and `valid` stays `true`. Then confirm `engine.can` still answers rather than hanging.
3. Make `auditor` with `.grant('read', '*')` and assign it to a new subject. Confirm reads on `document`, `team`, and `user` all pass, but `update` on `document` does not.
4. Try `grant('read', 'document')` and then check `read` on `document.draft`. It is denied. Change the grant to `'document.*'` and check again.

## Where we are

`src/roles.ts` holds three roles and the startup validation. `src/access.ts` seeds three subjects. Nothing yet can express "may Bob update *this* document" - that needs conditions, which is chapter 3.

## See also

* [Role inheritance](/duck-iam/core/roles/inheritance) - cycle and diamond handling in detail
* [rolesToPolicy](/duck-iam/core/roles/roles-to-policy) - the generated policy, dumped
* [Rule matching](/duck-iam/core/rule-matching) - the action and resource matchers
* [Validation](/duck-iam/advanced/validation) - every code the validator can emit
* [Chapter 3: policies, rules, and conditions](/duck-iam/course/chapter-3)