Files
traefikoidc/review_r71_test.go
T

149 lines
4.9 KiB
Go

package traefikoidc
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestServeHTTP_EventStream_EnforcesUserAllowlist is a regression for the
// SSE/WebSocket cutoff subjecting the streaming bypass to the user
// allowlist (isAllowedUser), mirroring the roles gate. Before the fix an
// authenticated user not in allowedUsers (or outside allowedUserDomains)
// could reach the backend just by setting Accept: text/event-stream.
func TestServeHTTP_EventStream_EnforcesUserAllowlist(t *testing.T) {
sessionManager := createTestSessionManager(t)
buildOidc := func(allowed map[string]struct{}, next http.Handler) *TraefikOidc {
return &TraefikOidc{
next: next,
logger: NewLogger("error"),
initComplete: make(chan struct{}),
sessionManager: sessionManager,
firstRequestStarted: 1,
metadataRefreshStartedAtomic: 1,
issuerURL: "https://provider.example.com",
allowedUsers: allowed,
}
}
// SSE request carrying an authenticated session for [email protected].
buildReq := func(t *testing.T) *http.Request {
req := httptest.NewRequest(http.MethodGet, "/events", nil)
req.Header.Set("Accept", "text/event-stream")
session, err := sessionManager.GetSession(req)
if err != nil {
t.Fatalf("create session: %v", err)
}
session.SetUserIdentifier("[email protected]")
if err := session.SetAuthenticated(true); err != nil {
t.Fatalf("mark authenticated: %v", err)
}
rec := httptest.NewRecorder()
if err := session.Save(req, rec); err != nil {
t.Fatalf("save session: %v", err)
}
for _, c := range rec.Result().Cookies() {
req.AddCookie(c)
}
return req
}
t.Run("user_not_in_allowlist_rejected", func(t *testing.T) {
nextCalled := false
oidc := buildOidc(map[string]struct{}{"[email protected]": {}},
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { nextCalled = true }))
close(oidc.initComplete)
rw := httptest.NewRecorder()
oidc.ServeHTTP(rw, buildReq(t))
if rw.Code != http.StatusForbidden {
t.Fatalf("expected 403 for SSE request from non-allowlisted user, got %d", rw.Code)
}
if nextCalled {
t.Error("backend must NOT be called for a non-allowlisted user")
}
})
t.Run("user_in_allowlist_forwarded", func(t *testing.T) {
nextCalled := false
oidc := buildOidc(map[string]struct{}{"[email protected]": {}},
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { nextCalled = true }))
close(oidc.initComplete)
rw := httptest.NewRecorder()
oidc.ServeHTTP(rw, buildReq(t))
if rw.Code != http.StatusOK && rw.Code != http.StatusUnauthorized {
t.Fatalf("expected forward for allowlisted user, got %d", rw.Code)
}
if !nextCalled {
t.Error("backend must be called for an allowlisted user")
}
})
}
// TestConfig_ValidateRejectsNegativeSessionMaxAge is a regression for a
// config-validation gap: a negative sessionMaxAge passed Validate(),
// producing a negative cookie MaxAge and an always-expired session
// (permanent lockout).
func TestConfig_ValidateRejectsNegativeSessionMaxAge(t *testing.T) {
base := Config{
ProviderURL: "https://provider.example.com",
CallbackURL: "/callback",
ClientID: "client-id",
ClientSecret: "client-secret",
SessionEncryptionKey: "01234567890123456789012345678901",
LogLevel: "info",
RateLimit: 10,
}
if err := base.Validate(); err != nil {
t.Fatalf("baseline config should validate: %v", err)
}
neg := base
neg.SessionMaxAge = -1
err := neg.Validate()
if err == nil {
t.Fatal("expected Validate to reject negative sessionMaxAge")
}
if !strings.Contains(err.Error(), "sessionMaxAge") {
t.Fatalf("expected sessionMaxAge error, got: %v", err)
}
}
// TestGetSecurityHeadersApplier_AppliesCrossOriginAndPermissions is a
// regression for config no-effect: PermissionsPolicy and the three
// Cross-Origin-* fields were parsed and documented but never applied.
func TestGetSecurityHeadersApplier_AppliesCrossOriginAndPermissions(t *testing.T) {
c := &Config{SecurityHeaders: &SecurityHeadersConfig{
Enabled: true,
PermissionsPolicy: "geolocation=()",
CrossOriginResourcePolicy: "same-origin",
CrossOriginOpenerPolicy: "same-origin",
CrossOriginEmbedderPolicy: "require-corp",
}}
applier := c.GetSecurityHeadersApplier()
if applier == nil {
t.Fatal("expected a non-nil header applier")
}
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
applier(rw, req)
h := rw.Header()
for name, want := range map[string]string{
"Permissions-Policy": "geolocation=()",
"Cross-Origin-Resource-Policy": "same-origin",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
} {
if got := h.Get(name); got != want {
t.Errorf("%s = %q, want %q", name, got, want)
}
}
}