Ctrl K
ComunidadGuías…

test/clustermode/modes_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//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)
		}
	})
}