633 lines
16 KiB
Markdown
633 lines
16 KiB
Markdown
# 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<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
|
|
|
|
```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' && (
|
|
<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
|
|
|
|
```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 <admin-token>
|
|
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`
|
|
|