Skip to main content

Roles & Authorization Module

Overview

The roles module provides CASL-based RBAC/ABAC authorization for the entire FlowPOS backend. It is a guard-only module — it has no API endpoints and no CRUD operations. Instead, it enforces access control on every protected endpoint across the system via NestJS guards and metadata decorators.

Authorization operates at two levels:

  • App-level (platform-wide): Root, Admin, Support, Default
  • Business-scoped: Owner, Admin, Cashier, StoreManager, SalesAssociate, and more

Architecture

roles/
├── roles.module.ts # NestJS module wiring
└── infrastructure/
├── roles.guard.ts # RolesGuard — CASL ability evaluation
└── roles.decorators.ts # Metadata decorators for controllers

Why no domain / application / interfaces layers?

This module is a cross-cutting concern, not a business entity. It does not manage its own data — roles are assigned via the business-users module and stored in Firebase custom claims. The guard reads these at runtime to make authorization decisions.

Dependency rule

controllers (any module) → decorators → guard → CASL policies (packages/global)

database (location / businessUser lookups)

How It Works

1. Controller decoration

Controllers annotate their handlers with permission metadata:

// Class-level resource (applies to all handlers)
@UseGuards(RolesGuard)
@PermissionResource(PolicyResource.Product)
export class ProductsController {

// Handler-level action
@PermissionAction(PolicyAction.Create)
@Post()
create(@Body() dto: CreateProductDto) { ... }

// Or use the composite decorator
@Permission(PolicyResource.Product, PolicyAction.Read)
@Get()
findAll() { ... }
}

2. Guard execution flow

When a request hits a decorated handler, RolesGuard.canActivate() executes:

  1. Extract metadata — reads @PermissionResource and @PermissionAction from handler/class
  2. Skip if undecorated — returns true if no metadata (route is unprotected at RBAC level)
  3. Resolve businessId — checks body → params → query → location DB lookup
  4. Extract user context — reads Firebase claims (db_user_id, role, role_by_business_id)
  5. Enrich from DB — if token lacks business role, queries businessUser table
  6. Build subject — merges body + params + query into a CASL subject, injects audit fields
  7. Create abilities — combines user rules (app-level) + business rules (business-scoped) via CASL
  8. Evaluate access — checks Owner → Admin → Root → specific permission, returns boolean

3. Access evaluation priority

The guard checks permissions in this order (first match wins):

PriorityCheckMeaning
1ability.can(All, All, subjectObject)Owner/Admin with full business access
2ability.can(All, resource, subjectObject)Full access to specific resource
3ability.can(All, All, {})Root (platform-wide superadmin)
4ability.can(action, resource, subjectObject)Specific permission check

Decorators

DecoratorLevelPurpose
@PermissionResource(resource)Class or methodSets which PolicyResource the handler protects
@PermissionAction(action)MethodSets which PolicyAction is required
@Permission(resource, action)MethodComposite — sets both in one call

PolicyAction values

Create, Read, Update, Delete, Misc, All

PolicyResource values

60+ resources including: Business, Location, User, Product, Sale, Order, CashRegisterSession, Promotion, Collection, Markdown, etc.

Full list: packages/global/policies/src/enums/resource.enums.ts


Role Hierarchy

App-level roles (AppRoleName)

RolePermissions
RootAll on All — unrestricted
AdminAll on All — platform admin
SupportLimited support access
DefaultCan create businesses, read services/UOM

Business-scoped roles (UniqueRoleName)

RolePermissions
OwnerAll on All within business
Admin / AdministratorFull access within business
StoreManagerBroad access, some restrictions
Cashier / SalesAssociateLimited to sales, register, basic reads
InventoryManagerInventory, purchasing, products
PurchasingManagerPurchasing, suppliers, GRN
AccountantFinanceManagerFinancial modules
WarehouseStaffInventory operations
Other specialized rolesGranular per-module access

Full definitions: packages/global/policies/src/rules/business.rules.ts


BusinessId Resolution

The guard resolves businessId using this fallback chain:

  1. request.body.businessId
  2. request.params.businessId
  3. request.query.businessId
  4. request.params.id (only for /businesses routes)
  5. DB lookup from request.body.locationIdlocation.businessId
  6. Firebase claim current_business_id
  7. First key from role_by_business_id

Audit Field Injection

For write operations, the guard overrides audit fields to prevent client spoofing:

ActionField set
CreatecreatedBy = userId
UpdateupdatedBy = userId

For read operations, minimal scoping is applied for specific resources (e.g., CashMovement.createdBy, CashRegisterSession.cashierId).


Usage in Other Modules

Every module that needs authorization:

  1. Imports RolesModule in its module definition
  2. Adds @UseGuards(RolesGuard) at the controller class level
  3. Adds @PermissionResource(...) at the class level
  4. Adds @PermissionAction(...) on each handler method

Currently used by 29+ controllers and 236+ handler methods across the codebase.


Key Files

FilePurpose
apps/backend/src/roles/roles.module.tsModule definition
apps/backend/src/roles/infrastructure/roles.guard.tsAuthorization guard
apps/backend/src/roles/infrastructure/roles.decorators.tsPermission decorators
packages/global/policies/src/rules/business.rules.tsBusiness-scoped CASL rules
packages/global/policies/src/rules/user.rules.tsApp-level CASL rules
packages/global/policies/src/enums/action.enums.tsPolicyAction enum
packages/global/policies/src/enums/resource.enums.tsPolicyResource enum
packages/global/enums/role.enums.tsRole name enums

Design Decisions

  1. Guard-only module — Roles are not a CRUD resource. They are defined in code (CASL rules) and assigned via business-users. No API needed.
  2. CASL over custom — CASL provides battle-tested attribute-based access control with condition evaluation, reducing custom authorization code.
  3. DB fallback for missing claims — Firebase tokens may not always contain role_by_business_id (e.g., first login). The guard falls back to a DB lookup.
  4. Request mutation — The guard enriches request.firebaseUser with DB-resolved roles so downstream handlers benefit from the lookup.
  5. Audit field injection — Prevents clients from spoofing createdBy/updatedBy by overwriting them in the CASL subject.