Skip to content

RBAC & Policy Definitions v1.0.0

Core Entities

Casbin Authorization Model

The RBAC system uses the scoped-RBAC Casbin model CASBIN_RBAC_DOMAIN_SCOPED_MODEL, shipped by @venizia/ignis core in .../enforcers/models/rbac-domain.model.ts:

ini
[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act, eft

[role_definition]
g = _, _, _
g2 = _, _
g3 = _, _
g4 = _, _
g5 = _, _

[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

[matchers]
m = g(r.sub, p.sub, r.dom) && (p.dom == "SYSTEM_WIDE" || (p.dom == "ANY_MEMBER" && g2(r.sub, r.dom)) || g3(r.dom, p.dom)) && (objectMatch(r.obj, p.obj) || g4(r.obj, p.obj)) && g5(r.act, p.act)
RelationCasbinCarries
grole groupingassign_role (User→Role) + role_inherits (Role→Role), domain-aware
g2membershipjoin_domain (User→Merchant/Organizer) - the dom axis
g3domain nestingdomain_inherits (Merchant ⊂ Organizer, Branch ⊂ Company)
g4resource nestingresource_inherits (obj hierarchy, e.g. OrderItem ⊂ Order)
g5action latticeaction_inherits (manage ⊃ read/write/execute; write ⊃ create/update/delete)
TokenMeaning
subSubject - User_<id> or Role_<id>
domRequest domain - Merchant_<id> from x-merchant-id (or none)
objObject - the permission/resource code, e.g. commerce.product.create
actAction - read / write / execute / manage / create / update / delete
eftEffect - allow (default) / deny

Effect is default-DENY (allow-and-deny): a request needs a matching allow and no matching deny, so an explicit deny overrides any allow.

A grant's domain scope (the dom on a p line) is one of:

ScopeMatches
SYSTEM_WIDEevery domain, bypassing membership
ANY_MEMBERevery domain the subject joined via g2
<Type>_<id>that domain, plus any g3-nested child

Runtime deep-dive. How the ScopedCasbinAdapter loads one principal's edges per request, domain resolution from x-merchant-id, role bypass and Redis enforcer caching are documented in Casbin Authorization.

PolicyDefinition Variants

PolicyDefinition is the single edge table - it replaces UserRole, PermissionMapping, and UserMapping. Each row is one of eight variants; the ScopedCasbinAdapter filters a principal's rows and emits the matching Casbin line.

VariantSubject → TargetCasbinEmitted line
grantRole|User → Permissionpp, <Role|User>_<id>, <SYSTEM_WIDE|ANY_MEMBER|Type_id>, <objectCode>, <action>, <allow|deny>
assign_roleUser → Rolegg, User_<id>, Role_<id>, <domain|*> (null domain ⇒ *)
join_domainUser → Merchant|Organizerg2g2, User_<id>, <Type>_<domainId>
role_inheritsRole → Role (DAG)gg, Role_<child>, Role_<parent>, *
domain_inheritsdomain nestingg3g3, <Type>_<childId>, <Type>_<parentId>
resource_inheritsresource(obj) nestingg4g4, <childCode>, <parentCode>
action_inheritsaction latticeg5g5, <childAction>, <parentAction>
merchant_roleRole available in a Merchant-none - @nx/core UI metadata; the adapter never reads it

The app-facing names live in @nx/core common/policy-variant.ts (PolicyVariants), wrapping the framework's AuthorizationPolicyVariants.

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.

GUEST (001_guest, priority 1) is the unauthenticated tier. Its grants use the SYSTEM_WIDE domain scope (match every domain), so pre-merchant onboarding/reference routes work before any merchant is selected.

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
             └─────────────────┘

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 range101 - 499 (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 (an assign_role row references it)409
PermissionCannot delete if granted to a role/user (a grant row references it)409

Role Deletion Cascade

When a deletable role is removed:

  1. Delete all grant rows: Role → Permission
  2. Delete all merchant_role rows: Role ↔ Merchant availability metadata
  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 direct grant rows (User → Permission)
  • GET /policy-definitions/users/{id}/permissions?mode=inheritassign_rolegrant chain (User → Role → Permission)
  • Default: inherit

PolicyDefinition Services

ServiceManages
RolePolicyDefinitionServiceRole↔Permission (grant), User↔Role (assign_role)
UserPolicyDefinitionServiceUser's roles, permissions, organizers, merchants (read)
OrganizerPolicyDefinitionServiceUser↔Organizer / User↔Merchant (join_domain)
PermissionPolicyDefinitionServiceUser↔Permission (grant, direct)
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', '600_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
600_operator600SYSTEM
500_organizer-owner500SYSTEM
110_cashier110SYSTEM
100_employee100SYSTEM
010_customer10SYSTEM
001_guest1SYSTEM

Default Users (0002)

UsernamePasswordRole Identifier
superadminSuperadmin123999_super-admin
adminAdmin123900_admin
ownerOwner123500_organizer-owner
employeeEmployee123100_employee

Cross-Package Permission Seeding

Every service package seeds its own permissions via migration processes (*-seed-permissions, alwaysRun: true) so the catalog stays in sync with code.

Role grants are coarse: seed-role-grants.ts (COARSE_MODULE_GRANTS) gives each fixed role one grant per module it can touch, targeting the module resource node. The action lattice (g5 action_inherits) and resource nesting (g4 resource_inherits) then cover every subject/operation beneath it - e.g. OWNER gets manage, while CASHIER/EMPLOYEE get a narrower action per module. Owner/cashier/employee grants use the ANY_MEMBER domain scope; guest uses SYSTEM_WIDE. Super Admin / Admin / Operator are enforcement-bypassed and need no grants.

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 identity.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.