PRD: Resource, Action & Domain Hierarchy
| Module | Permissions (CORE-02) | PRD ID | PRD-HIER-001 |
| Status | Draft | Owner | Phat Nguyen |
| Date | 2026-06-04 | Version | v0.1 |
| Packages | @nx/core · @nx/identity | URD | HIER · DECL |
TL;DR
Models permissions as a resource tree × action lattice × domain tree so a single coarse grant -
Sale:manageon an organizer - replaces hundreds of flat per-method rows, covering every subject, operation, and merchant beneath it. For platform owners and engineers this turns huge, brittle roles into a handful of legible grants and reduces declaring a controller's permissions to one helper call.
1. Context & Problem
The scoped-RBAC engine (ignis CASBIN_RBAC_DOMAIN_SCOPED_MODEL) ships three inheritance axes KICKO does not yet exploit: an action axis (g5, manage ⊃ read), a resource axis (g4 + dotted objectMatch, Order ⊃ Order.refund, Order ⊃ OrderItem), and a domain axis (g3, Organizer ⊃ Merchant).
Today permissions are flat: ~950 Subject.method codes, each granted individually. OWNER carries ~950 grant rows; cross-merchant "HQ sees all" is faked by writing one join_domain membership per merchant. There is no manage/write super-action and no declaration ergonomics - every package hand-rolls createPermission. The result is roles that are huge, brittle, slow to load, and hard to reason about. Modelling permissions as a tree × lattice × domain-tree lets a role collapse to a few coarse grants while fine roles stay precise, and makes declaring permissions one helper call.
2. Goals & Non-Goals
Goals
- Collapse role grants - OWNER becomes
(Sale, manage)instead of everySaleOrder.*row. - A three-tier action lattice:
manage ⊃ {write, read, execute},write ⊃ {create, update, delete}. - A resource hierarchy with no code renames - subjects keep their codes; module grouping plus cross-entity nesting via
g4, and free operation nesting via dottedobjectMatch. - A domain hierarchy
Merchant ⊂ Organizerviag3- HQ/management roles reach every merchant of an organizer through one organizer-scoped grant, retiring the per-merchant membership backfill. - A minimal-boilerplate declaration toolkit (
defineResource/crudPermissions/ role→grant matrix / idempotent structural-edge seeders), built in nx-seller first.
Non-Goals
- Porting the toolkit into IGNIS - deferred to a later increment (nx-seller first, port after).
- Arbitrary glob/regex permissions (
sales.*) - coarseness comes from the hierarchy, not globs. - Changing the role set or priority semantics - a separate concern.
- Time/shift-based permissions and a permission audit log.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Grant compaction | Grant rows per role drop from ~950 to tens |
| Declaration DX | Adding a new controller needs one declaration call (no CRUD enumeration) |
| Seed idempotency | migrate:dev re-runs change nothing; removed codes reconcile away |
| Authorization parity | Launchpad + every protected route keep identical allow/deny outcomes (regression-tested) |
| HQ reach | HQ/management roles reach all organizer merchants with zero per-merchant membership rows |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Platform owner / admin | Assign coarse, legible roles (Sale:manage) instead of editing hundreds of grant rows |
| Owner (HQ) | Reach every merchant of the organizer through one organizer-scoped grant |
| Engineer | Declare a controller's permissions + resource node + base action in one helper call |
| Operator | Trust that re-seeding is idempotent and stale codes are reconciled away |
Core scenarios: declare a controller's resource and operations in one call → seed the action lattice, resource roll-ups, and domain edges idempotently → assign roles as a small coarse matrix → a request resolves coverage through the action lattice, resource tree, and domain tree without any flat enumeration.
5. User Stories
- As an admin, I want to grant
(Sale, manage)to OWNER, so the role covers every sale subject and operation without enumerating each one. - As an HQ owner, I want one organizer-scoped grant to reach every merchant of my organizer, so I no longer depend on a per-merchant membership row.
- As an engineer, I want to declare a controller's resource node, operations, and base action in one helper call, so adding a route does not mean hand-writing CRUD permissions.
- As an operator, I want seeding to reconcile removed codes idempotently, so stale permissions and grants never accumulate.
- As a role designer, I want a broader action (
manage/write) to satisfy any narrower request, so I grant intent rather than every CRUD verb.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | A grant MAY target a resource at any level - module, subject, or operation - and covers all descendants | URD-HIER-001 |
| FR-2 | Operation resources nest under their subject by dotted code (SaleOrder.refund ⊂ SaleOrder) with no extra config | URD-HIER-002 |
| FR-3 | A subject MAY roll up to a module so a module-level grant covers every subject in it | URD-HIER-003 |
| FR-4 | A child entity MAY nest under a parent across naming (SaleOrderItem ⊂ SaleOrder) via g4 | URD-HIER-004 |
| FR-5 | Actions form a lattice - manage ⊃ {write, read, execute}, write ⊃ {create, update, delete} - broader satisfies narrower | URD-HIER-005 |
| FR-6 | A route requests a base action (read/create/update/delete/execute); a grant MAY use any tier including manage/write | URD-HIER-006 |
| FR-7 | An organizer-scoped grant applies to every merchant under it; a merchant-scoped grant applies only there | URD-HIER-007 |
| FR-8 | A head-quarter / management role reaches all organizer merchants via an organizer-scoped grant - no per-merchant membership row | URD-HIER-008 |
| FR-9 | A grant MAY pin to SYSTEM_WIDE (everywhere) or ANY_MEMBER (every joined merchant) | URD-HIER-009 |
| FR-10 | Permissions, the action lattice, and resource/domain hierarchy edges are declared in code and seeded idempotently | URD-DECL-001 |
| FR-11 | Declaring a controller's operations + resource node + base action takes one helper call | URD-DECL-002 |
| FR-12 | Role→grant assignment is declared as a small matrix (OWNER → Sale:manage), not a per-operation enumeration | URD-DECL-003 |
| FR-13 | Seeding reconciles removed codes idempotently - no stale permissions/grants accumulate | URD-DECL-004 |
| FR-14 | The toolkit is built in nx-seller first, with a clear path to port into IGNIS later | URD-DECL-005 |
Full requirement text and acceptance criteria live in the Permissions URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Authorization parity | Coverage resolution via objectMatch/g4 (resource), g5 (action), g3 (domain) keeps identical allow/deny outcomes vs the flat model |
| Seed idempotency | Structural edges (g3/g4/g5) and permission codes seed idempotently; re-runs are no-ops; removed codes reconcile away |
| Tenancy & authz | A grant resolves within one active merchant domain per request (x-merchant-id); organizer scope expands via g3, not per-merchant rows |
| Performance / scale | Grant rows per role collapse from ~950 to tens, shrinking policy load and per-request enforcement cost |
| Maintainability | One declaration helper per controller; no hand-rolled createPermission per package |
| i18n | User-facing permission names/descriptions remain bilingual ({ en, vi }) |
8. UX & Flows
This increment is authorization plumbing - no end-user screen. It surfaces through the existing permission/role admin UI (the Permission Matrix and role-grant views), where coarse grants now render as a handful of rows instead of ~950. See the Permission Hierarchy map.
9. Data & Domain
| Entity | Role |
|---|---|
| Permission (resource code) | Catalog of modules + subjects + custom operations - not ~950 method rows |
Action lattice (g5) | manage/write/read/execute/create/update/delete inheritance edges |
Resource edges (g4 + dotted objectMatch) | Module⊃subject roll-up and cross-entity / operation nesting |
Domain edges (g3) | Organizer ⊃ Merchant so organizer-scoped grants reach every merchant |
| Grant (policy) | (Role|User, resourceCode, action, domain, effect) |
| Role→grant matrix | Declarative coarse assignment (OWNER → Sale:manage) |
Conceptual only - full schema and the Casbin model live in the RBAC and Casbin Authorization developer docs.
10. Dependencies & Assumptions
Depends on
- Scoped-RBAC engine (ignis
CASBIN_RBAC_DOMAIN_SCOPED_MODEL) - provides theg3/g4/g5axes andobjectMatch. - Fixed & custom roles (URD-ROLE · URD-CROLE) - roles are the grant subjects.
- Permission catalog & grant/revoke (URD-PERM · URD-GRANT) - the catalog this hierarchy reshapes.
- Effective permissions & scope (URD-EFF) - resolution stays within the active merchant domain.
@nx/core(Casbin model + adapter + enforcer) and@nx/identity(roles, permissions, grants).
Assumptions
- Subject codes are stable - the hierarchy is additive (no code renames).
- The organizer→merchant relationship is available to seed
g3edges. - A regression suite covers launchpad and every protected route for allow/deny parity.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Coverage resolution diverges from the flat model | Regression-test launchpad + every protected route for identical allow/deny |
Exact g4 module-grouping seed list (which subjects roll up to which module) | Open - to be enumerated in the plan |
execute for custom operations granted per-operation vs rolled into manage only | Open - decide before seeding the action lattice |
| Migrating existing per-merchant owner memberships → organizer-scoped grants | Open - define the data-migration shape |
| Re-seeding could drop in-use codes | Reconciliation is idempotent and code-driven; re-runs are no-ops |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P3 - see URD feature catalog (HIER + DECL) |
| Rollout | Phased: (1) toolkit + action lattice (g5) + collapse OWNER/EMPLOYEE/CASHIER grants to coarse rows in nx-seller; (2) domain tree (g3) + retire per-merchant membership backfill + organizer-scoped management grants; (3) port toolkit to IGNIS (separate PRD) |
| Migration | Data migration: existing per-merchant owner memberships → organizer-scoped grants; structural-edge seed runs via migrate:dev |
| Launch criteria | Grant rows per role collapse to tens; migrate:dev idempotent; launchpad + every protected route keep identical allow/deny outcomes |
| Monitoring | Grant-row counts per role, seed idempotency check, authorization allow/deny regression suite |
13. FAQ
Does this require renaming subject codes? No. The hierarchy is additive - subjects keep their codes; grouping and nesting happen through g4 edges and dotted objectMatch.
How is Sale:manage different from a glob like sales.*? Coarseness comes from the resource tree plus the manage action, not regex globbing. There is no wildcard permission.
How does an HQ owner reach every merchant now? Through one organizer-scoped grant resolved via g3 - the per-merchant join_domain membership backfill is retired.
Does a broader action cover narrower ones? Yes. manage ⊃ {write, read, execute} and write ⊃ {create, update, delete}, so a manage grant satisfies any narrower request.
Is the toolkit going into IGNIS? Not in this increment - it is built in nx-seller first, with a clear path to port into IGNIS in a later PRD.
What happens to a permission code removed from a declaration? Seeding reconciles it away idempotently, along with its grants - no stale rows accumulate.
References
- URD: Permissions - Resource, Action & Domain Hierarchy · Permission Declaration
- Builds on: Fixed Roles · Custom Roles · Permission Catalog · Grant / Revoke · Effective Permissions & Scope
- Map: Permission Hierarchy · Permission Matrix
- Module: Permissions - overview + traceability
- Developer: @nx/identity · RBAC · Casbin Authorization