/** * Enhanced Role-Based Access Control (RBAC) Service * * Implements RBAC and ABAC per DoD/MilSpec requirements: * - NIST SP 800-53: AC-2 (Account Management), AC-3 (Access Enforcement) * - NIST SP 800-171: 3.1.1-3.1.23 (Access Control) * * Features: * - Hierarchical roles * - Dynamic permission assignment * - Attribute-Based Access Control (ABAC) * - Role separation of duties * - Least privilege enforcement * - Periodic access reviews */ import { getDb } from '../db' import { logger } from '../lib/logger' export interface Role { id: string name: string description?: string permissions: string[] isSystem: boolean createdAt: Date updatedAt: Date } export interface Permission { id: string name: string resourceType: string action: string conditions?: Record description?: string } export interface UserRole { id: string userId: string roleId: string grantedBy?: string grantedAt: Date expiresAt?: Date metadata?: Record } /** * Check if user has permission */ export async function hasPermission( userId: string, resourceType: string, action: string, resourceId?: string, context?: Record ): Promise { const db = getDb() // Get user's roles const rolesResult = await db.query( `SELECT r.id, r.name, r.permissions, rp.permission_id, p.name as permission_name, p.resource_type, p.action, p.conditions, rp.conditions as role_conditions FROM user_roles ur JOIN roles r ON ur.role_id = r.id LEFT JOIN role_permissions rp ON r.id = rp.role_id LEFT JOIN permissions p ON rp.permission_id = p.id WHERE ur.user_id = $1 AND (ur.expires_at IS NULL OR ur.expires_at > NOW()) AND r.name = 'SYSTEM_ADMIN' OR ( (p.resource_type = $2 OR p.resource_type = '*') AND (p.action = $3 OR p.action = '*') )`, [userId, resourceType, action] ) if (rolesResult.rows.length === 0) { return false } // Check for SYSTEM_ADMIN role (full access) const hasSystemAdmin = rolesResult.rows.some(row => row.name === 'SYSTEM_ADMIN') if (hasSystemAdmin) { return true } // Check permissions with conditions (ABAC) for (const row of rolesResult.rows) { if (!row.permission_id) { // Check role-level permissions const rolePermissions = row.permissions || [] if (rolePermissions.includes('*') || rolePermissions.includes(`${resourceType}:*`) || rolePermissions.includes(`${resourceType}:${action}`)) { // Check conditions if present if (row.role_conditions && !evaluateConditions(row.role_conditions, context)) { continue } return true } } else { // Check permission-level access if ((row.resource_type === resourceType || row.resource_type === '*') && (row.action === action || row.action === '*')) { // Check conditions if present const conditions = row.conditions || row.role_conditions if (conditions && !evaluateConditions(conditions, context)) { continue } return true } } } return false } /** * Evaluate ABAC conditions */ function evaluateConditions(conditions: Record, context?: Record): boolean { if (!conditions || Object.keys(conditions).length === 0) { return true // No conditions = allow } if (!context) { return false // Conditions required but no context provided } // Simple condition evaluation // In production, this should support more complex expressions for (const [key, value] of Object.entries(conditions)) { if (context[key] !== value) { return false } } return true } /** * Get user's roles */ export async function getUserRoles(userId: string): Promise { const db = getDb() const result = await db.query( `SELECT r.id, r.name, r.description, r.permissions, r.is_system, r.created_at, r.updated_at FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = $1 AND (ur.expires_at IS NULL OR ur.expires_at > NOW()) ORDER BY r.name`, [userId] ) return result.rows.map(row => ({ id: row.id, name: row.name, description: row.description, permissions: row.permissions || [], isSystem: row.is_system, createdAt: row.created_at, updatedAt: row.updated_at, })) } /** * Assign role to user */ export async function assignRole( userId: string, roleId: string, grantedBy: string, expiresAt?: Date, metadata?: Record ): Promise { const db = getDb() await db.query( `INSERT INTO user_roles (user_id, role_id, granted_by, expires_at, metadata) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (user_id, role_id) DO UPDATE SET granted_by = $3, expires_at = $4, metadata = $5`, [userId, roleId, grantedBy, expiresAt, JSON.stringify(metadata || {})] ) logger.info('Role assigned', { userId, roleId, grantedBy }) } /** * Revoke role from user */ export async function revokeRole(userId: string, roleId: string): Promise { const db = getDb() await db.query( 'DELETE FROM user_roles WHERE user_id = $1 AND role_id = $2', [userId, roleId] ) logger.info('Role revoked', { userId, roleId }) } /** * Get all roles */ export async function getAllRoles(): Promise { const db = getDb() const result = await db.query( 'SELECT id, name, description, permissions, is_system, created_at, updated_at FROM roles ORDER BY name' ) return result.rows.map(row => ({ id: row.id, name: row.name, description: row.description, permissions: row.permissions || [], isSystem: row.is_system, createdAt: row.created_at, updatedAt: row.updated_at, })) } /** * Create a new role */ export async function createRole( name: string, description?: string, permissions: string[] = [] ): Promise { const db = getDb() const result = await db.query( `INSERT INTO roles (name, description, permissions) VALUES ($1, $2, $3) RETURNING id, name, description, permissions, is_system, created_at, updated_at`, [name, description, JSON.stringify(permissions)] ) const row = result.rows[0] return { id: row.id, name: row.name, description: row.description, permissions: row.permissions || [], isSystem: row.is_system, createdAt: row.created_at, updatedAt: row.updated_at, } } /** * Check separation of duties * Ensures users don't have conflicting roles */ export async function checkSeparationOfDuties(userId: string, newRoleId: string): Promise { const db = getDb() // Get user's current roles const currentRoles = await getUserRoles(userId) // Get new role const newRoleResult = await db.query('SELECT name FROM roles WHERE id = $1', [newRoleId]) if (newRoleResult.rows.length === 0) { return false } const newRoleName = newRoleResult.rows[0].name // Define conflicting role pairs (example) const conflicts: Record = { 'SYSTEM_ADMIN': [], // System admin can have any role 'SECURITY_ADMIN': ['TENANT_ADMIN'], // Security admin shouldn't be tenant admin 'TENANT_ADMIN': ['SECURITY_ADMIN'], } const conflictingRoles = conflicts[newRoleName] || [] const hasConflict = currentRoles.some(role => conflictingRoles.includes(role.name)) if (hasConflict) { logger.warn('Separation of duties violation detected', { userId, newRole: newRoleName, conflictingRoles: currentRoles.map(r => r.name), }) return false } return true }