feat(phase2): Implement Management Audit-Log (Task 10)
Audit-Logging System: - Migration 007: management_audit_log table with indexes - Tracks all management portal actions - IP address, user-agent, request data logging - Token masking (only first 8 chars stored) - Success/failure tracking with error messages ManagementAuditLogRepository: - logAction() - Log management actions - getRecentLogs() - Get last N logs - getLogsByGroupId() - Get logs for specific group - getFailedActionsByIP() - Security monitoring - getStatistics() - Overview statistics - cleanupOldLogs() - Maintenance (90 days retention) Audit-Log Middleware: - Adds res.auditLog() helper function - Auto-captures IP, User-Agent - Integrated into all management routes - Non-blocking (errors don't fail main operation) Admin API Endpoints: - GET /api/admin/management-audit?limit=N - GET /api/admin/management-audit/stats - GET /api/admin/management-audit/group/:groupId Tested: ✅ Migration executed successfully ✅ Audit logs written on token validation ✅ Admin API returns logs with stats ✅ Token masking working ✅ Statistics accurate
This commit is contained in:
parent
0dce5fddac
commit
0f77db6f02
|
|
@ -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);
|
||||||
47
backend/src/middlewares/auditLog.js
Normal file
47
backend/src/middlewares/auditLog.js
Normal file
|
|
@ -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;
|
||||||
182
backend/src/repositories/ManagementAuditLogRepository.js
Normal file
182
backend/src/repositories/ManagementAuditLogRepository.js
Normal file
|
|
@ -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<number>} 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>} 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>} 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>} 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<Object>} 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<number>} 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();
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const DeletionLogRepository = require('../repositories/DeletionLogRepository');
|
const DeletionLogRepository = require('../repositories/DeletionLogRepository');
|
||||||
|
const ManagementAuditLogRepository = require('../repositories/ManagementAuditLogRepository');
|
||||||
const GroupCleanupService = require('../services/GroupCleanupService');
|
const GroupCleanupService = require('../services/GroupCleanupService');
|
||||||
|
const { getStatistics: getRateLimiterStats } = require('../middlewares/rateLimiter');
|
||||||
|
|
||||||
// GroupCleanupService ist bereits eine Instanz, keine Klasse
|
// GroupCleanupService ist bereits eine Instanz, keine Klasse
|
||||||
const cleanupService = GroupCleanupService;
|
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;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ const groupRepository = require('../repositories/GroupRepository');
|
||||||
const deletionLogRepository = require('../repositories/DeletionLogRepository');
|
const deletionLogRepository = require('../repositories/DeletionLogRepository');
|
||||||
const dbManager = require('../database/DatabaseManager');
|
const dbManager = require('../database/DatabaseManager');
|
||||||
const { rateLimitMiddleware, recordFailedTokenValidation } = require('../middlewares/rateLimiter');
|
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(rateLimitMiddleware);
|
||||||
|
router.use(auditLogMiddleware);
|
||||||
|
|
||||||
// Helper: Validate UUID v4 token format
|
// Helper: Validate UUID v4 token format
|
||||||
const validateToken = (token) => {
|
const validateToken = (token) => {
|
||||||
|
|
@ -40,12 +42,17 @@ router.get('/:token', async (req, res) => {
|
||||||
|
|
||||||
if (!groupData) {
|
if (!groupData) {
|
||||||
recordFailedTokenValidation(req); // Track brute-force attempts
|
recordFailedTokenValidation(req); // Track brute-force attempts
|
||||||
|
await res.auditLog('validate_token', false, null, 'Token not found or group deleted');
|
||||||
|
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Management token not found or group has been deleted'
|
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
|
// Return complete group data
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -54,6 +61,8 @@ router.get('/:token', async (req, res) => {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error validating management token:', error);
|
console.error('Error validating management token:', error);
|
||||||
|
await res.auditLog('validate_token', false, null, error.message);
|
||||||
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to validate management token'
|
error: 'Failed to validate management token'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user