Files
Sankofa/api/src/services/rbac.ts
T
defiQUG 9daf1fd378 Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements
- 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
2025-12-12 18:01:35 -08:00

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
}