test/clustermode/modes_test.go
Rama main · réplica de lectura del repositorio original.
//go:build clustermode
package clustermode
import (
"encoding/json"
"net/http"
"strings"
"testing"
"github.com/sophotechlabs/spinoza/internal/api"
)
func TestThroughARealAuthenticationProxy(t *testing.T) {
deployRealProxy(t)
t.Run("the proxy signs a keycloak identity before spinoza sees it", func(t *testing.T) {
alice := signIn(t, "alice")
session := whoami(t, alice)
if session.User != "alice" {
t.Fatalf("user = %q, want alice", session.User)
}
if session.Role != "admin" {
t.Fatalf("role = %q, want admin", session.Role)
}
if session.Mode != "proxy" {
t.Fatalf("mode = %q, want proxy", session.Mode)
}
})
t.Run("incoming identity headers are replaced", func(t *testing.T) {
alice := signIn(t, "alice")
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, base+pathSession, http.NoBody)
if err != nil {
t.Fatalf("building the forged request: %v", err)
}
req.Header.Set("X-Forwarded-User", "mallory")
req.Header.Set("X-Forwarded-Groups", "guests")
req.Header.Set("X-Spinoza-Proxy-Secret", "not-the-shared-secret")
resp, requestErr := alice.Do(req)
if requestErr != nil {
t.Fatalf("sending the forged request: %v", requestErr)
}
defer func() { _ = resp.Body.Close() }()
var session api.Session
if decodeErr := json.NewDecoder(resp.Body).Decode(&session); decodeErr != nil {
t.Fatalf("decoding the session: %v", decodeErr)
}
if session.User != "alice" || session.Role != "admin" {
t.Fatalf("session = %+v, want alice's identity", session)
}
})
t.Run("a direct request without the proxy secret is refused", func(t *testing.T) {
out, err := maybeKubectl(t, "-n", "payments", "exec", "deployment/web", "--",
"wget", "-S", "-O", "/dev/null", "http://spinoza.spinoza.svc.cluster.local:8080/api/overview")
if err == nil {
t.Fatalf("an unsigned direct request succeeded:\n%s", out)
}
if !strings.Contains(out, "401 Unauthorized") {
t.Fatalf("direct request said %q, want 401 Unauthorized", truncate(out))
}
})
t.Run("websockets cross the proxy with the signed identity", func(t *testing.T) {
reply := subscribeUntilReady(t, signIn(t, "carol"), api.ClientMsg{Version: "v1", Resource: "pods"})
if reply.Type != "snapshot" {
t.Fatalf("feed said %q: %s", reply.Type, reply.Message)
}
if got := strings.Join(namespacesIn(reply.Rows), ","); got != "payments" {
t.Fatalf("rows came from %q, want payments alone", got)
}
})
t.Run("the proxy identity is used for kubernetes writes", func(t *testing.T) {
defer func() {
_, _ = maybeKubectl(t, "-n", "payments", "scale", "deployment/web", "--replicas=1")
}()
bob := signIn(t, "bob")
status, message := post(t, bob, scaleTo("payments", "web", 2))
if status != http.StatusOK {
t.Fatalf("scaling payments gave %d: %s", status, message)
}
status, message = post(t, bob, scaleTo("default", "other", 2))
if status != http.StatusForbidden {
t.Fatalf("scaling default gave %d: %s", status, message)
}
if !strings.Contains(messageOf(t, message), `User "bob"`) {
t.Fatalf("the cluster refused %q, want it to name bob", message)
}
})
t.Run("signing out clears the proxy session", func(t *testing.T) {
alice := signIn(t, "alice")
direct := *alice
direct.CheckRedirect = func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
resp := request(t, &direct, http.MethodPost, "/auth/logout")
location := resp.Header.Get("Location")
_ = resp.Body.Close()
if location != base+"/oauth2/sign_out" {
t.Fatalf("location = %q, want the proxy sign-out", location)
}
resp, err := direct.Get(location)
if err != nil {
t.Fatalf("following the proxy sign-out: %v", err)
}
_ = resp.Body.Close()
resp = request(t, &direct, http.MethodGet, "/oauth2/auth")
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("proxy auth status = %d, want %d", resp.StatusCode, http.StatusUnauthorized)
}
})
}
func TestBehindAnAuthProxy(t *testing.T) {
values := baseValues()
values["auth.mode"] = "proxy"
values["auth.proxy.sharedSecret"] = "a-cluster-mode-proxy-secret-that-is-long-enough"
values["auth.adminGroups[0]"] = "platform-admins"
values["auth.editorGroups[0]"] = "platform"
values["auth.proxy.logoutURL"] = "https://spinoza.localtest.me:8443/oauth2/sign_out"
deploy(t, values)
t.Run("a request the proxy did not sign is turned away", func(t *testing.T) {
status, _ := read(t, anonymous(t), "/api/overview")
if status != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", status, http.StatusUnauthorized)
}
})
t.Run("the headers the proxy sets decide who you are", func(t *testing.T) {
session := asProxied(t, "alice@example.com", "platform-admins")
if session.User != "alice@example.com" {
t.Fatalf("user = %q, want the header", session.User)
}
if session.Role != "admin" {
t.Fatalf("role = %q, want admin from the group list", session.Role)
}
if session.Mode != "proxy" {
t.Fatalf("mode = %q, want proxy", session.Mode)
}
})
t.Run("the cluster acts as the person the proxy named", func(t *testing.T) {
status, message := proxiedPost(t, "bob", "platform", scaleTo("default", "other", 2))
if status != http.StatusForbidden {
t.Fatalf("status = %d, want %d", status, http.StatusForbidden)
}
if !strings.Contains(messageOf(t, message), `User "bob"`) {
t.Fatalf("the cluster refused %q, want it to name bob", message)
}
})
t.Run("signing out hands the browser to the proxy", func(t *testing.T) {
resp := proxiedRequest(t, http.MethodPost, "/auth/logout", "alice@example.com", "platform-admins", false)
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusFound {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusFound)
}
if resp.Header.Get("Location") != "https://spinoza.localtest.me:8443/oauth2/sign_out" {
t.Fatalf("location = %q, want the proxy's sign-out", resp.Header.Get("Location"))
}
})
}
func TestWithNothingAskingPeopleToSignIn(t *testing.T) {
values := baseValues()
values["auth.mode"] = "none"
values["auth.allowAnonymous"] = "true"
deploy(t, values)
t.Run("anybody who reaches it is an admin", func(t *testing.T) {
session := whoami(t, anonymous(t))
if !session.Authenticated || session.Role != "admin" {
t.Fatalf("session = %+v, want an admin", session)
}
if session.SignIn {
t.Fatal("a spinoza with no provider offered a login")
}
})
t.Run("the cluster is read as spinoza's own account", func(t *testing.T) {
status, overview := read(t, anonymous(t), "/api/overview")
if status != http.StatusOK {
t.Fatalf("status = %d, want %d: %s", status, http.StatusOK, truncate(overview))
}
})
}
func TestWithImpersonationOff(t *testing.T) {
values := baseValues()
values["auth.mode"] = "proxy"
values["auth.proxy.sharedSecret"] = "a-cluster-mode-proxy-secret-that-is-long-enough"
values["impersonate"] = "false"
values["rbac.write"] = "true"
values["auth.adminGroups[0]"] = "platform"
deploy(t, values)
t.Run("a write nobody is bound for still goes through, as the pod", func(t *testing.T) {
status, message := proxiedPost(t, "bob", "platform", scaleTo("default", "other", 3))
if status != http.StatusOK {
t.Fatalf("status = %d, want %d: %s", status, http.StatusOK, message)
}
})
t.Run("every account reads the whole cluster, because the pod does", func(t *testing.T) {
session := asProxied(t, "carol", "guests")
if !session.Scope.Everywhere {
t.Fatalf("scope = %+v, want the pod's own reach", session.Scope)
}
})
}
func TestWithTheNarrowerReadRole(t *testing.T) {
values := baseValues()
values["auth.mode"] = "none"
values["auth.allowAnonymous"] = "true"
values["rbac.read"] = "workloads"
deploy(t, values)
t.Run("workloads still come back", func(t *testing.T) {
reply := subscribeUntilReady(t, anonymous(t), api.ClientMsg{Version: "v1", Resource: "pods"})
if reply.Type != "snapshot" {
t.Fatalf("feed said %q: %s", reply.Type, reply.Message)
}
if len(reply.Rows) == 0 {
t.Fatal("no pods came back")
}
})
t.Run("secrets do not", func(t *testing.T) {
out, _ := maybeKubectl(t, "auth", "can-i", "list", "secrets",
"--as", "system:serviceaccount:spinoza:spinoza")
if strings.TrimSpace(out) != "no" {
t.Fatalf("the narrower role can list secrets: %q", strings.TrimSpace(out))
}
})
}
func TestWhenTheProviderPublishesInternalEndpoints(t *testing.T) {
values := oidcValues()
values["auth.oidc.internalIssuerURL"] = shimRealm
deploy(t, values)
t.Run("the browser is still sent somewhere it can reach", func(t *testing.T) {
away := loginRedirect(t)
if !strings.HasPrefix(away, "https://keycloak.localtest.me:8443/") {
t.Fatalf("the browser was sent to %q, which it cannot reach", away)
}
})
t.Run("and the login still completes", func(t *testing.T) {
session := whoami(t, signIn(t, "alice"))
if !session.Authenticated || session.User != "alice" {
t.Fatalf("session = %+v, want alice signed in", session)
}
})
}