Files
dashboard/backend/src/middleware/rbac.middleware.ts
Matthias Hochmeister 4ed76fe20d fix permissions
2026-03-25 09:07:31 +01:00

206 lines
6.3 KiB
TypeScript

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<void> => {
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<void> => {
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<AppRole> {
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<void> => {
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();
};
}