// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package mutation import ( "context" "encoding/json" "testing" "github.com/go-logr/logr" admissionv1 "k8s.io/api/admission/v1" authenticationv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/pkg/api/meta" "github.com/projectcapsule/capsule/pkg/api/rbac" "github.com/projectcapsule/capsule/pkg/runtime/configuration" capevents "github.com/projectcapsule/capsule/pkg/runtime/events" ) func TestNamespaceHandlerDoesNotInterceptUnlabelledAdministratorCreate(t *testing.T) { t.Parallel() ctx := context.Background() scheme := runtime.NewScheme() if err := corev1.AddToScheme(scheme); err != nil { t.Fatal(err) } if err := capsulev1beta2.AddToScheme(scheme); err != nil { t.Fatal(err) } const ( configurationName = "capsule" administratorName = "configured-administrator" ) configurationObject := &capsulev1beta2.CapsuleConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: configurationName}, Spec: capsulev1beta2.CapsuleConfigurationSpec{ Administrators: rbac.UserListSpec{{ Name: administratorName, Kind: rbac.UserOwner, }}, }, } cl := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(configurationObject). Build() cfg := configuration.NewCapsuleConfiguration(ctx, cl, cl, nil, configurationName) ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ Name: "unassigned", Labels: map[string]string{"example.com/label": "value"}, }} raw, err := json.Marshal(ns) if err != nil { t.Fatal(err) } response := NamespaceHandler(cfg).OnCreate( cl, cl, admission.NewDecoder(scheme), nil, )(ctx, admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ Object: runtime.RawExtension{Raw: raw}, UserInfo: authenticationv1.UserInfo{ Username: administratorName, }, }}) if response != nil { t.Fatalf("expected unlabelled administrator create not to be intercepted, got %#v", response) } } func TestNamespaceHandlerDoesNotInterceptFinalize(t *testing.T) { t.Parallel() scheme := runtime.NewScheme() if err := corev1.AddToScheme(scheme); err != nil { t.Fatal(err) } now := metav1.Now() oldNs := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "terminating", DeletionTimestamp: &now, }, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceTerminating}, } newNs := oldNs.DeepCopy() newNs.Spec.Finalizers = nil oldRaw, err := json.Marshal(oldNs) if err != nil { t.Fatal(err) } newRaw, err := json.Marshal(newNs) if err != nil { t.Fatal(err) } response := NamespaceHandler(nil).OnUpdate( nil, nil, admission.NewDecoder(scheme), nil, )(context.Background(), admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, SubResource: "finalize", Object: runtime.RawExtension{Raw: newRaw}, OldObject: runtime.RawExtension{Raw: oldRaw}, }}) if response != nil { t.Fatalf("finalize response = %#v, want no interception", response) } } func TestNamespaceHandlerRejectsTenantOwnerLabelMigrationWithEmptyOwnerReferences(t *testing.T) { t.Parallel() ctx := context.Background() scheme := testScheme(t) owner := rbac.CoreOwnerSpec{UserSpec: rbac.UserSpec{Name: "alice", Kind: rbac.UserOwner}} green := testTenant("green", "green-uid") green.Status.Owners = rbac.OwnerStatusListSpec{owner} blue := testTenant("blue", "blue-uid") configurationObject := &capsulev1beta2.CapsuleConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: "capsule"}, Status: capsulev1beta2.CapsuleConfigurationStatus{ Users: rbac.UserListSpec{owner.UserSpec}, }, } cl := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(configurationObject, green, blue). Build() cfg := configuration.NewCapsuleConfiguration(ctx, cl, cl, nil, configurationObject.Name) recorder := capevents.NewEventRecorder(nil, logr.Discard(), nil, nil) oldNs := testTenantNamespace("workloads", green) newNs := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ Name: oldNs.Name, Labels: map[string]string{meta.TenantLabel: blue.Name}, }} oldRaw, err := json.Marshal(oldNs) if err != nil { t.Fatal(err) } newRaw, err := json.Marshal(newNs) if err != nil { t.Fatal(err) } response := NamespaceHandler(cfg, OwnerReferenceHandler(cfg)).OnUpdate( cl, cl, admission.NewDecoder(scheme), recorder, )(ctx, admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ Object: runtime.RawExtension{Raw: newRaw}, OldObject: runtime.RawExtension{Raw: oldRaw}, UserInfo: authenticationv1.UserInfo{ Username: owner.Name, }, }}) if response == nil || response.Allowed { t.Fatalf("expected label migration patch to be denied, got %#v", response) } }