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