- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium, shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker. - Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft. - .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS, KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS, AI_MONTHLY_TOKEN_LIMIT. - CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy, seed, Passwort für craftvia_app, tsc, lint, build, npm run test. - docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets, Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke, Update/Rollback. build-and-push-images.sh baut craftvia-worker. - Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md und Skript-Kommentaren angepasst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
17 KiB
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:
Userbleibt die per-Mandant-Zeile (jetzt gedanklich „Mitgliedschaft") mit derselbenidund derselbentenantId.- Wir schneiden nur die Auth-Felder heraus in eine neue globale
Identityund hängenUser.identityId → Identity.idan. - In der Session bleibt
session.user.tenantIderhalten — es zeigt künftig auf den aktiven Mandanten. Dadurch bleiben ~356dbForTenant(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);emailbleibt 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.tsjwt/session-Callbacks (:276-299): neue Felder setzen,tenantId= aktiver Mandant.action-guard.ts:29-84und(app)/layout.tsx:37-120:sessionsValidAfterkünftig aus Identity lesen; Permissions bei Mandantenwechsel neu auflösen (heute beim Login eingefroren).rbac.ts(hasPermission/requirePermission, 43 Konsumenten): unverändert — liest weitersession.user.permissions, die aber pro aktivem Mandant befüllt werden.proxy.ts: neuer Pfad/select-tenantin 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
- Seite 1: E-Mail + Passwort (Feld „Organisation" entfällt).
signIngegen Identity (auth.ts:96vonuser.findManyaufidentity.findUnique({email})umstellen; Lockout/Dummy-Verify bleiben). - 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/mfabedient. - 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. - Volle Session ausstellen (Identity + MFA erfüllt),
memberships[]laden. - 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)sessionsValidAfterok? (4) MFA-Netz: verlangt Zielmandant MFA und nicht erfüllt → Enroll. Dann Token neu prägen:activeTenantId/activeMembershipId/permissions/tenantSlug. - Sicherheitskontrollen (Pflicht): aktiver
tenantIdserver-autoritativ (nie Client-Input); Re-Validierung bei jedem Wechsel; Audit „Identity → Mandant". (Siehe §8.) - Tenant-Switcher in
(app)/layout.tsx(Kopf/Sidebar) → ruftsetActiveTenant.
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 stattUser. 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(sessionsValidAfteran 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:
- Schema-Recut in
schema.prisma:Identityneu,Userverschlanken (+identityId),WebAuthnCredential/AuthTokenumhängen. - Forward-Migration(en):
identities(ohne RLS/Policy!),users-Spalten entfernen +identity_idFK,webauthn_credentialstenant_id/Policy entfernen +identity_id,auth_tokensprincipal-Umbau. RLS-Policies der betroffenen Tabellen anpassen (analogrls_enforce-Muster,20260730160000). db.ts:TENANT_MODELSanpassen (Identity raus lassen, WebAuthn entfernen).- Reseed:
prisma/seed.ts,provision.ts,bootstrap-admin.ts,sync-role-permissions.tsauf 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:
User.id= Mitgliedschaft, stabil lassen. Auth-Felder leben aufIdentity.Identityist global, nicht inTENANT_MODELS, keintenant_id, keine RLS-Policy.- Genau ein aktiver Mandant pro Session; server-autoritativ; bei Wechsel re-validieren.
- Keine „Passwort direkt setzen"-Anlage mehr — nur Einladung.
- 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
PlatformAdminin dieIdentity(Store getrennt lassen, nur Verweis).