9f641fec7673a2f438b592af8e82efdb328ee90c
77
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
962ef5a27a | TEN-05: lifecycle-code aus TEN-04 auf ten-03-basis portiert (registry liest previous_status/deletion_scheduled_at) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
4a30345e07 |
LIC-02: feature-flag-service-je-tenant
internal/flag: Store (Verwaltung) + Service (Auswertung mit TTL-Cache, Default 5s) — Unleash-Prinzip Flag-Verwaltung vs. Flag-Auswertung getrennt, als Kernfunktion des Core-Dienstes selbst statt separater Infrastruktur. evaluate() wendet drei Strategien in fester Reihenfolge an: global an/aus, Tenant-Zielgruppe, deterministischer Prozentsatz-Rollout (FNV-Hash aus Tenant+Key, stabil pro Tenant). IsEnabled liefert IMMER nur bool (kein Fehlerwert) — ein nicht erreichbarer Flag-Dienst kann damit keinen Aufrufer zum Absturz bringen: bei DB-Fehler wird der zuletzt bekannte Cache-Stand verwendet, ohne jeglichen Stand faellt der Dienst sicher auf false zurueck. Service.Invalidate erzwingt sofortiges Neuladen fuer den Schreiber selbst, andere Instanzen sehen Aenderungen spaetestens nach der TTL (Akzeptanzkriterium 3, kein Neustart noetig). Bugfix waehrend Tests: Store.Set uebergab ein nil-TargetTenantSlugs-Slice als SQL NULL statt leerem Array (NOT-NULL-Verletzung) — auf leeres Slice normalisiert. Akzeptanzkriterium 4 (Deaktivierung loescht keine Daten): dieses Paket besitzt ausschliesslich die eigene feature_flags-Zeile, hat keinerlei Code-Pfad, der Modul-Geschaeftsdaten anfassen koennte — Loeschung bleibt strukturell der Archive-Retention-Engine vorbehalten. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Cache-Invalidierungszeit automatisiert gemessen — TestService_CacheInvalidationTiming: Aenderung wirksam nach 153ms bei TTL=150ms (innerhalb Ziel+Toleranz), vorher nachweislich noch alter Stand. PASS. 2. Zielgruppen-Strategie liefert erwartete Auswertung — TestService_TargetTenantStrategy / TestEvaluate_TargetTenantStrategy. PASS. 3. Ausfall des Flag-Dienstes fuehrt zu dokumentiertem Fallback, kein Absturz — TestService_FallsBackOnStoreFailure (mit recover()-Absicherung): Fallback auf Cache-Stand bzw. sicheres false bei komplett unerreichbarer DB, geloggt. PASS. 4. Modul-Deaktivierung/Reaktivierung ohne Datenverlust — architektonisch durch fehlenden Code-Pfad sichergestellt (siehe oben), zusaetzlich durch TestService_InvalidateForcesImmediateRefresh (Toggle aus/an bleibt konsistent nachvollziehbar) mitabgedeckt. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
c895a67c4b | core: initial Go module skeleton (config, db pool, tenant registry migration) | ||
|
|
72261cc69f | first commit |