|
| 1 | +--- |
| 2 | +title: Delegated Administration |
| 3 | +description: Administration itself as a scoped grant — a business-unit subtree, an action set, and an assignable-set allowlist, with self-escalation structurally impossible (ADR-0090 D12). |
| 4 | +--- |
| 5 | + |
| 6 | +# Delegated Administration |
| 7 | + |
| 8 | +A group with fifty subsidiaries cannot manage every grant from headquarters — |
| 9 | +but handing each subsidiary a full admin is worse. ADR-0090 D12's answer: |
| 10 | +**administration itself becomes a scoped capability**. A delegate can onboard |
| 11 | +their own staff and reshuffle their own positions, but can never grant |
| 12 | +anything outside an explicit allowlist — *including to themselves* — and can |
| 13 | +never touch tenant-level assets. |
| 14 | + |
| 15 | +Because the scope lives on an ordinary permission set, it is distributed via |
| 16 | +positions, audited in the same tables, and explained by the same engine as |
| 17 | +every other grant. No parallel admin subsystem. |
| 18 | + |
| 19 | +## Authoring an admin scope |
| 20 | + |
| 21 | +```typescript |
| 22 | +export const FieldOpsDelegate = definePermissionSet({ |
| 23 | + name: 'field_ops_delegate', |
| 24 | + label: 'Field Ops Delegate Admin', |
| 25 | + // The scope authorizes WHAT may be administered… |
| 26 | + adminScope: { |
| 27 | + businessUnit: 'Field Operations', // WHERE: this sys_business_unit subtree |
| 28 | + includeSubtree: true, // default true |
| 29 | + manageAssignments: true, // user ↔ position rows (sys_user_position) |
| 30 | + manageBindings: false, // position ↔ set rows (sys_position_permission_set) |
| 31 | + authorEnvironmentSets: false, // may they author environment-owned sets? |
| 32 | + assignablePermissionSets: ['showcase_contributor', 'showcase_manager'], |
| 33 | + }, |
| 34 | + // …and plain CRUD on the RBAC link tables lets the requests through at all. |
| 35 | + // Both are required: table CRUD with NO scope is refused outright. |
| 36 | + objects: { |
| 37 | + sys_user_position: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true }, |
| 38 | + sys_position: { allowRead: true }, |
| 39 | + sys_permission_set: { allowRead: true }, |
| 40 | + sys_business_unit: { allowRead: true }, |
| 41 | + sys_business_unit_member: { allowRead: true }, |
| 42 | + sys_user: { allowRead: true }, |
| 43 | + }, |
| 44 | +}); |
| 45 | +``` |
| 46 | + |
| 47 | +`businessUnit` names the subtree root by `sys_business_unit.name`; a |
| 48 | +misconfigured name resolves to an empty subtree and **approves nothing** |
| 49 | +(fail-closed). |
| 50 | + |
| 51 | +## What the runtime gate enforces |
| 52 | + |
| 53 | +Writes to the governed tables (`sys_user_position`, |
| 54 | +`sys_position_permission_set`, `sys_user_permission_set`, |
| 55 | +`sys_permission_set`) pass through the `DelegatedAdminGate`: |
| 56 | + |
| 57 | +| Rule | Effect | |
| 58 | +|---|---| |
| 59 | +| Tenant admins pass through | The superuser wildcard reaches the ordinary CRUD/RLS checks — delegation constrains *delegates*, not HQ | |
| 60 | +| Subtree anchoring | Assignments a delegate creates must be anchored (`sys_user_position.business_unit_id`) inside their subtree, and the target user must sit inside it | |
| 61 | +| Allowlist, no self-escalation | Every set reached by the write — bound to the assigned position, or granted directly — must be on `assignablePermissionSets`. Granting an un-allowlisted set is refused **for anyone, including the delegate themselves** | |
| 62 | +| Action flags | `manageAssignments` / `manageBindings` / `authorEnvironmentSets` gate their table classes independently; re-composing a position (`manageBindings`) additionally requires every current holder to sit inside the subtree | |
| 63 | +| Strict containment | Granting or authoring a set that itself carries an `adminScope` requires a held scope that **strictly contains** it — handing your own exact scope to a peer is refused (no lateral propagation) | |
| 64 | +| Single-row writes | Delegates write single rows by id only — a broad filter-write cannot be boundary-checked | |
| 65 | +| Audit stamp | Every assignment a delegate creates is `granted_by`-stamped automatically | |
| 66 | +| Tenant-level assets stay tenant-level | The `everyone`/`guest` audience anchors and security-domain publishes are untouchable from any delegated scope | |
| 67 | +| No scope, no admin | Holders of plain CRUD on the RBAC tables with **no** scope are refused: administration is a scoped capability now, not a side effect of table access | |
| 68 | + |
| 69 | +Every rule above is exercised end-to-end by the showcase permission zoo |
| 70 | +(`packages/dogfood/test/showcase-permission-zoo.dogfood.test.ts`): the |
| 71 | +in-subtree allowlisted assignment passes with a `granted_by` stamp; the |
| 72 | +out-of-subtree anchor, the off-allowlist grant (to self), and the |
| 73 | +`manageBindings: false` binding write are each refused. |
| 74 | + |
| 75 | +## The assignment anchor |
| 76 | + |
| 77 | +Positions never bind to a business unit at the *definition* level — that |
| 78 | +would recreate the position-per-department explosion. The **assignment row** |
| 79 | +may: `sys_user_position.business_unit_id` names the unit the person holds the |
| 80 | +position *in* ("张三 is sales_manager **of 华东**"). It does exactly three |
| 81 | +things: anchors that assignment's depth grants to the subtree, provides the |
| 82 | +delegation boundary check above, and makes "manager of what" an auditable |
| 83 | +fact. Capability bits are never BU-scoped. |
| 84 | + |
| 85 | +## Explaining delegated decisions |
| 86 | + |
| 87 | +The [explain engine](/docs/permissions/explain) closes the loop twice: |
| 88 | + |
| 89 | +- explaining **another user** is authorized by `manage_users` *or* a delegated |
| 90 | + `adminScope` whose subtree covers that user — a delegate can diagnose their |
| 91 | + own people, and only their own people; |
| 92 | +- contributor attribution plus the `granted_by` stamp answer both *who |
| 93 | + granted this* and *who could have*. |
| 94 | + |
| 95 | +## See also |
| 96 | + |
| 97 | +- [Permission Sets](/docs/permissions/permission-sets) — the container `adminScope` rides on |
| 98 | +- [Positions](/docs/permissions/positions) — how delegates receive the scope |
| 99 | +- [Explain Engine](/docs/permissions/explain) |
| 100 | +- [Authorization Architecture](/docs/permissions/authorization) |
0 commit comments