import { Request, Response, NextFunction } from 'express'; import logger from '../utils/logger'; import { auditPermissionDenied } from './audit.middleware'; import { AuditResourceType } from '../services/audit.service'; import { permissionService } from '../services/permission.service'; // --------------------------------------------------------------------------- // AppRole — kept for backward compatibility (resolveRequestRole, bericht_text) // --------------------------------------------------------------------------- export type AppRole = | 'admin' | 'kommandant' | 'gruppenfuehrer' | 'mitglied' | 'bewerber'; /** * Middleware factory: requires the authenticated user to hold the given * permission. Permission is checked against the DB-driven permission system. * * Hardwired rules: * - `dashboard_admin` group always passes (full access). * - Maintenance mode blocks non-admin access per feature group. */ export function requirePermission(permission: string) { return async (req: Request, res: Response, next: NextFunction): Promise => { if (!req.user) { res.status(401).json({ success: false, message: 'Authentication required', }); return; } const groups: string[] = req.user?.groups ?? []; // Attach resolved role for downstream use (bericht_text redaction, etc.) (req as any).userRole = resolveRequestRole(req); // Hardwired: dashboard_admin always has full access if (groups.includes('dashboard_admin')) { next(); return; } // Check maintenance mode for the feature group const featureGroup = permission.split(':')[0]; if (permissionService.isFeatureInMaintenance(featureGroup)) { logger.info('Feature in maintenance mode', { userId: req.user.id, featureGroup, permission, path: req.path, }); res.status(403).json({ success: false, message: 'Diese Funktion befindet sich im Wartungsmodus', }); return; } // Check DB-driven permission if (!permissionService.hasPermission(groups, permission)) { logger.warn('Permission denied', { userId: req.user.id, groups, permission, path: req.path, }); auditPermissionDenied(req, AuditResourceType.SYSTEM, undefined, { required_permission: permission, user_groups: groups, }); res.status(403).json({ success: false, message: 'Keine Berechtigung', }); return; } next(); }; } /** * Middleware factory: passes if the user holds ANY of the listed permissions. * Useful when multiple roles should access the same read endpoint. * Maintenance mode is checked against the first permission's feature group. */ export function requireAnyPermission(...permissions: string[]) { return async (req: Request, res: Response, next: NextFunction): Promise => { if (!req.user) { res.status(401).json({ success: false, message: 'Authentication required' }); return; } const groups: string[] = req.user?.groups ?? []; (req as any).userRole = resolveRequestRole(req); if (groups.includes('dashboard_admin')) { next(); return; } // Maintenance check on the feature group of the first permission const featureGroup = permissions[0].split(':')[0]; if (permissionService.isFeatureInMaintenance(featureGroup)) { res.status(403).json({ success: false, message: 'Diese Funktion befindet sich im Wartungsmodus' }); return; } if (permissions.some(p => permissionService.hasPermission(groups, p))) { next(); return; } logger.warn('Permission denied (any-of)', { userId: req.user.id, groups, permissions, path: req.path, }); auditPermissionDenied(req, AuditResourceType.SYSTEM, undefined, { required_permissions: permissions, user_groups: groups, }); res.status(403).json({ success: false, message: 'Keine Berechtigung' }); }; } /** * Resolve the effective AppRole for a request. * Simplified: returns 'admin' for dashboard_admin, 'kommandant' for dashboard_kommando, * 'gruppenfuehrer' for specialist groups, else 'mitglied'. * Used for backward-compatible features like bericht_text redaction. */ export function resolveRequestRole(req: Request): AppRole { const groups: string[] = req.user?.groups ?? []; if (groups.includes('dashboard_admin')) return 'admin'; if (groups.includes('dashboard_kommando')) return 'kommandant'; if ( groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister') || groups.includes('dashboard_chargen') ) return 'gruppenfuehrer'; return 'mitglied'; } // Legacy exports for backward compatibility export function getUserRole(_userId: string): Promise { return Promise.resolve('mitglied'); } export function roleFromGroups(groups: string[]): AppRole { if (groups.includes('dashboard_admin')) return 'admin'; if (groups.includes('dashboard_kommando')) return 'kommandant'; if (groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister') || groups.includes('dashboard_chargen')) return 'gruppenfuehrer'; return 'mitglied'; } export function hasPermission(role: AppRole, _permission: string): boolean { return role === 'admin'; } /** * @deprecated Use requirePermission() instead. */ export function requireGroups(requiredGroups: string[]) { return async (req: Request, res: Response, next: NextFunction): Promise => { if (!req.user) { res.status(401).json({ success: false, message: 'Authentication required' }); return; } logger.warn('DEPRECATED: requireGroups() — migrate to requirePermission()', { requiredGroups }); const userGroups: string[] = req.user?.groups ?? []; const hasAccess = requiredGroups.some(g => userGroups.includes(g)); if (!hasAccess) { logger.warn('Group-based access denied', { userId: req.user.id, userGroups, requiredGroups, path: req.path, }); auditPermissionDenied(req, AuditResourceType.SYSTEM, undefined, { required_groups: requiredGroups, user_groups: userGroups, }); res.status(403).json({ success: false, message: 'Keine Berechtigung für diese Aktion', }); return; } next(); }; }