Skip to content

ADR-0002. RBAC via Casbin + PolicyDefinition edge table

FieldValue
StatusAccepted
Date2026-02-04
DecidersPhat Nguyen
Supersedes-

Context

  • We need a flexible RBAC model that supports:
    • System fixed roles (SUPER_ADMIN, ADMIN, OPERATOR, OWNER, CASHIER, EMPLOYEE, CUSTOMER, GUEST)
    • Custom merchant roles
    • Permission grants (Role → Permission)
    • Scoping (User can be SCOPED_OWNER for organizer X but EMPLOYEE for organizer Y)
  • Two natural approaches:
    1. Triple tables: User_Role, Role_Permission, plus separate scoping
    2. Single edge table with subject + target polymorphism

Decision

Use a single PolicyDefinition edge table whose variant selects one of eight edges. The ScopedCasbinAdapter (@venizia/ignis) filters a principal's rows and emits the matching Casbin line for the scoped-RBAC model CASBIN_RBAC_DOMAIN_SCOPED_MODEL.

VariantSubject → TargetCasbinPurpose
grantRole|User → Permissionp"this subject may <action> <resource>"
assign_roleUser → Roleg"user has role" (domain-scoped; null ⇒ every domain)
join_domainUser → Merchant|Organizerg2membership; powers ANY_MEMBER grants
role_inheritsRole → Rolegrole DAG
domain_inheritsdomain → domaing3Merchant ⊂ Organizer / Branch ⊂ Company
resource_inheritsresource → resourceg4obj hierarchy
action_inheritsaction → actiong5manage ⊃ read/write/execute
merchant_roleRole ↔ Merchant-@nx/core UI metadata (not read by the adapter)

A grant's domain carries the scope - SYSTEM_WIDE, ANY_MEMBER, or a concrete <Type>_<id> (which also covers g3-nested children). Effect is default-DENY with an explicit deny override. Per-edge changes take effect on the next enforcer cache expiry.

Consequences

ProsCons
Single table for all RBAC edges; uniform queryPolymorphic - no DB-level FK to subject/target
ANY_MEMBER / <Type>_<id> domain scopes let one user be different roles per merchant/organizerCasbin's evaluation cost grows with edge count
Custom merchant roles are first-class (not hard-coded)Permission-check latency requires Redis caching of the per-user line set
Resource/action/domain hierarchies (g3/g4/g5) keep grants coarse - one module grant cascades to every subject/operation/child-domainAudit/diff of "who can do X" requires graph traversal

Alternatives Considered

OptionProsConsWhy rejected
Triple tables (UserRole, RolePermission)Clear semanticsTwo more migrations + parallel write pathsWorse maintenance
Single jsonb permission set per UserFast readNo history; no group inheritance; impossible to query "who has X"Wrong primitive
Spicedb / external authzIndustry standardOperational overhead; vendor latencyPremature for our scale

References

  • core/src/models/schemas/identity/policy-definition/schema.ts
  • @nx/core common/policy-variant.ts (PolicyVariants)
  • services/policy-definition/ - 4 specialized writers
  • @venizia/ignis .../enforcers/models/rbac-domain.model.ts (CASBIN_RBAC_DOMAIN_SCOPED_MODEL)

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