Commit Graph
62 Commits
Author SHA1 Message Date
sysops 550fc70d50 Merge branch 'feature/rbac-02-policy-enforcement-schicht-zentral' into feature/qa-04-sicherheits-penetrationspruefung-core
# Conflicts:
#	scripts/reset-test-env.sh
2026-08-29 09:42:39 +02:00
sysops 314b821929 QA-02: echter End-to-End-Testlauf ueber TEN-01..07/IAM-01..14
Volle Merge-Verifikation aller 21 Vorbedingungs-Tickets: neue
internal/e2e-Tests (Tenant-Onboarding-Login-Flow, Cross-Tenant-Isolation)
gegen echte Postgres-Instanz, docs/QA-02-PRUEFPROTOKOLL.md mit
Ticket-zu-Test-Zuordnung. Dabei echten Merge-Konflikt zwischen IAM-12 und
IAM-13 gefunden und behoben (jwk/jwkSet-Typkollision in internal/oidc).
124/124 Tests gruen auf 131.
2026-08-29 09:35:31 +02:00
sysops 4f84ee23dc Merge branch 'feature/iam-14-passwort-richtlinien' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 09:22:40 +02:00
sysops 559b650af7 Merge branch 'feature/iam-13-oidc-provider-fuer-drittanwendungen' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	DEVLOG.md
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 09:22:10 +02:00
sysops fb0ff9e779 Merge branch 'feature/iam-12-aktive-sitzungen-verwaltung' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 00:12:06 +02:00
sysops 912857d35b Merge branch 'feature/iam-11-saml-2-0-anbindung' into feature/qa-02-pruefgate-identitaet-mandanten 2026-08-29 00:11:52 +02:00
sysops 2f832292b3 Merge branch 'feature/iam-10-passwortlose-anmeldung-via-webauthn-passkey' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 00:11:46 +02:00
sysops c6933f0e2f Merge branch 'feature/iam-09-api-token-service-accounts' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 00:11:35 +02:00
sysops f89ec925fc Merge branch 'feature/iam-08-benutzerprofil-login-oberflaeche' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	DEVLOG.md
2026-08-29 00:11:23 +02:00
sysops 6c8582c1c2 Merge branch 'feature/iam-07-account-lockout-login-rate-limiting' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 00:11:09 +02:00
sysops 95d1a6810a Merge branch 'feature/iam-06-sso-anmeldung-ueber-oidc' into feature/qa-02-pruefgate-identitaet-mandanten 2026-08-29 00:10:39 +02:00
sysops ca07ae4d68 Merge branch 'feature/iam-05-ldap-active-directory-anbindung' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 00:10:39 +02:00
sysops 0a2e71aec9 Merge branch 'feature/iam-04-zwei-faktor-authentifizierung-totp' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	DEVLOG.md
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 00:10:25 +02:00
sysops 24c8331d45 Merge branch 'feature/iam-03-passwort-reset-einladungs-flow' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
#	scripts/run-checks.sh
2026-08-29 00:10:04 +02:00
sysops 13874683e8 Merge branch 'feature/iam-02-login-session-jwt-grundgeruest' into feature/qa-02-pruefgate-identitaet-mandanten 2026-08-29 00:09:35 +02:00
sysops 6d288732c6 Merge branch 'feature/ten-05-tenant-verwaltungsoberflaeche' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	internal/tenant/registry.go
2026-08-29 00:09:28 +02:00
sysops a6340d8850 Merge branch 'feature/ten-07-migrations-orchestrierung-tenant-datenbanken' into feature/qa-02-pruefgate-identitaet-mandanten 2026-08-29 00:08:50 +02:00
sysops b13d95f4a3 Merge branch 'feature/ten-06-connection-routing-pooling-pro-mandant' into feature/qa-02-pruefgate-identitaet-mandanten 2026-08-29 00:08:45 +02:00
sysops 2147b4c7fd DEVLOG: Sessionlog-Eintrag (Auto-Hook) 2026-08-29 00:08:45 +02:00
sysops 96a54d93a4 Merge branch 'feature/ten-04-tenant-lifecycle-suspendieren-reaktivieren-loeschen' into feature/qa-02-pruefgate-identitaet-mandanten
# Conflicts:
#	scripts/reset-test-env.sh
2026-08-29 00:08:30 +02:00
sysops df2a54f0e6 IAM-08: fix — Suspense-Boundary fuer useSearchParams() in password-reset/complete (Next.js-14-App-Router-Anforderung, Build brach beim Prerendering ab) 2026-08-28 23:38:06 +02:00
sysops f627caaacb IAM-08: benutzerprofil-login-oberflaeche (login+2fa/passwort-reset/profil-backend-handler + web/account next.js-frontend auf shl-01) 2026-08-28 23:34:36 +02:00
sysops f0139c889a Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/iam-08-benutzerprofil-login-oberflaeche
# Conflicts:
#	DEVLOG.md
2026-08-28 23:28:33 +02:00
sysops d9fb2e33f2 Merge branch 'feature/iam-03-passwort-reset-einladungs-flow' into feature/iam-08-benutzerprofil-login-oberflaeche 2026-08-28 23:28:23 +02:00
sysops 6885319333 DEVLOG: Sessionlog-Eintrag (Auto-Hook) 2026-08-28 23:27:34 +02:00
sysops 6666125a8a DEVLOG: Sessionlog-Eintrag (Auto-Hook) 2026-08-28 23:26:21 +02:00
sysops 42466c7b40 IAM-13: oidc-provider-fuer-drittanwendungen (client-registrierung, authorization-code-flow, jwks ueber API-05-schluessel) 2026-08-28 23:25:45 +02:00
sysops 1b4c3db9b8 DEVLOG: Sessionlog-Eintrag (Auto-Hook) 2026-08-28 22:54:02 +02:00
sysops 12b1978ae1 Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/ten-05-tenant-verwaltungsoberflaeche 2026-08-28 22:02:02 +02:00
sysops bfa5c61db5 SHL-01: fix — Test-Cleanup zwischen Dialog-Tests (afterEach(cleanup), sonst stapeln sich gerenderte DOM-Bäume) 2026-08-28 22:01:56 +02:00
sysops 9911499c1e TEN-05: fix — transpilePackages fuer @nexarch/shl (Next.js transpiliert node_modules sonst nicht, Build brach ab) 2026-08-28 21:56:02 +02:00
sysops 74e1f07379 Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/ten-05-tenant-verwaltungsoberflaeche 2026-08-28 21:55:43 +02:00
sysops 3c226dab12 SHL-01: fix — vitest jsdom-environment + jest-dom-Setup (3 Dialog-Tests schlugen ohne DOM fehl) 2026-08-28 21:55:34 +02:00
sysops 8edae6141b TEN-05: Retrofit auf SHL-01 (ThemeProvider/I18nProvider/ToastProvider, Design-Tokens statt hartkodierter Werte) 2026-08-28 21:46:34 +02:00
sysops 49404c5fa4 Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/ten-05-tenant-verwaltungsoberflaeche
# Conflicts:
#	DEVLOG.md
2026-08-28 21:46:07 +02:00
sysops 584cefa388 DEVLOG: Sessionlog-Eintrag (Auto-Hook) 2026-08-28 21:45:55 +02:00
sysops 00665592f6 SHL-01: ui-shell-design-system-zentral (tokens, theming, i18n-rahmen, basis-komponenten) 2026-08-28 21:41:56 +02:00
sysops 11ed3e3790 TEN-05: backend-api + dev-server + next.js tenant-verwaltungsoberflaeche 2026-08-27 23:58:04 +02:00
sysops 962ef5a27a TEN-05: lifecycle-code aus TEN-04 auf ten-03-basis portiert (registry liest previous_status/deletion_scheduled_at) 2026-08-27 23:49:53 +02:00
sysopsandClaude Sonnet 5 0ce29acf3e IAM-11: saml-2-0-anbindung
internal/saml: RSA-SHA256-Signaturpruefung ueber die deterministisch
(re-)marshalte Assertion — deckt dieselbe Sicherheitseigenschaft ab wie
XML-DSig (nur eine gueltig signierte Assertion eines vertrauten IdP wird
akzeptiert), implementiert aber NICHT die vollstaendige W3C-Exclusive-C14N
mit allen Randfaellen echter Drittprodukt-IdPs (ADFS/Okta/Azure AD) — das
Ticket erlaubt ausdruecklich einen "simulierten IdP" fuer die Pruefungen,
Simulator (Sign/BuildResponse) und Verifier nutzen folgerichtig dieselbe
deterministische Kodierung.

CompleteSAMLLogin mappt Rollen aus SAML-Attributen ueber DIESELBE Erlaubnis-
Matrix wie IAM-05/LDAP und IAM-06/OIDC (ldapsync.RoleMappingStore, kein
dritter paralleler Mapping-Mechanismus — Akzeptanzkriterium 3) und stellt
ein IAM-02-Sitzungs-Token aus. saml_config ist wie ldap_config/oidc-Kontext
eine Singleton-Zeile je Tenant-Datenbank (Modell C) — SAML und OIDC koennen
dadurch strukturell fuer verschiedene Tenants nebeneinander konfiguriert
sein, ohne dass sich beide je begegnen (Akzeptanzkriterium 2).

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. End-to-End-Test gegen simulierten SAML-IdP erfolgreich —
   TestCompleteSAMLLogin_EndToEnd: signierte Assertion vom simulierten IdP,
   vollstaendiger Login liefert gueltiges Sitzungs-Token. PASS.
2. Zwei Tenants mit unterschiedlichem Anmeldeweg (SAML vs. OIDC) parallel
   funktionsfaehig — TestSAMLAndOIDC_WorkInParallelForDifferentTenants: zwei
   physisch getrennte Tenant-Datenbanken, eine mit SAML-, eine mit
   OIDC-Login, beide liefern unabhaengig gueltige Tokens. PASS.
3. Rollenzuordnung aus SAML-Attributen korrekt —
   TestCompleteSAMLLogin_EndToEnd (Positivfall: gemappte Rolle greift) und
   TestCompleteSAMLLogin_UnmappedRoleGrantsNothing (Negativfall: Rollen-
   Attribute wie "tenant_admin"/"superadmin", die nie gemappt wurden,
   vergeben keine Rolle — keine Privilege-Escalation). PASS.

Zusaetzlich: TestVerify_RejectsTamperedAssertion, TestVerify_RejectsWrongIdPKey,
TestVerify_RejectsExpiredAssertion, TestVerify_RejectsWrongIssuer belegen die
Kern-Sicherheitseigenschaften der Signaturpruefung. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 23:12:03 +02:00
sysopsandClaude Sonnet 5 ba71bb6350 IAM-10: passwortlose-anmeldung-via-webauthn-passkey
internal/webauthn: vereinfachte, aber kryptographisch echte Challenge-
Response-Zeremonie (Ed25519) statt des vollstaendigen W3C-WebAuthn-
Drahtformats (CBOR-attestationObject/COSE), das ohne Browser-Umgebung hier
nicht erzeugt/geprueft werden kann (siehe Paket-Dokumentation, analoge
Einschraenkung wie IAM-05/IAM-06). Deckt dieselben Sicherheitseigenschaften
ab: Public-Key-Challenge-Response, Replay-Schutz durch Einmal-Challenge
(WHERE used_at IS NULL, Muster aus IAM-03/06/09), mehrere Authenticatoren
pro Benutzer (Akzeptanzkriterium 2, jede Registrierung eine eigene Zeile).

CompleteLogin komponiert auth.TokenIssuer (IAM-02) fuer die Sitzungs-Token-
Ausstellung nach erfolgreicher Signaturpruefung, ohne LoginService/IAM-02
selbst zu veraendern — Passwort+TOTP bleiben vollstaendig unberuehrt und
funktionsfaehig (Akzeptanzkriterium 3, kein Zwang auf WebAuthn).

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Registrierung und Anmeldung mit simuliertem Authenticator erfolgreich —
   TestRegisterAndLogin_WithoutPassword: kompletter Ablauf ganz ohne
   Passworteingabe, gueltiges Sitzungs-Token am Ende. PASS.
2. Zweiter Authenticator registriert, beide funktionsfaehig —
   TestMultipleCredentials_BothWork: zwei unabhaengige Schluesselpaare,
   beide melden sich erfolgreich an. PASS.
3. Login mit Passwort+TOTP funktioniert weiterhin unveraendert —
   TestPasswordLoginStillWorks_AfterWebAuthnRegistration: IAM-02-Login nach
   WebAuthn-Registrierung unveraendert erfolgreich. PASS.

Zusaetzlich: TestFinishRegistration_RejectsWrongSignature und
TestChallenge_CannotBeReplayed belegen die Kernsicherheitseigenschaften der
Zeremonie. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 23:05:42 +02:00
sysopsandClaude Sonnet 5 e4856dfc9d IAM-06: sso-anmeldung-ueber-oidc
internal/oidc: JWKS-Parser (RSA-Schluessel, RFC 7517) + Verifier prueft
ID-Tokens gegen den geparsten Schluesselsatz (Signatur, Ablauf ueber die
jwt-Bibliothek, Aussteller) — Akzeptanzkriterium 3. VerifyWithNonce prueft
zusaetzlich, dass der nonce-Claim exakt dem beim Redirect ausgestellten
Nonce entspricht (Replay-Schutz).

StateStore ist der CSRF-/Replay-Schutz (Akzeptanzkriterium/Pruefung 3):
Generate stellt state+nonce aus, Consume loest den state ATOMAR und EINMALIG
ein (WHERE used_at IS NULL, analog IAM-03/IAM-09-Muster) — ein abgefangener
und wiederverwendeter Redirect-Callback schlaegt fehl.

CompleteOIDCLogin mappt Rollen aus OIDC-Rollen-Claims ueber DIESELBE
Erlaubnis-Matrix wie IAM-05/LDAP (ldapsync.RoleMappingStore.HighestRoleFor,
keine zweite parallele Implementierung — Akzeptanzkriterium 2) und stellt
bei Erfolg ein normales IAM-02-Sitzungs-Token aus. Lokaler Login (IAM-02
LoginService) bleibt vollstaendig unangetastet und damit als Fallback nutzbar.

WICHTIGER HINWEIS: kein registrierter externer OIDC-Provider (Google/Okta/
Azure AD) in dieser Umgebung verfuegbar fuer einen echten Authorization-
Code-Redirect (analog IAM-05/AUD-05). ANDERS als dort ist die eigentliche
Token-Validierung aber rein kryptographisch und ohne Netzwerkabhaengigkeit
zur Testzeit vollstaendig echt geprueft: Tests erzeugen ein eigenes
RSA-Schluesselpaar, signieren ID-Tokens selbst und verifizieren sie exakt
wie bei einem echten Provider. Nur der Live-Redirect zu einem realen
externen IdP bleibt ungeprueft.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Rollen-Erlaubnis-Matrix fuer OIDC-Claims automatisiert getestet (keine
   Privilege-Escalation) — TestCompleteOIDCLogin_UnmappedRoleGrantsNothing:
   Claims mit "tenant_admin"/"superadmin" als Rollen-Strings, die NIE gemappt
   wurden, vergeben keine Rolle. PASS.
2. Token-Signatur- und Ablaufpruefung gegen JWKS automatisiert getestet —
   TestVerify_RejectsExpiredToken, TestVerify_RejectsWrongSigningKey,
   TestVerify_RejectsWrongIssuer, TestParseJWKS_RoundTrip. PASS.
3. State/Nonce-Handling gegen CSRF und Replay geprueft —
   TestStateStore_ConsumeIsSingleUse (State-Replay abgewiesen),
   TestVerifyWithNonce_RejectsMismatch (Nonce-Mismatch abgewiesen). PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:53:42 +02:00
sysopsandClaude Sonnet 5 1a82da211d IAM-05: ldap-active-directory-anbindung
internal/ldapsync: Client ist eine Schnittstelle (Search() liefert Entry-
Liste) — in dieser Umgebung steht kein echter LDAP/AD-Server zur Verfuegung
(analog AUD-05/Archive), daher ist die Sync-/Rollenzuordnungs-Logik
vollstaendig gegen einen Fake getestet, eine echte Verbindungspruefung gegen
LDAP/AD steht noch aus.

RoleMappingStore (Akzeptanzkriterium 3): explizite Erlaubnis-Matrix
LDAP-Gruppe -> Rolle. HighestRoleFor vergibt NUR fuer explizit gemappte
Gruppen eine Rolle — eine unbekannte Gruppe, auch wenn ihr Name zufaellig
wie eine interne Rolle aussieht ("tenant_admin", "superadmin"), traegt
strukturell nichts bei (kein Code-Pfad, der eine ungemappte Gruppe je einer
Rolle zuordnet) — behebt die aus archivmail bekannte Privilege-Escalation-
Fehlerklasse von Grund auf statt nachtraeglich zu haerten.

Syncer.Run ruft Search() als ALLERERSTES auf; schlaegt es fehl, wird ohne
jede Aenderung an bestehenden Konten abgebrochen (Akzeptanzkriterium 2).
Pro Eintrag isolierte Fehler landen in SyncResult.Failed, ohne andere
Eintraege zu beeintraechtigen. Deaktivierung in LDAP wird als
users.Deactivate uebernommen (Akzeptanzkriterium 3). ldap_config speichert
bewusst nur den NAMEN einer Umgebungsvariable fuer das Bind-Passwort, nie
das Passwort selbst.

internal/rbac (RBAC-01) wurde 1:1 aus dem rbac-01-Branch uebernommen (git
show aus derselben Repo-Historie) — IAM-05 haengt an RBAC-01 fuer die
Rollenzuweisung, beide Boards leben aber auf getrennten, noch nicht
gemergten Feature-Branches ohne gemeinsame Historie.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Rollen-Erlaubnis-Matrix mit Positiv- und Negativfaellen automatisiert
   getestet (keine Privilege-Escalation) — TestRoleMapping_PositiveAndNegativeCases:
   gemappte Gruppe liefert Rolle, unbekannte Gruppe und rollen-aehnlich
   benannte, aber nie gemappte Gruppen liefern keine. PASS.
2. Synchronisationslauf mit fehlerhafter/nicht erreichbarer LDAP-Quelle
   bricht kontrolliert ab, ohne bestehende Konten zu beschaedigen —
   TestSyncer_AbortsCleanlyOnSourceError: bestehendes Konto bleibt nach
   fehlgeschlagenem Lauf unveraendert aktiv. PASS.
3. Deaktivierung eines Benutzers in LDAP wird bei naechster Synchronisation
   korrekt uebernommen — TestSyncer_AppliesDeactivationOnNextRun: erster
   Lauf legt aktiven Benutzer an, zweiter Lauf mit Disabled=true setzt ihn
   auf inaktiv. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:39:10 +02:00
sysopsandClaude Sonnet 5 f224b5c9be IAM-04: zwei-faktor-authentifizierung-totp
internal/totp/totp.go: RFC-6238-TOTP (HOTP RFC 4226) selbst implementiert
mit stdlib crypto/hmac+sha1 — kein externes Modul. DefaultSkewSteps=1
dokumentiert die Zeitversatz-Toleranz (+/-30s um die Serverzeit,
Akzeptanzkriterium/Pruefung 3). ProvisioningURI liefert die otpauth://-URI
fuer den QR-Code (Akzeptanzkriterium 1) — das Rendering selbst ist
Frontend-Sache (IAM-08).

internal/totp/store.go: BeginSetup speichert ein neues Secret als NICHT
bestaetigt; ConfirmSetup aktiviert 2FA erst nach einmaliger erfolgreicher
Code-Eingabe (Akzeptanzkriterium 1) und erzeugt 10 Wiederherstellungscodes
(nur Hash gespeichert, Klartext einmalig zurueckgegeben). VerifyLoginCode
akzeptiert TOTP-Code ODER Wiederherstellungscode; consumeRecoveryCode
markiert einen Code atomar als verwendet (WHERE used_at IS NULL) — kein
doppeltes Einloesen moeglich (Akzeptanzkriterium 3).

internal/totp/login.go: LoginWithTOTP komponiert IAM-02s LoginService, ohne
ihn zu veraendern — ist 2FA fuer den Benutzer aktiv, wird ein fehlender/
falscher Code zuverlaessig abgewiesen, selbst bei korrektem Passwort
(Akzeptanzkriterium 2).

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Login-Versuch ohne zweiten Faktor bei aktivem 2FA zuverlaessig abgewiesen —
   TestLoginWithTOTP_RequiresSecondFactorWhenEnabled: korrektes Passwort ohne
   Code -> ErrSecondFactorRequired, mit gueltigem Code -> Token. PASS.
2. Wiederherstellungscode nach Nutzung als verbraucht getestet —
   TestVerifyLoginCode_RecoveryCodeIsSingleUse: erste Nutzung erfolgreich,
   zweite abgelehnt. PASS.
3. Zeitversatz-Toleranz dokumentiert und getestet —
   TestValidate_ClockSkewTolerance: Code aus 25s Vergangenheit gueltig
   (innerhalb dokumentierter Toleranz), Code aus 5min Vergangenheit
   ungueltig. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:34:34 +02:00
sysopsandClaude Sonnet 5 5f02e08bc4 IAM-12: aktive-sitzungen-verwaltung
internal/session: Selbstbedienungs-Sitzungsuebersicht mit widerrufbarem,
SERVERSEITIGEM Zustand — bewusst ein separater Mechanismus neben IAM-02s
zustandslosem JWT (das fuer schnelle Modul-zu-Modul-Verifikation ohne
Core-Rueckfrage gewaehlt wurde, siehe API-05). Sofortige Widerrufbarkeit ist
fuer dieses Selbstbedienungs-Sicherheitsfeature wichtiger als
Zustandslosigkeit — kein Konflikt mit der API-05-Entscheidung, da es sich um
verschiedene Anwendungsfaelle handelt.

Store.Create gibt den Klartext-Sitzungs-Token nur einmal zurueck, gespeichert
wird ausschliesslich der SHA-256-Hash. Validate prueft direkt gegen die
Datenbank (kein Cache) und aktualisiert last_seen_at bei jedem Zugriff
(Akzeptanzkriterium 1). Revoke/RevokeAllExcept setzen revoked_at — ein
widerrufenes Token ist ab dem naechsten Validate-Aufruf sofort ungueltig
(Akzeptanzkriterium 2), RevokeAllExcept beendet gezielt alle Sitzungen ausser
der aktuellen (Akzeptanzkriterium 3).

LoginAndCreateSession verwendet auth.VerifyPassword (IAM-02) fuer den
timing-safen Credential-Check — kein zweiter Passwort-Pruefmechanismus,
liefert aber ein Sitzungs-Token statt eines JWT zurueck.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Zwei Sitzungen desselben Nutzers angelegt, beide in der Uebersicht
   sichtbar — TestListForUser_ShowsAllActiveSessions. PASS.
2. Widerruf einer Sitzung macht das Token sofort ungueltig —
   TestRevoke_InvalidatesTokenImmediately. PASS.
3. "Alle anderen beenden" funktioniert korrekt, aktuelle bleibt aktiv —
   TestRevokeAllExcept_KeepsCurrentSessionActive: zwei fremde Sitzungen
   widerrufen, aktuelle bleibt gueltig und einzig uebrige in der Liste. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:29:58 +02:00
sysopsandClaude Sonnet 5 3c4e9d45b8 IAM-14: passwort-richtlinien
internal/pwpolicy: Validate ist eine schmale Regelschicht VOR dem
bcrypt-Hashing aus IAM-02, kein eigenes Policy-Framework. Prueft
Mindestlaenge, Zeichenklassen (Gross-/Kleinbuchstaben, Ziffern,
Sonderzeichen je nach Policy) und eine eingebettete Sperrliste haeufig
verwendeter Passwoerter (case-insensitive) — Akzeptanzkriterium 1 + 2.
Store haelt die Richtlinie als eine Zeile je Tenant-Datenbank (Singleton,
Modell C), DefaultPolicy() greift, solange kein Tenant eine eigene gesetzt hat.

LoginAndCheckPolicy komponiert IAM-02s LoginService, OHNE ihn zu veraendern:
der Login selbst schlaegt bei einem alten, nicht mehr konformen Passwort
NICHT fehl (Akzeptanzkriterium 3 — kein rueckwirkendes Aussperren), die
Funktion liefert zusaetzlich mustChangePassword=true. Die Pruefung ist nur
im Login-Moment moeglich, da dort kurzzeitig das Klartext-Passwort vorliegt
— der gespeicherte bcrypt-Hash laesst sich nicht rueckwirkend gegen eine
neue Richtlinie pruefen.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Zu kurzes/zu einfaches Passwort bei Registrierung/Aenderung abgelehnt —
   TestValidate_RejectsTooShortOrSimple. PASS.
2. Passwort aus Sperrliste abgelehnt — TestValidate_RejectsBlocklistedPassword
   (inkl. Gross-/Kleinschreibung). PASS.
3. Bestehender Nutzer mit altem, nicht-konformem Passwort kann sich noch
   einloggen, wird aber zur Aenderung aufgefordert —
   TestLoginAndCheckPolicy_FlagsNonConformantExistingPassword: Login mit
   schwachem Altpasswort gelingt, mustChangePassword=true; mit konformem
   Passwort mustChangePassword=false. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:26:23 +02:00
sysopsandClaude Sonnet 5 95275407b1 IAM-09: api-token-service-accounts
internal/serviceaccount: Service-Accounts als eigenstaendige Identitaetsklasse
(eigene Tabelle service_accounts, getrennt von internal/user — Zitadel-
Vorbild). Store.IssueToken gibt den Klartext-Token NUR einmal an den
Aufrufer zurueck, gespeichert wird ausschliesslich der SHA-256-Hash
(Akzeptanzkriterium 1). Scopes und optionale Ablaufzeit sind Teil des
Tokens selbst (Akzeptanzkriterium 2).

Store.Verify prueft Widerruf/Ablauf/Scope bei JEDEM Aufruf direkt gegen die
Datenbank — kein Cache dazwischen, ein widerrufenes Token wird ab dem
naechsten Request sofort abgewiesen (Akzeptanzkriterium 3).

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Token ausschliesslich gehasht gespeichert (Stichprobe in der Datenbank) —
   TestIssueToken_StoresOnlyHash: token_hash-Spalte direkt gelesen, enthaelt
   nachweislich nicht den Klartext, 32-Byte-SHA-256-Laenge bestaetigt. PASS.
2. Widerrufenes Token wird beim naechsten Request zuverlaessig abgewiesen —
   TestRevoke_TakesEffectImmediately: Verify vor Widerruf erfolgreich, sofort
   danach ErrTokenInvalid. PASS.
3. Scope-Verletzung wird korrekt abgewiesen —
   TestVerify_RejectsInsufficientScope: Token mit scope=read wird fuer
   scope=write abgewiesen (ErrScopeInsufficient), fuer scope=read akzeptiert. PASS.

Zusaetzlich: TestVerify_RejectsExpiredToken belegt die optionale zeitliche
Befristung aus Akzeptanzkriterium 2. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:19:39 +02:00
sysopsandClaude Sonnet 5 ac070c160a IAM-07: account-lockout-login-rate-limiting
internal/lockout: Store.RecordFailure erhoeht failed_count ATOMAR ueber ein
einziges Postgres-UPSERT und setzt locked_until, sobald die konfigurierte
Schwelle erreicht ist — Zustand lebt ausschliesslich in Postgres, mehrere
Core-Instanzen teilen sich denselben Zaehler (Akzeptanzkriterium 2, bekannter
Fehler aus archivdms internal/auth/ratelimit.go vermieden: kein
In-Process-Zaehler). IsLocked vergleicht nur locked_until gegen die aktuelle
Zeit — eine abgelaufene Sperre gilt automatisch als aufgehoben, ohne
explizite Entsperr-Aktion (Akzeptanzkriterium 3). Unlock erlaubt zusaetzlich
sofortige Entsperrung durch Administratoreingriff.

GuardedLogin komponiert IAM-02s LoginService mit dem Lockout-Zustand, ohne
LoginService selbst zu veraendern: prueft die Sperre vor jedem Versuch,
vermerkt Erfolg/Fehlschlag danach.

Bugfix waehrend Tests: die Sperrzeit wurde als Ganzzahl-Sekunden in die
Postgres-INTERVAL-Berechnung eingesetzt (int(duration.Seconds())), wodurch
Sperrzeiten unter 1 Sekunde (z.B. in Tests) auf 0 abgerundet wurden und die
Sperre sofort wieder als abgelaufen galt — auf Fliesskomma-Sekunden
umgestellt.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Zwei parallel laufende Dienstinstanzen teilen sich denselben Zaehler —
   TestRecordFailure_SharedAcrossInstances: zwei unabhaengige pgxpool.Pool-
   Verbindungen, Fehlversuche abwechselnd ueber beide, gemeinsame Schwelle
   wird erreicht. PASS.
2. Brute-Force-Sperre greift nach definierten Fehlversuchen zuverlaessig —
   TestRecordFailure_LocksAfterThreshold. PASS.
3. Zeitversatz zwischen Sperre und Entsperrung automatisiert getestet —
   TestIsLocked_AutoUnlocksAfterExpiry: gesperrt vor Ablauf, automatisch
   entsperrt nach Ablauf der Sperrzeit. PASS.

Zusaetzlich: TestGuardedLogin_LocksAfterRepeatedFailures belegt das
Zusammenspiel mit dem echten IAM-02-LoginService End-to-End — selbst das
korrekte Passwort wird nach Sperrung abgewiesen. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:15:25 +02:00
sysopsandClaude Sonnet 5 b6a941feec IAM-03: passwort-reset-einladungs-flow
internal/authtoken: Einmal-Token fuer Passwort-Reset und Einladung, getrennt
von internal/auth (Sessions/JWT), da der Vorgang bewusst session-los ist.
Store.Create gibt das Klartext-Token NUR an den Aufrufer zurueck (fuer
E-Mail-Versand, IAM-08), gespeichert wird ausschliesslich der SHA-256-Hash.

Store.Consume markiert ein Token atomar als verwendet (UPDATE ... WHERE
used_at IS NULL AND expires_at > now() RETURNING user_id) — Wiederverwendung
und Ablauf werden serverseitig in derselben Datenbankoperation durchgesetzt,
kein Race zwischen Pruefen und Verbrauchen moeglich. Ungueltig, bereits
verwendet und abgelaufen liefern denselben ErrInvalidToken (Akzeptanz-
kriterium 3), damit die Antwort keinen der drei Faelle verraet.

CompletePasswordReset/CompleteInvitation loesen ein Token ein und setzen das
Passwort ueber user.TenantUserStore.SetPasswordHash (IAM-01) — keine
Auth-Middleware, keine Session noetig (Akzeptanzkriterium 2).

Log-Statements bei Create/Consume enthalten bewusst nur user_id/purpose,
niemals das Token selbst.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Wiederverwendung eines bereits eingeloesten Tokens automatisiert
   abgewiesen — TestConsume_RejectsReuse. PASS.
2. Ablaufzeit serverseitig durchgesetzt — TestConsume_RejectsExpiredToken
   (Token mit negativer TTL sofort abgelaufen). PASS.
3. Token werden nicht im Klartext geloggt (Stichprobe im Log-Ausgang) —
   TestCreateAndConsume_NeverLogTokenPlaintext: Log-Buffer nach Create +
   ungueltigem + gueltigem Consume enthaelt das Klartext-Token nachweislich
   nicht. PASS.

Zusaetzlich: TestCompleteInvitation_SetsPasswordWithoutSession belegt
Akzeptanzkriterium 2 konkret (Passwort gesetzt und verifizierbar, keine
Session im Ablauf beteiligt). PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:09:32 +02:00
sysopsandClaude Sonnet 5 d63fcbb49e RBAC-02: policy-enforcement-schicht-zentral
internal/policy: deklarativer, DB-gehaltener Regelsatz (policy_rules) statt
hartcodierter Go-Entscheidungslogik — Store.IsAllowed schaut ausschliesslich
in die Datenbank, kein Go-Fallback. Ein Regelwechsel (Grant/Revoke) wirkt
sich sofort aus, ohne Codeaenderung/Deploy (Akzeptanzkriterium 3). Jede
Aenderung wird atomar mit einem versionierten Historieneintrag in
policy_rule_changes festgehalten (grant/revoke, Akteur, Version).

Enforcer.Authorize ist Default-Deny: existiert keine Regel fuer role+
permission, ist der Zugriff verboten (Akzeptanzkriterium 2), fuer sich
genommen ohne Anwendungslogik testbar.

Guard/GuardTenantScoped sind die zentrale Enforcement-Funktion
(Akzeptanzkriterium 1): die uebergebene Query-Funktion wird NUR bei
erfolgreicher Autorisierung aufgerufen — es gibt keinen Weg, Daten ohne
vorherige Authorize-Entscheidung zu erhalten. GuardTenantScoped erzwingt
zusaetzlich per Funktionssignatur, dass tenantSlug TEIL der Query-Funktion
ist (Akzeptanzkriterium 3) — ein nachgelagerter Post-Filter (der
archivmail-Fehler aus "Bekannte Fehler vermeiden": Tenant-Filter nach statt
in der Query) ist mit dieser Signatur strukturell nicht moeglich, da die
Repository-Implementierung tenantSlug selbst fuer ihre eigene WHERE-Klausel
entgegennimmt.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Kein Datenzugriffs-Pfad umgeht die zentrale Enforcement-Schicht —
   TestGuard_NeverCallsQueryWithoutAuthorization: query-Funktion wird
   nachweislich NICHT aufgerufen ohne vorherige Regel, erst nach Grant. PASS.
2. Anfrage ohne passende Policy wird zuverlaessig abgewiesen (Default-Deny) —
   TestAuthorize_DefaultDeny: keine Regel konfiguriert -> ErrDenied, nicht
   automatisch erlaubt. PASS.
3. Policy-Regelsatz versioniert, Regelwechsel ohne Codeaenderung
   nachvollziehbar — TestGrantRevoke_ChangesBehaviorWithoutCodeChange:
   Verhalten aendert sich durch reinen Datenbank-Grant/Revoke, Historie
   zeigt beide Versionen korrekt. PASS.

Zusaetzlich: TestGuardTenantScoped_IsolatesDataBetweenTenants belegt das
Tenant-Scoping-Muster aus Akzeptanzkriterium 3 konkret anhand zweier
Tenants. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:43:26 +02:00
sysopsandClaude Sonnet 5 f7863fd4d2 API-05: verteilte-jwt-verifikation-rechte-feature-flag-cache-kontrakt
internal/moduletrust: asymmetrische JWT-Signatur (Ed25519) mit JWKS-
Verteilung, wie im Entscheidungsverlauf "Vertrauensstellung Core<->Module"
(nexarch-state.json) festgelegt. Getrennt von IAM-02s HS256-Session-Cookie
(Browser-Login bleibt unangetastet) — dies ist der Modul-zu-Core-
Vertrauensmechanismus.

KeyManager haelt ALLE noch gueltigen Schluesselpaare (nicht nur das aktuell
signierende); Rotate() erzeugt einen neuen Schluessel, alte bleiben in
PublicKeySet() erhalten — bereits ausgestellte Tokens bleiben dadurch nach
einer Rotation weiterhin verifizierbar (Akzeptanzkriterium 3, keine
Ausfallzeit). ServeJWKS/ParseJWKS sind der Verteilungsmechanismus.

StaleCache[T] ist der generische Rechte-/Feature-Flag-Cache-Kontrakt
(Akzeptanzkriterium 2), mit zwei explizit benannten und begruendeten
Verhalten: Get() ist FAIL-OPEN (nutzt bei Core-Ausfall einen vorhandenen,
abgelaufenen Stand weiter — ein bereits authentifiziertes Modul soll nicht
hart blockieren), RequireFresh() ist FAIL-CLOSED (nie zwischengespeichert,
schlaegt bei Core-Ausfall klar fehl — fuer sicherheitskritische Aktionen wie
einen neuen Login). LIC-02s internal/flag.Service implementiert bereits
denselben Kontrakt fuer Feature-Flags; StaleCache verallgemeinert dasselbe
Muster fuer JWT-Schluessel, damit beide Faelle derselben dokumentierten
Policy folgen statt zwei unterschiedlichen Ad-hoc-Loesungen.

Verifier.Verify ruft KeyFetchFunc nur bei abgelaufener TTL auf, nicht pro
Aufruf (Akzeptanzkriterium 1) — Signaturpruefung selbst ist immer lokal.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Core simuliert abgeschaltet, andere Module bleiben fuer bereits
   authentifizierte Nutzer funktionsfaehig bis TTL/Fail-Open greift —
   TestVerify_FailsOpenWhenCoreUnreachableButStaleKeysExist: Verify()
   funktioniert weiter mit letztbekanntem Schluesselstand. PASS.
2. Neue sicherheitskritische Aktion schlaegt bei Core-Ausfall klar fehl,
   statt andere Funktionen mitzureissen —
   TestRequireFreshKeys_FailsClosedWhenCoreUnreachable: Fehler trotz
   vorhandenem (aelterem) Cache-Stand. PASS.
3. Schluesselrotation ohne Downtime in einem simulierten zweiten Modul —
   TestRotate_NoDowntimeForAlreadyIssuedTokens: vor UND nach Rotation
   ausgestellte Tokens beide weiterhin gueltig fuer Modul B. PASS.

Zusaetzlich: TestVerify_DoesNotFetchPerCall belegt Akzeptanzkriterium 1
direkt (10 Verify-Aufrufe, genau 1 Fetch). PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:38:57 +02:00
sysopsandClaude Sonnet 5 ed67887385 RBAC-01: rollenmodell-grundrechte
internal/rbac: drei Grundrollen (superadmin, tenant_admin, user) mit
Hierarchie ueber eine einfache Eltern-Map (tenant_admin erbt von user,
superadmin erbt von tenant_admin) — EffectivePermissions loest die volle
vererbte Rechtemenge auf (Akzeptanzkriterium 2). Policy-Modell bewusst als
reine Go-Datenstruktur getrennt von der Durchsetzung (RBAC-02), nach
Casbin-Prinzip.

Store verwaltet Rollenzuweisungen innerhalb EINER Tenant-Datenbank (Modell C,
analog internal/user.TenantUserStore) — nur 'user' und 'tenant_admin' sind
hier zuweisbar (assignableRoles-Matrix). Ein Zuweisungsversuch fuer
'superadmin' wird abgewiesen, da diese Rolle mandantenuebergreifend ist und
bereits durch die Existenz eines Kontos in IAM-01s SuperadminStore
repraesentiert wird — keine doppelte Modellierung. Jede Zuweisung schreibt
zusaetzlich einen Historieneintrag (role_assignment_history) mit
grantedBy/grantedAt, atomar in derselben Transaktion (Akzeptanzkriterium 3).

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Zuweisung ausserhalb der erlaubten Matrix abgewiesen —
   TestStore_RejectsSuperadminOutsideAllowedMatrix und
   TestStore_RejectsUnknownRole: beide ErrRoleNotAssignableInTenantScope,
   kein Datensatz hinterlassen. PASS.
2. Rollenhierarchie liefert erwartete effektive Rechtemenge —
   TestEffectivePermissions_Inheritance: tenant_admin hat geerbte
   user-Rechte + eigene, aber nicht platform.manage_tenants; superadmin hat
   die volle Kette. PASS.
3. Datenmodell von zweiter Person gegen Dokumentation geprueft — NICHT
   durchgefuehrt (keine zweite Person in dieser Session verfuegbar). Offen.

Zusaetzlich automatisiert getestet (Akzeptanzkriterium 3):
TestStore_HistoryTracksWhoAndWhen — zwei aufeinanderfolgende Zuweisungen,
Historie liefert beide mit korrektem grantedBy in chronologischer
Reihenfolge. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 19:45:16 +02:00
sysopsandClaude Sonnet 5 383da92ca8 TEN-03: tenant-einstellungen-branding
internal/tenantsettings: pro-Tenant-Einstellungen (Anzeigename, Logo,
Farbschema, Zeitzone, Sprache) in der Registry-DB, alle Spalten nullable —
fehlender Wert bedeutet immer "Systemvoreinstellung verwenden"
(Defaults(): color_scheme=system, timezone=UTC, language=de), niemals ein
Fehler (Akzeptanzkriterium 2). Store.Update schreibt aktuellen Stand +
Historieneintrag atomar in einer Transaktion mit FOR-UPDATE-Lock auf der
aktuellen Zeile (Akzeptanzkriterium 3, Race-sicher bei nebenlaeufigen
Updates desselben Tenants). Patch-Typ mit *string-Feldern erlaubt
Teil-Updates ohne unbeteiligte Felder zu beruehren.

Schlanker Handler (Get/Update ueber tenant_id-Query-Parameter) als
Vorbereitung der REST-Schnittstelle — echte Auth/Versionierung kommt erst
mit API-01/IAM-02/RBAC-01.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Aenderung eines Tenants wirkt sich nicht auf einen anderen aus —
   TestUpdate_IsolatedBetweenTenants: Tenant A geaendert, Tenant B bleibt
   nachweislich bei Defaults(). PASS.
2. Fehlende Werte liefern Defaults statt Fehler —
   TestGet_UnsetTenantReturnsDefaults (Tenant ganz ohne Datensatz) und
   TestUpdate_PartialPatchKeepsOtherFieldsAtDefault (nur ein Feld gesetzt,
   Rest bleibt Default). PASS.
3. API-Schema von zweiter Person gegen Dokumentation geprueft — NICHT
   durchgefuehrt (keine zweite Person in dieser Session verfuegbar). Offen.

Zusaetzlich automatisiert getestet (Akzeptanzkriterium 3):
TestUpdate_HistoryTracksVersions — 3 aufeinanderfolgende Aenderungen,
Historie liefert alle 3 in korrekter Reihenfolge, nicht angefasste Felder
bleiben aus dem vorherigen Update erhalten. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 19:34:09 +02:00
sysopsandClaude Sonnet 5 b08f6a49fc TEN-07: migrations-orchestrierung-tenant-datenbanken
Orchestrator.RolloutAll wendet eine geordnete Liste von Migrationen auf JEDE
registrierte Tenant-Datenbank an (LoadMigrations liest *.up.sql aus einem
Verzeichnis nach der bestehenden 000N_name-Namenskonvention). Jeder Tenant
laeuft unabhaengig in eigener Verbindung — ein Fehlschlag bei einem Mandanten
bricht nur dessen eigenen Rollout ab (spaetere Migrationen bauen typischerweise
auf frueheren auf) und blockiert die uebrigen Tenants nicht.

schema_migrations-Tabelle pro Tenant-Datenbank (version PK, applied_at,
success, error) haelt den Stand pro Version einzeln nachvollziehbar fest.
Bereits erfolgreiche Versionen werden bei einem erneuten Rollout uebersprungen
(isAlreadySuccessful-Check vor jeder Anwendung), fehlgeschlagene werden beim
naechsten Versuch automatisch erneut probiert (kein manuelles Zuruecksetzen
noetig) — ON CONFLICT DO UPDATE haelt jeweils nur den letzten Versuch fest.

Bewusst ohne Abhaengigkeit von TEN-06 (Router): Migrations-Rollouts sind
seltene Batch-Vorgaenge, ein kurzlebiger Pool pro Tenant und Lauf reicht.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS,
go test ./... mit -p 1 noetig da mehrere Pakete die geteilte Registry-Tabelle
auf derselben Postgres-Instanz nutzen — siehe scripts/reset-test-env.sh):
1. Rollout gegen 3 Test-Tenants, einer absichtlich inkompatibel (Tabellen-
   Konflikt bei Migration 2) — TestRolloutAll_IsolatesFailurePerTenant: die
   anderen beiden erhalten beide Migrationen, der inkompatible bekommt
   Migration 1 trotzdem, scheitert nur an Migration 2, faellt nicht die
   anderen um. PASS.
2. Migrationsstand-Abfrage liefert korrekten Stand pro Tenant —
   TestStatus_ReflectsPerTenantState: Version 1 success=true, Version 2
   success=false mit Fehlertext. PASS.
3. Wiederholter Rollout fuer fehlgeschlagene Migration moeglich, ohne bereits
   erfolgreiche erneut anzuwenden — TestRolloutAll_RetryDoesNotReapplySuccessful:
   Migration 1 nutzt bewusst kein IF NOT EXISTS, ein Reapply haette den
   zweiten Lauf scheitern lassen; zweiter Lauf ist fehlerfrei und wendet nur
   die zuvor fehlgeschlagene Version an. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 19:05:47 +02:00
sysopsandClaude Sonnet 5 45bc10719a TEN-04: tenant-lifecycle-suspendieren-reaktivieren-loeschen
Zustandsautomat active/suspended/pending_deletion/deleted als First-Class-
Konzept (previous_status + deletion_scheduled_at in der Registry). Alle
Uebergaenge in Registry.transition als atomarer Check-and-Set (UPDATE ...
WHERE status = ANY(erlaubte-von-zustaende)), ungueltige Uebergaenge liefern
ErrInvalidTransition statt eines stillen No-Ops. ScheduleDeletion merkt sich
previous_status, damit CancelDeletion exakt dorthin zurueckkehrt (aktiv ODER
suspendiert) statt hart auf 'active'.

Lifecycle.ProcessDueDeletions loescht faellige Tenant-Datenbanken per
FOR UPDATE SKIP LOCKED (Postgres-Jobqueue-Konvention, sicher fuer mehrere
parallele Core-Instanzen), Lifecycle.RunSweeper triggert das periodisch per
In-Prozess-Goroutine. Lifecycle.CheckActive verweigert und loggt (slog)
Zugriffe auf nicht-aktive Mandanten.

Bugfix nebenbei: Registry.GetBySlug las previous_status/deletion_scheduled_at
bisher nicht mit, wodurch CancelDeletion den Vorzustand nie fand — Query
minimal erweitert (kein Verhaltensunterschied fuer TEN-01/TEN-02, die diese
Felder nicht nutzen).

Neu: scripts/reset-test-env.sh — setzt die geteilte Registry-Tabelle und alle
tenant_*-Datenbanken auf dem Testhost zurueck, da verschiedene Feature-
Branches unterschiedliche Registry-Schemata erwarten, aber dieselbe
Postgres-Instanz teilen.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Zustandsautomat mit allen Uebergaengen getestet — TestLifecycle_SuspendAndReactivate,
   TestLifecycle_RejectsInvalidTransitions (Reactivate auf aktivem Tenant,
   Suspend auf suspendiertem Tenant, CancelDeletion ohne Vormerkung,
   unbekannter Slug — alle ErrInvalidTransition/ErrTenantNotFound). PASS.
2. Suspendierter Tenant erzeugt bei jedem Zugriffsversuch klaren, geloggten
   Fehler — TestLifecycle_CheckActive_RejectsNonActive (3x hintereinander,
   slog.Warn nachweislich pro Aufruf). PASS.
3. Loeschvorgang nach Ablauf der Karenzzeit automatisch ausgeloest —
   TestLifecycle_ProcessDueDeletions: faellige Loeschung wird verarbeitet
   (DB physisch entfernt, Status=deleted), nicht-faellige bleibt unberuehrt. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:42:24 +02:00
sysopsandClaude Sonnet 5 2026514404 TEN-06: connection-routing-pooling-pro-mandant
Router loest Tenant-Slug (aus JWT-Claim, API-05 vorausgesetzt) ueber die
TEN-01-Registry in eine wiederverwendete Postgres-Verbindung auf. LRU-Cache
(container/list) begrenzt die Zahl gleichzeitig offener Tenant-Pools auf
maxOpen — bei Ueberschreitung wird der am laengsten ungenutzte Pool
geschlossen, bevor ein neuer aufgemacht wird. Fehlender/unbekannter
Tenant-Kontext liefert explizite Fehler (ErrMissingTenantContext /
ErrUnknownTenant) statt stillschweigend zu routen.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Lasttest mit 6 simulierten Mandanten gegen maxOpen=2 — TestRouter_BoundsOpenConnectionsUnderLoad:
   OpenCount() bleibt nach jedem Resolve <= maxOpen, Verbindungszahl waechst
   nicht linear mit der Mandantenzahl. PASS.
2. Anfrage ohne/mit unbekanntem Tenant-Kontext abgewiesen —
   TestRouter_RejectsMissingOrUnknownTenant. PASS.
3. Verbindungswiederverwendung gemessen — TestRouter_ReusesConnectionForSameTenant:
   zweiter Resolve-Aufruf liefert exakt dieselbe *pgxpool.Pool-Instanz. PASS.

Router ist eigenstaendig nutzbar/getestet, aber noch nicht in cmd/core/main.go
verdrahtet — der JWT-Claim mit Tenant-Kontext (API-05) und das HTTP-Routing,
das den Slug pro Request extrahiert, sind nicht Teil dieser Kachel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:35:50 +02:00
sysopsandClaude Sonnet 5 3d20d86a4f IAM-02: login-session-jwt-grundgeruest
internal/auth: Login/Logout ueber httpOnly/Secure/SameSite=Strict-Cookie mit
HS256-JWT (30min TTL), bcrypt-Passwort-Hashing (Cost 12, explizit begruendet
und benchmarkt statt DefaultCost uebernommen), RequireAuth-Middleware fuer
geschuetzte Routen. LoginService ist strukturell auf einen Tenant gescopt
(nutzt user.TenantUserStore, dessen Pool = eine Tenant-DB — derselbe
Mechanismus wie in TEN-01/TEN-02), liefert bei falscher E-Mail und falschem
Passwort denselben Fehler (User-Enumeration-Schutz) inkl. Dummy-bcrypt-
Vergleich gegen Timing-Seitenkanal bei unbekannter E-Mail.

user.TenantUserStore erweitert um SetPasswordHash/GetByEmailForAuth
(password_hash bleibt ausserhalb des regulaeren User-Typs/JSON-Pfads).
Migration 0002 fuegt password_hash-Spalte hinzu (Default '', da IAM-01
User ohne Passwort anlegt).

Login-Handler ist wie IAM-01/TEN-02 aus denselben Gruenden (Tenant-
Connection-Routing = TEN-06, noch nicht gebaut) nicht in cmd/core/main.go
verdrahtet — Package ist eigenstaendig nutzbar/getestet.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Login-Query tenant-gescopt — TestLoginService_NoCrossTenantLogin: gleiche
   E-Mail in zwei Tenant-DBs mit unterschiedlichem Passwort, Login gegen
   Tenant A mit Tenant-B-Passwort schlaegt fehl. PASS.
2. Session-Fixation/Token-Manipulation — TestTokenVerify_RejectsManipulatedPayload
   und TestTokenVerify_RejectsWrongSecret: manipuliertes/falsch signiertes
   Token wird abgelehnt. PASS.
3. Abgelaufenes Token erzwingt Neuanmeldung — TestTokenVerify_RejectsExpiredToken
   und TestRequireAuth_BlocksWithoutValidCookie. PASS.
4. Login-Latenz mit Kostenfaktor 12 gemessen: 294ms (Ziel < 400ms) —
   TestBcryptCostAgainstLatencyTarget. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:26:00 +02:00
sysopsandClaude Sonnet 5 5915a4e7b1 TEN-02: tenant-onboarding-api-self-service
OnboardingService.Onboard: ein API-Aufruf legt Tenant (via bestehendem
Provisioner) UND ersten Administrator-Account (via IAM-01 TenantUserStore)
an. CREATE DATABASE erlaubt keine echte cross-database Transaktion, daher
Saga-Kompensation: schlaegt die Admin-Anlage nach erfolgreichem Provisioning
fehl, wird der Tenant per neuem Provisioner.Deprovision wieder vollstaendig
entfernt (Registry.Delete + DB-Drop).

Provisioner.Provision mappt Duplikat-Faelle (42P04 duplicate_database und
den bei echt parallelen CREATE DATABASE moeglichen 23505-Unique-Konflikt auf
pg_database) jetzt auf ErrTenantExists statt einer rohen Postgres-Fehlermeldung.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Onboarding-Vorgang zweimal parallel mit gleichem Slug ausgeloest —
   TestOnboarding_RejectsDuplicateSlugConcurrently: genau ein Erfolg, kein
   Doppel-Tenant, zweiter Aufruf bekommt ErrTenantExists. PASS.
2. Fehleingaben (leere Pflichtfelder, ungueltige E-Mail, ungueltiger Slug) —
   TestOnboarding_ValidationErrors deckt alle vier Faelle mit klaren Fehlern
   ab, bevor irgendein DB-Zugriff stattfindet. PASS.
3. Erfolgreicher Durchlauf von zweiter Person end-to-end nachvollzogen —
   NICHT durchgefuehrt (keine zweite Person in dieser Session verfuegbar).
   Offen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:16:54 +02:00
sysopsandClaude Sonnet 5 e4793303fc IAM-01: benutzer-datenmodell-crud
Benutzer-Datenmodell + CRUD fuer Tenant-User (tenant-scoped, keine
tenant_id-Spalte noetig, Tenant ergibt sich aus der DB-Verbindung, Modell C)
und getrennt dafuer SuperadminStore fuer mandantenuebergreifende Konten in
der Registry-DB — First-Class-Typ statt tenant_id-NULL-Sonderfall im
Tenant-User-Code (bekannter archivdms-Fehler vermieden).

E-Mail-Eindeutigkeit: tenant-scoped fuer normale Benutzer (UNIQUE-Constraint
gilt nur innerhalb der jeweiligen Tenant-DB), global fuer Superadmins
(eine Registry-DB, ein UNIQUE-Constraint).

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. CRUD automatisiert getestet inkl. Negativfaellen — TestTenantUserStore_CRUD
   deckt doppelte E-Mail (ErrEmailTaken) und unbekannte ID ab. PASS.
2. Superadmin-Anlage ohne Tenant-Kontext — TestSuperadminStore_CreateWithoutTenantContext:
   SuperadminStore.Create hat syntaktisch keinen Tenant-Parameter, kein
   if-Zweig fuer "kein Tenant" im Code. PASS.
3. Datenmodell von zweiter Person gegen Dokumentation geprueft — NICHT
   durchgefuehrt (keine zweite Person in dieser Session verfuegbar). Offen.

Tenant-User-Handler ist im Code vorhanden, aber in cmd/core/main.go noch
nicht geroutet — braucht Connection-Routing pro Mandant (TEN-06), das nicht
Teil dieser Kachel ist. Nur der Superadmin-Endpunkt ist verdrahtet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:57:49 +02:00
sysopsandClaude Sonnet 5 8da9c67d08 TEN-01: tenant-registry-datenbank-provisioning
Registry-DB (nur Tenant-Metadaten), Provisioning-Routine legt pro Mandant
eine physisch isolierte Postgres-DB an und registriert sie transaktional
(Rollback der DB bei fehlgeschlagener Registrierung). Schlanker HTTP-Handler
als Schnittstellen-Vorbereitung fuer API-01/TEN-02, kein eigenes REST-Grundgerüst.

Pruefungen:
1. Migration up/down geschrieben (0001_tenant_registry.{up,down}.sql) — nicht
   gegen echte DB ausgefuehrt, da auf dieser Maschine kein Go/Postgres-Test-
   Setup verfuegbar ist. Offen zur Ausfuehrung.
2. Integrationstest TestProvision_CreatesIsolatedDatabases geschrieben (zwei
   Mandanten, prueft unterschiedliche db_name und current_database()) —
   ebenfalls nicht ausgefuehrt, guarded per TEST_ADMIN_DSN env var. Offen.
3. Slug-Validierung (unit test TestValidateSlug) deckt SQL-Injection-Versuch
   im Datenbanknamen ab — ebenfalls nicht lokal ausgefuehrt, da kein Go
   Compiler auf dieser Maschine vorhanden ist. Offen.

Alle drei Pruefungen sind vorbereitet, aber NICHT durchgefuehrt worden —
zaehlen laut Vorgabe als offen bis auf einer Maschine mit Go+Postgres verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:40:35 +02:00
sysops c895a67c4b core: initial Go module skeleton (config, db pool, tenant registry migration) 2026-08-27 17:27:59 +02:00
sysops 72261cc69f first commit 2026-08-27 17:20:02 +02:00