Remote Eaze
Security

Permissions & Maker-Checker

4-layer hybrid permission engine, RBAC management API, Redis caching, and multi-level approval workflows.

The system implements a hybrid permission model combining role-based access control (RBAC) with conditional rules, amount thresholds, and multi-level approval workflows. The permission engine is a pure function in packages/shared — no I/O, runs identically on backend and frontend.

Permission Engine

Decision Flow

Request
  → L0: System Admin Bypass     (roleSlug === "SYSTEM_ADMIN" → allow)
  → L1: Base Permission Matrix   (role × entity × action → deny if missing)
  → L2b: VALIDATION Rules        (tenant-wide hard blocks → deny if match)
  → L2b: PERMISSION Rules        (role-specific overrides → first match wins)
  → L2a: Amount Thresholds       (currency + amount range → strict deny if no match)
  → Fallback: L1 result

Each layer can short-circuit. The engine returns a PermissionResult:

{
  allowed: boolean;
  requiredLevels?: number;  // 0-3 approval levels needed
  reason?: string;
  meta?: {
    layer?: "L0" | "L1" | "L2a" | "L2b";
    matchedRuleId?: string;
    thresholdApplied?: boolean;
  }
}

Layer 0 — System Admin Bypass

If actor.roleSlug === "SYSTEM_ADMIN", the engine returns { allowed: true, requiredLevels: 0 } immediately. No further evaluation occurs.

Layer 1 — Base Permission Matrix (Gatekeeper)

Looks up cache.permissions[entityType][action]. If the entry is missing or allowed === false, access is denied immediately. This is the hard floor — Layers 2a/2b can only refine what Layer 1 already allows.

Each entry also carries a requiredLevel (0 = no approval needed, 1–3 = needs that many approval levels).

Layer 2b — Condition-Based Rules

Two subtypes, both evaluated before thresholds:

VALIDATION rules (tenant-wide, no role binding):

  • Hard blocks that apply to all roles. If conditions match → deny with a custom validationMessage.
  • Example: "No transactions on holidays."

PERMISSION rules (role-specific):

  • Override base permissions when conditions match. Can change requiredLevels and canCreate/canApprove_l1/l2/l3.
  • Sorted by priority — first match wins.
  • Example: "High-risk customers need Level 3 approval."

Condition evaluation supports operators: EQ, NE, GT, LT, IN, NOT_IN, CONTAINS combined with AND/OR logical operators. Missing data for a condition = condition fails (strict).

Layer 2a — Amount Thresholds

Only runs when data.amount is present. Requires data.currency. Finds the matching threshold by currency + min/max amount range. If found, maps the action to the threshold's canCreate/canApprove_l1/l2/l3 booleans. If no matching range exists, access is denied (strict mode).

Overlapping ranges for the same role/entity/currency are rejected at creation time.

Permission Context

Every permission check receives:

{
  actor: { userId, roleId, roleSlug, tenantId, branchId?, branchRestrict? }
  resource: { entityType, entityId? }
  action: string   // "create" | "read" | "update" | "delete" | "export" | "approve_l1" | "approve_l2" | "approve_l3"
  data?: { amount?, currency?, ...fields }
}

Entity Registry

The system defines 38 entity types across these categories:

CategoryExamples
Identity & Accessrole, permission_matrix, transaction_threshold, transaction_rule, user
Licenselicense (special actions: update_contract, update_config)
System/Configbranch, department, cost_centre, tenant_currency, custom_field_definition, etc.
Reference Datacustomer_type, customer_rating, title, gender, source_funds, etc.
Financial Configaccount_type, product, trans_code, work_day_year, sequence, etc.
CRMcustomer, agent
Ledgeraccount, facility, account_mandate, account_statement, etc.
Product Rulesaccount_condition, commission_type, facility_class, activity_rule

Some entities have restricted actions (e.g., product_balance is read/export only; account_statement is read/export only; work_day_month is read/update only).

Redis Caching

Permissions are cached per role at key permissions:{tenantId}:{roleId} with a 1-hour TTL. All users sharing a role share one cache entry.

The cache contains all three layers: base matrix, thresholds, and rules — built from DB by buildUserPermissionCache() on cache miss.

Invalidation triggers:

  • Role-specific mutation (matrix, threshold, or permission rule change) → invalidateRolePermissions(tenantId, roleId) deletes the specific key
  • Tenant-wide mutation (validation rule change) → invalidateTenantPermissions(tenantId) pattern-scans and deletes all permissions:{tenantId}:* keys

Access Control API

All routes under /api/v1/access-control. Every endpoint requires requireAuth + requirePermission guards.

Role Management

MethodPathPermissionDescription
POST/rolesrole / createCreate role. Auto-generates slug from name. SYSTEM_ADMIN provides tenantId; tenant users scoped to own tenant.
GET/rolesrole / readList roles with pagination, search, isActive filter, custom field filters.
GET/roles/:idrole / readGet role details (includes tenantName from License).
PATCH/roles/:idrole / updateUpdate name, description, isActive, customFields. Cannot modify SYSTEM_ADMIN.
DELETE/roles/:idrole / deleteSoft-delete. Cannot delete SYSTEM_ADMIN or roles with assigned users.

Permission Matrix (Layer 1)

MethodPathPermissionDescription
GET/roles/:id/permissionspermission_matrix / readFull matrix for a role (all entity types and their allowed actions).
PUT/roles/:id/permissionspermission_matrix / updateReplace actions for a specific entityType. Transaction: delete old → create new. Cannot modify SYSTEM_ADMIN.

Thresholds (Layer 2a)

MethodPathPermissionDescription
POST/roles/:id/thresholdstransaction_threshold / createCreate amount threshold. Validates no overlapping ranges for same currency/entity.
GET/roles/:id/thresholdstransaction_threshold / readList all thresholds for a role.
DELETE/thresholds/:idtransaction_threshold / deleteSoft-delete a threshold.

Rules (Layer 2b)

MethodPathPermissionDescription
POST/roles/:id/rulestransaction_rule / createCreate a PERMISSION rule (role-specific) with conditions.
POST/rulestransaction_rule / createCreate a global VALIDATION rule (tenant-wide). SYSTEM_ADMIN provides tenantId.
GET/rulestransaction_rule / readList rules with optional entityType and tenantId filters.
DELETE/rules/:idtransaction_rule / deleteSoft-delete a rule.

Protected Roles

The SYSTEM_ADMIN role (tenantId "SYSTEM") cannot be renamed, deactivated, deleted, or have its permissions modified through the API.

Tenant Admin Default Permissions

When a new tenant is provisioned (license created), a TENANT_ADMIN role is automatically seeded with full access (create, read, update, delete) to all 37 non-license entities. For license, it receives only read and update_config — no create, delete, or update_contract (those are System Admin only).

Guard Integration

Two patterns are used in route handlers:

Declarative — for standard CRUD:

preHandler: [requirePermission("customer", "create")]

The guard fetches the cached permissions, calls checkPermission(), attaches request.permissionResult (including requiredLevels), and denies with audit log on failure.

Imperative — for approval workflows where the action depends on record state:

const cache = await request.server.permissions.getUserPermissions(user.tenantId, user.roleId);
const requiredAction = getApprovalAction(entity.recordStatus); // approve_l1, l2, or l3
const result = checkPermission(cache, { actor, resource, action: requiredAction, data });
if (!result.allowed) throw new UnauthorizedError(result.reason);

Maker-Checker Workflow

Entities Requiring Approval

11 operational/financial entities require the maker-checker workflow:

customer, agent, account, facility, product, trans_code, work_day_year, account_condition, commission_type, facility_class, activity_rule

Approval State Machine

CAPTURED → PENDING_AUTH_L3 → PENDING_AUTH_L2 → PENDING_AUTH_L1 → AUTHORIZED
                                                                ↘ REJECTED (can resubmit)
                                                                ↘ DENIED (terminal)

State Transitions

ActionValid FromResult
submitCAPTURED, REJECTEDEnters approval chain at the level determined by requiredLevels (0 → AUTHORIZED, 1 → PENDING_AUTH_L1, 2 → L2, 3 → L3)
approveAny PENDING_AUTH_L*Advances to next level (L3→L2→L1→AUTHORIZED)
rejectAny PENDING_AUTH_L*REJECTED (allows editing and resubmission)
denyAny PENDING_AUTH_L*, REJECTEDDENIED (terminal, no resubmission)

Edit Restrictions

  • Editable: CAPTURED and REJECTED only
  • Read-only: All PENDING states and terminal states (AUTHORIZED, DENIED)

Integration with Permission Engine

The requiredLevels that determines entry point into the approval chain comes from the permission engine result. It can originate from:

  1. Layer 1 base requiredLevel on the permission matrix entry
  2. Layer 2a threshold override (higher amounts → more approvals)
  3. Layer 2b rule override (conditions like risk rating → more approvals)

Each approval action (approve_l1, approve_l2, approve_l3) is itself a permission-checked action. The approver must have that specific action allowed for the entity type in their own role's permission matrix.

Workflow Helpers

Pure utility functions in packages/shared/src/workflow/helpers.ts:

FunctionPurpose
validateStatusTransition(status, action)Guards illegal transitions, throws on violation
getNextApprovalStatus(status)Advances chain: L3→L2→L1→AUTHORIZED
getInitialPendingStatus(requiredLevels)Maps 0→AUTHORIZED, 1→L1, 2→L2, 3→L3
canEditRecord(status)True only for CAPTURED and REJECTED
isTerminalStatus(status)True for AUTHORIZED and DENIED
isPendingApproval(status)True for any PENDING_AUTH_L*
getApprovalLevel(status)Returns 1, 2, or 3 (or null)

On this page