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 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;
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user