Commit Graph
115 Commits
Author SHA1 Message Date
sysops 98b3bcdf43 Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/lic-04-lizenz-modul-verwaltungsoberflaeche 2026-08-28 22:02: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 554c9aae66 AUD-04: fix — transpilePackages fuer @nexarch/shl (Next.js transpiliert node_modules sonst nicht, Build brach ab) 2026-08-28 21:56:27 +02:00
sysops 1ba90319a2 Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/aud-04-audit-log-ansicht 2026-08-28 21:56:20 +02:00
sysops 5f3eb16fae LIC-04: fix — transpilePackages fuer @nexarch/shl (Next.js transpiliert node_modules sonst nicht, Build brach ab) 2026-08-28 21:56:15 +02:00
sysops c8f72c30f3 Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/lic-04-lizenz-modul-verwaltungsoberflaeche 2026-08-28 21:56:07 +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 f344f79326 AUD-04: Retrofit auf SHL-01 (ThemeProvider/I18nProvider/ToastProvider, Design-Tokens statt hartkodierter Werte) 2026-08-28 21:47:23 +02:00
sysops c4bcfa8caf Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/aud-04-audit-log-ansicht
# Conflicts:
#	DEVLOG.md
2026-08-28 21:47:09 +02:00
sysops 7c53a099c7 LIC-04: Retrofit auf SHL-01 (ThemeProvider/I18nProvider/ToastProvider, Design-Tokens statt hartkodierter Werte) 2026-08-28 21:46:59 +02:00
sysops 25168a18db Merge branch 'feature/shl-01-ui-shell-design-system-zentral' into feature/lic-04-lizenz-modul-verwaltungsoberflaeche
# Conflicts:
#	DEVLOG.md
2026-08-28 21:46:43 +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 46fccd9c09 API-10: test-fix — rotatemasterkey-assertion nur fuer eigene test-tenants pruefen (geteilte test-db) 2026-08-28 10:04:51 +02:00
sysops a631ac8770 API-10: internal/apiserver-port wieder entfernen (ungenutzt, zieht internal/auth als fehlende abhaengigkeit nach) 2026-08-28 10:03:41 +02:00
sysops bc2126f3b1 API-10: master-key-verwaltung-tenant-schluesselhierarchie-kms-anbindung (envelope encryption, isolierte tenant-keks, rotation) 2026-08-28 10:03:27 +02:00
sysops 3fcd8f92af API-09: mtls-zwischen-modul-instanzen-verteilte-installation (interne ca, rotation ohne ausfallzeit, stufe-1-optout) 2026-08-28 09:32:51 +02:00
sysops 43dca005f2 API-08: SetHeaders-helfer fuer redirect-freie umgebungen + devserver fuer csp-live-verifikation 2026-08-28 09:16:20 +02:00
sysops 24a522e10c API-08: security-header-baseline-fuer-alle-frontends (gemeinsame middleware, csp/hsts/coverage-scan) 2026-08-28 09:11:33 +02:00
sysops a67adcefa1 API-03: zentrales-rate-limiting-api-gateway-schicht (postgres-basierter shared state) 2026-08-28 08:14:20 +02:00
sysops 3b96d8ef41 AUD-04: backend-authorizer + dev-server + next.js audit-log-ansicht 2026-08-28 00:10:48 +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
sysops ac48935261 LIC-04: next.js 14.2.35 (aktuellster patch der 14-linie), gitignore fuer web-build-artefakte 2026-08-27 23:46:50 +02:00
sysops 0620baa993 LIC-04: dev-server fuer adminapi + next.js lizenz-modul-verwaltungsoberflaeche 2026-08-27 23:40:05 +02:00
sysops eed73eca8f LIC-04: backend-api fuer lizenz-modul-verwaltungsoberflaeche (flag.List, adminapi-paket) 2026-08-27 23:37:11 +02:00
sysopsandClaude Sonnet 5 034865f5a0 CFG-03: benachrichtigungs-kanaele-e-mail-in-app
internal/channels: konkrete Zustellkanaele fuer CFG-02s Dispatcher.
TemplateStore.Resolve loest Vorlagen pro Tenant auf und faellt auf
GlobalTemplateScope zurueck, wenn ein Tenant keine eigene gesetzt hat
(Akzeptanzkriterium 3). Render nutzt text/template mit
Option("missingkey=error") — ein fehlender Platzhalter bricht das Rendering
MIT FEHLER ab, statt eine unvollstaendige Nachricht zu erzeugen
(Akzeptanzkriterium 1).

EmailSender implementiert notify.Sender: rendert ZUERST die Vorlage, bevor
ueberhaupt eine SMTP-Verbindung aufgebaut wird — schlaegt das Rendering
fehl, wird nie ein Netzwerkzugriff versucht. Ein anschliessend fehl-
schlagender SMTP-Versand liefert einen Fehler, den CFG-02s bereits
getestete Wiederholungslogik verarbeitet (kein zweiter Retry-Mechanismus
hier). InAppSender persistiert In-App-Nachrichten ueber InAppStore
(Akzeptanzkriterium 2, ueber API abrufbar/als gelesen markierbar). Router
waehlt den Kanal anhand Notification.Channel — ein neuer Kanal wird per
Register() ergaenzt, ohne Dispatcher oder Router umzubauen.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Vorlagenrendering mit fehlenden Platzhaltern bricht kontrolliert ab —
   TestRender_MissingPlaceholderAborts und
   TestEmailSender_AbortsBeforeSMTPWhenTemplateMissing (Fehler kommt von der
   Vorlagenaufloesung, kein SMTP-Verbindungsversuch). PASS.
2. In-App-Benachrichtigung nach Markierung als gelesen korrekt gefuehrt —
   TestInAppStore_MarkReadIsReflectedCorrectly. PASS.
3. E-Mail-Versand bei nicht erreichbarem SMTP-Server loest dokumentiertes
   Retry-Verhalten ueber CFG-02 aus —
   TestEmailSender_TriggersDispatcherRetryOnUnreachableSMTP: echter
   EmailSender gegen unerreichbaren Host, ueber notify.Dispatcher
   eingereiht, nach ausgeschoepften Wiederholungen status=failed mit
   korrekter Versuchszahl. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 23:23:45 +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 bab582a38a RBAC-03: gruppen-abteilungen
internal/rbac/group.go: GroupStore verwaltet Gruppen innerhalb GENAU EINER
Tenant-Datenbank (Modell C, analog Store/TenantUserStore) — Tenant-Isolation
ist damit strukturell erfuellt, keine zusaetzliche Filterlogik noetig
(Akzeptanzkriterium 3). Nur 'user'/'tenant_admin' sind auf Gruppenebene
zuweisbar (dieselbe assignableRoles-Matrix wie bei direkter Zuweisung) —
superadmin bleibt mandantenuebergreifend und ausserhalb jeder Gruppenlogik.

EffectivePermissionsForUser vereinigt die direkte Rollenzuweisung (RBAC-01
Store) mit allen Rechten aus Gruppenrollen, live berechnet bei jedem Aufruf
statt zwischengespeichert — RemoveMember/DeleteGroup wirken dadurch sofort
(Akzeptanzkriterium 3). ON DELETE CASCADE auf group_members entzieht beim
Loeschen einer Gruppe die Mitgliedschaften automatisch, Benutzerkonten
selbst bleiben unberuehrt.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Gruppenrolle wirkt korrekt auf ALLE aktuellen Mitglieder —
   TestGroup_RoleAffectsAllCurrentMembers: zwei Mitglieder, beide erhalten
   die Gruppenrolle-Rechte. PASS.
2. Entfernen eines Benutzers aus der Gruppe entzieht Rechte sofort —
   TestGroup_RemoveMemberRevokesRightsImmediately. PASS.
3. Gruppen sauber tenant-isoliert (Stichprobe ueber zwei Tenants) —
   TestGroup_TenantIsolation: Gruppe in Tenant A taucht in Tenant B nicht
   auf. PASS.

Zusaetzlich: TestGroup_DeleteGroupRevokesRightsWithoutDeletingUser belegt
Akzeptanzkriterium 3 (Loeschung ohne Benutzerkonto-Verlust) konkret. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 22:00:48 +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 27f9866264 API-01: rest-api-grundgeruest-versionierung
internal/apiserver: Server.Handle(version, pattern, h) registriert Routen
unter /api/{version}/... (Akzeptanzkriterium 1) — verschiedene Versionen
sind unabhaengige Pfade im ServeMux, eine neue Version beeintraechtigt
bestehende nicht. HandleV1 ist die Kurzform fuer die aktuelle Hauptversion.

Einheitliches Fehlerschema {"error":{"code","message"}} ueber WriteError
(Akzeptanzkriterium 2) — bewusst NICHT auth.RequireAuth aus IAM-02
wiederverwendet, da dessen Klartext-Fehlerantworten nicht zum einheitlichen
JSON-Schema passen wuerden; stattdessen authAndTenantContext nutzt
auth.TokenIssuer.Verify direkt (dieselbe Kryptographie, keine Duplikation)
und antwortet im API-01-Schema, auch bei 401.

authAndTenantContext ist die Middleware, die JEDEM ueber Handle registrierten
Endpunkt Tenant-/Benutzerkontext bereitstellt (Akzeptanzkriterium 3, ueber
apiserver.FromContext abrufbar) — kein Handler prueft Auth selbst.
loggingMiddleware protokolliert jede Anfrage strukturiert.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Fehlerhafte Anfrage liefert ueber mehrere Endpunkte hinweg dasselbe
   Fehlerschema (Stichprobe) — TestErrorFormat_ConsistentAcrossEndpoints:
   zwei unabhaengige Endpunkte, beide liefern 401 im identischen
   {"error":{"code","message"}}-Schema. PASS.
2. Middleware-Kette nachweislich von jedem Endpunkt durchlaufen —
   TestMiddleware_SetsRequestContextForEveryEndpoint: zwei Endpunkte lesen
   RequestContext, beide erhalten korrekten UserID/TenantSlug aus dem Token. PASS.
3. Versionswechsel (fiktive v2-Route) ohne v1 zu beeintraechtigen —
   TestVersioning_V2DoesNotAffectV1: v1 vor und nach Anlage von v2 liefert
   unveraendert dieselbe Antwort. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:32:27 +02:00
sysopsandClaude Sonnet 5 b23cd1961f API-02: modul-registry-aktivierungspruefung
internal/moduleregistry: Registry.Register traegt Fachmodule mit Name,
Version und benoetigten Feature-Flags ein (Akzeptanzkriterium 1), fehlende
Pflichtangaben werden abgewiesen. IsActive kombiniert Registrierung + LIC-02
Feature-Flag-Auswertung (ALLE benoetigten Flags muessen fuer den Tenant
aktiv sein) — ein nicht registriertes Modul ist nie aktiv. List liefert alle
Module fuer Statusseite/Lizenzoberflaeche (Akzeptanzkriterium 3).

RequireActiveModule ist die zentrale Durchsetzungs-Middleware (Casbin-
Prinzip): weist Anfragen an ein deaktiviertes Modul ab, BEVOR der
Modul-Handler ueberhaupt aufgerufen wird (Akzeptanzkriterium 2) —
Pruefung per Test belegt, dass der Handler bei Deaktivierung nachweislich
nicht erreicht wird.

Service-Credentials (Akzeptanzkriterium 4): Registry.Provision stellt pro
Modul-Instanz Client-ID + Secret aus, gespeichert wird nur der SHA-256-Hash
des Secrets. Registry.Authenticate vergleicht timing-safe (dieselbe
subtle.ConstantTimeCompare-Referenzimplementierung wie AUD-02).
RequireServiceCredential-Middleware liest X-Client-Id/X-Client-Secret und
weist Aufrufe ohne gueltiges Credential mit 401 ab, bevor der Core-seitige
Endpunkt (z.B. Audit-Nachlieferung, Nutzungsmeldung) erreicht wird.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Anfrage an deaktiviertes Modul nachweislich vor Modul-Logik abgewiesen —
   TestRequireActiveModule_BlocksBeforeHandler: handlerReached bleibt false
   bei 403, wird true erst nach Aktivierung bei 200. PASS.
2. Registrierung mit fehlenden Pflichtangaben abgewiesen —
   TestRegister_RejectsMissingFields (leerer Name, leere Version). PASS.
3. Registry-Abfrage liefert konsistente Daten nach Aktivierung/Deaktivierung —
   TestIsActive_ReflectsFlagStateConsistently: aus/an/aus-Zyklus, IsActive
   folgt dem Flag-Zustand korrekt. PASS.
4. Aufruf mit ungueltigem/fehlendem Service-Credential abgewiesen, mit
   gueltigem angenommen — TestRequireServiceCredential_RejectsInvalidAcceptsValid
   und TestProvisionAndAuthenticate (falsches Secret, unbekannte Client-ID,
   korrektes Credential). PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:17:50 +02:00
sysopsandClaude Sonnet 5 d447869246 LIC-03: nutzungszaehler-quotas
internal/usage: Store.Increment aktualisiert Zaehlerstaende ueber ein
einziges atomares SQL-UPSERT (value = value + delta) statt Read-Modify-Write
in Go — haelt Zaehlerstaende bei parallelen Schreibzugriffen konsistent
(Akzeptanzkriterium 1), ganz ohne Anwendungs-Lock. Quotas sind Konfiguration
(usage_quotas-Tabelle je Tenant+Metrik), kein Hardcode.

Check/Enforce leiten aus Zaehlerstand + Quota eine definierte Reaktion ab
(StatusOK/Warning bei 80%/Exceeded, Akzeptanzkriterium 2) — Enforce ruft eine
uebergebene Reaction-Funktion auf, wenn der Status nicht OK ist; die
konkrete Sperr-/Benachrichtigungslogik bleibt beim Aufrufer (z.B. TEN-02 vor
Benutzeranlage), Enforce garantiert nur zuverlaessiges Ausloesen. Fehlende
Quota-Konfiguration bedeutet unbegrenzt (StatusOK), kein Fehler.

RunPeriodicAggregation ist das Aggregations-Grundgerüst (Akzeptanzkriterium 1:
"periodisch aggregiert") — dieselbe In-Prozess-Worker-Goroutine-Konvention
wie internal/tenant.Lifecycle.RunSweeper. Die konkrete Aggregationsquelle
(Zeilen zaehlen in Modul-Tabellen) haengt vom jeweiligen Modul ab und ist
nicht Teil dieser Kachel.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Quota-Ueberschreitung automatisiert erkannt, definierte Reaktion
   ausgeloest — TestEnforce_TriggersReactionOnExceeded: Reaction-Callback
   wird mit StatusExceeded aufgerufen. PASS.
2. Aggregationsjob liefert bei parallelen Schreibzugriffen konsistente
   Zaehlerstaende — TestIncrement_ConsistentUnderConcurrentWrites: 50
   nebenlaeufige Increments, Endstand exakt 50 (kein Lost Update). PASS.
3. Zaehlerstand eines Tenants beeinflusst nicht den eines anderen —
   TestIncrement_IsolatedBetweenTenants. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:10:11 +02:00
sysopsandClaude Sonnet 5 6f532d8350 CFG-02: benachrichtigungs-dispatcher-core-service-fuer-module
internal/notify: Dispatcher.Enqueue ist die EINE schmale Schnittstelle, ueber
die Module Benachrichtigungen ausloesen (Akzeptanzkriterium 1) — kein Modul
baut eigenen Versandcode. Warteschlange ist die Postgres-Tabelle
notification_jobs (Projekt-Konvention statt Redis/AMQP), existiert
ausschliesslich in der Datenbank, nicht im Prozessspeicher.

Dispatcher.ProcessDue holt faellige Jobs per FOR UPDATE SKIP LOCKED
(dieselbe Konvention wie internal/tenant.Lifecycle.ProcessDueDeletions) —
serialisiert konkurrierende Worker/Module, verhindert doppelte Zustellung.
Fehlschlag erhoeht attempts und plant next_attempt_at mit linearem Backoff;
nach max_attempts wird der Job kontrolliert auf status=failed gesetzt statt
endlos wiederholt zu werden (Akzeptanzkriterium 2).

Sender ist eine schmale Schnittstelle fuer die eigentlichen Kanaele
(E-Mail/In-App = CFG-03, nicht Teil dieser Kachel) — der Dispatcher kennt
nur "zustellen oder nicht", keine Kanal-Details.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Neustart waehrend offener Zustellung verliert keine Nachricht —
   TestQueue_SurvivesRestartWithoutMessageLoss: Enqueue durch eine
   Dispatcher-Instanz, Verarbeitung durch eine komplett neue (simulierter
   Neustart), Nachricht wird trotzdem zugestellt. PASS.
2. Wiederholungslogik greift bei simuliertem Fehler und bricht kontrolliert
   ab — TestProcessDue_RetriesThenGivesUpAfterMaxAttempts: 3 Versuche bei
   max_attempts=3, danach status=failed, keine weitere Verarbeitung. PASS.
3. Zwei Module loesen gleichzeitig aus, beide korrekt zugestellt —
   TestProcessDue_ConcurrentDispatchBothDelivered: zwei parallele
   ProcessDue-Aufrufe, beide Nachrichten je genau einmal zugestellt, keine
   Doppelzustellung. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:06:36 +02:00
sysopsandClaude Sonnet 5 c9e8edbbf0 AUD-03: audit-log-export-filter-api
internal/audit/export.go: StreamCSV/StreamJSON filtern nach Tenant, Akteur,
Aktion und Zeitraum (Akzeptanzkriterium 1) und schreiben Zeile fuer Zeile
ueber rows.Next() DIREKT auf den uebergebenen io.Writer — zu keinem
Zeitpunkt wird das komplette Ergebnis im Speicher aufgebaut (Akzeptanz-
kriterium 3). JSON-Export als JSON Lines statt einem grossen Array, um
Streaming ohne Sonderbehandlung von Klammern/Kommas zu ermoeglichen.

ExportHandler (Akzeptanzkriterium 2) schreibt direkt auf http.ResponseWriter
— derselbe Streaming-Pfad wie in Tests, kein Zwischenpuffer nur fuer HTTP.
Authorize ist eine schmale Schnittstelle (Vorbild: AUD-05 RetentionRegistrar-
Muster), da die eigentliche Rollenpruefung RBAC-02 (Policy-Enforcement) ist
und nicht Teil dieser Kachel — der Handler kennt nur "darf dieser Aufrufer
exportieren", nicht wie das entschieden wird.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Export mit hoher Eintragszahl ohne uebermaessigen Speicherverbrauch —
   TestExport_StreamsLargeResultWithoutExcessiveMemory: 20.000 Eintraege,
   Heap-Wachstum waehrend Export nur ~1.8KB (Schwelle 3MB). PASS.
2. Filterkombinationen automatisiert gegen erwartete Ergebnismengen —
   TestExport_FilterCombinations (Tenant/Actor/Action einzeln und kombiniert)
   und TestExport_TimeRangeFilter (innerhalb/ausserhalb Zeitraum). PASS.
3. Zugriff ohne passende Berechtigung abgewiesen —
   TestExportHandler_RejectsWithoutAuthorization: fehlender/falscher caller
   -> 403, berechtigter caller -> 200. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:02:56 +02:00