206 lines
6.3 KiB
TypeScript
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();
|
|
};
|
|
}
|