internal/resources/authorization_test.go
Rama main · réplica de lectura del repositorio original.
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()))
}
}