Files
patrickandClaude Sonnet 5 e57b0d23b6
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
API-16: persistente modul-signaturschlüssel und rotation
Signaturschluessel der Modul-JWTs liegen verschluesselt in der Mandanten-Datenbank
(module_signing_keys), werden von mehreren Instanzen geteilt und rotieren mit
Ueberlappung; nicht lesbarer Speicher ergibt 503 statt Token/JWKS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:42:00 +02:00

77 lines
2.6 KiB
Go

package moduletoken
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
"gitea.perlbach24.de/scripte/nexarch/internal/moduletrust"
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
"gitea.perlbach24.de/scripte/nexarch/internal/user"
)
type passthroughCipher struct{}
func (passthroughCipher) Encrypt(pt []byte) ([]byte, error) { return append([]byte(nil), pt...), nil }
func (passthroughCipher) Decrypt(ct []byte) ([]byte, error) { return append([]byte(nil), ct...), nil }
// Ist der Schluesselspeicher nicht lesbar, gibt es weder Token (503) noch JWKS (503) — nach Wiederanlauf wieder beides.
func TestPersistentKeys_StoreOutageYields503ForTokenAndJWKS(t *testing.T) {
now := time.Now()
store := moduletrust.NewMemKeyStore()
km, err := moduletrust.NewPersistentKeyManager(context.Background(), moduletrust.PersistentConfig{
Store: store, Cipher: passthroughCipher{}, TokenTTL: time.Minute,
ActivationDelay: time.Minute, RefreshInterval: time.Second,
Now: func() time.Time { return now },
})
if err != nil {
t.Fatal(err)
}
sessions := auth.NewTokenIssuer("test-secret-fuer-sitzungen")
mux := http.NewServeMux()
RegisterJWKS(mux, km)
RegisterToken(mux, NewHandler(km, &fakeUsers{u: user.User{ID: "u1", Status: user.StatusActive}}, &fakeRoles{role: rbac.RoleUser}, "acme", time.Minute),
sessions, revHolder{&fakeRevocation{}})
srv := httptest.NewServer(mux)
defer srv.Close()
session, err := sessions.Issue("u1", "acme")
if err != nil {
t.Fatal(err)
}
call := func(method, path string, withCookie bool) int {
req, _ := http.NewRequest(method, srv.URL+path, nil)
if withCookie {
req.AddCookie(&http.Cookie{Name: auth.CookieName, Value: session})
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
return resp.StatusCode
}
if got := call(http.MethodPost, TokenPath, true); got != http.StatusOK {
t.Fatalf("token bei laufendem speicher: %d", got)
}
now = now.Add(5 * time.Second)
store.SetFailing(true)
if got := call(http.MethodPost, TokenPath, true); got != http.StatusServiceUnavailable {
t.Fatalf("token bei speicherausfall: %d, erwartet 503", got)
}
if got := call(http.MethodGet, JWKSPath, false); got != http.StatusServiceUnavailable {
t.Fatalf("jwks bei speicherausfall: %d, erwartet 503", got)
}
store.SetFailing(false)
if got := call(http.MethodPost, TokenPath, true); got != http.StatusOK {
t.Fatalf("token nach wiederanlauf: %d", got)
}
if got := call(http.MethodGet, JWKSPath, false); got != http.StatusOK {
t.Fatalf("jwks nach wiederanlauf: %d", got)
}
}