Skip to content

PRD: Resource, Action & Domain Hierarchy

ModulePermissions (CORE-02)PRD IDPRD-HIER-001
StatusDraftOwnerPhat Nguyen
Date2026-06-04Versionv0.1
Packages@nx/core · @nx/identityURDHIER · DECL

TL;DR

Models permissions as a resource tree × action lattice × domain tree so a single coarse grant - Sale:manage on 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 every SaleOrder.* 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 dotted objectMatch.
  • A domain hierarchy Merchant ⊂ Organizer via g3 - 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

MetricTarget / signal
Grant compactionGrant rows per role drop from ~950 to tens
Declaration DXAdding a new controller needs one declaration call (no CRUD enumeration)
Seed idempotencymigrate:dev re-runs change nothing; removed codes reconcile away
Authorization parityLaunchpad + every protected route keep identical allow/deny outcomes (regression-tested)
HQ reachHQ/management roles reach all organizer merchants with zero per-merchant membership rows

4. Personas & Use Cases

PersonaGoal in this feature
Platform owner / adminAssign 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
EngineerDeclare a controller's permissions + resource node + base action in one helper call
OperatorTrust 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

#RequirementURD ref
FR-1A grant MAY target a resource at any level - module, subject, or operation - and covers all descendantsURD-HIER-001
FR-2Operation resources nest under their subject by dotted code (SaleOrder.refund ⊂ SaleOrder) with no extra configURD-HIER-002
FR-3A subject MAY roll up to a module so a module-level grant covers every subject in itURD-HIER-003
FR-4A child entity MAY nest under a parent across naming (SaleOrderItem ⊂ SaleOrder) via g4URD-HIER-004
FR-5Actions form a lattice - manage ⊃ {write, read, execute}, write ⊃ {create, update, delete} - broader satisfies narrowerURD-HIER-005
FR-6A route requests a base action (read/create/update/delete/execute); a grant MAY use any tier including manage/writeURD-HIER-006
FR-7An organizer-scoped grant applies to every merchant under it; a merchant-scoped grant applies only thereURD-HIER-007
FR-8A head-quarter / management role reaches all organizer merchants via an organizer-scoped grant - no per-merchant membership rowURD-HIER-008
FR-9A grant MAY pin to SYSTEM_WIDE (everywhere) or ANY_MEMBER (every joined merchant)URD-HIER-009
FR-10Permissions, the action lattice, and resource/domain hierarchy edges are declared in code and seeded idempotentlyURD-DECL-001
FR-11Declaring a controller's operations + resource node + base action takes one helper callURD-DECL-002
FR-12Role→grant assignment is declared as a small matrix (OWNER → Sale:manage), not a per-operation enumerationURD-DECL-003
FR-13Seeding reconciles removed codes idempotently - no stale permissions/grants accumulateURD-DECL-004
FR-14The toolkit is built in nx-seller first, with a clear path to port into IGNIS laterURD-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

AreaRequirement
Authorization parityCoverage resolution via objectMatch/g4 (resource), g5 (action), g3 (domain) keeps identical allow/deny outcomes vs the flat model
Seed idempotencyStructural edges (g3/g4/g5) and permission codes seed idempotently; re-runs are no-ops; removed codes reconcile away
Tenancy & authzA grant resolves within one active merchant domain per request (x-merchant-id); organizer scope expands via g3, not per-merchant rows
Performance / scaleGrant rows per role collapse from ~950 to tens, shrinking policy load and per-request enforcement cost
MaintainabilityOne declaration helper per controller; no hand-rolled createPermission per package
i18nUser-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

EntityRole
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 matrixDeclarative 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 the g3/g4/g5 axes and objectMatch.
  • 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 g3 edges.
  • A regression suite covers launchpad and every protected route for allow/deny parity.

11. Risks & Open Questions

Risk / questionMitigation / status
Coverage resolution diverges from the flat modelRegression-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 onlyOpen - decide before seeding the action lattice
Migrating existing per-merchant owner memberships → organizer-scoped grantsOpen - define the data-migration shape
Re-seeding could drop in-use codesReconciliation is idempotent and code-driven; re-runs are no-ops

12. Release Plan & Launch Criteria

AspectPlan
PhaseP3 - see URD feature catalog (HIER + DECL)
RolloutPhased: (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)
MigrationData migration: existing per-merchant owner memberships → organizer-scoped grants; structural-edge seed runs via migrate:dev
Launch criteriaGrant rows per role collapse to tens; migrate:dev idempotent; launchpad + every protected route keep identical allow/deny outcomes
MonitoringGrant-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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.