12-layer enterprise RBAC: identity, roles, ALLOW/DENY overrides, module gating, data scoping, delegation, escalation, SoD, policy exceptions, consent, status-based ACL, and audit
InfraTraq implements a Role-Based Access Control (RBAC) system with hierarchical permissions, multi-entity and multi-project scoping, MFA enforcement, session management, delegation of authority, and a comprehensive audit trail. The system governs access across all 1,220+ screens with granular per-feature, per-action permissions.
| Status | Description | Allowed Actions | Next States |
|---|---|---|---|
| Active | User account is active and can authenticate | Login, Access Resources, Update Profile | Locked, Suspended, Inactive |
| Locked | Account locked after failed login attempts | View Only (admin), Admin Unlock | Active |
| Suspended | Account temporarily suspended by admin action | View Only (admin), Reactivate | Active, Inactive |
| Inactive | Account deactivated; no access permitted | View Only (admin), Reactivate | Active |
| MFA Pending | MFA enabled but not yet verified on this session | Submit MFA Code, Cancel Login | Active (verified), Locked (failed) |
| Delegated | User has active delegation granting additional permissions | Access Delegated Resources, View Delegation | Active (delegation expired) |
| Password Expired | Password age exceeds policy max_age_days | Change Password Only | Active |
| Terminated | User permanently removed from the system | Audit View Only | — |
module.feature.actionuser_id — PK, core user account identifierusername — Unique login nameemail — User email addressfirst_name, last_name — User display name fieldspassword_hash — bcrypt-hashed passwordis_active — Account active flagis_locked — Account lockout flaglocked_until — Temporary lockout expiry (auto-unlock after cooldown)mfa_enabled — Multi-factor authentication toggledepartment_id — FK → organization.department (for data scoping)reporting_to — FK → admin.user (self-referential, org hierarchy for escalation)role_id — PK, named role identifierrole_name — Display name of the rolerole_code — Unique machine-readable codeis_system_role — Protected system role flag (cannot be deleted)permission_id — PK, granular permission identifierpermission_code — Unique machine-readable code (e.g., PROC_PO_APPROVE)permission_name — Human-readable display namemodule — Functional module (procurement, finance, etc.)feature — Feature within module (purchase_order, budget, etc.)action — CRUD action (create, read, update, delete, approve, export)resource — Target resource typeid — PK, many-to-many mappingrole_id — FK → admin.rolepermission_id — FK → admin.permissionid — PK, user-role assignmentuser_id — FK → admin.userrole_id — FK → admin.roleentity_id — FK → organization.company_entity (scope)project_id — FK → project.project (scope)valid_from, valid_to — Validity periodsession_id — PK, active session trackeruser_id — FK → admin.usertoken_hash — SHA-256 hash of JWT tokenip_address — Client IP addressdevice_type — Browser/device user-agentexpires_at — Session expiration timestampmfa_id — PK, MFA configuration per useruser_id — FK → admin.usermfa_type — TOTP, SMS, or backup codeis_verified — Whether MFA has been verified/activateddelegation_id — PK, temporary permission delegationdelegator_id — FK → admin.user (person granting)delegate_id — FK → admin.user (person receiving)module_scope — Restrict delegation to specific moduleentity_types — Restrict to specific entity types (PO, WO, invoice, etc.)amount_limit — Maximum monetary authority (INR) for delegated approvalsvalid_from, valid_to — Delegation validity windowcreated_by — FK → admin.user (who created the delegation record)revoked_at, revoke_reason — Early termination trackingid — PK, immutable authentication event loguser_id — FK → admin.useraction — Event type (login, mfa, permission_denied, etc.)ip_address — Source IP of the eventstatus — success, failed, locked, etc.policy_id — PK, organization-wide password rulesmin_length — Minimum password lengthmax_age_days — Maximum password age before forced changelockout_threshold — Failed attempts before account lockoutid — PK, previous password hash recorduser_id — FK → admin.userpassword_hash — bcrypt hash of a previously used passwordcreated_at — When the password was setid — PK, SSO identity provider configurationtenant_id — FK → admin.tenantprovider_code — Unique code (e.g., azure_ad, okta, google)provider_name — Display name of the IdPprotocol — SAML 2.0, OIDC, or OAuth 2.0client_id, client_secret_vault_key — OAuth credentials (secret stored in vault)metadata_url — SAML metadata or OIDC discovery URLconfig — JSONB additional configuration (claim mappings, scopes)is_active — Provider enabled/disabled flagid — PK, user-to-SSO-provider mappinguser_id — FK → admin.userprovider_id — FK → admin.sso_providerexternal_id — User identifier in the external IdPexternal_email — Email from the IdP assertionlast_login_at — Last SSO-based login timestampid — PK, API key for service-to-service authenticationuser_id — FK → admin.user (owner)key_hash — SHA-256 hash of the API key (plain key shown once at creation)name — Descriptive label for the keyscopes — JSON array of permitted scopes (e.g., ["read:projects","write:reports"])rate_limit — Requests per minute limitexpires_at — Expiry date (mandatory, max 1 year)last_used_at — Last activity timestampis_active — Revocable active flagid — PK, registered application/moduleapp_code — Unique machine-readable code (e.g., PROCUREMENT, FINANCE)app_name — Display name for navigationroute_url — Base URL path for the applicationicon — Font Awesome icon classdisplay_order — Sidebar sort orderis_active — Module enabled/disabled per deploymentid — PK, user-to-application junctionuser_id — FK → admin.userapp_id — FK → admin.applicationgranted_by — FK → admin.user (who granted access)granted_at — Timestamp of grantis_active — Revocable flagid — PK, GDPR/DPDPA consent recorduser_id — FK → admin.userconsent_type — Type: data_processing, marketing, analytics, third_party_sharingpurpose — Human-readable purpose descriptionis_consented — Current consent stateconsent_text — Full legal text presented to userconsented_at — When consent was givenconsent_ip_address, consent_user_agent — Evidence of consent originwithdrawn_at, withdrawal_reason — Withdrawal trackingversion — Consent version for re-consent on policy updatesid — PK, remediation action for SoD violationviolation_id — FK → fraud.sod_violationremediation_type — remove_role, add_compensating_control, accept_risk, reassign_dutydescription — Remediation action detailsassigned_to — FK → admin.user (responsible person)target_date — Expected completion datecompletion_date — Actual completion dateevidence_url — Link to supporting evidencestatus — pending, in_progress, completed, overdueid — PK, time-bound policy bypass requesttenant_id — FK → admin.tenantpolicy_code, policy_name — Which policy is being exceptedentity_type, entity_id — Target entity (user, role, transaction)exception_reason — Why the exception is neededbusiness_justification — Business case for the exceptionrisk_assessment — Risk level: low, medium, high, criticalmitigating_controls — Compensating controls in placerequested_by, approved_by — FK → admin.userstatus — requested, approved, rejected, expired, revokedvalid_from, valid_to — Exception validity windowid — PK, configurable compliance rule per tenanttenant_id — FK → admin.tenantrule_code, rule_name — Unique rule identifier and display namecategory — financial, operational, security, regulatoryseverity — info, warning, critical, blockingapplies_to — Target scope (e.g., all_users, finance_roles, admin_roles)threshold_amount — Monetary threshold for rule trigger (INR)condition_expression — JSONB rule expression evaluated at runtimeaction_on_violation — warn, block, escalate, log_onlyis_active — Rule enabled/disabled toggleid — PK, configurable status definition per moduletenant_id — FK → admin.tenantmodule_type — Which module (purchase_order, work_order, invoice, etc.)code, name — Machine code and display nameis_initial — Whether this is the default starting statusis_terminal — Whether this status is a final state (no further transitions)allows_edit — Whether the entity can be edited in this statusdisplay_order — Sort order in UI dropdownsid — PK, allowed state machine transitionfrom_status_id — FK → system.status_master (source state)to_status_id — FK → system.status_master (target state)required_role — Role code required to perform this transitionrequires_approval — Whether transition needs approval workflowauto_notify — Auto-send notification on transitionis_active — Transition rule enabled/disabledid — PK, recall/withdraw a submitted approval requestentity_type, entity_id — The entity being recalled (PO, WO, invoice, etc.)withdrawal_reason — Why the approval is being withdrawnwithdrawn_by — FK → admin.user (who initiated withdrawal)status — requested, approved, rejectedapproved_by — FK → admin.user (who approved the withdrawal)new_status — Status the entity reverts to after withdrawalsession_id — Reference to admin.user_sessionuser_id, tenant_id — User and tenant contextevent_type — session_created, session_renewed, session_expired, session_revokedip_address, device_info — Client context at event timetimestamp — Event timestamp with millisecond precisionviolation_id — Reference to fraud.sod_violationuser_id, tenant_id — Who triggered the violationconflicting_roles — Array of conflicting role pairsconflicting_actions — Array of conflicting permission tuplesrisk_score — Computed risk score (0-100)detection_method — real_time, scheduled_scan, access_reviewremediation_status — Tracking of remediation progressevent_type — policy_exception_requested, rule_triggered, compliance_check_passed, etc.entity_type, entity_id — What entity was involvedrule_code — Which compliance rule was evaluatedresult — passed, warned, blocked, exception_appliedactor_id, tenant_id — Who performed the actiondetails — Full event context (JSONB)User submits credentials. The system validates the username and password_hash against admin.user. Rate limiting is applied (e.g., 10 attempts per minute). If is_locked = true, login is immediately rejected. Failed attempts are recorded in admin.login_audit.
If mfa_enabled = true on the user record, the system requires a second factor from admin.mfa_config. Supported types: TOTP (authenticator app), SMS OTP, or backup codes. The session is not created until MFA is verified.
On successful authentication, a JWT is generated containing claims for user_id, entity_id, and project_id scope. A record is created in admin.user_session with the token_hash, ip_address, device_type, and expires_at.
On each API request: validate the JWT token, extract the user_id, load all active roles from admin.user_role (filtered by current entity/project scope and validity period), then aggregate all permissions from admin.role_permission joined to admin.permission.
The requested action is expressed as module.feature.action (e.g., procurement.purchase_order.approve). The system checks whether this tuple exists in the user's effective permission set. If not found, the request is denied and logged.
A user's role assignment in admin.user_role may be scoped to a specific entity_id and/or project_id. A NULL scope means "all entities/projects." The system filters permissions based on the request's target entity and project context.
If the user has an active delegation in admin.delegation (where valid_from ≤ now ≤ valid_to), the delegated user's permissions (filtered by module_scope) are merged into the effective permission set. This enables temporary authority transfer during leave or travel.
Every login attempt (success/failure), permission denial, critical action (role change, user lock/unlock, delegation), and session lifecycle event is logged to admin.login_audit. Logs are immutable and retained per compliance policy (SOX, ISO 27001).
class AuthService { /** Authenticate user — validate credentials + check lockout + audit */ async authenticate(username, password) { const user = await User.findOne({ username }); if (!user) { await this.auditLog(null, 'login', 'user_not_found'); throw new AuthError(); } if (user.is_locked) { await this.auditLog(user.user_id, 'login', 'account_locked'); throw new AccountLockedError(); } const valid = await bcrypt.compare(password, user.password_hash); if (!valid) { await this.incrementFailedAttempts(user.user_id); await this.auditLog(user.user_id, 'login', 'invalid_password'); throw new AuthError(); } await this.auditLog(user.user_id, 'login', 'success'); return user; } /** Verify MFA — TOTP validation against user's mfa_config */ async verifyMFA(userId, code) { const config = await MfaConfig.findOne({ user_id: userId, is_verified: true }); if (!config) throw new MfaNotConfiguredError(); const valid = totp.verify({ token: code, secret: config.secret }); if (!valid) { await this.auditLog(userId, 'mfa', 'failed'); throw new MfaError(); } await this.auditLog(userId, 'mfa', 'success'); return true; } /** Create session — JWT generation with entity/project claims */ async createSession(userId) { const roles = await this.getEffectiveRoles(userId); const token = jwt.sign( { user_id: userId, roles: roles.map(r => r.role_code) }, process.env.JWT_SECRET, { expiresIn: '8h' } ); await UserSession.create({ user_id: userId, token_hash: sha256(token), ip_address: req.ip, device_type: req.headers['user-agent'], expires_at: new Date(Date.now() + 8 * 3600000) }); return token; } } class PermissionService { /** Core permission check — module.feature.action against user's effective permissions */ async hasPermission(userId, module, feature, action, entityId, projectId) { const roles = await this.getEffectiveRoles(userId, entityId, projectId); const roleIds = roles.map(r => r.role_id); const perms = await RolePermission.findAll({ role_id: { $in: roleIds }, include: [{ model: Permission, where: { module, feature, action } }] }); if (perms.length === 0) { await this.auditLog(userId, 'permission_denied', `${module}.${feature}.${action}`); return false; } return true; } /** Get effective roles — union of direct roles + delegated roles */ async getEffectiveRoles(userId, entityId, projectId) { const now = new Date(); // Direct roles scoped to entity/project const direct = await UserRole.findAll({ user_id: userId, entity_id: { $in: [entityId, null] }, project_id: { $in: [projectId, null] }, valid_from: { $lte: now }, valid_to: { $gte: now } }); // Delegated roles from active delegations const delegations = await Delegation.findAll({ delegate_id: userId, valid_from: { $lte: now }, valid_to: { $gte: now } }); const delegatedRoles = []; for (const d of delegations) { const roles = await UserRole.findAll({ user_id: d.delegator_id }); delegatedRoles.push(...roles.filter(r => !d.module_scope || r.module === d.module_scope)); } return [...direct, ...delegatedRoles]; } /** Enforce password policy — complexity + history check */ async enforcePasswordPolicy(userId, newPassword) { const policy = await PasswordPolicy.findOne({ is_active: true }); if (newPassword.length < policy.min_length) throw new PolicyError('too_short'); if (!/[A-Z]/.test(newPassword)) throw new PolicyError('needs_uppercase'); if (!/[0-9]/.test(newPassword)) throw new PolicyError('needs_digit'); if (!/[!@#$%]/.test(newPassword)) throw new PolicyError('needs_special'); // Check password history (last 5 passwords) const history = await PasswordHistory.findAll({ user_id: userId, limit: 5 }); for (const h of history) { if (await bcrypt.compare(newPassword, h.hash)) throw new PolicyError('reused'); } return true; } }
| Rule | Condition | Action |
|---|---|---|
| Password Complexity | New password does not meet policy (min length, uppercase, digit, special char) | Reject with specific guidance |
| Session Timeout | Session exceeds configurable idle/absolute timeout | Invalidate token, require re-login |
| Max Concurrent Sessions | User exceeds max allowed active sessions | Terminate oldest session |
| IP-Based Access Control | Login from non-whitelisted IP (for admin roles) | Block access + notify security team |
| Failed Login Lockout | 5 consecutive failed login attempts | Set is_locked = true, require admin unlock |
| Self-Grant Prevention | User attempts to assign permissions to themselves | Block action, log to audit trail |
| System Role Protection | Attempt to delete a role where is_system_role = true | Reject deletion, display warning |
| Event | Trigger Condition | Auto Action |
|---|---|---|
| Failed Login (5x) | 5 consecutive failed password attempts | Lock account (is_locked = true) + email admin notification |
| Password Expiry | Password age exceeds max_age_days from policy | Force password change on next login |
| Delegation Created | New record inserted in admin.delegation | Notify delegate via email/SMS + log audit entry |
| Session Expired | expires_at < NOW() on user_session | Cleanup token, remove session record |
| Role Changed | Insert/update/delete on admin.user_role | Invalidate cached permissions, force re-evaluation on next request |
| MFA Enabled | User enables MFA in profile settings | Require MFA verification on next login before session creation |
login_audit recordsvalid_from and valid_to dates; auto-expire when period endsvalid_to — creates permanent shadow accessInfraTraq implements a comprehensive 12-layer RBAC framework aligned with enterprise security standards (SOX, ISO 27001, DPDPA). Each layer builds on the previous, evaluated in sequence during every access request.
Before checking fine-grained permissions, the system verifies whether the user has been granted access to the application/module itself. This controls sidebar visibility and navigation gating — users who don't have access to a module cannot see it in the sidebar or navigate to its screens.
user_app_accessuser_app_access.is_active = true for the current usersystem.feature_flag can override module availability (e.g., disable ESG module for tenants without license)system.module_config controls license_type and max_users per module per tenantAfter verifying module access and permissions, the system applies row-level security to filter data based on the user's organizational scope. This ensures users only see records belonging to their authorized scope level.
// Row-Level Security Middleware function applyDataScope(query, user, scopeLevel) { // Always apply tenant isolation (mandatory) query.where.tenant_id = user.tenant_id; switch (scopeLevel) { case 'entity': query.where.entity_id = { $in: user.assigned_entity_ids }; break; case 'department': query.where.department_id = user.department_id; break; case 'location': query.where.location_id = { $in: user.assigned_location_ids }; break; case 'own': query.where.created_by = user.user_id; break; // 'all' = no additional filter (tenant scope only) } return query; }
entity scope cannot see records from other entities even if they have admin permissionsSoD enforcement prevents users from holding conflicting roles (e.g., "Create PO" + "Approve PO"). When conflicts are detected, they are logged, flagged for remediation, and tracked through completion. Enhanced with fraud.sod_violation columns and the new fraud.sod_remediation table.
conflicting_actions — Array of conflicting permission tuplesentity_type / entity_id — What entity triggered the violationviolation_type — real_time, scheduled_scan, access_reviewexception_granted — Whether a policy exception overrides this violationexception_reason — Business justification for the exceptiontarget_date, completion_date, and evidence_urlPolicy exceptions provide a controlled, auditable mechanism to temporarily bypass security rules. Each exception requires business justification, risk assessment, management approval, and has an explicit validity window. Managed through the compliance schema.
compliance_rule defines configurable rules per tenantwarn (allow + alert), block (deny), escalate (route to compliance officer), log_onlyGDPR and India's Digital Personal Data Protection Act (DPDPA) 2023 require explicit, informed consent before processing personal data. InfraTraq tracks consent at the individual level with full audit trail of consent and withdrawal events.
consent_ip_address and consent_user_agent provide evidence of originThe system presents consent collection UI at first login and on policy version changes. Users can view and manage their consents from Account Settings → Privacy & Consent. Withdrawal of required consents triggers account restriction (read-only mode) until consent is re-granted or data deletion is requested.
Beyond role-based permissions, entity access is further gated by the current status of the entity. A Purchase Order in "Approved" status cannot be edited by anyone, regardless of their role. Status transitions are governed by a configurable state machine stored in system.status_master and system.status_transition.
// Status-Based ACL Check async function canTransition(userId, entityType, entityId, targetStatus) { const entity = await getEntity(entityType, entityId); const currentStatus = await StatusMaster.findById(entity.status_id); // Terminal status — no further transitions allowed if (currentStatus.is_terminal) return { allowed: false, reason: 'terminal_status' }; // Check if transition is defined in state machine const transition = await StatusTransition.findOne({ from_status_id: currentStatus.id, to_status_id: targetStatus.id, is_active: true }); if (!transition) return { allowed: false, reason: 'invalid_transition' }; // Verify user has required role for this transition const userRoles = await getUserRoleCodes(userId); if (!userRoles.includes(transition.required_role)) return { allowed: false, reason: 'insufficient_role' }; // Check if transition requires approval workflow if (transition.requires_approval) return { allowed: true, requires_approval: true }; return { allowed: true, requires_approval: false }; }
allows_edit determines if fields can be modified in a given statusis_terminal marks end-of-lifecycle statuses (no further transitions)required_role (e.g., only Approver can move to "Approved")auto_notify sends notifications on status change to relevant stakeholderstenant_id scopingInfraTraq uses a dual-database audit architecture: PostgreSQL for structured, transactional audit records and MongoDB for high-volume, JSON-rich event logs. This provides both relational integrity and the flexibility of document-based storage for complex audit payloads.
| Audit Domain | PostgreSQL (RDBMS) | MongoDB Collection | Retention |
|---|---|---|---|
| Authentication Events | admin.login_audit | auth_events | 2 years / 7 years |
| Session Lifecycle | admin.user_session | session_lifecycle_log | Active / 90 days |
| Permission Denials | admin.login_audit (action=denied) | auth_events | 2 years / 7 years |
| Role & Permission Changes | admin.user_role, admin.role_permission | entity_change_log | Permanent / 7 years |
| SoD Violations | fraud.sod_violation | sod_violation_log | Permanent / 7 years |
| Compliance Events | compliance.policy_exception, compliance.compliance_rule | compliance_audit_log | Permanent / 7 years |
| Entity CRUD Operations | Per-module tables (history records) | entity_change_log | Permanent / 7 years |
| Consent Events | admin.user_consent | compliance_audit_log | 7 years (DPDPA) |
| API Key Usage | admin.api_key.last_used_at | api_request_log | Rolling / 90 days |
actor_id, ip_address, timestamp, and tenant_idold_values / new_values stored for field-level change trackingEvery access request passes through this complete 19-step evaluation sequence. The flow implements a DENY-wins algorithm where explicit denials at any layer override all grants.
Validate credentials (password / SSO token / API key) against admin.user, admin.sso_provider, or admin.api_key. Check is_active, is_locked, and locked_until.
Compare password_updated_at against password_policy.max_age_days. If expired, force password change. Check admin.password_history to prevent reuse of last N passwords.
If mfa_enabled = true, require TOTP/SMS/backup code from admin.mfa_config. Fail → increment attempts → lock if threshold exceeded.
Create JWT with claims (user_id, tenant_id, entity_ids, role_codes). Insert into admin.user_session. Log to MongoDB session_lifecycle_log.
Verify required consents exist in admin.user_consent. If consent version mismatch, present re-consent UI. Block access to personal data features until consent is granted.
Extract tenant_id from JWT claims. All subsequent queries are filtered by tenant (absolute isolation boundary for SaaS).
Query admin.user_app_access for the target module. If no active grant exists, deny access and hide module from navigation. Also check system.feature_flag and system.module_config.license_type.
Fetch all active roles from admin.user_role where valid_from ≤ now ≤ valid_to, scoped to the current entity and project context.
Check admin.delegation for active delegations where user is the delegate. Merge delegated permissions (filtered by module_scope and amount_limit) into the effective set.
Join admin.role_permission → admin.permission to build the full set of module.feature.action tuples for all effective roles.
Check admin.user_permission_override for explicit ALLOW or DENY entries. DENY always wins — an explicit DENY overrides any role-based ALLOW. Priority: Explicit DENY > Explicit ALLOW > Role Grant > Default Deny.
Verify the requested module.feature.action exists in the resolved permission set. If not found → DENY and log to admin.login_audit.
Apply row-level security filter (tenant → entity → department → location → own record) based on the user's scope level. Filter query results accordingly.
For sensitive operations (approvals, financial transactions), run SoD check against fraud.sod_rule. If conflict detected, log to fraud.sod_violation and MongoDB sod_violation_log. Block or warn per rule configuration.
Run active rules from compliance.compliance_rule. Check thresholds (e.g., PO amount > ₹50L requires dual approval). Action: warn, block, escalate, or log_only per rule severity.
If blocked by SoD or compliance rule, check compliance.policy_exception for an active, approved exception covering this entity/action within the validity window.
For write/transition operations, verify the entity's current status allows the action via system.status_master.allows_edit and system.status_transition. Terminal statuses block all modifications.
All 17 checks passed — execute the requested operation. For state transitions, update the entity status and trigger auto_notify if configured on the transition rule.
Log the complete action to both PostgreSQL (admin.login_audit + entity history tables) and MongoDB (entity_change_log with JSON old_values/new_values). Include actor, timestamp, IP, and full change payload.
user_permission_override → immediate deny, no further checksuser_permission_override → grant access (unless blocked by Priority 1)role_permission aggregation → grant access