Ctrl K
ComunidadGuías…

test/clustermode/impersonation_test.go

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

Archivo original ↗Preguntar a KiBot
//go:build clustermode

package clustermode

import (
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"testing"

	"github.com/sophotechlabs/spinoza/internal/api"
)

func scaleTo(namespace, name string, replicas int) string {
	query := url.Values{
		"action":    {"scale"},
		"group":     {"apps"},
		"version":   {"v1"},
		"resource":  {"deployments"},
		"namespace": {namespace},
		"name":      {name},
		"replicas":  {strconv.Itoa(replicas)},
	}
	return "/api/action?" + query.Encode()
}

func restart(namespace, name string) string {
	query := url.Values{
		"action":    {"restart"},
		"group":     {"apps"},
		"version":   {"v1"},
		"resource":  {"deployments"},
		"namespace": {namespace},
		"name":      {name},
	}
	return "/api/action?" + query.Encode()
}

func objectPath(namespace, name string) string {
	query := url.Values{
		"version":   {"v1"},
		"resource":  {"configmaps"},
		"namespace": {namespace},
		"name":      {name},
	}
	return "/api/object?" + query.Encode()
}

func logsFor(namespace, pod string) api.ClientMsg {
	return api.ClientMsg{
		Type:      "logs-subscribe",
		Namespace: namespace,
		Name:      pod,
		TailLines: 5,
	}
}

func TestEveryWayOfReachingTheClusterActsAsThePersonWhoAsked(t *testing.T) {
	values := oidcValues()
	values["nodeShell"] = "true"
	values["auth.adminGroups[1]"] = "platform"
	values["extraArgs[0]"] = "--pprof"
	deploy(t, values)

	defaultPod := podIn(t, "default", "app=other")

	t.Run("the service account itself may not do any of this", func(t *testing.T) {
		account := "system:serviceaccount:spinoza:spinoza"
		for _, verb := range []string{"create pods/exec", "create pods/portforward", "create pods"} {
			parts := strings.SplitN(verb, " ", 2)
			out, err := maybeKubectl(t, "auth", "can-i", parts[1], "-n", "payments", "--as", account)
			if err == nil && strings.TrimSpace(out) == "yes" {
				t.Fatalf("the service account may %s, so nothing below proves impersonation", verb)
			}
		}
	})

	t.Run("a write", func(t *testing.T) {
		defer func() {
			kubectl(t, "-n", "payments", "scale", "deployment/web", "--replicas=1")
			kubectl(t, "-n", "payments", "rollout", "status", "deployment/web", "--timeout=5m")
		}()
		bob := signIn(t, "bob")
		status, message := post(t, bob, scaleTo("payments", "web", 2))
		if status != http.StatusOK {
			t.Fatalf("scaling in its own namespace gave %d: %s", status, message)
		}
		status, message = post(t, bob, scaleTo("default", "other", 2))
		if status != http.StatusForbidden {
			t.Fatalf("scaling elsewhere 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("a restart", func(t *testing.T) {
		bob := signIn(t, "bob")
		status, message := post(t, bob, restart("payments", "web"))
		if status != http.StatusOK {
			t.Fatalf("restarting in its own namespace gave %d: %s", status, message)
		}
		kubectl(t, "-n", "payments", "rollout", "status", "deployment/web", "--timeout=5m")
		status, message = post(t, bob, restart("default", "other"))
		if status != http.StatusForbidden {
			t.Fatalf("restarting elsewhere 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("an apply and delete", func(t *testing.T) {
		name := "cluster-mode-edit"
		defer func() {
			_, _ = maybeKubectl(t, "-n", "payments", "delete", "configmap", name, "--ignore-not-found=true")
			_, _ = maybeKubectl(t, "-n", "default", "delete", "configmap", name, "--ignore-not-found=true")
		}()
		_, _ = maybeKubectl(t, "-n", "payments", "delete", "configmap", name, "--ignore-not-found=true")
		_, _ = maybeKubectl(t, "-n", "default", "delete", "configmap", name, "--ignore-not-found=true")
		kubectl(t, "-n", "payments", "create", "configmap", name, "--from-literal=message=before")
		kubectl(t, "-n", "default", "create", "configmap", name, "--from-literal=message=before")
		allowedResourceVersion := strings.TrimSpace(kubectl(t, "-n", "payments", "get", "configmap", name,
			"-o", "jsonpath={.metadata.resourceVersion}"))
		allowedUID := strings.TrimSpace(kubectl(t, "-n", "payments", "get", "configmap", name,
			"-o", "jsonpath={.metadata.uid}"))
		deniedResourceVersion := strings.TrimSpace(kubectl(t, "-n", "default", "get", "configmap", name,
			"-o", "jsonpath={.metadata.resourceVersion}"))
		bob := signIn(t, "bob")
		payload := fmt.Sprintf(`{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"%s","namespace":"payments","resourceVersion":"%s"},"data":{"message":"from-bob"}}`, name, allowedResourceVersion)
		status, message := put(t, bob, objectPath("payments", name), payload)
		if status != http.StatusOK {
			t.Fatalf("applying in its own namespace gave %d: %s", status, message)
		}
		stored := kubectl(t, "-n", "payments", "get", "configmap", name, "-o", "jsonpath={.data.message}")
		if stored != "from-bob" {
			t.Fatalf("stored message = %q, want from-bob", stored)
		}
		status, message = delete(t, bob, objectPath("payments", name)+"&uid="+url.QueryEscape(allowedUID))
		if status != http.StatusNoContent {
			t.Fatalf("deleting in its own namespace gave %d: %s", status, message)
		}
		deniedPayload := fmt.Sprintf(`{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"%s","namespace":"default","resourceVersion":"%s"},"data":{"message":"from-bob"}}`, name, deniedResourceVersion)
		status, message = put(t, bob, objectPath("default", name), deniedPayload)
		if status != http.StatusForbidden {
			t.Fatalf("applying elsewhere 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("a shell", func(t *testing.T) {
		paymentsPod := podIn(t, "payments", "app=web")
		opened := shell(t, signIn(t, "alice"), "/api/exec?namespace=payments&pod="+paymentsPod)
		if !strings.HasPrefix(opened, "OPENED") {
			t.Fatalf("alice could not open a shell: %s", opened)
		}
		refused := shell(t, signIn(t, "bob"), "/api/exec?namespace=default&pod="+defaultPod)
		if !strings.Contains(refused, "refused before the shell opened (403)") {
			t.Fatalf("the cluster refused %q, want an HTTP 403", refused)
		}
		if !strings.Contains(refused, "you may not create pods/exec here") {
			t.Fatalf("the cluster refused %q, want an exec authorization denial", refused)
		}
	})

	t.Run("a port forward, which would land on the server rather than on anyone", func(t *testing.T) {
		paymentsPod := podIn(t, "payments", "app=web")
		status, message := post(t, signIn(t, "alice"),
			"/api/portforward?kind=Pod&namespace=payments&name="+paymentsPod+"&port=80")
		if status != http.StatusForbidden {
			t.Fatalf("status = %d, want %d even for an admin", status, http.StatusForbidden)
		}
		if !strings.Contains(messageOf(t, message), "run it yourself") {
			t.Fatalf("body = %q, want it to say why", message)
		}
	})

	t.Run("a profile, which carries whatever the caches hold", func(t *testing.T) {
		refused, message := read(t, signIn(t, "carol"), "/debug/pprof/symbol")
		if refused != http.StatusForbidden {
			t.Fatalf("a viewer got %d from a profile: %s", refused, truncate(message))
		}
		if !strings.Contains(messageOf(t, message), "this needs admin") {
			t.Fatalf("body = %q, want it to name the role", message)
		}
		allowed, _ := read(t, signIn(t, "alice"), "/debug/pprof/symbol")
		if allowed != http.StatusOK {
			t.Fatalf("an admin got %d from a profile", allowed)
		}
	})

	t.Run("a debug container", func(t *testing.T) {
		bob := signIn(t, "bob")
		status, message := post(t, bob, "/api/debug?namespace=default&pod="+defaultPod)
		if status < 400 {
			t.Fatalf("bob started a debug container where he only reads: %d %s", status, message)
		}
		if !strings.Contains(messageOf(t, message), "bob") {
			t.Fatalf("kubectl refused %q, want it to name bob", message)
		}
	})

	t.Run("a node shell", func(t *testing.T) {
		refused := shell(t, signIn(t, "bob"), "/api/nodeshell?node="+aNode(t))
		if strings.HasPrefix(refused, "OPENED") {
			t.Fatal("bob opened a root shell on a node with no binding for it")
		}
		if !strings.Contains(refused, "refused before the shell opened (403)") {
			t.Fatalf("the cluster refused %q, want an HTTP 403", refused)
		}
		if !strings.Contains(refused, "you may not create pods in kube-system") {
			t.Fatalf("the cluster refused %q, want a node-shell authorization denial", refused)
		}
	})

	t.Run("logs", func(t *testing.T) {
		paymentsPod := podIn(t, "payments", "app=web")
		carol := signIn(t, "carol")
		reply := subscribe(t, carol, logsFor("default", defaultPod))
		if reply.Type != "error" {
			t.Fatalf("logs in another namespace returned %q, want an error", reply.Type)
		}
		if !strings.Contains(reply.Message, "you may not get pods/log here") {
			t.Fatalf("the cluster refused %q, want a log authorization denial", reply.Message)
		}
		mine := subscribe(t, carol, logsFor("payments", paymentsPod))
		if mine.Type == "error" {
			t.Fatalf("carol could not read logs in her own namespace: %s", mine.Message)
		}
	})

	t.Run("helm", func(t *testing.T) {
		installProbe(t, "default")
		defer removeProbe(t, "default")
		bob := signIn(t, "bob")
		status, message := post(t, bob, "/api/helm/action?action=uninstall&namespace=default&name=probe")
		if status < 400 {
			t.Fatalf("bob uninstalled a release where he only reads: %d %s", status, message)
		}
		if !strings.Contains(messageOf(t, message), "bob") {
			t.Fatalf("helm refused %q, want it to name bob", message)
		}

		installProbe(t, "payments")
		alice := signIn(t, "alice")
		status, message = post(t, alice, "/api/helm/action?action=uninstall&namespace=payments&name=probe")
		if status != http.StatusOK {
			t.Fatalf("alice could not uninstall a release: %d %s", status, message)
		}
	})

	t.Run("helm rollback and uninstall", func(t *testing.T) {
		installProbe(t, "payments")
		defer func() {
			removeProbe(t, "payments")
		}()
		run(t, "helm", "--kube-context", context1(t), "upgrade", "probe", "chart",
			"--namespace", "payments", "--set", "message=changed", "--wait", "--timeout", "2m")
		bob := signIn(t, "bob")
		status, message := post(t, bob,
			"/api/helm/action?action=rollback&revision=1&namespace=payments&name=probe")
		if status != http.StatusOK {
			t.Fatalf("bob could not roll back a release in payments: %d %s", status, message)
		}
		stored := kubectl(t, "-n", "payments", "get", "configmap", "probe", "-o", "jsonpath={.data.message}")
		if stored != "hello" {
			t.Fatalf("rolled back message = %q, want hello", stored)
		}
		status, message = post(t, bob,
			"/api/helm/action?action=uninstall&namespace=payments&name=probe")
		if status != http.StatusOK {
			t.Fatalf("bob could not uninstall a release in payments: %d %s", status, message)
		}
	})
}