Files
cairnobs/enterprise/internal/oidc/oidc_test.go
T
jcoffey-dev 3eb0f4c589 Phase 4: SSO scaffolding, RBAC enforcement, tenant-scoped dashboards, audit logging, K8s deployment
RBAC (api/internal/authz) is live on /query and /dashboards, backed by a
new enterprise/ module (session issuance, audit logging, RBAC storage,
OIDC/SAML protocol wiring) that core never imports -- only calls over
HTTP. Found and fixed a real cross-tenant vulnerability in dashboards
(no tenant_id filtering at all) while writing the threat model doc.

Two things are explicitly NOT done, documented rather than hidden:
tenant isolation for log data itself (/query still shares one ClickHouse
connection and Tantivy index across every tenant -- RBAC controls who
can query, not what a query can see), and human SSO login (protocol
wiring exists, no HTTP handler calls it yet). See
docs/security/threat-model.md and docs/phase-4-runbook.md.

Also adds deploy/ (Go Operator + Helm chart, validated offline only --
no cluster was reachable in this environment).
2026-08-13 22:16:59 -07:00

76 lines
2.5 KiB
Go

package oidc
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestNewRejectsMissingConfig(t *testing.T) {
_, err := New(context.Background(), Config{})
if err == nil {
t.Fatalf("expected an error for an empty config")
}
}
// TestNewDiscoversRealIssuer spins up a real HTTP server serving a
// minimal valid OIDC discovery document and confirms New() actually
// performs discovery against it successfully -- not just "the code
// compiles and looks plausible." Doesn't cover the full Exchange() flow
// (needs a signed JWKS/token response, real crypto scaffolding better
// suited to task 5's end-to-end auth integration tests), but discovery
// is exactly the step that would silently break on a URL-construction or
// JSON-shape mistake, so it's worth actually running.
func TestNewDiscoversRealIssuer(t *testing.T) {
mux := http.NewServeMux()
srv := httptest.NewServer(mux)
defer srv.Close()
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"issuer": srv.URL,
"authorization_endpoint": srv.URL + "/authorize",
"token_endpoint": srv.URL + "/token",
"jwks_uri": srv.URL + "/jwks",
"userinfo_endpoint": srv.URL + "/userinfo",
"response_types_supported": []string{"code"},
"subject_types_supported": []string{"public"},
"id_token_signing_alg_values_supported": []string{"RS256"},
})
})
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{}})
})
p, err := New(context.Background(), Config{
IssuerURL: srv.URL, ClientID: "sentry", ClientSecret: "secret", RedirectURL: "http://localhost/callback",
})
if err != nil {
t.Fatalf("New: %v", err)
}
if p.AuthCodeURL("state123") == "" {
t.Fatalf("expected a non-empty auth code URL")
}
}
func TestNewStateIsNonEmptyAndUnique(t *testing.T) {
a, err := NewState()
if err != nil {
t.Fatalf("NewState: %v", err)
}
b, err := NewState()
if err != nil {
t.Fatalf("NewState: %v", err)
}
if a == "" || b == "" {
t.Fatalf("expected non-empty state values")
}
if a == b {
t.Fatalf("expected two calls to NewState to produce different values")
}
}