RBAC & Policy Definitions v1.0.0
Core Entities
Casbin Authorization Model
The RBAC system uses a Casbin domain-aware model defined in src/security/casbin-model.ts:
[request_definition]
r = sub, dom, obj, act
[policy_definition]
p = sub, dom, obj, act, eft
[role_definition]
g = _, _, _
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
[matchers]
m = g(r.sub, p.sub, r.dom) && keyMatch(r.dom, p.dom) && r.obj == p.obj && r.act == p.act| Component | Description |
|---|---|
| sub | Subject — User_<id> or Role_<id> |
| dom | Domain — Merchant_<id> on a membership; * (global) on a role-permission policy |
| obj | Object (resource/permission code being accessed) |
| act | Action (create, read, update, delete, execute) |
| eft | Effect (allow or deny) |
| g = _, _, _ | Domain-aware role grouping: user → role IN a merchant (per-merchant scoping lives here) |
The policy effect combines allow/deny: a request is permitted if at least one allow rule matches and no deny rule matches. Role permissions are emitted domain-agnostic (p, Role, *, …); keyMatch accepts any request domain, and the g membership lines constrain which merchants the role applies in. A domain matching function (keyMatch) is registered on g so a global membership g, user, role, * matches any domain.
Runtime deep-dive. How the adapter loads policy per user, per-merchant domain resolution, HQ-owner expansion, role bypass and enforcer caching are documented in Casbin Authorization.
PolicyDefinition Patterns
PolicyDefinition is the central linking table — it replaces UserRole, PermissionMapping, and UserMapping with one flexible model supporting both group assignments and policy grants.
| Variant | Subject → Target | Meaning | Example |
|---|---|---|---|
group | User → Role | Assign role to user | User X has "Operator" role |
group | User → Organizer | Map user to org | User X belongs to Org A |
group | User → Merchant | Map user to merchant | User X works at Merchant B |
group | Role → Organizer | Scope role to org | Custom role belongs to Org A |
group | Role → Merchant | Scope role to merchant | Custom role for Merchant B |
policy | Role → Permission | Grant permission to role | "Operator" can product.create |
policy | User → Permission | Direct user permission | User X can order.delete |
Fixed System Roles
Eight roles seeded on migration (alwaysRun: true). Cannot be modified or deleted.
| Identifier | Name EN | Name VI | Priority | Scope |
|---|---|---|---|---|
999_super-admin | Super Admin | Siêu Quản Trị Viên | 999 | System (from IGNIS AuthorizationRoles) |
900_admin | Admin | Quản Trị Viên | 900 | System (from IGNIS) |
600_operator | Operator | Vận Hành Viên | 600 | System |
500_organizer-owner | Organizer Owner | Chủ Doanh Nghiệp | 500 | Organization |
110_cashier | Cashier | Thu Ngân | 110 | Merchant |
100_employee | Employee | Nhân Viên | 100 | Merchant |
010_customer | Customer | Khách Hàng | 10 | Customer |
001_guest | Guest | Khách | 1 | Global |
CASHIER is a merchant-level staff role (same tier as EMPLOYEE). Its priority
110falls inside the custom-role band (101–499) — that is allowed for fixed roles; the band only constrains user-created CUSTOM roles. CASHIER currently carries the same permission set as EMPLOYEE across every package (see Cross-Package Permission Seeding).
GUEST (
001_guest, priority 1) is a global unauthenticated-tier role (inGLOBAL_ROLE_IDENTIFIERS) with no backend permissions — like Customer, but global rather than merchant-scoped.
Role Hierarchy
Priority 999 ┌─────────────────┐
│ SUPER_ADMIN │ ← full system access
└─────────────────┘
Priority 900 ┌─────────────────┐
│ ADMIN │ ← administration
└─────────────────┘
Priority 600 ┌─────────────────┐
│ OPERATOR │ ← system operations
└─────────────────┘
Priority 500 ┌─────────────────┐
│ OWNER │ ← organizer scope
└─────────────────┘
101–499 ┌─────────────────┐
│ CUSTOM ROLES │ ← user-created
└─────────────────┘
Priority 110 ┌─────────────────┐
│ CASHIER │ ← merchant scope (fixed, within custom band)
└─────────────────┘
Priority 100 ┌─────────────────┐
│ EMPLOYEE │ ← merchant scope
└─────────────────┘
Priority 10 ┌─────────────────┐
│ CUSTOMER │ ← end user
└─────────────────┘
Priority 1 ┌─────────────────┐
│ GUEST │ ← global, unauthenticated (no perms)
└─────────────────┘AppFixedRoles Helper
AppFixedRoles.isDefaultRole(identifier) // true if any of the 8 fixed roles
AppFixedRoles.isSystemUser(roles) // true if SUPER_ADMIN, ADMIN, or OPERATOR
AppFixedRoles.isOrganizerOwner(roles) // true if OWNERIdentifier format: {priority:3 zero-padded}_{kebab-case-name} — generated by AuthorizationRole.build() with _ delimiter.
Custom Roles
| Rule | Value |
|---|---|
| Priority range | 101 – 499 (RolePriorities.MIN / MAX) |
| Type | CUSTOM |
| Identifier | Auto-generated: {paddedPriority}_{kebabCase(name.en)} |
| Uniqueness | Per scope (global for system, per org/merchant for scoped) |
Scope Rules by Creator
| Creator Role | Allowed Scopes |
|---|---|
| System users (SUPER_ADMIN, ADMIN, OPERATOR) | Any: system / organizer / merchant |
| Organizer Owner | Own organizer or own merchants only |
| Other users (Employee, etc.) | Own merchant only |
Ownership is validated against the creator's organizerIds[] and merchantIds[] from the JWT token.
Business Rules
Priority Guards
- Cannot create/update/delete roles with equal or higher priority
- Cannot grant/revoke roles with equal or higher priority via PolicyDefinition
- Prevents privilege escalation
Fixed Role Protection
- System roles (
AppFixedRoles.DEFAULT_ROLE_IDENTIFIERS) →403on update or delete
Deletion Constraints
| Entity | Constraint | Error |
|---|---|---|
| Role | Cannot delete if users assigned (PolicyDef USER→ROLE exists) | 409 |
| Permission | Cannot delete if granted to role/user (PolicyDef with targetType=Permission exists) | 409 |
Role Deletion Cascade
When a deletable role is removed:
- Delete all
policyrecords: ROLE→PERMISSION - Delete all
grouprecords: ROLE→ORGANIZER/MERCHANT (scope mappings) - Soft-delete the Role entity
Permission Validation
| Field | Constraint |
|---|---|
action | Must be valid AuthorizationActions: create, read, update, delete, execute |
scope | Must be valid PolicyDomains: SYSTEM, ORGANIZER, MERCHANT |
subject | Must be a registered authorize model principal (from IGNIS MetadataRegistry) |
code | Globally unique |
API Reference
Roles — /roles
| Method | Path | Description |
|---|---|---|
GET | /roles | List (paginated) |
GET | /roles/count | Count |
GET | /roles/:id | Get by ID |
POST | /roles | Create custom role |
PATCH | /roles/:id | Update role |
DELETE | /roles/:id | Soft-delete (with guards) |
CreateRoleRequest:
| Field | Type | Required |
|---|---|---|
name | { en, vi } | Yes |
description | { en, vi } | No |
priority | number (100–500) | Yes |
status | enum | No |
organizer | { id } | No (scope) |
merchant | { id } | No (scope) |
Permissions — /permissions
| Method | Path | Description |
|---|---|---|
GET | /permissions | List |
GET | /permissions/count | Count |
GET | /permissions/:id | Get by ID |
POST | /permissions | Create |
PATCH | /permissions/:id | Update (code is immutable) |
DELETE | /permissions/:id | Soft-delete (with grant check) |
CreatePermissionRequest:
| Field | Type | Required |
|---|---|---|
code | string | Yes (globally unique) |
name | { en, vi } | Yes |
description | { en, vi } | No |
subject | string | Yes |
action | string | Yes |
scope | string | Yes |
parentId | string | No |
Policy Definitions — /policy-definitions
Base CRUD (read-only):
| Method | Path | Description |
|---|---|---|
GET | /policy-definitions | List (paginated, filtered) |
GET | /policy-definitions/:id | Get by ID |
Role sub-endpoints — /policy-definitions/roles/{roleId}/{type}:
| Method | Path | type | Description |
|---|---|---|---|
GET | .../roles/{id}/permissions | permissions | List role's permissions |
POST | .../roles/{id}/permissions | permissions | Grant/revoke permissions |
GET | .../roles/{id}/users | users | List role's users |
POST | .../roles/{id}/users | users | Assign/remove users |
User sub-endpoints — /policy-definitions/users/{userId}/{type}:
| Method | Path | type | Description |
|---|---|---|---|
GET | .../users/{id}/roles | roles | List user's roles |
GET | .../users/{id}/permissions | permissions | List user's permissions (query: mode=direct|inherit) |
GET | .../users/{id}/organizers | organizers | List user's organizers |
GET | .../users/{id}/merchants | merchants | List user's merchants |
POST | .../users/{id}/{type} | any | Grant/revoke targets |
Organizer/Merchant sub-endpoints:
| Method | Path | Description |
|---|---|---|
GET | .../organizers/{id} | List organizer's users |
POST | .../organizers/{id} | Assign/remove users |
GET | .../commerces/{id} | List merchant's users |
POST | .../commerces/{id} | Assign/remove users |
Request Body for Grant/Revoke
// For role and user targets
ManageRoleTargetsRequest / ManageUserTargetsRequest {
action: 'grant' | 'revoke',
ids: string[], // min 1
domain?: string // optional scope label
}
// For organizer/merchant targets
ManageGroupTargetsRequest {
action: 'grant' | 'revoke',
ids: string[] // min 1
}Response
{ granted?: number, revoked?: number, skipped?: number }Permission Resolution
GET /policy-definitions/users/{id}/permissions?mode=direct→ only USER→PERMISSION policiesGET /policy-definitions/users/{id}/permissions?mode=inherit→ USER→ROLE→PERMISSION chain- Default:
inherit
PolicyDefinition Services
| Service | Manages |
|---|---|
RolePolicyDefinitionService | Role↔Permission (POLICY), Role↔User (GROUP), User↔Role |
UserPolicyDefinitionService | User's roles, permissions, organizers, merchants (read) |
OrganizerPolicyDefinitionService | User↔Organizer (GROUP), User↔Merchant (GROUP) |
PermissionPolicyDefinitionService | User↔Permission (POLICY, direct grants) |
BasePolicyDefinitionService | Shared privilege escalation validation |
JWT Authorization Flow
useRequestContext() Output
const { currentUser, userId, roles, isAlwaysAllowed } = useRequestContext();
currentUser.priority.highest // max priority across all roles
currentUser.priority.lowest // min priority
currentUser.organizers // [{ id: "org1" }, ...]
currentUser.merchants // [{ id: "mer1" }, ...]
roles // string[] of identifiers, e.g. ["999_super-admin"]
isAlwaysAllowed // true if SUPER_ADMIN or ADMINFrontend Integration
Decode JWT
const payload = decodeJwt(token);
const roles = payload.roles; // Array<{ id, identifier, priority }>
const organizerIds = payload.organizerIds?.split(',').filter(Boolean) ?? [];
const merchantIds = payload.merchantIds?.split(',').filter(Boolean) ?? [];Check Access
const isAdmin = roles.some(r => ['999_super-admin', '900_admin'].includes(r.identifier));
const isSystem = roles.some(r => ['999_super-admin', '900_admin', '997_operator'].includes(r.identifier));
const hasOrg = (orgId: string) => organizerIds.includes(orgId);Permission-Aware UI
const perms = await fetch(`/policy-definitions/users/${userId}/permissions`);
const codes = new Set(perms.map(p => p.code));
const canCreateProduct = codes.has('commerce.product.create');Seed Data
Fixed Roles (0001)
| Identifier | Priority | Type |
|---|---|---|
999_super-admin | 999 | SYSTEM |
900_admin | 900 | SYSTEM |
997_operator | 997 | SYSTEM |
899_organizer-owner | 899 | SYSTEM |
110_cashier | 110 | SYSTEM |
898_employee | 898 | SYSTEM |
011_customer | 11 | SYSTEM |
001_guest | 1 | SYSTEM |
Default Users (0002)
| Username | Password | Role Identifier |
|---|---|---|
superadmin | Superadmin123 | 999_super-admin |
admin | Admin123 | 900_admin |
owner | Owner123 | 899_organizer-owner |
employee | Employee123 | 898_employee |
Cross-Package Permission Seeding
Every service package seeds its own permissions via migration processes (*-seed-permissions). These run with alwaysRun: true to ensure permissions stay in sync with code changes.
As a lenient baseline, each package grants its full permission set to OWNER, EMPLOYEE, and CASHIER (CASHIER mirrors EMPLOYEE) so the app runs without 403 friction; per-role tightening happens later via the policy-definition API. To give a new fixed role permissions, add its identifier to the grantsByRoleIdentifier map in each relevant *-seed-role-permissions process.
| Package | Permission Prefix | Example Codes |
|---|---|---|
@nx/identity | identity.* | identity.user.create, identity.role.update |
@nx/commerce | commerce.* | commerce.product.create, commerce.merchant.read |
@nx/sale | sale.* | sale.order.create, sale.check.read |
@nx/finance | finance.* | finance.wallet.create, finance.transaction.read |
@nx/inventory | inventory.* | inventory.stock.update, inventory.purchase-order.create |
@nx/payment | payment.* | payment.webhook-config.create |
@nx/pricing | pricing.* | pricing.fare.create, pricing.tax.read |
@nx/ledger | ledger.* | ledger.generate, ledger.download |
@nx/signal | signal.* | signal.client.read |
All permissions are stored in the public.Permission table and can be granted to roles or users via PolicyDefinition.
Related Pages
- Authentication — Sign-in JWT generation
- User Management — User creation with role assignment
- Customer Management — Customer role
- MFA & OTP — OTP-based authentication, rate limiting
- Identity Overview — Architecture