apply security audit
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import atemschutzController from '../controllers/atemschutz.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requireGroups } from '../middleware/rbac.middleware';
|
||||
|
||||
const ADMIN_GROUPS = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'];
|
||||
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'];
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -15,13 +12,13 @@ router.get('/stats', authenticate, atemschutzController.getStats.bind(atems
|
||||
router.get('/my-status', authenticate, atemschutzController.getMyStatus.bind(atemschutzController));
|
||||
router.get('/:id', authenticate, atemschutzController.getOne.bind(atemschutzController));
|
||||
|
||||
// ── Write — admin + kommandant ───────────────────────────────────────────────
|
||||
// ── Write — gruppenfuehrer+ ─────────────────────────────────────────────────
|
||||
|
||||
router.post('/', authenticate, requireGroups(WRITE_GROUPS), atemschutzController.create.bind(atemschutzController));
|
||||
router.patch('/:id', authenticate, requireGroups(WRITE_GROUPS), atemschutzController.update.bind(atemschutzController));
|
||||
router.post('/', authenticate, requirePermission('atemschutz:write'), atemschutzController.create.bind(atemschutzController));
|
||||
router.patch('/:id', authenticate, requirePermission('atemschutz:write'), atemschutzController.update.bind(atemschutzController));
|
||||
|
||||
// ── Delete — admin only ──────────────────────────────────────────────────────
|
||||
// ── Delete — kommandant+ ────────────────────────────────────────────────────
|
||||
|
||||
router.delete('/:id', authenticate, requireGroups(ADMIN_GROUPS), atemschutzController.delete.bind(atemschutzController));
|
||||
router.delete('/:id', authenticate, requirePermission('atemschutz:delete'), atemschutzController.delete.bind(atemschutzController));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import authController from '../controllers/auth.controller';
|
||||
import { optionalAuth } from '../middleware/auth.middleware';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -14,9 +14,9 @@ router.post('/callback', authController.handleCallback);
|
||||
/**
|
||||
* @route POST /api/auth/logout
|
||||
* @desc Logout user
|
||||
* @access Public (optional auth for logging purposes)
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/logout', optionalAuth, authController.handleLogout);
|
||||
router.post('/logout', authenticate, authController.handleLogout);
|
||||
|
||||
/**
|
||||
* @route POST /api/auth/refresh
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import bookingController from '../controllers/booking.controller';
|
||||
import { authenticate, optionalAuth } from '../middleware/auth.middleware';
|
||||
import { requireGroups } from '../middleware/rbac.middleware';
|
||||
|
||||
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_fahrmeister', 'dashboard_moderator'];
|
||||
const ADMIN_GROUPS = ['dashboard_admin'];
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -22,13 +19,13 @@ router.get('/calendar-token', authenticate, bookingController.getCalendarToken.b
|
||||
// ── Write operations ──────────────────────────────────────────────────────────
|
||||
|
||||
router.post('/', authenticate, bookingController.create.bind(bookingController));
|
||||
router.patch('/:id', authenticate, requireGroups(WRITE_GROUPS), bookingController.update.bind(bookingController));
|
||||
router.patch('/:id', authenticate, requirePermission('bookings:write'), bookingController.update.bind(bookingController));
|
||||
|
||||
// Soft-cancel (sets abgesagt=TRUE)
|
||||
router.delete('/:id', authenticate, requireGroups(WRITE_GROUPS), bookingController.cancel.bind(bookingController));
|
||||
router.delete('/:id', authenticate, requirePermission('bookings:write'), bookingController.cancel.bind(bookingController));
|
||||
|
||||
// Hard-delete (admin only)
|
||||
router.delete('/:id/force', authenticate, requireGroups(ADMIN_GROUPS), bookingController.hardDelete.bind(bookingController));
|
||||
router.delete('/:id/force', authenticate, requirePermission('bookings:delete'), bookingController.hardDelete.bind(bookingController));
|
||||
|
||||
// ── Single booking read — after specific routes to avoid path conflicts ───────
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import equipmentController from '../controllers/equipment.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requireGroups } from '../middleware/rbac.middleware';
|
||||
|
||||
const ADMIN_GROUPS = ['dashboard_admin'];
|
||||
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_fahrmeister', 'dashboard_zeugmeister'];
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -18,15 +15,15 @@ router.get('/vehicle-warnings', authenticate, equipmentController.getVehicleW
|
||||
router.get('/vehicle/:fahrzeugId', authenticate, equipmentController.getByVehicle.bind(equipmentController));
|
||||
router.get('/:id', authenticate, equipmentController.getEquipment.bind(equipmentController));
|
||||
|
||||
// ── Write — admin + fahrmeister ──────────────────────────────────────────────
|
||||
// ── Write — gruppenfuehrer+ ────────────────────────────────────────────────
|
||||
|
||||
router.post('/', authenticate, requireGroups(WRITE_GROUPS), equipmentController.createEquipment.bind(equipmentController));
|
||||
router.patch('/:id', authenticate, requireGroups(WRITE_GROUPS), equipmentController.updateEquipment.bind(equipmentController));
|
||||
router.patch('/:id/status', authenticate, requireGroups(WRITE_GROUPS), equipmentController.updateStatus.bind(equipmentController));
|
||||
router.post('/:id/wartung', authenticate, requireGroups(WRITE_GROUPS), equipmentController.addWartung.bind(equipmentController));
|
||||
router.post('/', authenticate, requirePermission('equipment:write'), equipmentController.createEquipment.bind(equipmentController));
|
||||
router.patch('/:id', authenticate, requirePermission('equipment:write'), equipmentController.updateEquipment.bind(equipmentController));
|
||||
router.patch('/:id/status', authenticate, requirePermission('equipment:write'), equipmentController.updateStatus.bind(equipmentController));
|
||||
router.post('/:id/wartung', authenticate, requirePermission('equipment:write'), equipmentController.addWartung.bind(equipmentController));
|
||||
|
||||
// ── Delete — admin only ──────────────────────────────────────────────────────
|
||||
|
||||
router.delete('/:id', authenticate, requireGroups(ADMIN_GROUPS), equipmentController.deleteEquipment.bind(equipmentController));
|
||||
router.delete('/:id', authenticate, requirePermission('equipment:delete'), equipmentController.deleteEquipment.bind(equipmentController));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import eventsController from '../controllers/events.controller';
|
||||
import { authenticate, optionalAuth } from '../middleware/auth.middleware';
|
||||
import { requireGroups } from '../middleware/rbac.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/** Groups that may create, update, or cancel events */
|
||||
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_moderator'];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Categories
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -20,34 +17,34 @@ router.get('/kategorien', authenticate, eventsController.listKategorien.bind(eve
|
||||
|
||||
/**
|
||||
* POST /api/events/kategorien
|
||||
* Create a new category. Requires admin or moderator.
|
||||
* Create a new category. Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.post(
|
||||
'/kategorien',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:categories'),
|
||||
eventsController.createKategorie.bind(eventsController)
|
||||
);
|
||||
|
||||
/**
|
||||
* PATCH /api/events/kategorien/:id
|
||||
* Update an existing category. Requires admin or moderator.
|
||||
* Update an existing category. Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.patch(
|
||||
'/kategorien/:id',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:categories'),
|
||||
eventsController.updateKategorie.bind(eventsController)
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/events/kategorien/:id
|
||||
* Delete a category (only if no events reference it). Requires admin or moderator.
|
||||
* Delete a category (only if no events reference it). Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.delete(
|
||||
'/kategorien/:id',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:categories'),
|
||||
eventsController.deleteKategorie.bind(eventsController)
|
||||
);
|
||||
|
||||
@@ -106,23 +103,23 @@ router.get(
|
||||
|
||||
/**
|
||||
* POST /api/events/import
|
||||
* Bulk import events from CSV data. Requires admin or moderator.
|
||||
* Bulk import events from CSV data. Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.post(
|
||||
'/import',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:write'),
|
||||
eventsController.importEvents.bind(eventsController)
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/events
|
||||
* Create a new event. Requires admin or moderator.
|
||||
* Create a new event. Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:write'),
|
||||
eventsController.createEvent.bind(eventsController)
|
||||
);
|
||||
|
||||
@@ -134,34 +131,34 @@ router.get('/:id', authenticate, eventsController.getById.bind(eventsController)
|
||||
|
||||
/**
|
||||
* PATCH /api/events/:id
|
||||
* Update an existing event. Requires admin or moderator.
|
||||
* Update an existing event. Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.patch(
|
||||
'/:id',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:write'),
|
||||
eventsController.updateEvent.bind(eventsController)
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/events/:id
|
||||
* Soft-cancel an event (sets abgesagt=TRUE + reason). Requires admin or moderator.
|
||||
* Soft-cancel an event (sets abgesagt=TRUE + reason). Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:write'),
|
||||
eventsController.cancelEvent.bind(eventsController)
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/events/:id/delete
|
||||
* Hard-delete an event permanently. Requires admin or moderator.
|
||||
* Hard-delete an event permanently. Requires gruppenfuehrer+.
|
||||
*/
|
||||
router.post(
|
||||
'/:id/delete',
|
||||
authenticate,
|
||||
requireGroups(WRITE_GROUPS),
|
||||
requirePermission('events:write'),
|
||||
eventsController.deleteEvent.bind(eventsController)
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import vehicleController from '../controllers/vehicle.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requireGroups } from '../middleware/rbac.middleware';
|
||||
|
||||
const ADMIN_GROUPS = ['dashboard_admin'];
|
||||
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_fahrmeister'];
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -16,15 +13,15 @@ router.get('/alerts', authenticate, vehicleController.getAlerts.bind(vehicleCont
|
||||
router.get('/:id', authenticate, vehicleController.getVehicle.bind(vehicleController));
|
||||
router.get('/:id/wartung', authenticate, vehicleController.getWartung.bind(vehicleController));
|
||||
|
||||
// ── Write — admin only ────────────────────────────────────────────────────────
|
||||
// ── Write — kommandant+ ──────────────────────────────────────────────────────
|
||||
|
||||
router.post('/', authenticate, requireGroups(ADMIN_GROUPS), vehicleController.createVehicle.bind(vehicleController));
|
||||
router.patch('/:id', authenticate, requireGroups(ADMIN_GROUPS), vehicleController.updateVehicle.bind(vehicleController));
|
||||
router.delete('/:id', authenticate, requireGroups(ADMIN_GROUPS), vehicleController.deleteVehicle.bind(vehicleController));
|
||||
router.post('/', authenticate, requirePermission('vehicles:write'), vehicleController.createVehicle.bind(vehicleController));
|
||||
router.patch('/:id', authenticate, requirePermission('vehicles:write'), vehicleController.updateVehicle.bind(vehicleController));
|
||||
router.delete('/:id', authenticate, requirePermission('vehicles:delete'), vehicleController.deleteVehicle.bind(vehicleController));
|
||||
|
||||
// ── Status + maintenance log — admin + fahrmeister ────────────────────────────
|
||||
// ── Status + maintenance log — gruppenfuehrer+ ──────────────────────────────
|
||||
|
||||
router.patch('/:id/status', authenticate, requireGroups(WRITE_GROUPS), vehicleController.updateVehicleStatus.bind(vehicleController));
|
||||
router.post('/:id/wartung', authenticate, requireGroups(WRITE_GROUPS), vehicleController.addWartung.bind(vehicleController));
|
||||
router.patch('/:id/status', authenticate, requirePermission('vehicles:status'), vehicleController.updateVehicleStatus.bind(vehicleController));
|
||||
router.post('/:id/wartung', authenticate, requirePermission('vehicles:status'), vehicleController.addWartung.bind(vehicleController));
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user