571 lines
18 KiB
Go
571 lines
18 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var testSigner = NewSigner([]byte("ein-test-secret-das-lang-genug-ist-fuer-tests"))
|
|
|
|
// ── Sitzungen ────────────────────────────────────────────────────────────────
|
|
|
|
func TestSessionRoundTrip(t *testing.T) {
|
|
cookie := testSigner.SignSession("key:abc123", time.Now().Add(time.Hour))
|
|
|
|
subject, _, ok := testSigner.VerifySession(cookie)
|
|
if !ok {
|
|
t.Fatal("frisch signierte Sitzung wurde als ungültig abgelehnt")
|
|
}
|
|
if subject != "key:abc123" {
|
|
t.Errorf("subject = %q, erwartet %q", subject, "key:abc123")
|
|
}
|
|
}
|
|
|
|
// TestSessionRejectsWrongSecret ist der Kern des Verfahrens: ohne das richtige
|
|
// Geheimnis darf sich niemand eine Sitzung ausstellen können.
|
|
func TestSessionRejectsWrongSecret(t *testing.T) {
|
|
cookie := testSigner.SignSession(SubjectPlatformAdmin, time.Now().Add(time.Hour))
|
|
foreign := NewSigner([]byte("ein-voellig-anderes-secret"))
|
|
|
|
if _, _, ok := foreign.VerifySession(cookie); ok {
|
|
t.Error("Sitzung wurde mit falschem Secret akzeptiert")
|
|
}
|
|
}
|
|
|
|
// TestSessionRejectsTampering prüft den naheliegendsten Angriff: den Cookie
|
|
// auslesen (er ist nur base64, nicht verschlüsselt), "key:xyz" durch "platform"
|
|
// ersetzen und wieder einsetzen.
|
|
func TestSessionRejectsTampering(t *testing.T) {
|
|
cookie := testSigner.SignSession("key:abc123", time.Now().Add(time.Hour))
|
|
_, signature, _ := strings.Cut(cookie, ".")
|
|
|
|
forged := base64.RawURLEncoding.EncodeToString(
|
|
fmt.Appendf(nil, "%s|%d", SubjectPlatformAdmin, time.Now().Add(time.Hour).Unix()),
|
|
)
|
|
|
|
if _, _, ok := testSigner.VerifySession(forged + "." + signature); ok {
|
|
t.Error("manipulierter Cookie wurde akzeptiert — die Signatur greift nicht")
|
|
}
|
|
}
|
|
|
|
// TestSessionTTLPerKind prüft die Obergrenze je Sitzungsart: Ein
|
|
// Plattform-Admin-Cookie darf höchstens PlatformAdminSessionTTL gelten. Länger
|
|
// laufende (etwa noch mit den früheren 30 Tagen ausgestellte) werden
|
|
// abgelehnt, obwohl sie korrekt signiert sind. Key-Sitzungen behalten 30 Tage.
|
|
func TestSessionTTLPerKind(t *testing.T) {
|
|
platform := PlatformSubject("abcdef0123456789", "")
|
|
now := time.Now()
|
|
|
|
cases := []struct {
|
|
subject string
|
|
expiry time.Time
|
|
want bool
|
|
}{
|
|
{platform, now.Add(PlatformAdminSessionTTL), true},
|
|
{platform, now.Add(30 * 24 * time.Hour), false},
|
|
{"key:ws:abc123", now.Add(SessionTTL), true},
|
|
{"wsadmin:ws:abc123", now.Add(SessionTTL), true},
|
|
{"key:ws:abc123", now.Add(SessionTTL + time.Hour), false},
|
|
}
|
|
for _, tc := range cases {
|
|
_, _, ok := testSigner.VerifySession(testSigner.SignSession(tc.subject, tc.expiry))
|
|
if ok != tc.want {
|
|
t.Errorf("%s, gültig noch %v: ok = %v, erwartet %v", tc.subject, tc.expiry.Sub(now).Round(time.Hour), ok, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSessionRejectsExpired baut die Nutzlast von Hand mit einem Zeitstempel in
|
|
// der Vergangenheit und signiert sie korrekt — so wird wirklich nur die
|
|
// Ablaufprüfung getestet, ohne eine Stunde zu warten.
|
|
func TestSessionRejectsExpired(t *testing.T) {
|
|
payload := fmt.Sprintf("key:abc123|%d", time.Now().Add(-time.Minute).Unix())
|
|
cookie := base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + testSigner.sign(payload)
|
|
|
|
if _, _, ok := testSigner.VerifySession(cookie); ok {
|
|
t.Error("abgelaufene Sitzung wurde akzeptiert")
|
|
}
|
|
}
|
|
|
|
// TestSessionRejectsMalformed wirft der Prüfung kaputte Eingaben vor die Füße.
|
|
// Wichtig ist vor allem, dass nichts davon zu einem Absturz führt.
|
|
func TestSessionRejectsMalformed(t *testing.T) {
|
|
cases := []string{
|
|
"",
|
|
"kein-punkt-drin",
|
|
".",
|
|
"nicht-base64!!!.abcdef",
|
|
base64.RawURLEncoding.EncodeToString([]byte("ohne-trenner")) + ".abcdef",
|
|
base64.RawURLEncoding.EncodeToString([]byte("key:a|keine-zahl")) + ".abcdef",
|
|
}
|
|
|
|
for _, cookie := range cases {
|
|
if _, _, ok := testSigner.VerifySession(cookie); ok {
|
|
t.Errorf("kaputter Cookie wurde akzeptiert: %q", cookie)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKeyIDFromSubject(t *testing.T) {
|
|
if id, ok := KeyIDFromSubject("key:abc123"); !ok || id != "abc123" {
|
|
t.Errorf("KeyIDFromSubject = (%q, %v), erwartet (\"abc123\", true)", id, ok)
|
|
}
|
|
if _, ok := KeyIDFromSubject(SubjectPlatformAdmin); ok {
|
|
t.Error("Plattform-Admin hat fälschlich eine Key-ID geliefert")
|
|
}
|
|
}
|
|
|
|
// ── CSRF ─────────────────────────────────────────────────────────────────────
|
|
|
|
func TestCSRFToken(t *testing.T) {
|
|
token := testSigner.CSRFToken("key:abc123")
|
|
|
|
if !testSigner.ValidCSRFToken(token, "key:abc123") {
|
|
t.Error("gültiges CSRF-Token wurde abgelehnt")
|
|
}
|
|
if testSigner.ValidCSRFToken(token, "key:anderer") {
|
|
t.Error("CSRF-Token einer Sitzung galt auch für eine andere")
|
|
}
|
|
if testSigner.ValidCSRFToken("", "key:abc123") {
|
|
t.Error("leeres CSRF-Token wurde akzeptiert")
|
|
}
|
|
}
|
|
|
|
// TestTokenDomainSeparation prüft, dass ein CSRF-Token (das offen im HTML steht)
|
|
// nicht als Session-Signatur durchgeht.
|
|
func TestTokenDomainSeparation(t *testing.T) {
|
|
subject := "key:abc123"
|
|
csrf := testSigner.CSRFToken(subject)
|
|
|
|
if _, _, ok := testSigner.VerifySession(
|
|
base64.RawURLEncoding.EncodeToString([]byte(subject)) + "." + csrf,
|
|
); ok {
|
|
t.Error("ein CSRF-Token wurde als gültige Session-Signatur akzeptiert")
|
|
}
|
|
}
|
|
|
|
// ── API-Keys ─────────────────────────────────────────────────────────────────
|
|
|
|
func TestGenerateKeyFormat(t *testing.T) {
|
|
key, err := GenerateKey()
|
|
if err != nil {
|
|
t.Fatalf("Key konnte nicht erzeugt werden: %v", err)
|
|
}
|
|
if !strings.HasPrefix(key, KeyPrefix) {
|
|
t.Errorf("Key %q hat nicht das Präfix %q", key, KeyPrefix)
|
|
}
|
|
if !LooksLikeKey(key) {
|
|
t.Errorf("selbst erzeugter Key besteht die Formprüfung nicht: %q", key)
|
|
}
|
|
}
|
|
|
|
// TestGenerateKeyIsUnique prüft, dass wirklich Zufall im Spiel ist.
|
|
func TestGenerateKeyIsUnique(t *testing.T) {
|
|
seen := make(map[string]bool)
|
|
for i := 0; i < 100; i++ {
|
|
key, err := GenerateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seen[key] {
|
|
t.Fatal("zweimal derselbe Key erzeugt")
|
|
}
|
|
seen[key] = true
|
|
}
|
|
}
|
|
|
|
func TestHashKey(t *testing.T) {
|
|
key := "wk_" + strings.Repeat("a", 64)
|
|
|
|
if HashKey(key) != HashKey(key) {
|
|
t.Error("derselbe Key ergibt unterschiedliche Hashes")
|
|
}
|
|
if HashKey(key) == HashKey(key+"x") {
|
|
t.Error("unterschiedliche Keys ergeben denselben Hash")
|
|
}
|
|
// Der Hash darf den Key nicht enthalten — sonst wäre die Datenbank so gut
|
|
// wie eine Klartext-Ablage.
|
|
if strings.Contains(HashKey(key), key) {
|
|
t.Error("der Hash enthält den Key im Klartext")
|
|
}
|
|
}
|
|
|
|
func TestLooksLikeKey(t *testing.T) {
|
|
valid := KeyPrefix + strings.Repeat("ab", 32)
|
|
|
|
cases := map[string]bool{
|
|
valid: true,
|
|
"": false,
|
|
"wk_": false,
|
|
strings.Repeat("ab", 32): false, // ohne Präfix
|
|
KeyPrefix + "zz": false, // zu kurz
|
|
KeyPrefix + strings.Repeat("g", 64): false, // kein Hex
|
|
}
|
|
|
|
for key, want := range cases {
|
|
if got := LooksLikeKey(key); got != want {
|
|
t.Errorf("LooksLikeKey(%.20q…) = %v, erwartet %v", key, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWorkspaceAdminTokenIsNoKey: Die beiden Arten dürfen sich nie
|
|
// überschneiden. Ginge ein Workspace-Admin-Token als API-Key durch, stünde
|
|
// ihm die MCP-Schnittstelle offen — und umgekehrt käme ein Projekt-Key an
|
|
// die Workspace-Verwaltung.
|
|
func TestWorkspaceAdminTokenIsNoKey(t *testing.T) {
|
|
token, err := GenerateWorkspaceAdminToken()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(token, WorkspaceAdminPrefix) || !LooksLikeWorkspaceAdminToken(token) {
|
|
t.Errorf("selbst erzeugtes Token besteht die Formprüfung nicht: %q", token)
|
|
}
|
|
if LooksLikeKey(token) {
|
|
t.Error("ein Workspace-Admin-Token geht als API-Key durch")
|
|
}
|
|
|
|
key, err := GenerateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if LooksLikeWorkspaceAdminToken(key) {
|
|
t.Error("ein API-Key geht als Workspace-Admin-Token durch")
|
|
}
|
|
}
|
|
|
|
// ── Rollen ───────────────────────────────────────────────────────────────────
|
|
|
|
func TestRolePermissions(t *testing.T) {
|
|
cases := []struct {
|
|
role Role
|
|
valid bool
|
|
write bool
|
|
manageKeys bool
|
|
}{
|
|
{RoleRead, true, false, false},
|
|
{RoleWrite, true, true, false},
|
|
{RoleAdmin, true, true, true},
|
|
{Role("unsinn"), false, false, false},
|
|
{Role(""), false, false, false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(string(tc.role), func(t *testing.T) {
|
|
if tc.role.Valid() != tc.valid {
|
|
t.Errorf("Valid() = %v, erwartet %v", tc.role.Valid(), tc.valid)
|
|
}
|
|
if tc.role.CanWrite() != tc.write {
|
|
t.Errorf("CanWrite() = %v, erwartet %v", tc.role.CanWrite(), tc.write)
|
|
}
|
|
if tc.role.CanManageKeys() != tc.manageKeys {
|
|
t.Errorf("CanManageKeys() = %v, erwartet %v", tc.role.CanManageKeys(), tc.manageKeys)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Cookie-Policy ────────────────────────────────────────────────────────────
|
|
|
|
func TestCookiePolicySecure(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
env string
|
|
host string
|
|
want bool
|
|
}{
|
|
{"Standard, echte Domain", "", "munin.intern", true},
|
|
{"Standard, localhost", "", "localhost:8080", false},
|
|
{"Standard, 127.0.0.1", "", "127.0.0.1:8080", false},
|
|
{"erzwungen an, localhost", "true", "localhost:8080", true},
|
|
{"erzwungen aus, echte Domain", "false", "munin.intern", false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
policy := NewCookiePolicy(tc.env)
|
|
r := httptest.NewRequest(http.MethodGet, "http://"+tc.host+"/", nil)
|
|
r.Host = tc.host
|
|
|
|
if got := policy.Secure(r); got != tc.want {
|
|
t.Errorf("Secure = %v, erwartet %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCookiePolicyApplySetsFlags(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, "http://munin.intern/", nil)
|
|
r.Host = "munin.intern"
|
|
|
|
cookie := &http.Cookie{Name: "test", Value: "wert"}
|
|
NewCookiePolicy("").Apply(cookie, r)
|
|
|
|
if !cookie.HttpOnly {
|
|
t.Error("HttpOnly fehlt")
|
|
}
|
|
if !cookie.Secure {
|
|
t.Error("Secure fehlt")
|
|
}
|
|
if cookie.SameSite != http.SameSiteLaxMode {
|
|
t.Errorf("SameSite = %v, erwartet Lax", cookie.SameSite)
|
|
}
|
|
if cookie.Path != "/" {
|
|
t.Errorf("Path = %q, erwartet \"/\"", cookie.Path)
|
|
}
|
|
}
|
|
|
|
// ── Rate-Limiting ────────────────────────────────────────────────────────────
|
|
|
|
func TestRateLimiterBlocksAfterMax(t *testing.T) {
|
|
limiter := NewRateLimiter()
|
|
const ip = "203.0.113.7"
|
|
|
|
for i := 0; i < rateLimitMax; i++ {
|
|
if allowed, _ := limiter.Allow(ip); !allowed {
|
|
t.Fatalf("bereits nach %d Versuchen gesperrt — zu früh", i)
|
|
}
|
|
limiter.RecordFailure(ip)
|
|
}
|
|
|
|
allowed, retryAfter := limiter.Allow(ip)
|
|
if allowed {
|
|
t.Errorf("nach %d Fehlversuchen immer noch erlaubt", rateLimitMax)
|
|
}
|
|
if retryAfter <= 0 {
|
|
t.Errorf("retryAfter = %v, erwartet einen positiven Wert", retryAfter)
|
|
}
|
|
}
|
|
|
|
func TestRateLimiterIsPerIP(t *testing.T) {
|
|
limiter := NewRateLimiter()
|
|
for i := 0; i < rateLimitMax; i++ {
|
|
limiter.RecordFailure("203.0.113.7")
|
|
}
|
|
|
|
if allowed, _ := limiter.Allow("198.51.100.4"); !allowed {
|
|
t.Error("eine andere IP wurde mitgesperrt")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiterResetClearsCounter(t *testing.T) {
|
|
limiter := NewRateLimiter()
|
|
const ip = "203.0.113.7"
|
|
|
|
for i := 0; i < rateLimitMax; i++ {
|
|
limiter.RecordFailure(ip)
|
|
}
|
|
limiter.Reset(ip)
|
|
|
|
if allowed, _ := limiter.Allow(ip); !allowed {
|
|
t.Error("nach Reset immer noch gesperrt")
|
|
}
|
|
}
|
|
|
|
// ── Schreibbudget ────────────────────────────────────────────────────────────
|
|
|
|
func TestWriteBudgetAllowsUpToLimit(t *testing.T) {
|
|
budget := NewWriteBudget(3, time.Hour)
|
|
const project = "p1"
|
|
|
|
for i := 1; i <= 3; i++ {
|
|
ok, _, over := budget.Consume(project, 0)
|
|
if !ok {
|
|
t.Fatalf("Aufruf %d von 3 abgelehnt", i)
|
|
}
|
|
if over != 0 {
|
|
t.Errorf("Aufruf %d: ueberschreitung = %d, erwartet 0", i, over)
|
|
}
|
|
}
|
|
|
|
ok, retryAfter, over := budget.Consume(project, 0)
|
|
if ok {
|
|
t.Error("der vierte Aufruf wurde angenommen")
|
|
}
|
|
if retryAfter <= 0 {
|
|
t.Errorf("retryAfter = %v, erwartet einen positiven Wert", retryAfter)
|
|
}
|
|
// Genau 1 beim ersten abgelehnten Aufruf — daran hängt, dass die
|
|
// Überschreitung nur einmal je Fenster protokolliert wird.
|
|
if over != 1 {
|
|
t.Errorf("ueberschreitung = %d, erwartet 1 beim ersten abgelehnten Aufruf", over)
|
|
}
|
|
|
|
if _, _, over := budget.Consume(project, 0); over != 2 {
|
|
t.Errorf("ueberschreitung = %d, erwartet 2 beim zweiten abgelehnten Aufruf", over)
|
|
}
|
|
}
|
|
|
|
func TestWriteBudgetIsPerProject(t *testing.T) {
|
|
budget := NewWriteBudget(1, time.Hour)
|
|
|
|
budget.Consume("p1", 0)
|
|
if ok, _, _ := budget.Consume("p1", 0); ok {
|
|
t.Fatal("Vorbedingung verfehlt: p1 ist noch nicht am Limit")
|
|
}
|
|
|
|
if ok, _, _ := budget.Consume("p2", 0); !ok {
|
|
t.Error("ein anderes Projekt wurde mitgesperrt")
|
|
}
|
|
}
|
|
|
|
// TestWriteBudgetProjectLimitBeatsDefault prüft die Auflösung der Obergrenze:
|
|
// erst die eigene Angabe des Projekts, sonst der Standard.
|
|
func TestWriteBudgetProjectLimitBeatsDefault(t *testing.T) {
|
|
budget := NewWriteBudget(100, time.Hour)
|
|
|
|
budget.Consume("eng", 1)
|
|
if ok, _, _ := budget.Consume("eng", 1); ok {
|
|
t.Error("die eigene Obergrenze (1) wurde nicht angewandt")
|
|
}
|
|
|
|
// 0 heißt "keine eigene Angabe" — dann gilt der großzügige Standard.
|
|
if ok, _, _ := budget.Consume("standard", 0); !ok {
|
|
t.Error("bei projektLimit 0 wurde nicht der Standard verwendet")
|
|
}
|
|
}
|
|
|
|
// TestWriteBudgetZeroDefaultDisables prüft die Notbremse: WRITE_LIMIT_HOURLY=0
|
|
// schaltet die Begrenzung vollständig ab.
|
|
func TestWriteBudgetZeroDefaultDisables(t *testing.T) {
|
|
budget := NewWriteBudget(0, time.Hour)
|
|
|
|
for i := 0; i < 50; i++ {
|
|
if ok, _, _ := budget.Consume("p1", 0); !ok {
|
|
t.Fatalf("Aufruf %d abgelehnt, obwohl die Begrenzung aus ist", i+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteBudgetRemainingCountsDown(t *testing.T) {
|
|
budget := NewWriteBudget(5, time.Hour)
|
|
|
|
if free, limit := budget.Remaining("p1", 0); free != 5 || limit != 5 {
|
|
t.Errorf("frei/grenze = %d/%d, erwartet 5/5 vor dem ersten Aufruf", free, limit)
|
|
}
|
|
|
|
budget.Consume("p1", 0)
|
|
budget.Consume("p1", 0)
|
|
|
|
if free, _ := budget.Remaining("p1", 0); free != 3 {
|
|
t.Errorf("frei = %d, erwartet 3 nach zwei Aufrufen", free)
|
|
}
|
|
|
|
// Remaining selbst darf nichts verbrauchen.
|
|
if free, _ := budget.Remaining("p1", 0); free != 3 {
|
|
t.Errorf("frei = %d — Remaining hat mitgezählt", free)
|
|
}
|
|
}
|
|
|
|
// TestWriteBudgetWindowExpires prüft, dass ein abgelaufenes Fenster den Zähler
|
|
// zurücksetzt statt ihn fortzuschreiben.
|
|
func TestWriteBudgetWindowExpires(t *testing.T) {
|
|
budget := NewWriteBudget(1, time.Millisecond)
|
|
|
|
budget.Consume("p1", 0)
|
|
if ok, _, _ := budget.Consume("p1", 0); ok {
|
|
t.Fatal("Vorbedingung verfehlt: p1 ist noch nicht am Limit")
|
|
}
|
|
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
if ok, _, _ := budget.Consume("p1", 0); !ok {
|
|
t.Error("nach Ablauf des Fensters immer noch gesperrt")
|
|
}
|
|
}
|
|
|
|
// ── Client-IP / Trusted Proxy ────────────────────────────────────────────────
|
|
|
|
func TestClientIPIgnoresForwardedByDefault(t *testing.T) {
|
|
if err := SetTrustedProxies(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.RemoteAddr = "10.0.0.5:1234"
|
|
r.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
|
|
// Ohne TRUSTED_PROXY ist der Header frei erfunden und muss ignoriert werden —
|
|
// sonst könnte sich jeder Angreifer bei jedem Versuch eine neue IP geben und
|
|
// das Rate-Limiting damit aushebeln.
|
|
if got := ClientIP(r); got != "10.0.0.5" {
|
|
t.Errorf("ClientIP = %q, erwartet %q", got, "10.0.0.5")
|
|
}
|
|
}
|
|
|
|
func TestClientIPUsesForwardedFromTrustedProxy(t *testing.T) {
|
|
if err := SetTrustedProxies("10.0.0.0/8"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer SetTrustedProxies("")
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.RemoteAddr = "10.0.0.5:1234"
|
|
r.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
|
|
if got := ClientIP(r); got != "1.2.3.4" {
|
|
t.Errorf("ClientIP = %q, erwartet %q", got, "1.2.3.4")
|
|
}
|
|
}
|
|
|
|
// TestClientIPTakesRightmostUntrusted prüft den Fall, dass der Client selbst
|
|
// schon einen X-Forwarded-For mitschickt und der Proxy nur anhängt.
|
|
func TestClientIPTakesRightmostUntrusted(t *testing.T) {
|
|
if err := SetTrustedProxies("10.0.0.0/8"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer SetTrustedProxies("")
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.RemoteAddr = "10.0.0.5:1234"
|
|
// "9.9.9.9" ist die Erfindung des Angreifers, "1.2.3.4" seine echte Adresse.
|
|
r.Header.Set("X-Forwarded-For", "9.9.9.9, 1.2.3.4")
|
|
|
|
if got := ClientIP(r); got != "1.2.3.4" {
|
|
t.Errorf("ClientIP = %q, erwartet %q — die erfundene IP wurde übernommen", got, "1.2.3.4")
|
|
}
|
|
}
|
|
|
|
func TestSetTrustedProxiesRejectsGarbage(t *testing.T) {
|
|
if err := SetTrustedProxies("keine-ip"); err == nil {
|
|
t.Error("unsinnige Angabe wurde akzeptiert")
|
|
}
|
|
SetTrustedProxies("")
|
|
}
|
|
|
|
func TestPlatformSubjectRoundTrip(t *testing.T) {
|
|
cases := []struct{ fingerprint, workspaceID string }{
|
|
{"abc123", ""},
|
|
{"abc123", "ws-1"},
|
|
}
|
|
for _, f := range cases {
|
|
subject := PlatformSubject(f.fingerprint, f.workspaceID)
|
|
fp, ws, ok := ParsePlatformSubject(subject)
|
|
if !ok || fp != f.fingerprint || ws != f.workspaceID {
|
|
t.Errorf("%q → (%q, %q, %v), erwartet (%q, %q, true)", subject, fp, ws, ok, f.fingerprint, f.workspaceID)
|
|
}
|
|
}
|
|
|
|
// Alte Formen ohne Fingerabdruck und fremde Subjects sind keine.
|
|
for _, subject := range []string{"platform", "platform:ws-1", "platform/", "platform/:ws-1", "key:ws:1"} {
|
|
if _, _, ok := ParsePlatformSubject(subject); ok {
|
|
t.Errorf("%q wurde als Subject des Plattform-Admins erkannt", subject)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFingerprintDependsOnSecret(t *testing.T) {
|
|
a := NewSigner([]byte("geheimnis-eins-geheimnis-eins-geheimnis")).Fingerprint("wert")
|
|
b := NewSigner([]byte("geheimnis-zwei-geheimnis-zwei-geheimnis")).Fingerprint("wert")
|
|
if a == b {
|
|
t.Error("gleicher Fingerabdruck trotz anderem SESSION_SECRET")
|
|
}
|
|
if a != NewSigner([]byte("geheimnis-eins-geheimnis-eins-geheimnis")).Fingerprint("wert") {
|
|
t.Error("Fingerabdruck ist nicht stabil")
|
|
}
|
|
}
|