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

@@ -1,87 +1,16 @@
import { Router, Request, Response, NextFunction } from 'express';
import memberController from '../controllers/member.controller';
import { authenticate } from '../middleware/auth.middleware';
import logger from '../utils/logger';
import { requirePermission } from '../middleware/rbac.middleware';
const router = Router();
// ----------------------------------------------------------------
// Role/permission middleware
//
// The JWT currently carries: { userId, email, authentikSub }.
// Roles come from Authentik group membership stored in the
// `groups` array on the UserInfo response. The auth controller
// already upserts the user in the DB on every login; this
// middleware resolves the role from req.user (extended below).
//
// Until a full roles column exists in the users table, roles are
// derived from a well-known Authentik group naming convention:
//
// "feuerwehr-admin" → AppRole 'admin'
// "feuerwehr-kommandant" → AppRole 'kommandant'
// (everything else) → AppRole 'mitglied'
//
// The groups are passed through the JWT as req.user.groups (added
// by the extended type below). Replace this logic with a DB
// lookup once a roles column is added to users.
// Apply authentication to every route in this router.
// requirePermission is applied per-route because PATCH allows the
// owner to update their own limited fields even without 'members:write'.
// ----------------------------------------------------------------
type AppRole = 'admin' | 'kommandant' | 'mitglied';
/**
* Resolves the AppRole from Authentik groups attached to the JWT.
* Mutates req.user.role so downstream controllers can read it directly.
*/
const resolveRole = (req: Request, _res: Response, next: NextFunction): void => {
if (req.user) {
const groups: string[] = (req.user as any).groups ?? [];
if (groups.includes('feuerwehr-admin')) {
req.user.role = 'admin';
} else if (groups.includes('feuerwehr-kommandant')) {
req.user.role = 'kommandant' as any;
} else {
req.user.role = 'mitglied' as any;
}
logger.debug('resolveRole', { userId: req.user.id, role: req.user.role });
}
next();
};
/**
* Factory: creates a middleware that enforces the minimum required role.
* Role hierarchy: admin > kommandant > mitglied
*/
const requirePermission = (permission: 'members:read' | 'members:write') => {
return (req: Request, res: Response, next: NextFunction): void => {
const role = (req.user as any)?.role ?? 'mitglied';
const writeRoles: AppRole[] = ['admin', 'kommandant'];
const readRoles: AppRole[] = ['admin', 'kommandant', 'mitglied'];
const allowed =
permission === 'members:write'
? writeRoles.includes(role)
: readRoles.includes(role);
if (!allowed) {
res.status(403).json({
success: false,
message: 'Keine Berechtigung für diese Aktion.',
});
return;
}
next();
};
};
// ----------------------------------------------------------------
// Apply authentication + role resolution to every route in this
// router. Note: requirePermission is applied per-route because
// PATCH allows the owner to update their own limited fields even
// without 'members:write'.
// ----------------------------------------------------------------
router.use(authenticate, resolveRole);
router.use(authenticate);
// IMPORTANT: The static /stats route must be registered BEFORE
// the dynamic /:userId route, otherwise Express would match