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).
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user