diff --git a/backend/src/database/migrations/007_create_management_audit_log.sql b/backend/src/database/migrations/007_create_management_audit_log.sql new file mode 100644 index 0000000..e4ef5f0 --- /dev/null +++ b/backend/src/database/migrations/007_create_management_audit_log.sql @@ -0,0 +1,32 @@ +-- Migration 007: Create management audit log table +-- Date: 2025-11-11 +-- Description: Track all management portal actions for security and compliance + +-- ============================================================================ +-- Table: management_audit_log +-- Purpose: Audit trail for all user actions via management portal +-- ============================================================================ +CREATE TABLE IF NOT EXISTS management_audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id TEXT, -- Group ID (NULL if token validation failed) + management_token TEXT, -- Management token used (partially masked in queries) + action TEXT NOT NULL, -- Action type: 'validate_token', 'revoke_consent', 'update_metadata', 'add_image', 'delete_image', 'delete_group' + success BOOLEAN NOT NULL DEFAULT 1, -- Whether action succeeded + error_message TEXT, -- Error message if action failed + ip_address TEXT, -- Client IP address + user_agent TEXT, -- Client user agent + request_data TEXT, -- JSON of request data (sanitized) + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + + -- Foreign key (optional, NULL if group was deleted) + FOREIGN KEY (group_id) REFERENCES groups(group_id) ON DELETE SET NULL +); + +-- ============================================================================ +-- Indexes for query performance +-- ============================================================================ +CREATE INDEX IF NOT EXISTS idx_audit_group_id ON management_audit_log(group_id); +CREATE INDEX IF NOT EXISTS idx_audit_action ON management_audit_log(action); +CREATE INDEX IF NOT EXISTS idx_audit_success ON management_audit_log(success); +CREATE INDEX IF NOT EXISTS idx_audit_created_at ON management_audit_log(created_at); +CREATE INDEX IF NOT EXISTS idx_audit_ip ON management_audit_log(ip_address); diff --git a/backend/src/middlewares/auditLog.js b/backend/src/middlewares/auditLog.js new file mode 100644 index 0000000..c3ebfa8 --- /dev/null +++ b/backend/src/middlewares/auditLog.js @@ -0,0 +1,47 @@ +/** + * Audit-Log Middleware für Management Routes + * Loggt alle Aktionen im Management Portal für Security & Compliance + */ + +const auditLogRepository = require('../repositories/ManagementAuditLogRepository'); + +/** + * Middleware zum Loggen von Management-Aktionen + * Fügt res.auditLog() Funktion hinzu + */ +const auditLogMiddleware = (req, res, next) => { + // Extrahiere Client-Informationen + const ipAddress = req.ip || req.connection.remoteAddress || 'unknown'; + const userAgent = req.get('user-agent') || 'unknown'; + const managementToken = req.params.token || null; + + /** + * Log-Funktion für Controllers + * @param {string} action - Aktion (z.B. 'validate_token', 'revoke_consent') + * @param {boolean} success - Erfolg + * @param {string} groupId - Gruppen-ID (optional) + * @param {string} errorMessage - Fehlermeldung (optional) + * @param {Object} requestData - Request-Daten (optional) + */ + res.auditLog = async (action, success, groupId = null, errorMessage = null, requestData = null) => { + try { + await auditLogRepository.logAction({ + groupId, + managementToken, + action, + success, + errorMessage, + ipAddress, + userAgent, + requestData + }); + } catch (error) { + console.error('Failed to write audit log:', error); + // Audit-Log-Fehler sollen die Hauptoperation nicht blockieren + } + }; + + next(); +}; + +module.exports = auditLogMiddleware; diff --git a/backend/src/repositories/ManagementAuditLogRepository.js b/backend/src/repositories/ManagementAuditLogRepository.js new file mode 100644 index 0000000..c6589da --- /dev/null +++ b/backend/src/repositories/ManagementAuditLogRepository.js @@ -0,0 +1,182 @@ +/** + * ManagementAuditLogRepository + * + * Repository für Management Audit Logging + * Verwaltet management_audit_log Tabelle + */ + +const dbManager = require('../database/DatabaseManager'); + +class ManagementAuditLogRepository { + + /** + * Log eine Management-Aktion + * @param {Object} logData - Audit-Log-Daten + * @param {string} logData.groupId - Gruppen-ID (optional) + * @param {string} logData.managementToken - Management-Token (wird maskiert) + * @param {string} logData.action - Aktion (validate_token, revoke_consent, etc.) + * @param {boolean} logData.success - Erfolg + * @param {string} logData.errorMessage - Fehlermeldung (optional) + * @param {string} logData.ipAddress - IP-Adresse + * @param {string} logData.userAgent - User-Agent + * @param {Object} logData.requestData - Request-Daten (wird als JSON gespeichert) + * @returns {Promise} ID des Log-Eintrags + */ + async logAction(logData) { + // Maskiere Token (zeige nur erste 8 Zeichen) + const maskedToken = logData.managementToken + ? logData.managementToken.substring(0, 8) + '...' + : null; + + // Sanitiere Request-Daten (entferne sensible Daten) + const sanitizedData = logData.requestData ? { + ...logData.requestData, + managementToken: undefined // Token nie loggen + } : null; + + const query = ` + INSERT INTO management_audit_log + (group_id, management_token, action, success, error_message, ip_address, user_agent, request_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `; + + const result = await dbManager.run(query, [ + logData.groupId || null, + maskedToken, + logData.action, + logData.success ? 1 : 0, + logData.errorMessage || null, + logData.ipAddress || null, + logData.userAgent || null, + sanitizedData ? JSON.stringify(sanitizedData) : null + ]); + + return result.lastID; + } + + /** + * Hole letzte N Audit-Einträge + * @param {number} limit - Anzahl der Einträge (default: 100) + * @returns {Promise} Array von Audit-Einträgen + */ + async getRecentLogs(limit = 100) { + const query = ` + SELECT + id, + group_id, + management_token, + action, + success, + error_message, + ip_address, + user_agent, + request_data, + created_at + FROM management_audit_log + ORDER BY created_at DESC + LIMIT ? + `; + + const logs = await dbManager.all(query, [limit]); + + // Parse request_data JSON + return logs.map(log => ({ + ...log, + requestData: log.request_data ? JSON.parse(log.request_data) : null, + request_data: undefined + })); + } + + /** + * Hole Audit-Logs für eine Gruppe + * @param {string} groupId - Gruppen-ID + * @returns {Promise} Array von Audit-Einträgen + */ + async getLogsByGroupId(groupId) { + const query = ` + SELECT + id, + group_id, + management_token, + action, + success, + error_message, + ip_address, + user_agent, + request_data, + created_at + FROM management_audit_log + WHERE group_id = ? + ORDER BY created_at DESC + `; + + const logs = await dbManager.all(query, [groupId]); + + return logs.map(log => ({ + ...log, + requestData: log.request_data ? JSON.parse(log.request_data) : null, + request_data: undefined + })); + } + + /** + * Hole fehlgeschlagene Aktionen nach IP + * @param {string} ipAddress - IP-Adresse + * @param {number} hours - Zeitraum in Stunden (default: 24) + * @returns {Promise} Array von fehlgeschlagenen Aktionen + */ + async getFailedActionsByIP(ipAddress, hours = 24) { + const query = ` + SELECT + id, + group_id, + management_token, + action, + error_message, + created_at + FROM management_audit_log + WHERE ip_address = ? + AND success = 0 + AND created_at >= datetime('now', '-${hours} hours') + ORDER BY created_at DESC + `; + + return await dbManager.all(query, [ipAddress]); + } + + /** + * Statistiken für Audit-Log + * @returns {Promise} Statistiken + */ + async getStatistics() { + const query = ` + SELECT + COUNT(*) as totalActions, + SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successfulActions, + SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failedActions, + COUNT(DISTINCT group_id) as uniqueGroups, + COUNT(DISTINCT ip_address) as uniqueIPs, + MAX(created_at) as lastAction + FROM management_audit_log + `; + + return await dbManager.get(query); + } + + /** + * Lösche alte Audit-Logs (Cleanup) + * @param {number} days - Lösche Logs älter als X Tage (default: 90) + * @returns {Promise} Anzahl gelöschter Einträge + */ + async cleanupOldLogs(days = 90) { + const query = ` + DELETE FROM management_audit_log + WHERE created_at < datetime('now', '-${days} days') + `; + + const result = await dbManager.run(query); + return result.changes; + } +} + +module.exports = new ManagementAuditLogRepository(); diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 38ea942..295a65e 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -1,7 +1,9 @@ const express = require('express'); const router = express.Router(); const DeletionLogRepository = require('../repositories/DeletionLogRepository'); +const ManagementAuditLogRepository = require('../repositories/ManagementAuditLogRepository'); const GroupCleanupService = require('../services/GroupCleanupService'); +const { getStatistics: getRateLimiterStats } = require('../middlewares/rateLimiter'); // GroupCleanupService ist bereits eine Instanz, keine Klasse const cleanupService = GroupCleanupService; @@ -135,4 +137,91 @@ router.get('/cleanup/preview', async (req, res) => { }); +// Rate-Limiter Statistiken (für Monitoring) +router.get('/rate-limiter/stats', async (req, res) => { + try { + const stats = getRateLimiterStats(); + + res.json({ + success: true, + ...stats + }); + } catch (error) { + console.error('[Admin API] Error fetching rate-limiter stats:', error); + res.status(500).json({ + error: 'Internal server error', + message: error.message + }); + } +}); + +// Management Audit-Log (letzte N Einträge) +router.get('/management-audit', async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 100; + + if (limit < 1 || limit > 1000) { + return res.status(400).json({ + error: 'Invalid limit', + message: 'Limit must be between 1 and 1000' + }); + } + + const logs = await ManagementAuditLogRepository.getRecentLogs(limit); + + res.json({ + success: true, + logs: logs, + total: logs.length, + limit: limit + }); + } catch (error) { + console.error('[Admin API] Error fetching management audit log:', error); + res.status(500).json({ + error: 'Internal server error', + message: error.message + }); + } +}); + +// Management Audit-Log Statistiken +router.get('/management-audit/stats', async (req, res) => { + try { + const stats = await ManagementAuditLogRepository.getStatistics(); + + res.json({ + success: true, + ...stats + }); + } catch (error) { + console.error('[Admin API] Error fetching audit log stats:', error); + res.status(500).json({ + error: 'Internal server error', + message: error.message + }); + } +}); + +// Management Audit-Log nach Group-ID +router.get('/management-audit/group/:groupId', async (req, res) => { + try { + const { groupId } = req.params; + const logs = await ManagementAuditLogRepository.getLogsByGroupId(groupId); + + res.json({ + success: true, + groupId: groupId, + logs: logs, + total: logs.length + }); + } catch (error) { + console.error('[Admin API] Error fetching audit log for group:', error); + res.status(500).json({ + error: 'Internal server error', + message: error.message + }); + } +}); + + module.exports = router; diff --git a/backend/src/routes/management.js b/backend/src/routes/management.js index 9c1b384..49d9eb0 100644 --- a/backend/src/routes/management.js +++ b/backend/src/routes/management.js @@ -4,9 +4,11 @@ const groupRepository = require('../repositories/GroupRepository'); const deletionLogRepository = require('../repositories/DeletionLogRepository'); const dbManager = require('../database/DatabaseManager'); const { rateLimitMiddleware, recordFailedTokenValidation } = require('../middlewares/rateLimiter'); +const auditLogMiddleware = require('../middlewares/auditLog'); -// Apply rate limiting to all management routes +// Apply middleware to all management routes router.use(rateLimitMiddleware); +router.use(auditLogMiddleware); // Helper: Validate UUID v4 token format const validateToken = (token) => { @@ -40,12 +42,17 @@ router.get('/:token', async (req, res) => { if (!groupData) { recordFailedTokenValidation(req); // Track brute-force attempts + await res.auditLog('validate_token', false, null, 'Token not found or group deleted'); + return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } + // Log successful token validation + await res.auditLog('validate_token', true, groupData.groupId); + // Return complete group data res.json({ success: true, @@ -54,6 +61,8 @@ router.get('/:token', async (req, res) => { } catch (error) { console.error('Error validating management token:', error); + await res.auditLog('validate_token', false, null, error.message); + res.status(500).json({ success: false, error: 'Failed to validate management token'