Ctrl K
ComunidadGuías…

internal/resources/authorization_test.go

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

Archivo original ↗Preguntar a KiBot
package resources

import (
	"errors"
	"slices"
	"sync"
	"testing"

	authv1 "k8s.io/api/authorization/v1"
	"k8s.io/apimachinery/pkg/runtime"
	k8sfake "k8s.io/client-go/kubernetes/fake"
	k8stesting "k8s.io/client-go/testing"

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

type resourceAuthorizer struct {
	mu      sync.Mutex
	asked   []authv1.ResourceAttributes
	refused map[string]bool
}

func (a *resourceAuthorizer) answer(action k8stesting.Action) (bool, runtime.Object, error) {
	create, ok := action.(k8stesting.CreateAction)
	if !ok {
		return true, nil, errors.New("action is not a create")
	}
	review, ok := create.GetObject().(*authv1.SelfSubjectAccessReview)
	if !ok {
		return true, nil, errors.New("created object is not an access review")
	}
	attributes := *review.Spec.ResourceAttributes
	a.mu.Lock()
	a.asked = append(a.asked, attributes)
	denied := a.refused[attributes.Verb+" "+attributes.Namespace]
	a.mu.Unlock()
	review.Status.Allowed = !denied
	if denied {
		review.Status.Reason = "revoked"
	}
	return true, review, nil
}

func (a *resourceAuthorizer) questions() []authv1.ResourceAttributes {
	a.mu.Lock()
	defer a.mu.Unlock()
	return slices.Clone(a.asked)
}

func managerWithAuthorizer(rules *resourceAuthorizer) *Manager {
	cs := k8sfake.NewClientset()
	cs.PrependReactor("create", "selfsubjectaccessreviews", rules.answer)
	return &Manager{perms: access.New(cs)}
}

func TestManagerAuthorizationUsesCachedAndFreshReviews(t *testing.T) {
	rules := &resourceAuthorizer{refused: map[string]bool{}}
	mgr := managerWithAuthorizer(rules)
	ctx := auth.WithIdentity(t.Context(), auth.Identity{User: "alice"})
	check := access.Check{Verb: "get", Resource: "pods", Namespace: "prod"}

	if err := mgr.Authorize(ctx, check); err != nil {
		t.Fatalf("authorize: %v", err)
	}
	if err := mgr.Authorize(ctx, check); err != nil {
		t.Fatalf("cached authorize: %v", err)
	}
	if questions := rules.questions(); len(questions) != 1 {
		t.Fatalf("cached questions = %d, want 1", len(questions))
	}
	if err := mgr.Reauthorize(ctx, check); err != nil {
		t.Fatalf("reauthorize: %v", err)
	}
	if questions := rules.questions(); len(questions) != 2 {
		t.Fatalf("fresh questions = %d, want 2", len(questions))
	}
}

func TestManagerAuthorizationNeedsNoReviewerOutsideClusterMode(t *testing.T) {
	mgr := &Manager{}
	check := access.Check{Verb: "get", Resource: "pods", Namespace: "prod"}

	if err := mgr.Authorize(t.Context(), check); err != nil {
		t.Fatalf("authorize: %v", err)
	}
	if err := mgr.Reauthorize(t.Context(), check); err != nil {
		t.Fatalf("reauthorize: %v", err)
	}
}

func TestImpersonatedCountsNeedAMetadataClient(t *testing.T) {
	mgr := &Manager{}
	ctx := auth.WithIdentity(t.Context(), auth.Identity{User: "alice"})

	counts := mgr.Counts(ctx)

	if counts.Counts == nil || len(counts.Counts) != 0 {
		t.Fatalf("counts = %v, want a known empty result without a metadata client", counts.Counts)
	}
}

func securedDeploymentDescriptor() api.ResourceDescriptor {
	return api.ResourceDescriptor{
		Group:      "apps",
		Resource:   "deployments",
		Kind:       "Deployment",
		Namespaced: true,
	}
}

func TestSharedFeedAdmissionChecksExactListAndWatchAccess(t *testing.T) {
	rules := &resourceAuthorizer{refused: map[string]bool{}}
	mgr := managerWithAuthorizer(rules)
	ctx := auth.WithIdentity(t.Context(), auth.Identity{User: "alice"})
	checks, err := mgr.requireRead(ctx, everything(), securedDeploymentDescriptor(), "prod")
	if err != nil {
		t.Fatalf("require read: %v", err)
	}
	if len(checks) != 2 {
		t.Fatalf("checks = %+v", checks)
	}
	questions := rules.questions()
	if len(questions) != 2 {
		t.Fatalf("questions = %+v", questions)
	}
	for _, question := range questions {
		if question.Group != "apps" || question.Resource != "deployments" || question.Namespace != "prod" {
			t.Fatalf("question = %+v", question)
		}
	}
	verbs := []string{questions[0].Verb, questions[1].Verb}
	slices.Sort(verbs)
	if !slices.Equal(verbs, []string{"list", "watch"}) {
		t.Fatalf("verbs = %q", verbs)
	}
}

func TestNamespaceScopedFeedChecksEveryAdmittedNamespace(t *testing.T) {
	rules := &resourceAuthorizer{refused: map[string]bool{}}
	mgr := managerWithAuthorizer(rules)
	ctx := auth.WithIdentity(t.Context(), auth.Identity{User: "alice"})
	seen := filterFor(api.Scope{Namespaces: []string{"storefront", "payments"}})
	checks, err := mgr.requireRead(ctx, seen, securedDeploymentDescriptor(), "")
	if err != nil {
		t.Fatalf("require read: %v", err)
	}
	want := []access.Check{
		{Verb: "list", Group: "apps", Resource: "deployments", Namespace: "payments"},
		{Verb: "watch", Group: "apps", Resource: "deployments", Namespace: "payments"},
		{Verb: "list", Group: "apps", Resource: "deployments", Namespace: "storefront"},
		{Verb: "watch", Group: "apps", Resource: "deployments", Namespace: "storefront"},
	}
	if !slices.Equal(checks, want) {
		t.Fatalf("checks = %+v, want %+v", checks, want)
	}
}

func TestSharedFeedAdmissionFailsOnOneDeniedPermission(t *testing.T) {
	rules := &resourceAuthorizer{refused: map[string]bool{"watch prod": true}}
	mgr := managerWithAuthorizer(rules)
	ctx := auth.WithIdentity(t.Context(), auth.Identity{User: "alice"})
	_, err := mgr.requireRead(ctx, everything(), securedDeploymentDescriptor(), "prod")
	if !errors.Is(err, access.ErrDenied) {
		t.Fatalf("error = %v", err)
	}
	if err.Error() != "kubernetes authorization denied: revoked" {
		t.Fatalf("error = %q", err)
	}
}

func TestSubscriptionReauthorizationBypassesTheAdmissionCache(t *testing.T) {
	rules := &resourceAuthorizer{refused: map[string]bool{}}
	mgr := managerWithAuthorizer(rules)
	ctx := auth.WithIdentity(t.Context(), auth.Identity{User: "alice"})
	checks, err := mgr.requireRead(ctx, everything(), securedDeploymentDescriptor(), "prod")
	if err != nil {
		t.Fatalf("admission: %v", err)
	}
	rules.mu.Lock()
	rules.refused["watch prod"] = true
	rules.mu.Unlock()
	sub := &Subscription{owner: mgr, checks: checks}
	err = sub.Reauthorize(ctx)
	if !errors.Is(err, access.ErrDenied) {
		t.Fatalf("reauthorize error = %v", err)
	}
	if len(rules.questions()) != 4 {
		t.Fatalf("questions = %d, want admission and fresh reauthorization", len(rules.questions()))
	}
}