# 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 ```sql -- 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) ```typescript export type UserRole = 'USER' | 'MANAGEMENT' | 'ADMIN'; interface UserAttributes { // ... other fields role: UserRole; // RBAC role isAdmin: boolean; // DEPRECATED } class User extends Model { 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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) ```typescript // 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) ```typescript // 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) ```typescript // 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 ```typescript // 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) ```typescript // 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 ```typescript // 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 ```typescript // Show admin menu only for ADMIN {user?.role === 'ADMIN' && ( System Configuration )} // Show management dashboard for MANAGEMENT and ADMIN {(user?.role === 'MANAGEMENT' || user?.role === 'ADMIN') && ( Organization Dashboard )} // Show all requests for MANAGEMENT and ADMIN {hasManagementAccess(user) && ( All Requests )} ``` --- ## Migration Guide ### Running the Migration ```bash # 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) ```sql -- 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) ```bash # Update user role (ADMIN only) POST /api/v1/admin/users/:userId/role Authorization: Bearer Content-Type: application/json { "role": "MANAGEMENT" } ``` --- ## Testing ### Test Scenarios ```typescript 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 ```typescript // ✅ 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 ```typescript // ✅ 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 ```typescript // ✅ 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 ```typescript // 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`