Re_Backend/docs/RBAC_IMPLEMENTATION.md

16 KiB

Role-Based Access Control (RBAC) Implementation

Overview

The system now supports three user roles for granular access control:

Role Access Level Use Case
USER Standard Default role for all users - create/view own requests
MANAGEMENT Enhanced Read View all requests across organization (read-only)
ADMIN Full Access System configuration, user management, all workflows

User Roles

1. USER (Default)

Permissions:

  • Create new workflow requests
  • View own requests
  • Participate in assigned workflows (as approver/spectator)
  • Add work notes to requests they're involved in
  • Upload documents to own requests
  • Cannot view other users' requests (unless added as participant)
  • Cannot access system configuration
  • Cannot manage users or roles

Use Case: Regular employees creating and managing their workflow requests


2. MANAGEMENT

Permissions:

  • All USER permissions
  • View ALL requests across organization (read-only)
  • Access comprehensive dashboards with organization-wide analytics
  • Export reports across all departments
  • View TAT performance metrics for all approvers
  • Cannot approve/reject requests (unless explicitly added as approver)
  • Cannot modify system configuration
  • Cannot manage user roles

Use Case: Department heads, managers, auditors needing visibility into all workflows


3. ADMIN

Permissions:

  • All MANAGEMENT permissions
  • All USER permissions
  • Manage system configuration
  • Assign user roles
  • Manage holiday calendar
  • Configure email/notification settings
  • Access audit logs
  • Manage AI provider settings

Use Case: System administrators, IT staff managing the workflow platform


Database Schema

Migration Applied

-- Create ENUM type for roles
CREATE TYPE user_role_enum AS ENUM ('USER', 'MANAGEMENT', 'ADMIN');

-- Add role column to users table
ALTER TABLE users 
ADD COLUMN role user_role_enum NOT NULL DEFAULT 'USER';

-- Migrate existing data
UPDATE users 
SET role = CASE 
  WHEN is_admin = true THEN 'ADMIN'
  ELSE 'USER'
END;

-- Create index for performance
CREATE INDEX idx_users_role ON users(role);

Updated Users Table

users {
    uuid user_id PK
    varchar email UK
    varchar display_name
    varchar department
    varchar designation
    boolean is_active
    user_role_enum role ← NEW FIELD
    boolean is_admin ← DEPRECATED (kept for compatibility)
    timestamp created_at
    timestamp updated_at
}

Backend Implementation

Model (User.ts)

export type UserRole = 'USER' | 'MANAGEMENT' | 'ADMIN';

interface UserAttributes {
  // ... other fields
  role: UserRole; // RBAC role
  isAdmin: boolean; // DEPRECATED
}

class User extends Model<UserAttributes> {
  public role!: UserRole;
  
  // Helper methods
  public isUserRole(): boolean {
    return this.role === 'USER';
  }
  
  public isManagementRole(): boolean {
    return this.role === 'MANAGEMENT';
  }
  
  public isAdminRole(): boolean {
    return this.role === 'ADMIN';
  }
  
  public hasManagementAccess(): boolean {
    return this.role === 'MANAGEMENT' || this.role === 'ADMIN';
  }
  
  public hasAdminAccess(): boolean {
    return this.role === 'ADMIN';
  }
}

Middleware Usage

1. Require Admin Only

import { requireAdmin } from '@middlewares/authorization.middleware';

// Only ADMIN can access
router.post('/admin/config', authenticate, requireAdmin, adminController.updateConfig);
router.post('/admin/users/:userId/role', authenticate, requireAdmin, adminController.updateUserRole);
router.post('/admin/holidays', authenticate, requireAdmin, adminController.addHoliday);

2. Require Management or Admin

import { requireManagement } from '@middlewares/authorization.middleware';

// MANAGEMENT and ADMIN can access (read-only for management)
router.get('/reports/all-requests', authenticate, requireManagement, reportController.getAllRequests);
router.get('/analytics/department', authenticate, requireManagement, analyticsController.getDepartmentStats);
router.get('/dashboard/organization', authenticate, requireManagement, dashboardController.getOrgWideStats);

3. Flexible Role Checking

import { requireRole } from '@middlewares/authorization.middleware';

// Multiple role options
router.get('/workflows/search', authenticate, requireRole(['MANAGEMENT', 'ADMIN']), workflowController.search);
router.post('/workflows/export', authenticate, requireRole(['MANAGEMENT', 'ADMIN']), workflowController.export);

// Any authenticated user
router.get('/profile', authenticate, requireRole(['USER', 'MANAGEMENT', 'ADMIN']), userController.getProfile);

4. Programmatic Role Checking in Controllers

import { hasManagementAccess, hasAdminAccess } from '@middlewares/authorization.middleware';

export async function getWorkflows(req: Request, res: Response) {
  const user = req.user;
  
  // Management and Admin can see ALL workflows
  if (hasManagementAccess(user)) {
    const allWorkflows = await WorkflowRequest.findAll();
    return res.json({ success: true, data: allWorkflows });
  }
  
  // Regular users only see their own workflows
  const userWorkflows = await WorkflowRequest.findAll({
    where: { initiatorId: user.userId }
  });
  
  return res.json({ success: true, data: userWorkflows });
}

Example Route Implementations

Admin Routes (ADMIN only)

// src/routes/admin.routes.ts
import { Router } from 'express';
import { authenticate } from '@middlewares/auth.middleware';
import { requireAdmin } from '@middlewares/authorization.middleware';
import * as adminController from '@controllers/admin.controller';

const router = Router();

// All admin routes require ADMIN role
router.use(authenticate, requireAdmin);

// System configuration
router.get('/config', adminController.getConfig);
router.put('/config', adminController.updateConfig);

// User role management
router.put('/users/:userId/role', adminController.updateUserRole);
router.get('/users/admins', adminController.getAllAdmins);
router.get('/users/management', adminController.getAllManagement);

// Holiday management
router.post('/holidays', adminController.createHoliday);
router.delete('/holidays/:holidayId', adminController.deleteHoliday);

export default router;

Management Routes (MANAGEMENT + ADMIN)

// src/routes/management.routes.ts
import { Router } from 'express';
import { authenticate } from '@middlewares/auth.middleware';
import { requireManagement } from '@middlewares/authorization.middleware';
import * as managementController from '@controllers/management.controller';

const router = Router();

// All management routes require MANAGEMENT or ADMIN role
router.use(authenticate, requireManagement);

// Organization-wide dashboards (read-only)
router.get('/dashboard/organization', managementController.getOrgDashboard);
router.get('/requests/all', managementController.getAllRequests);
router.get('/analytics/tat-performance', managementController.getTATPerformance);
router.get('/analytics/approver-stats', managementController.getApproverStats);
router.get('/reports/export', managementController.exportReports);

// Department-wise analytics
router.get('/analytics/department/:deptName', managementController.getDepartmentAnalytics);

export default router;

Workflow Routes (Mixed Permissions)

// src/routes/workflow.routes.ts
import { Router } from 'express';
import { authenticate } from '@middlewares/auth.middleware';
import { requireManagement, requireRole } from '@middlewares/authorization.middleware';
import * as workflowController from '@controllers/workflow.controller';

const router = Router();

// USER: Create own request (all roles can do this)
router.post('/workflows', authenticate, workflowController.create);

// USER: View own requests (filtered by role in controller)
router.get('/workflows/my-requests', authenticate, workflowController.getMyRequests);

// MANAGEMENT + ADMIN: Search all requests
router.get('/workflows/search', authenticate, requireManagement, workflowController.searchAll);

// ADMIN: Delete workflow
router.delete('/workflows/:id', authenticate, requireRole(['ADMIN']), workflowController.delete);

export default router;

Controller Implementation Examples

Example 1: Dashboard with Role-Based Data

// src/controllers/dashboard.controller.ts
import { hasManagementAccess } from '@middlewares/authorization.middleware';

export async function getDashboard(req: Request, res: Response) {
  const user = req.user;
  
  // MANAGEMENT and ADMIN: See organization-wide stats
  if (hasManagementAccess(user)) {
    const stats = await dashboardService.getOrganizationStats();
    return res.json({
      success: true,
      data: {
        ...stats,
        scope: 'organization', // Indicates full visibility
        userRole: user.role
      }
    });
  }
  
  // USER: See only personal stats
  const stats = await dashboardService.getUserStats(user.userId);
  return res.json({
    success: true,
    data: {
      ...stats,
      scope: 'personal', // Indicates limited visibility
      userRole: user.role
    }
  });
}

Example 2: User Role Update (ADMIN only)

// src/controllers/admin.controller.ts
export async function updateUserRole(req: Request, res: Response) {
  const { userId } = req.params;
  const { role } = req.body;
  
  // Validate role
  if (!['USER', 'MANAGEMENT', 'ADMIN'].includes(role)) {
    return res.status(400).json({
      success: false,
      error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN'
    });
  }
  
  // Update user role
  const user = await User.findByPk(userId);
  if (!user) {
    return res.status(404).json({
      success: false,
      error: 'User not found'
    });
  }
  
  const oldRole = user.role;
  user.role = role;
  
  // Sync is_admin for backward compatibility
  user.isAdmin = (role === 'ADMIN');
  await user.save();
  
  // Log role change
  console.log(`✅ User role updated: ${user.email} - ${oldRole}${role}`);
  
  return res.json({
    success: true,
    message: `User role updated from ${oldRole} to ${role}`,
    data: {
      userId: user.userId,
      email: user.email,
      role: user.role
    }
  });
}

Frontend Integration

Update Auth Context

// Frontend: src/contexts/AuthContext.tsx
interface User {
  userId: string;
  email: string;
  displayName: string;
  role: 'USER' | 'MANAGEMENT' | 'ADMIN'; // ← Add role
}

// Helper functions
export function isAdmin(user: User | null): boolean {
  return user?.role === 'ADMIN';
}

export function isManagement(user: User | null): boolean {
  return user?.role === 'MANAGEMENT' || user?.role === 'ADMIN';
}

export function hasManagementAccess(user: User | null): boolean {
  return user?.role === 'MANAGEMENT' || user?.role === 'ADMIN';
}

Role-Based UI Rendering

// Show admin menu only for ADMIN
{user?.role === 'ADMIN' && (
  <NavItem to="/admin/config">
    <Settings /> System Configuration
  </NavItem>
)}

// Show management dashboard for MANAGEMENT and ADMIN
{(user?.role === 'MANAGEMENT' || user?.role === 'ADMIN') && (
  <NavItem to="/dashboard/organization">
    <TrendingUp /> Organization Dashboard
  </NavItem>
)}

// Show all requests for MANAGEMENT and ADMIN
{hasManagementAccess(user) && (
  <NavItem to="/requests/all">
    <FileText /> All Requests
  </NavItem>
)}

Migration Guide

Running the Migration

# Run migration to add role column
npm run migrate

# Verify migration
psql -d royal_enfield_db -c "SELECT email, role, is_admin FROM users LIMIT 10;"

Expected Results

Before Migration:
+-------------------------+-----------+
| email                   | is_admin  |
+-------------------------+-----------+
| admin@royalenfield.com  | true      |
| user1@royalenfield.com  | false     |
+-------------------------+-----------+

After Migration:
+-------------------------+-----------+-----------+
| email                   | role      | is_admin  |
+-------------------------+-----------+-----------+
| admin@royalenfield.com  | ADMIN     | true      |
| user1@royalenfield.com  | USER      | false     |
+-------------------------+-----------+-----------+

Assigning Roles

Via SQL (Direct Database)

-- Make user a MANAGEMENT role
UPDATE users 
SET role = 'MANAGEMENT', is_admin = false 
WHERE email = 'manager@royalenfield.com';

-- Make user an ADMIN role
UPDATE users 
SET role = 'ADMIN', is_admin = true 
WHERE email = 'admin@royalenfield.com';

-- Revert to USER role
UPDATE users 
SET role = 'USER', is_admin = false 
WHERE email = 'user@royalenfield.com';

Via API (Admin Endpoint)

# Update user role (ADMIN only)
POST /api/v1/admin/users/:userId/role
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "role": "MANAGEMENT"
}

Testing

Test Scenarios

describe('RBAC Tests', () => {
  test('USER cannot access admin config', async () => {
    const response = await request(app)
      .get('/api/v1/admin/config')
      .set('Authorization', `Bearer ${userToken}`);
    
    expect(response.status).toBe(403);
    expect(response.body.error).toContain('Admin access required');
  });
  
  test('MANAGEMENT can view all requests', async () => {
    const response = await request(app)
      .get('/api/v1/management/requests/all')
      .set('Authorization', `Bearer ${managementToken}`);
    
    expect(response.status).toBe(200);
    expect(response.body.data).toBeInstanceOf(Array);
  });
  
  test('ADMIN can update user roles', async () => {
    const response = await request(app)
      .put(`/api/v1/admin/users/${userId}/role`)
      .set('Authorization', `Bearer ${adminToken}`)
      .send({ role: 'MANAGEMENT' });
    
    expect(response.status).toBe(200);
    expect(response.body.data.role).toBe('MANAGEMENT');
  });
});

Best Practices

1. Always Use Role Column

// ✅ GOOD: Use new role system
if (user.role === 'ADMIN') {
  // Admin logic
}

// ❌ BAD: Don't use deprecated is_admin
if (user.isAdmin) {
  // Deprecated approach
}

2. Use Helper Functions

// ✅ GOOD: Use provided helpers
if (user.hasManagementAccess()) {
  // Management or Admin logic
}

// ❌ BAD: Manual checking
if (user.role === 'MANAGEMENT' || user.role === 'ADMIN') {
  // Verbose
}

3. Route Protection

// ✅ GOOD: Clear role requirements
router.get('/sensitive-data', authenticate, requireManagement, controller.getData);

// ❌ BAD: Role checking in controller only
router.get('/sensitive-data', authenticate, controller.getData); // No middleware check

Backward Compatibility

The is_admin field is DEPRECATED but kept for backward compatibility:

  • Existing code using is_admin will continue to work
  • Migration automatically syncs role and is_admin
  • ⚠️ New code should use role instead of is_admin
  • 📅 is_admin will be removed in future version

Sync Logic

// When updating role, sync is_admin
user.role = 'ADMIN';
user.isAdmin = true; // Auto-sync

user.role = 'USER';
user.isAdmin = false; // Auto-sync

Quick Reference

Task Code Example
Check if ADMIN user.role === 'ADMIN' or user.isAdminRole()
Check if MANAGEMENT user.role === 'MANAGEMENT' or user.isManagementRole()
Check if USER user.role === 'USER' or user.isUserRole()
Check Management+ user.hasManagementAccess()
Middleware: Admin only requireAdmin
Middleware: Management+ requireManagement
Middleware: Custom roles requireRole(['ADMIN', 'MANAGEMENT'])
Update role (SQL) UPDATE users SET role = 'MANAGEMENT' WHERE email = '...'
Update role (API) PUT /admin/users/:userId/role { role: 'MANAGEMENT' }

Support

For questions or issues:

  • Check migration logs: logs/migration.log
  • Review user roles: SELECT email, role FROM users;
  • Test role access: Use provided test scenarios

Migration File: src/migrations/20251112-add-user-roles.ts
Model File: src/models/User.ts
Middleware File: src/middlewares/authorization.middleware.ts