UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

635 lines (558 loc) 17.4 kB
/** * Authentication Layer for Critical Operations * Provides secure authentication and authorization for sensitive operations */ import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import { CommandSanitizer } from './commandSanitizer'; import { resourceManager } from './resourceManager'; import { stateManager } from './threadSafeState'; export interface AuthenticationCredentials { userId: string; apiKey: string; sessionToken?: string; permissions: string[]; expiresAt: number; } export interface AuthenticationResult { authenticated: boolean; authorized: boolean; userId?: string; permissions?: string[]; reason?: string; sessionToken?: string; } export interface OperationContext { operation: string; resourceId?: string; requesterId?: string; timestamp: number; metadata?: Record<string, any>; } export class AuthenticationLayer { private static readonly SESSION_DURATION = 2 * 60 * 60 * 1000; // 2 hours private static readonly API_KEY_LENGTH = 64; // 64 chars hex private static readonly SESSION_TOKEN_LENGTH = 32; // 32 chars hex private static readonly MAX_SESSIONS_PER_USER = 5; private static readonly AUTH_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes // Secure storage for active sessions private static activeSessions = stateManager.getMap<string, AuthenticationCredentials>('activeSessions'); // Authentication cache to reduce overhead private static authCache = stateManager.getMap<string, { result: AuthenticationResult; cachedAt: number }>('authCache'); // Critical operations that require authentication private static readonly CRITICAL_OPERATIONS = new Set([ 'createProject', 'killSession', 'executeScheduledProject', 'addTeamMember', 'removeTeamMember', 'blockCommit', 'approveCommit', 'generateQAKeyPair', 'registerQAEngineer', 'createCryptographicQAApproval' ]); // Permission levels for different operations private static readonly OPERATION_PERMISSIONS = { 'createProject': ['project.create', 'admin'], 'killSession': ['session.kill', 'admin'], 'executeScheduledProject': ['project.execute', 'admin'], 'addTeamMember': ['team.manage', 'admin'], 'removeTeamMember': ['team.manage', 'admin'], 'blockCommit': ['qa.block', 'qa.engineer', 'admin'], 'approveCommit': ['qa.approve', 'qa.engineer', 'admin'], 'generateQAKeyPair': ['qa.keys', 'admin'], 'registerQAEngineer': ['qa.register', 'admin'], 'createCryptographicQAApproval': ['qa.approve', 'qa.engineer', 'admin'] }; /** * Generate a new API key for a user */ public static generateApiKey(): string { return crypto.randomBytes(this.API_KEY_LENGTH / 2).toString('hex'); } /** * Generate a secure session token */ private static generateSessionToken(): string { return crypto.randomBytes(this.SESSION_TOKEN_LENGTH / 2).toString('hex'); } /** * Create authentication credentials for a user */ public static async createUserCredentials( userId: string, permissions: string[] ): Promise<{ apiKey: string; userId: string; permissions: string[] }> { // Validate inputs if (!userId || typeof userId !== 'string') { throw new Error('Invalid user ID'); } const userValidation = CommandSanitizer.validateProjectName(userId); // Reuse project name validation if (!userValidation.isValid) { throw new Error(`Invalid user ID format: ${userValidation.errors.join(', ')}`); } if (!Array.isArray(permissions)) { throw new Error('Permissions must be an array'); } // Generate secure API key const apiKey = this.generateApiKey(); // Store credentials securely const credentialsDir = '/var/n8n/auth'; try { await fs.promises.access(credentialsDir); } catch { await fs.promises.mkdir(credentialsDir, { recursive: true, mode: 0o700 }); } const credentialsPath = path.join(credentialsDir, `${userId}.json`); const credentials = { userId, apiKeyHash: crypto.createHash('sha256').update(apiKey).digest('hex'), permissions, createdAt: Date.now(), lastUsed: null }; await fs.promises.writeFile(credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 }); return { apiKey, userId, permissions }; } /** * Authenticate a user with API key */ public static async authenticateUser( userId: string, apiKey: string ): Promise<AuthenticationResult> { try { // Input validation if (!userId || !apiKey) { return { authenticated: false, authorized: false, reason: 'Missing credentials' }; } // Check cache first const cacheKey = `${userId}:${crypto.createHash('sha256').update(apiKey).digest('hex').substring(0, 16)}`; const cached = this.authCache.safeGet(cacheKey); if (cached && (Date.now() - cached.cachedAt) < this.AUTH_CACHE_DURATION) { return cached.result; } // Load user credentials const credentialsDir = '/var/n8n/auth'; const credentialsPath = path.join(credentialsDir, `${userId}.json`); try { await fs.promises.access(credentialsPath); } catch { const result = { authenticated: false, authorized: false, reason: 'User not found' }; await this.authCache.safeSet(cacheKey, { result, cachedAt: Date.now() }); return result; } const credentials = JSON.parse(await fs.promises.readFile(credentialsPath, 'utf8')); // Verify API key const providedKeyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); if (!crypto.timingSafeEqual( Buffer.from(credentials.apiKeyHash, 'hex'), Buffer.from(providedKeyHash, 'hex') )) { const result = { authenticated: false, authorized: false, reason: 'Invalid API key' }; try { await this.authCache.safeSet(cacheKey, { result, cachedAt: Date.now() }); } catch (error) { console.warn('Failed to cache auth result:', error.message); } return result; } // Generate session token const sessionToken = this.generateSessionToken(); const expiresAt = Date.now() + this.SESSION_DURATION; // Clean up old sessions for this user await this.cleanupUserSessions(userId); // Store active session const sessionCredentials: AuthenticationCredentials = { userId, apiKey, sessionToken, permissions: credentials.permissions, expiresAt }; try { await this.activeSessions.safeSet(sessionToken, sessionCredentials); } catch (error) { console.error('Failed to store active session:', error.message); throw new Error('Failed to create session'); } // Update last used timestamp credentials.lastUsed = Date.now(); await fs.promises.writeFile(credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 }); const result = { authenticated: true, authorized: true, userId, permissions: credentials.permissions, sessionToken }; try { await this.authCache.safeSet(cacheKey, { result, cachedAt: Date.now() }); } catch (error) { console.warn('Failed to cache auth result:', error.message); } return result; } catch (error) { return { authenticated: false, authorized: false, reason: `Authentication error: ${error.message}` }; } } /** * Authenticate using session token */ public static authenticateSession(sessionToken: string): AuthenticationResult { try { if (!sessionToken) { return { authenticated: false, authorized: false, reason: 'Missing session token' }; } const session = this.activeSessions.safeGet(sessionToken); if (!session) { return { authenticated: false, authorized: false, reason: 'Invalid session token' }; } // Check expiration if (Date.now() > session.expiresAt) { // Note: We can't await in this sync method, so we fire and forget this.activeSessions.safeDelete(sessionToken).catch(error => { console.warn('Failed to delete expired session:', error.message); }); return { authenticated: false, authorized: false, reason: 'Session expired' }; } return { authenticated: true, authorized: true, userId: session.userId, permissions: session.permissions, sessionToken }; } catch (error) { return { authenticated: false, authorized: false, reason: `Session authentication error: ${error.message}` }; } } /** * Authorize an operation for a user */ public static authorizeOperation( operation: string, userPermissions: string[] ): { authorized: boolean; reason?: string } { try { // Check if operation requires authentication if (!this.CRITICAL_OPERATIONS.has(operation)) { return { authorized: true }; // Non-critical operations allowed } // Get required permissions for operation const requiredPermissions = this.OPERATION_PERMISSIONS[operation]; if (!requiredPermissions) { return { authorized: false, reason: 'Unknown operation' }; } // Check if user has any of the required permissions const hasPermission = requiredPermissions.some(perm => userPermissions.includes(perm) ); if (!hasPermission) { return { authorized: false, reason: `Missing required permissions: ${requiredPermissions.join(' or ')}` }; } return { authorized: true }; } catch (error) { return { authorized: false, reason: `Authorization error: ${error.message}` }; } } /** * Complete authentication and authorization check */ public static async authenticateAndAuthorize( operation: string, credentials: { userId?: string; apiKey?: string; sessionToken?: string }, context?: OperationContext ): Promise<AuthenticationResult> { try { let authResult: AuthenticationResult; // Try session authentication first if (credentials.sessionToken) { authResult = this.authenticateSession(credentials.sessionToken); } // Fall back to API key authentication else if (credentials.userId && credentials.apiKey) { authResult = await this.authenticateUser(credentials.userId, credentials.apiKey); } else { return { authenticated: false, authorized: false, reason: 'No valid credentials provided' }; } // If authentication failed, return early if (!authResult.authenticated) { return authResult; } // Check authorization for the operation const authzResult = this.authorizeOperation(operation, authResult.permissions || []); if (!authzResult.authorized) { return { authenticated: true, authorized: false, userId: authResult.userId, permissions: authResult.permissions, reason: authzResult.reason }; } // Log the operation for audit trail if (context) { await this.logOperation(authResult.userId!, operation, context); } return authResult; } catch (error) { return { authenticated: false, authorized: false, reason: `Authentication/authorization error: ${error.message}` }; } } /** * Log operation for audit trail */ private static async logOperation( userId: string, operation: string, context: OperationContext ): Promise<void> { try { const logDir = '/var/log/n8n/auth'; try { await fs.promises.access(logDir); } catch { await fs.promises.mkdir(logDir, { recursive: true, mode: 0o700 }); } const logEntry = { timestamp: new Date().toISOString(), userId, operation, context: { resourceId: context.resourceId, requesterId: context.requesterId, metadata: context.metadata }, auditHash: '' }; // Generate audit hash logEntry.auditHash = crypto .createHash('sha256') .update(JSON.stringify(logEntry, Object.keys(logEntry).sort())) .digest('hex'); const logPath = path.join(logDir, `operations_${new Date().toISOString().split('T')[0]}.log`); await fs.promises.appendFile(logPath, JSON.stringify(logEntry) + '\n', { mode: 0o600 }); } catch (error) { console.warn('Failed to log operation:', error.message); } } /** * Clean up expired sessions and old user sessions */ private static async cleanupUserSessions(userId: string): Promise<void> { try { const userSessions = this.activeSessions.safeEntries() .filter(([_, session]) => session.userId === userId); // Remove expired sessions for (const [token, session] of userSessions) { if (Date.now() > session.expiresAt) { try { await this.activeSessions.safeDelete(token); } catch (error) { console.warn(`Failed to delete expired session ${token}:`, error.message); } } } // Limit sessions per user const validUserSessions = this.activeSessions.safeEntries() .filter(([_, session]) => session.userId === userId) .sort((a, b) => b[1].expiresAt - a[1].expiresAt); if (validUserSessions.length >= this.MAX_SESSIONS_PER_USER) { // Remove oldest sessions const toRemove = validUserSessions.slice(this.MAX_SESSIONS_PER_USER - 1); for (const [token] of toRemove) { try { await this.activeSessions.safeDelete(token); } catch (error) { console.warn(`Failed to delete old session ${token}:`, error.message); } } } } catch (error) { console.error('Failed to cleanup user sessions:', error.message); } } /** * Revoke a session */ public static async revokeSession(sessionToken: string): Promise<boolean> { try { return await this.activeSessions.safeDelete(sessionToken); } catch (error) { console.error('Failed to revoke session:', error.message); return false; } } /** * Revoke all sessions for a user */ public static async revokeUserSessions(userId: string): Promise<number> { let revokedCount = 0; try { const entries = this.activeSessions.safeEntries(); for (const [token, session] of entries) { if (session.userId === userId) { try { await this.activeSessions.safeDelete(token); revokedCount++; } catch (error) { console.warn(`Failed to revoke session ${token}:`, error.message); } } } } catch (error) { console.error('Failed to revoke user sessions:', error.message); } return revokedCount; } /** * Get statistics about active sessions */ public static getSessionStats(): { activeSessions: number; uniqueUsers: number; expiringSoon: number; } { const now = Date.now(); const fiveMinutes = 5 * 60 * 1000; const uniqueUsers = new Set(); let expiringSoon = 0; for (const session of this.activeSessions.safeValues()) { if (now < session.expiresAt) { uniqueUsers.add(session.userId); if (session.expiresAt - now < fiveMinutes) { expiringSoon++; } } } return { activeSessions: this.activeSessions.safeSize(), uniqueUsers: uniqueUsers.size, expiringSoon }; } /** * Initialize authentication system (setup default admin if needed) */ public static async initialize(): Promise<void> { try { // Ensure auth directory exists const authDir = '/var/n8n/auth'; try { await fs.promises.access(authDir); } catch { await fs.promises.mkdir(authDir, { recursive: true, mode: 0o700 }); } // Check if any admin exists const files = await fs.promises.readdir(authDir); const hasAdmin = await Promise.all(files.map(async file => { try { const credPath = path.join(authDir, file); const creds = JSON.parse(await fs.promises.readFile(credPath, 'utf8')); return creds.permissions && creds.permissions.includes('admin'); } catch { return false; } })).then(results => results.some(Boolean)); // Create default admin if none exists if (!hasAdmin) { const defaultAdminId = 'system_admin'; const defaultPermissions = ['admin']; const { apiKey } = await this.createUserCredentials(defaultAdminId, defaultPermissions); console.log(`Created default admin credentials:`); console.log(`User ID: ${defaultAdminId}`); console.log(`API Key: ${apiKey}`); console.log(`Permissions: ${defaultPermissions.join(', ')}`); console.log(`Store these credentials securely!`); } // Schedule periodic cleanup resourceManager.createInterval(async () => { await this.performPeriodicCleanup(); }, 10 * 60 * 1000, 'Authentication periodic cleanup'); } catch (error) { console.warn('Failed to initialize authentication system:', error.message); } } /** * Perform periodic cleanup of expired sessions and cache */ private static async performPeriodicCleanup(): Promise<void> { const now = Date.now(); try { // Clean expired sessions const sessionEntries = this.activeSessions.safeEntries(); for (const [token, session] of sessionEntries) { if (now > session.expiresAt) { try { await this.activeSessions.safeDelete(token); } catch (error) { console.warn(`Failed to delete expired session ${token}:`, error.message); } } } // Clean expired cache entries const cacheEntries = this.authCache.safeEntries(); for (const [key, cached] of cacheEntries) { if (now - cached.cachedAt > this.AUTH_CACHE_DURATION) { try { await this.authCache.safeDelete(key); } catch (error) { console.warn(`Failed to delete expired cache entry ${key}:`, error.message); } } } } catch (error) { console.error('Failed to perform periodic cleanup:', error.message); } } }