Skip to content

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:

ini
[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
ComponentDescription
subSubject — User_<id> or Role_<id>
domDomain — Merchant_<id> on a membership; * (global) on a role-permission policy
objObject (resource/permission code being accessed)
actAction (create, read, update, delete, execute)
eftEffect (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.

VariantSubject → TargetMeaningExample
groupUser → RoleAssign role to userUser X has "Operator" role
groupUser → OrganizerMap user to orgUser X belongs to Org A
groupUser → MerchantMap user to merchantUser X works at Merchant B
groupRole → OrganizerScope role to orgCustom role belongs to Org A
groupRole → MerchantScope role to merchantCustom role for Merchant B
policyRole → PermissionGrant permission to role"Operator" can product.create
policyUser → PermissionDirect user permissionUser X can order.delete

Fixed System Roles

Eight roles seeded on migration (alwaysRun: true). Cannot be modified or deleted.

IdentifierName ENName VIPriorityScope
999_super-adminSuper AdminSiêu Quản Trị Viên999System (from IGNIS AuthorizationRoles)
900_adminAdminQuản Trị Viên900System (from IGNIS)
600_operatorOperatorVận Hành Viên600System
500_organizer-ownerOrganizer OwnerChủ Doanh Nghiệp500Organization
110_cashierCashierThu Ngân110Merchant
100_employeeEmployeeNhân Viên100Merchant
010_customerCustomerKhách Hàng10Customer
001_guestGuestKhách1Global

CASHIER is a merchant-level staff role (same tier as EMPLOYEE). Its priority 110 falls 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 (in GLOBAL_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

typescript
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 OWNER

Identifier format: {priority:3 zero-padded}_{kebab-case-name} — generated by AuthorizationRole.build() with _ delimiter.

Custom Roles

RuleValue
Priority range101499 (RolePriorities.MIN / MAX)
TypeCUSTOM
IdentifierAuto-generated: {paddedPriority}_{kebabCase(name.en)}
UniquenessPer scope (global for system, per org/merchant for scoped)

Scope Rules by Creator

Creator RoleAllowed Scopes
System users (SUPER_ADMIN, ADMIN, OPERATOR)Any: system / organizer / merchant
Organizer OwnerOwn 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) → 403 on update or delete

Deletion Constraints

EntityConstraintError
RoleCannot delete if users assigned (PolicyDef USER→ROLE exists)409
PermissionCannot delete if granted to role/user (PolicyDef with targetType=Permission exists)409

Role Deletion Cascade

When a deletable role is removed:

  1. Delete all policy records: ROLE→PERMISSION
  2. Delete all group records: ROLE→ORGANIZER/MERCHANT (scope mappings)
  3. Soft-delete the Role entity

Permission Validation

FieldConstraint
actionMust be valid AuthorizationActions: create, read, update, delete, execute
scopeMust be valid PolicyDomains: SYSTEM, ORGANIZER, MERCHANT
subjectMust be a registered authorize model principal (from IGNIS MetadataRegistry)
codeGlobally unique

API Reference

Roles — /roles

MethodPathDescription
GET/rolesList (paginated)
GET/roles/countCount
GET/roles/:idGet by ID
POST/rolesCreate custom role
PATCH/roles/:idUpdate role
DELETE/roles/:idSoft-delete (with guards)

CreateRoleRequest:

FieldTypeRequired
name{ en, vi }Yes
description{ en, vi }No
prioritynumber (100–500)Yes
statusenumNo
organizer{ id }No (scope)
merchant{ id }No (scope)

Permissions — /permissions

MethodPathDescription
GET/permissionsList
GET/permissions/countCount
GET/permissions/:idGet by ID
POST/permissionsCreate
PATCH/permissions/:idUpdate (code is immutable)
DELETE/permissions/:idSoft-delete (with grant check)

CreatePermissionRequest:

FieldTypeRequired
codestringYes (globally unique)
name{ en, vi }Yes
description{ en, vi }No
subjectstringYes
actionstringYes
scopestringYes
parentIdstringNo

Policy Definitions — /policy-definitions

Base CRUD (read-only):

MethodPathDescription
GET/policy-definitionsList (paginated, filtered)
GET/policy-definitions/:idGet by ID

Role sub-endpoints/policy-definitions/roles/{roleId}/{type}:

MethodPathtypeDescription
GET.../roles/{id}/permissionspermissionsList role's permissions
POST.../roles/{id}/permissionspermissionsGrant/revoke permissions
GET.../roles/{id}/usersusersList role's users
POST.../roles/{id}/usersusersAssign/remove users

User sub-endpoints/policy-definitions/users/{userId}/{type}:

MethodPathtypeDescription
GET.../users/{id}/rolesrolesList user's roles
GET.../users/{id}/permissionspermissionsList user's permissions (query: mode=direct|inherit)
GET.../users/{id}/organizersorganizersList user's organizers
GET.../users/{id}/merchantsmerchantsList user's merchants
POST.../users/{id}/{type}anyGrant/revoke targets

Organizer/Merchant sub-endpoints:

MethodPathDescription
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

typescript
// 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

typescript
{ granted?: number, revoked?: number, skipped?: number }

Permission Resolution

  • GET /policy-definitions/users/{id}/permissions?mode=direct → only USER→PERMISSION policies
  • GET /policy-definitions/users/{id}/permissions?mode=inherit → USER→ROLE→PERMISSION chain
  • Default: inherit

PolicyDefinition Services

ServiceManages
RolePolicyDefinitionServiceRole↔Permission (POLICY), Role↔User (GROUP), User↔Role
UserPolicyDefinitionServiceUser's roles, permissions, organizers, merchants (read)
OrganizerPolicyDefinitionServiceUser↔Organizer (GROUP), User↔Merchant (GROUP)
PermissionPolicyDefinitionServiceUser↔Permission (POLICY, direct grants)
BasePolicyDefinitionServiceShared privilege escalation validation

JWT Authorization Flow

useRequestContext() Output

typescript
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 ADMIN

Frontend Integration

Decode JWT

typescript
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

typescript
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

typescript
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)

IdentifierPriorityType
999_super-admin999SYSTEM
900_admin900SYSTEM
997_operator997SYSTEM
899_organizer-owner899SYSTEM
110_cashier110SYSTEM
898_employee898SYSTEM
011_customer11SYSTEM
001_guest1SYSTEM

Default Users (0002)

UsernamePasswordRole Identifier
superadminSuperadmin123999_super-admin
adminAdmin123900_admin
ownerOwner123899_organizer-owner
employeeEmployee123898_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.

PackagePermission PrefixExample Codes
@nx/identityidentity.*identity.user.create, identity.role.update
@nx/commercecommerce.*commerce.product.create, commerce.merchant.read
@nx/salesale.*sale.order.create, sale.check.read
@nx/financefinance.*finance.wallet.create, finance.transaction.read
@nx/inventoryinventory.*inventory.stock.update, inventory.purchase-order.create
@nx/paymentpayment.*payment.webhook-config.create
@nx/pricingpricing.*pricing.fare.create, pricing.tax.read
@nx/ledgerledger.*ledger.generate, ledger.download
@nx/signalsignal.*signal.client.read

All permissions are stored in the public.Permission table and can be granted to roles or users via PolicyDefinition.

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