- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
294 lines
7.5 KiB
TypeScript
294 lines
7.5 KiB
TypeScript
/**
|
|
* 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<string, any>
|
|
description?: string
|
|
}
|
|
|
|
export interface UserRole {
|
|
id: string
|
|
userId: string
|
|
roleId: string
|
|
grantedBy?: string
|
|
grantedAt: Date
|
|
expiresAt?: Date
|
|
metadata?: Record<string, any>
|
|
}
|
|
|
|
/**
|
|
* Check if user has permission
|
|
*/
|
|
export async function hasPermission(
|
|
userId: string,
|
|
resourceType: string,
|
|
action: string,
|
|
resourceId?: string,
|
|
context?: Record<string, any>
|
|
): Promise<boolean> {
|
|
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<string, any>, context?: Record<string, any>): 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<Role[]> {
|
|
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<string, any>
|
|
): Promise<void> {
|
|
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<void> {
|
|
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<Role[]> {
|
|
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<Role> {
|
|
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<boolean> {
|
|
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<string, string[]> = {
|
|
'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
|
|
}
|
|
|