Files
craftvia/docs/FEINDESIGN-identity-mandanten.md
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +02:00

17 KiB
Raw Blame History

Feindesign & Implementierungsplan — Zentrale Identität + Mandanten-Mitgliedschaften (certvia)

Produkt: certvia (ISMS-Tool). Grundlage: KONZEPT-identity-mandanten.md (Option C, Entscheidungen A–E + Two-Step-Login + Passphrasen). Dieses Dokument ist der umsetzbare Bauplan für ein mehrköpfiges, noch zu onboardendes Entwicklerteam inkl. PM. Alle Datei:Zeile-Angaben beziehen sich auf Branch dev.

0. Wie dieses Dokument zu lesen ist

  • PM: §7 (Meilensteine), §8 (Risiken), §9 (DoD), §11 (Rollen/Cadence), §12 (Aufwand).
  • Entwickler: §1–§6 (Feindesign), §10 (Onboarding + „Goldene Regeln").
  • Alle: §1 ist die eine Leitentscheidung, aus der alles Weitere folgt.

1. Leitentscheidung (das Sicherheitsnetz): User.id bleibt stabil

Wir führen nicht eine große Umverkabelung durch. Stattdessen:

  • User bleibt die per-Mandant-Zeile (jetzt gedanklich „Mitgliedschaft") mit derselben id und derselben tenantId.
  • Wir schneiden nur die Auth-Felder heraus in eine neue globale Identity und hängen User.identityId → Identity.id an.
  • In der Session bleibt session.user.tenantId erhalten — es zeigt künftig auf den aktiven Mandanten. Dadurch bleiben ~356 dbForTenant(session.user.tenantId)-Stellen, alle 6 Owner-FKs (assets/processes/risks/measures.owner_id, user_roles, webauthn) und das RLS-Modell unangetastet.

Folge: Der Umbau konzentriert sich auf Login, Session-Aufbau, Mandantenauswahl, Nutzer-Lifecycle — nicht auf die 356 Fachstellen. Das ist die risikoärmste Schneise.

Zusatz-Vereinfachung (Entscheidung D): Es gibt keinen Produktivdatenbestand (nur Testdaten). Deshalb kein Backfill/Merge, sondern Schema-Neuschnitt + Reseed. Der aufwändigste und riskanteste Teil eines solchen Umbaus entfällt komplett.


2. Zieldatenmodell (konkret)

Neu — Identity (global, KEIN tenantId, NICHT in TENANT_MODELS): id, email @unique, passwordHash, mustChangePassword, mfaSecret, mfaEnrolledAt, recoveryCodes, lastTotpStep, failedLogins, lockedUntil, sessionsValidAfter, status, createdAt, updatedAt. → Übernimmt exakt die Felder, die heute in User (schema.prisma:98-139) und PlatformAdmin doppelt liegen.

Geändert — User = Mitgliedschaft (bleibt in TENANT_MODELS, behält tenantId+id):

  • entfernt (wandern zu Identity): passwordHash, mfaSecret, mfaEnrolledAt, recoveryCodes, lastTotpStep, failedLogins, lockedUntil, mustChangePassword, sessionsValidAfter, isPlatformAdmin.
  • neu: identityId → Identity.
  • bleibt: tenantId, name, status: UserStatus, userRoles, alle Ownership-Relationen. @@unique([tenantId, email]) → ersetzt durch @@unique([tenantId, identityId]) (eine Person max. 1 Mitgliedschaft je Mandant); email bleibt als Denormalisierung optional oder entfällt (Quelle = Identity).

WebAuthnCredential (schema.prisma:143-160): userId → identityId, tenantId entfällt → raus aus TENANT_MODELS, RLS-Policy der Tabelle entfernen (Passkeys sind identitäts-, nicht mandantengebunden).

AuthToken (schema.prisma:1679-1700): principalType/principalId → auf identity umstellen; neuer TokenType: "invitation" (heute wird password_reset mit 7-Tage-TTL zweckentfremdet).

PlatformAdmin (schema.prisma:211-234): bleibt getrennt (Entscheidung E). Option (empfohlen, klein): eine Identity kann zusätzlich Plattform-Admin sein → PlatformAdmin.identityId als Verweis, Store aber getrennt. Nicht zwingend für Phase 1.

TENANT_MODELS (db.ts:81-148): Identity nicht aufnehmen (globaler Lookup über Owner-prisma, wie heute der Login). WebAuthnCredential entfernen. User bleibt drin.


3. Session-/Token-Shape (neu)

JWT/Session (next-auth.d.ts erweitern):

identityId
activeTenantId      → gespiegelt als session.user.tenantId  (⇒ 356 Call-Sites unverändert!)
activeMembershipId  (= User.id des aktiven Mandanten)
memberships[]       [{ tenantId, tenantSlug, membershipId, tenantName }]  (schlanke Liste für den Switcher)
permissions[]       (des AKTIVEN Mandanten)
tenantSlug          (des aktiven Mandanten)
isPlatformAdmin, mfaEnrolled, tokenIssuedAt

Kritische Regel: session.user.tenantId === activeTenantId, immer genau ein aktiver Mandant. Kippt das (leer/mehrdeutig), kippt der RLS-Kontext → Fail-closed.

Konsumenten anpassen (aber minimal):

  • auth.ts jwt/session-Callbacks (:276-299): neue Felder setzen, tenantId = aktiver Mandant.
  • action-guard.ts:29-84 und (app)/layout.tsx:37-120: sessionsValidAfter künftig aus Identity lesen; Permissions bei Mandantenwechsel neu auflösen (heute beim Login eingefroren).
  • rbac.ts (hasPermission/requirePermission, 43 Konsumenten): unverändert — liest weiter session.user.permissions, die aber pro aktivem Mandant befüllt werden.
  • proxy.ts: neuer Pfad /select-tenant in Post-Login-Gate; „angemeldet, aber kein aktiver Mandant → /select-tenant".

4. Die Flows (Feindesign)

4.1 Login (Two-Step, MFA beim Login) — login/page.tsx, auth.ts

  1. Seite 1: E-Mail + Passwort (Feld „Organisation" entfällt). signIn gegen Identity (auth.ts:96 von user.findMany auf identity.findUnique({email}) umstellen; Lockout/Dummy-Verify bleiben).
  2. MFA-pending-Zustand: kurzlebiger, signierter, einzweckiger Server-State (kein voller Session-Cookie). Realisierung: eigener NextAuth-Step oder ein mfa_pending-JWT mit 5 min TTL, das nur /login/mfa bedient.
  3. Seite 2: MFA — angezeigt, wenn Identity MFA hat oder ≥ 1 Mitgliedschaft in MFA-Pflicht-Mandant. TOTP/Passkey verifizieren (mfa.ts:47, Replay via Identity.lastTotpStep) oder Enroll erzwingen.
  4. Volle Session ausstellen (Identity + MFA erfüllt), memberships[] laden.
  5. Weiterleitung: 1 Mitgliedschaft → direkt /dashboard (activeTenant gesetzt); mehrere → /select-tenant; keine → Hinweisseite.

4.2 Mandantenauswahl & -Wechsel — neu: /select-tenant + Action setActiveTenant

  • Server Action setActiveTenant(membershipId): (1) Membership gehört zur Session-Identity? (2) Mitgliedschaft+Mandant ACTIVE? (3) sessionsValidAfter ok? (4) MFA-Netz: verlangt Zielmandant MFA und nicht erfüllt → Enroll. Dann Token neu prägen: activeTenantId/activeMembershipId/permissions/tenantSlug.
  • Sicherheitskontrollen (Pflicht): aktiver tenantId server-autoritativ (nie Client-Input); Re-Validierung bei jedem Wechsel; Audit „Identity → Mandant". (Siehe §8.)
  • Tenant-Switcher in (app)/layout.tsx (Kopf/Sidebar) → ruft setActiveTenant.

4.3 Einladung (Standard-Anlageweg) — auth-selfservice.ts, auth-token.ts, neue Seite /invite

  • Neuer TokenType: "invitation" + Zielseite /invite?token= (Erst-Passwort + optional MFA auf der Identity setzen), statt Zweckentfremdung von /reset.
  • Neutralität: Der einladende Admin sieht immer „Einladung gesendet" — unabhängig davon, ob die Identity schon existierte (kein Cross-Tenant-Leak).
  • Existiert die Identity → Mail „Sie wurden zu Mandant X hinzugefügt" + Bestätigungslink (Beitritt annehmen). Existiert nicht → „Konto einrichten + beitreten".

4.4 Nutzeranlage

  • Plattform (platform-users.ts:89): createTenantUser → Identity finden/erstellen + Membership + Rollen; pwMode "set" + Fallback-Passwort entfernen; Betreiber darf direkt verknüpfen.
  • Kunde (tenant-users.ts:139): createUser → immer Einladung; „Passwort setzen"-Zweig raus.
  • Onboarding-Wizard (onboarding-team.ts:166): inviteFunctionHolder → Identity-Einladung statt User+Passwort.
  • UI (user-forms.tsx): pwMode-Auswahl entfällt → reines Einladungsformular (Name, E-Mail, Rollen).

4.5 MFA-Pflicht-Durchsetzung (Mandanten-Policy trifft Identity-MFA)

  • Gates umstellen: (app)/layout.tsx:73-79, enroll-mfa/page.tsx:24-32, (platform)/layout.tsx:27 → prüfen Identity.mfaEnrolledAt statt User.
  • mfa-policy.ts:7 (resolveMfaRequired, mandantenweit) bleibt. Regel: strengster betretener Mandant gewinnt; da MFA an der Identity hängt, deckt eine Einrichtung alle ab.

4.6 Passwort/MFA-Reset → Identity-Ebene, raus aus Mandantenverwaltung

  • Entfernen: platform-users.ts:152 (resetTenantUserPassword), tenant-users.ts:207 (resetUserPassword) + UI-Bindungen (admin/[id]/page.tsx:205, settings/users/page.tsx:105). Ersatz: Identity-Self-Service (Recovery-Codes) + Plattform-Ebene.
  • Umleiten auf Identity: auth-recovery.ts (gesamt), account.ts:66 (changeOwnPassword), sessions.ts:34/92 (sessionsValidAfter an Identity), webauthn.ts:73 (Passkey an Identity), secret-crypto.ts-Nutzung unverändert (Schlüssel bleibt).

5. Migrationsstrategie (DB)

Dank Entscheidung D kein Backfill. Ablauf:

  1. Schema-Recut in schema.prisma: Identity neu, User verschlanken (+identityId), WebAuthnCredential/AuthToken umhängen.
  2. Forward-Migration(en): identities (ohne RLS/Policy!), users-Spalten entfernen + identity_id FK, webauthn_credentials tenant_id/Policy entfernen + identity_id, auth_tokens principal-Umbau. RLS-Policies der betroffenen Tabellen anpassen (analog rls_enforce-Muster, 20260730160000).
  3. db.ts: TENANT_MODELS anpassen (Identity raus lassen, WebAuthn entfernen).
  4. Reseed: prisma/seed.ts, provision.ts, bootstrap-admin.ts, sync-role-permissions.ts auf Identity+Membership. Testumgebung frisch aufsetzen.

Für die Test-/Coolify-Instanz: einmalig DB leeren (nur Testdaten) → migrate deploy → Seed. Kein Sonderpfad nötig.


6. Arbeitspakete (Workstreams) für das Team

WS Inhalt Kernfiles Abhängig von
WS0 Fundament Identity-Modell, Schema-Recut, Migrationen, TENANT_MODELS, RLS-Anpassung WebAuthn schema.prisma, prisma/migrations/*, db.ts — (Blocker)
WS1 Auth-Kern Login gegen Identity, Two-Step + MFA-pending, Token/Session-Shape, jwt/session-Callbacks auth.ts, next-auth.d.ts WS0
WS2 Mandantenkontext /select-tenant, setActiveTenant, Tenant-Switcher, Guards + Permissions bei Wechsel, proxy.ts action-guard.ts, (app)/layout.tsx, proxy.ts, rbac.ts WS1
WS3 Einladungs-Lifecycle invitation-Token, /invite-Seite, Anlage überall auf Einladung, „Passwort setzen" raus auth-token.ts, auth-selfservice.ts, platform-users.ts, tenant-users.ts, onboarding-team.ts, user-forms.tsx WS0
WS4 Passwort/MFA an Identity Reset/Change/Email-Change, MFA/Passkey/Recovery, Sessions-Invalidierung, Reset raus aus Mgmt auth-recovery.ts, account.ts, sessions.ts, webauthn.ts, platform.ts WS0
WS5 UI Login-Seiten (2-stufig), /select-tenant, Switcher, Einladungsformulare login/*, neue Seiten, (app)/layout.tsx WS1, WS2
WS6 Seed/Provision/Bootstrap Reseed auf Identity, provision.ts, bootstrap-admin.ts, sync-role-permissions.ts genannte WS0
WS7 Tests & Gate neue scripts/test-*: Identity-Login, Tenant-Switch-Isolation, MFA-Enforcement multi-tenant, Einladung, Two-Step-Bypass scripts/test-*.ts fortlaufend

Parallelisierung: Nach WS0 laufen WS1, WS3, WS4, WS6 parallel. WS2 setzt auf WS1, WS5 auf WS1+WS2. WS7 durchgehend (jede Story bringt ihren Test mit).


7. Meilensteine (PM-Sicht)

M Ergebnis (Demo-fähig) umfasst
M0 Onboarding abgeschlossen, Dev-Umgebungen laufen, Gate grün §10
M1 Fundament: Schema+Migration+Reseed grün, RLS-Tests grün WS0, WS6, WS7-Basis
M2 Login gegen Identity (Two-Step) + Single-Membership-Nutzer landet im Dashboard WS1, WS5-Login
M3 Multi-Membership: Auswahl + Wechsel + MFA-Netz + Isolation nachgewiesen WS2, WS5-Switcher, WS7-Isolation
M4 Einladungs-Lifecycle (Plattform+Kunde+Wizard), „Passwort setzen" entfernt WS3
M5 Passwort/MFA-Reset auf Identity, Reset raus aus Mandantenverwaltung WS4
M6 Härtung, vollständige Test-Suite, Doku, STAND aktualisiert, Deploy WS7, Doku

8. Risiken & Gegenmaßnahmen

Risiko Gegenmaßnahme
RLS kippt, wenn kein/mehrdeutiger aktiver Mandant session.user.tenantId immer = genau eine validierte Membership; fail-closed; Guard wirft bei leer
356 Call-Sites anfassen Leitentscheidung §1: User.id + session.user.tenantId-Semantik erhalten → Call-Sites unverändert
Two-Step „halb angemeldet"-Bypass MFA-pending-State ist einzweckig + kurzlebig, keine App-Session vor MFA
Rechte veraltet bei Wechsel ohne Re-Login setActiveTenant löst Permissions neu auf (nicht Token-eingefroren)
Breiterer Blast-Radius eines Credentials starke Passphrase-Policy + MFA + kurze Session + globaler Kill-Switch (Identity.sessionsValidAfter)
Plattform/Tenant-Cookie-Kollision bereits gelöst (getrennte Cookie-Namen, platform-auth.ts) — beibehalten
Team nicht eingearbeitet M0-Onboarding als eigener Meilenstein, „Goldene Regeln" §10, Pair auf WS0

9. Definition of Done

Pro Story: tsc + lint + build grün · zugehöriges scripts/test-*.ts grün · keine neuen dbForTenant-Regressionen · Doku-Schnipsel im PR. Gesamt (M6): alle scripts/test-* grün (inkl. neuer Isolations-/Enforcement-Tests) · Login/Auswahl/Wechsel/Einladung/Reset im Browser verifiziert · RLS-Test mit Multi-Membership-Nutzer nachweist, dass Wechsel keine Fremddaten sichtbar macht · docs/STAND-dev-branch.md + dieses Doc aktualisiert · sauber nach dev (beide Remotes) integriert.


10. Onboarding der neuen Entwickler (M0)

Pflichtlektüre (in dieser Reihenfolge): docs/HANDOVER-DEV.md → docs/SPEC.md → docs/STAND-dev-branch.md → dieses Doc → KONZEPT-identity-mandanten.md.

Dev-Setup: Node + Postgres (pgvector-Image), .env aus .env.example, npx prisma migrate deploy, Demo-Seed (RUN_DEMO_SEED), npm run dev. Demo-Logins in prisma/seed.ts.

Validierungs-Gate (vor jedem PR): npx tsc --noEmit · npm run lint · npm run build · alle 17 scripts/test-*.ts.

RLS-Mentalmodell (Pflichtverständnis): dbForTenant(tenantId) setzt pro Transaktion app.tenant_id; TENANT_MODELS sagt, welche Modelle mandantengefiltert sind; globale Modelle (Kataloge, künftig Identity) laufen über den Owner-prisma. Nie ein globales Modell in TENANT_MODELS aufnehmen und nie ein tenant-Modell ohne Kontext lesen.

DevOps-Workflow: Feature-Branch → git merge --no-ff nach dev → Gate grün → Push auf beide Remotes (origin = git.certvia.de, local-gitea) → docs/STAND-dev-branch.md pflegen. (certvia ist strikt getrennt von anderen Produkten — nichts vermischen.)

Goldene Regeln dieses Umbaus:

  1. User.id = Mitgliedschaft, stabil lassen. Auth-Felder leben auf Identity.
  2. Identity ist global, nicht in TENANT_MODELS, kein tenant_id, keine RLS-Policy.
  3. Genau ein aktiver Mandant pro Session; server-autoritativ; bei Wechsel re-validieren.
  4. Keine „Passwort direkt setzen"-Anlage mehr — nur Einladung.
  5. MFA/Passwort gehören der Identity — kein Mandanten-Admin-Reset.

11. Rollen & Cadence (Beispielbesetzung: 1 Lead + 2 Devs + PM)

Rolle Verantwortung
Tech-Lead / Senior WS0 + WS1 (Fundament + Auth-Kern), Review aller Auth-/RLS-PRs, Sicherheitskontrollen §8
Dev A WS2 (Mandantenkontext/Guards) + WS5 (UI)
Dev B WS3 (Einladung) + WS4 (Passwort/MFA an Identity) + WS6 (Seed)
alle WS7 (jede Story bringt ihren Test)
PM Meilenstein-Tracking (§7), Risiken (§8), DoD-Abnahme (§9), Entscheidungs-Eskalation (Phase-2-Punkte), Cadence

Cadence-Vorschlag: WS0 als Pairing (Lead + je 1 Dev) — dient zugleich als Onboarding-Vehikel; danach 2-Wochen-Iterationen entlang M1–M6, Demo je Meilenstein, PR-Review verpflichtend für Auth/RLS.


12. Aufwandsschätzung (grob, Personentage)

Annahme: 3 Devs, noch nicht eingearbeitet → Ramp-up eingepreist. Ohne Produktivdaten-Migration (Entscheidung D).

Block PT (Bereich)
M0 Onboarding (3 Personen) 6–9
WS0 Fundament + Migration + Reseed 5–8
WS1 Auth-Kern (Two-Step, Session-Shape) 8–12
WS2 Mandantenkontext + Guards + Switcher 6–9
WS3 Einladungs-Lifecycle 5–8
WS4 Passwort/MFA an Identity 6–9
WS5 UI 4–6
WS6 Seed/Provision/Bootstrap 2–4
WS7 Tests & Härtung 5–8
Summe ≈ 47–73 PT

Kalenderdauer bei 3 Devs mit Parallelisierung (§6) + PM-Overhead: ≈ 5–7 Wochen bis M6 (inkl. Onboarding-Woche). Kritischer Pfad: WS0 → WS1 → WS2 → WS5. WS3/WS4/WS6 laufen daneben.


13. Phase 2 (bewusst später, kein Blocker)

  • Per-Mandant-Schalter „bei jedem Betreten Step-up erzwingen" (Hochsicherheits-Kunden).
  • E-Mail-Änderung als Identity-Operation (Sonderfälle/Merge).
  • Optionale Konsolidierung PlatformAdmin in die Identity (Store getrennt lassen, nur Verweis).