Files
certvia/src/server/rbac.ts
T
MartinandClaude Fable 5 880afed391 Iteration 1: Auth (lokale Accounts), RBAC, RLS-Policies, i18n
- Auth.js v5 mit Credentials-Provider: Argon2id-Verifikation, JWT-Session
  mit Mandanten-Kontext, Rollen und Permissions; Login-/Logout-Flow (deutsch)
- RBAC-Katalog (28 Permissions, 5 Rollen-Blueprints) mit serverseitigem
  requirePermission; Seed legt Demo-Mandant und 4 Demo-Nutzer an
- Route-Gate über Next-16-proxy.ts (UX-Ebene), autoritative Prüfung
  serverseitig via requireSession/requirePermission
- Prisma-Migrationen: init + Row-Level-Security-Policies (zweite
  Verteidigungslinie, Scharfschaltung in Härtungs-Iteration dokumentiert)
- i18n-Gerüst mit next-intl (de aktiv, en vorbereitet), Audit-Log-Helper
- Login/Logout end-to-end im Browser verifiziert; Build/Lint/Typecheck grün

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:09:54 +02:00

153 lines
3.3 KiB
TypeScript

/**
* RBAC catalog: granular permissions bundled into per-tenant roles.
* See docs/SPEC.md §3. Permissions are global rows (no tenantId),
* roles are created per tenant from ROLE_DEFS at tenant provisioning.
*/
export const PERMISSIONS = [
// Administration
"tenant:manage",
"user:read",
"user:manage",
// Assets & BIA (one module)
"asset:read",
"asset:write",
"bia:read",
"bia:write",
// Risk
"risk:read",
"risk:write",
"risk:accept",
// Compliance
"soa:read",
"soa:write",
// Measures / tasks
"measure:read",
"measure:write",
// Policies
"policy:read",
"policy:write",
"policy:approve",
// Incidents
"incident:report",
"incident:manage",
// Audits
"audit:read",
"audit:conduct",
// Suppliers
"supplier:read",
"supplier:write",
// Evidence / documents
"evidence:read",
"evidence:write",
// Reporting / management review
"report:read",
"review:manage",
// Chat
"chat:use",
] as const;
export type Permission = (typeof PERMISSIONS)[number];
/** Role blueprints instantiated for every tenant (key → name + permissions). */
export const ROLE_DEFS: Record<
string,
{ name: string; permissions: readonly Permission[] }
> = {
"tenant-admin": {
name: "Mandanten-Admin",
permissions: ["tenant:manage", "user:read", "user:manage", "report:read", "chat:use"],
},
isb: {
name: "ISB / CISO",
permissions: [
"user:read",
"asset:read",
"asset:write",
"bia:read",
"bia:write",
"risk:read",
"risk:write",
"risk:accept",
"soa:read",
"soa:write",
"measure:read",
"measure:write",
"policy:read",
"policy:write",
"policy:approve",
"incident:report",
"incident:manage",
"audit:read",
"supplier:read",
"supplier:write",
"evidence:read",
"evidence:write",
"report:read",
"review:manage",
"chat:use",
],
},
auditor: {
name: "Auditor",
permissions: [
"asset:read",
"bia:read",
"risk:read",
"soa:read",
"measure:read",
"policy:read",
"audit:read",
"audit:conduct",
"supplier:read",
"evidence:read",
"report:read",
],
},
owner: {
name: "Asset-/Risk-Owner",
permissions: [
"asset:read",
"asset:write",
"bia:read",
"risk:read",
"risk:write",
"measure:read",
"measure:write",
"policy:read",
"incident:report",
"evidence:read",
"evidence:write",
"chat:use",
],
},
user: {
name: "Mitarbeiter",
permissions: ["policy:read", "incident:report", "measure:read", "chat:use"],
},
};
export class ForbiddenError extends Error {
constructor(permission: Permission) {
super(`Fehlende Berechtigung: ${permission}`);
this.name = "ForbiddenError";
}
}
/** Server-side permission check — UI hiding is convenience, this is security. */
export function requirePermission(
session: { user?: { permissions?: string[] } } | null,
permission: Permission
): void {
if (!session?.user?.permissions?.includes(permission)) {
throw new ForbiddenError(permission);
}
}
export function hasPermission(
session: { user?: { permissions?: string[] } } | null,
permission: Permission
): boolean {
return session?.user?.permissions?.includes(permission) ?? false;
}