165 lines
6.2 KiB
JavaScript
165 lines
6.2 KiB
JavaScript
// Audit event definitions and data access helpers for administrative activity.
|
|
|
|
const AUDIT_EVENT_CATEGORIES = Object.freeze({
|
|
AUTHENTICATION: 'authentication',
|
|
SECURITY: 'security',
|
|
SESSIONS: 'sessions',
|
|
USERS: 'users',
|
|
ROLES: 'roles',
|
|
SETTINGS: 'system-settings',
|
|
SLIDES: 'slides',
|
|
TEMPLATES: 'templates',
|
|
PLAYLISTS: 'playlists',
|
|
SCREENS: 'screens',
|
|
ANNOUNCEMENTS: 'announcements',
|
|
CANVAS_SIZES: 'canvas-sizes',
|
|
API_SOURCES: 'api-sources',
|
|
RSS_FEEDS: 'rss-feeds',
|
|
TIMETABLES: 'timetables',
|
|
WEATHER: 'weather',
|
|
SCREEN_CONTROLS: 'screen-controls'
|
|
});
|
|
const AUDIT_CATEGORY_KEYS = Object.freeze(Object.values(AUDIT_EVENT_CATEGORIES));
|
|
const AUDIT_CATEGORY_LABELS = Object.freeze({
|
|
authentication: 'Authentication',
|
|
security: 'Security',
|
|
sessions: 'Sessions',
|
|
users: 'Users',
|
|
roles: 'Roles',
|
|
'system-settings': 'System Settings',
|
|
slides: 'Slides',
|
|
templates: 'Templates',
|
|
playlists: 'Playlists',
|
|
screens: 'Screens',
|
|
announcements: 'Announcements',
|
|
'canvas-sizes': 'Canvas Sizes',
|
|
'api-sources': 'API Sources',
|
|
'rss-feeds': 'RSS Feeds',
|
|
timetables: 'Timetables',
|
|
weather: 'Weather',
|
|
'screen-controls': 'Screen Controls'
|
|
});
|
|
const SCREEN_CONTROL_COMMAND_KEYS = Object.freeze(['pause', 'blackout', 'reload', 'navigation', 'moveclient', 'setclientname']);
|
|
const SCREEN_CONTROL_COMMAND_LABELS = Object.freeze({
|
|
pause: 'Pause / Resume',
|
|
blackout: 'Blackout / Restore',
|
|
reload: 'Reload',
|
|
navigation: 'Forward / Back',
|
|
moveclient: 'Move client',
|
|
setclientname: 'Rename client'
|
|
});
|
|
const { fetchAppSettings } = require('./app-settings');
|
|
|
|
function formatUserAgentLabel(userAgent) {
|
|
const value = String(userAgent || '').trim();
|
|
if (!value) return '';
|
|
const browserMatch = value.match(/(?:Edg|OPR|Chrome|Firefox|Version|Electron)\/([\d.]+)/i);
|
|
let browser = '';
|
|
if (/Edg\//i.test(value)) browser = 'Edge';
|
|
else if (/OPR\//i.test(value)) browser = 'Opera';
|
|
else if (/Electron\//i.test(value)) browser = 'Electron';
|
|
else if (/Chrome\//i.test(value)) browser = 'Chrome';
|
|
else if (/Firefox\//i.test(value)) browser = 'Firefox';
|
|
else if (/Version\/.*Safari\//i.test(value)) browser = 'Safari';
|
|
const browserLabel = browserMatch && browser ? browser + ' ' + browserMatch[1] : browser;
|
|
let operatingSystem = '';
|
|
if (/Windows NT/i.test(value)) operatingSystem = 'Windows';
|
|
else if (/Macintosh|Mac OS X/i.test(value)) operatingSystem = 'macOS';
|
|
else if (/Android/i.test(value)) operatingSystem = 'Android';
|
|
else if (/iPhone|iPad|iPod/i.test(value)) operatingSystem = 'iOS';
|
|
else if (/Linux/i.test(value)) operatingSystem = 'Linux';
|
|
return [browserLabel, operatingSystem].filter(Boolean).join(' on ') || value;
|
|
}
|
|
|
|
function normalizeDetails(details) {
|
|
if (details === undefined || details === null) {
|
|
return null;
|
|
}
|
|
return JSON.stringify(details);
|
|
}
|
|
|
|
function buildAuditChanges(previousValues, nextValues) {
|
|
const previous = previousValues && typeof previousValues === 'object' ? previousValues : {};
|
|
const next = nextValues && typeof nextValues === 'object' ? nextValues : {};
|
|
const changes = {};
|
|
const keys = new Set(Object.keys(previous).concat(Object.keys(next)));
|
|
|
|
keys.forEach(function (key) {
|
|
if (JSON.stringify(previous[key]) !== JSON.stringify(next[key])) {
|
|
changes[key] = { from: previous[key], to: next[key] };
|
|
}
|
|
});
|
|
|
|
return changes;
|
|
}
|
|
|
|
function getRequestMetadata(req) {
|
|
const forwardedAddress = String(req && req.headers && req.headers['x-forwarded-for'] || '').split(',')[0].trim();
|
|
return {
|
|
ipAddress: forwardedAddress || String(req && req.ip || req && req.socket && req.socket.remoteAddress || '').trim() || null,
|
|
userAgent: String(req && req.headers && req.headers['user-agent'] || '').trim() || null
|
|
};
|
|
}
|
|
|
|
async function recordAuditEvent(pool, event) {
|
|
const input = event && typeof event === 'object' ? event : {};
|
|
const category = String(input.category || '').trim().toLowerCase();
|
|
const eventType = String(input.eventType || '').trim().toLowerCase();
|
|
if (!category || !eventType) {
|
|
throw new Error('Audit events require a category and event type.');
|
|
}
|
|
|
|
await pool.query(
|
|
`INSERT INTO o_audit_events
|
|
(category, event_type, actor_user_id, target_type, target_id, target_label, ip_address, user_agent, details_json)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
category,
|
|
eventType,
|
|
Number.isInteger(Number(input.actorUserId)) && Number(input.actorUserId) > 0 ? Number(input.actorUserId) : null,
|
|
String(input.targetType || '').trim() || null,
|
|
String(input.targetId || '').trim() || null,
|
|
String(input.targetLabel || '').trim() || null,
|
|
String(input.ipAddress || '').trim() || null,
|
|
String(input.userAgent || '').trim() || null,
|
|
normalizeDetails(input.details)
|
|
]
|
|
);
|
|
}
|
|
|
|
async function recordRequestAuditEvent(pool, req, event) {
|
|
try {
|
|
const settings = await fetchAppSettings(pool);
|
|
const category = String(event && event.category || '').trim().toLowerCase();
|
|
const enabledCategories = Array.isArray(settings['audit.categories']) ? settings['audit.categories'] : AUDIT_CATEGORY_KEYS;
|
|
if (!settings['audit.enabled'] || !enabledCategories.includes(category)) {
|
|
return;
|
|
}
|
|
if (category === 'screen-controls') {
|
|
const command = String(event && event.eventType || '').replace(/^screen-control\./, '').trim().toLowerCase();
|
|
const commandGroup = command === 'previous' || command === 'next' ? 'navigation' : command;
|
|
const enabledCommands = Array.isArray(settings['audit.screen_control_commands']) ? settings['audit.screen_control_commands'] : [];
|
|
if (!enabledCommands.includes(commandGroup)) {
|
|
return;
|
|
}
|
|
}
|
|
const metadata = settings['audit.include_request_metadata'] ? getRequestMetadata(req) : {};
|
|
await recordAuditEvent(pool, Object.assign({}, event, metadata));
|
|
} catch (error) {
|
|
// Auditing must not turn a successful login or administration action into a failed request.
|
|
console.error('Unable to record audit event:', error.message);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
AUDIT_EVENT_CATEGORIES,
|
|
AUDIT_CATEGORY_KEYS,
|
|
AUDIT_CATEGORY_LABELS,
|
|
SCREEN_CONTROL_COMMAND_KEYS,
|
|
SCREEN_CONTROL_COMMAND_LABELS,
|
|
formatUserAgentLabel,
|
|
getRequestMetadata,
|
|
buildAuditChanges,
|
|
recordAuditEvent,
|
|
recordRequestAuditEvent
|
|
}; |