apply security audit

This commit is contained in:
Matthias Hochmeister
2026-03-11 13:51:01 +01:00
parent 93a87a7ae9
commit 3c9b7d3446
19 changed files with 247 additions and 341 deletions

View File

@@ -3,36 +3,6 @@ import tokenService from '../services/token.service';
import userService from '../services/user.service';
import logger from '../utils/logger';
import { JwtPayload } from '../types/auth.types';
import { auditPermissionDenied } from './audit.middleware';
import { AuditResourceType } from '../services/audit.service';
// ---------------------------------------------------------------------------
// Application roles — extend as needed when Authentik group mapping is added
// ---------------------------------------------------------------------------
export type AppRole = 'admin' | 'member' | 'viewer';
export const Permission = {
ADMIN_ACCESS: 'admin:access',
MEMBER_WRITE: 'member:write',
MEMBER_READ: 'member:read',
INCIDENT_WRITE:'incident:write',
INCIDENT_READ: 'incident:read',
EXPORT: 'export',
} as const;
export type Permission = typeof Permission[keyof typeof Permission];
// Simple permission → required role mapping.
// Adjust once Authentik group sync is implemented.
const PERMISSION_ROLES: Record<Permission, AppRole[]> = {
'admin:access': ['admin'],
'member:write': ['admin', 'member'],
'member:read': ['admin', 'member', 'viewer'],
'incident:write': ['admin', 'member'],
'incident:read': ['admin', 'member', 'viewer'],
'export': ['admin'],
};
// Extend Express Request type to include user
declare global {
@@ -42,7 +12,7 @@ declare global {
id: string; // UUID
email: string;
authentikSub: string;
role?: AppRole; // populated when role is stored in DB / JWT
role?: string; // populated when role is stored in DB / JWT
groups?: string[];
};
}
@@ -122,6 +92,7 @@ export const authenticate = async (
email: decoded.email,
authentikSub: decoded.authentikSub,
groups: decoded.groups ?? [],
role: decoded.role,
};
logger.debug('User authenticated successfully', {
@@ -139,60 +110,6 @@ export const authenticate = async (
}
};
// ---------------------------------------------------------------------------
// Role-based access control middleware
// ---------------------------------------------------------------------------
/**
* requirePermission — factory that returns Express middleware enforcing a
* specific permission. Must be placed after `authenticate` in the chain.
*
* Usage:
* router.get('/admin/audit-log', authenticate, requirePermission('admin:access'), handler);
*
* When access is denied, a PERMISSION_DENIED audit entry is written before
* the 403 response is sent.
*
* NOTE: Until Authentik group → role mapping is persisted to the users table
* or JWT, this middleware checks req.user.role. Temporary workaround:
* hard-code specific admin user IDs via the ADMIN_USER_IDS env variable, OR
* add a `role` column to the users table (recommended).
*/
export const requirePermission = (permission: Permission) => {
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
if (!req.user) {
res.status(401).json({ success: false, message: 'Not authenticated' });
return;
}
const userRole: AppRole = req.user.role ?? 'viewer';
const allowedRoles = PERMISSION_ROLES[permission];
if (!allowedRoles.includes(userRole)) {
logger.warn('Permission denied', {
userId: req.user.id,
permission,
userRole,
path: req.path,
});
// Audit the denied access — fire-and-forget
auditPermissionDenied(req, AuditResourceType.SYSTEM, undefined, {
required_permission: permission,
user_role: userRole,
});
res.status(403).json({
success: false,
message: 'Insufficient permissions',
});
return;
}
next();
};
};
/**
* Optional authentication middleware
* Attaches user if token is valid, but doesn't require it

View File

@@ -48,6 +48,19 @@ const PERMISSION_ROLE_MIN: Record<string, AppRole> = {
'admin:access': 'admin',
'audit:read': 'admin',
'audit:export': 'admin',
'members:read': 'mitglied',
'members:write': 'kommandant',
'vehicles:write': 'kommandant',
'vehicles:status': 'gruppenfuehrer',
'vehicles:delete': 'admin',
'equipment:write': 'gruppenfuehrer',
'equipment:delete': 'admin',
'events:write': 'gruppenfuehrer',
'events:categories': 'gruppenfuehrer',
'atemschutz:write': 'gruppenfuehrer',
'atemschutz:delete': 'kommandant',
'bookings:write': 'gruppenfuehrer',
'bookings:delete': 'admin',
};
function hasPermission(role: AppRole, permission: string): boolean {
@@ -103,7 +116,9 @@ export function requirePermission(permission: string) {
return;
}
const role = await getUserRole(req.user.id);
const role = (req.user as any).role
? (req.user as any).role as AppRole
: await getUserRole(req.user.id);
// Attach role to request for downstream use (e.g., bericht_text redaction)
(req as Request & { userRole?: AppRole }).userRole = role;
@@ -149,7 +164,9 @@ export function requireGroups(requiredGroups: string[]) {
return;
}
const userGroups: string[] = (req.user as any).groups ?? [];
logger.warn('DEPRECATED: requireGroups() — migrate to requirePermission()', { requiredGroups });
const userGroups: string[] = req.user?.groups ?? [];
const hasAccess = requiredGroups.some(g => userGroups.includes(g));
if (!hasAccess) {