fix: Enforce namespace metadata validation for capsule users only (#2042)

Signed-off-by: Tom Stockwell <tstockwell@redhat.com>
This commit is contained in:
Tom Stockwell
2026-07-20 11:10:14 +02:00
committed by GitHub
parent 8e99f7c155
commit abff646dab
2 changed files with 81 additions and 4 deletions
+77
View File
@@ -12,6 +12,7 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
@@ -20,6 +21,8 @@ import (
)
var _ = Describe("creating a Namespace with user-specified labels and annotations", Ordered, Label("namespace", "metadata", "forbidden"), func() {
originConfig := &capsulev1beta2.CapsuleConfiguration{}
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-user-metadata-forbidden",
@@ -62,7 +65,14 @@ var _ = Describe("creating a Namespace with user-specified labels and annotation
},
}
admin := rbac.UserSpec{
Name: "admin",
Kind: "User",
}
JustBeforeEach(func() {
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
@@ -70,13 +80,28 @@ var _ = Describe("creating a Namespace with user-specified labels and annotation
}).Should(Succeed())
TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.Administrators = []rbac.UserSpec{admin}
})
})
JustAfterEach(func() {
EventuallyDeletion(tnt)
Eventually(func() error {
c := &capsulev1beta2.CapsuleConfiguration{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
return err
}
c.Spec = originConfig.Spec
return k8sClient.Update(context.Background(), c)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("should allow", func() {
ctx := context.TODO()
By("specifying non-forbidden labels", func() {
ns := NewNamespace("", map[string]string{
"bim": "baz",
@@ -114,6 +139,58 @@ var _ = Describe("creating a Namespace with user-specified labels and annotation
g.Expect(ns.GetAnnotations()).To(HaveKeyWithValue("managed.projectcapsule.dev/source", "capsule"))
}, defaultTimeoutInterval, time.Second).Should(Succeed())
})
By("creating a Namespace with forbidden labels as an admin", func() {
ns := NewNamespace("", map[string]string{
"foo": "bar",
"gatsby-custom": "value",
meta.TenantLabel: tnt.GetName(),
})
NamespaceCreation(ns, admin, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
})
By("creating a Namespace with forbidden annotations as an admin", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
ns.SetAnnotations(map[string]string{"foo": "bar", "gatsby-custom": "value"})
NamespaceCreation(ns, admin, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
})
By("updating a Namespace with a new forbidden label as an admin", func() {
ns := NewNamespace("admin-update-labels", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
original := ns.DeepCopy()
ns.Labels["foo"] = "bar"
ns.Labels["gatsby-custom"] = "value"
Eventually(func() error {
return impersonationClient(admin.Name, nil).Patch(ctx, ns, client.MergeFrom(original))
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("updating a Namespace with a new forbidden annotation as an admin", func() {
ns := NewNamespace("admin-update-annotations", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
original := ns.DeepCopy()
ns.SetAnnotations(map[string]string{"foo": "bar", "gatsby-custom": "value"})
Eventually(func() error {
return impersonationClient(admin.Name, nil).Patch(ctx, ns, client.MergeFrom(original))
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
It("should fail when creating a Namespace", func() {
@@ -30,14 +30,14 @@ func UserMetadataHandler() handlers.TypedHandlerWithTenantUser[*corev1.Namespace
func (h *userMetadataHandler) OnCreate(
_ client.Client,
_ client.Reader,
_ users.AdmissionUser,
user users.AdmissionUser,
ns *corev1.Namespace,
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
if tnt.Spec.NamespaceOptions != nil {
if tnt.Spec.NamespaceOptions != nil && user.IsCapsule() {
labels, annotations, err := userMetadataForValidation(ns, nil, tnt)
if err != nil {
return ad.ErroredResponse(err)
@@ -64,7 +64,7 @@ func (h *userMetadataHandler) OnCreate(
func (h *userMetadataHandler) OnUpdate(
_ client.Client,
_ client.Reader,
_ users.AdmissionUser,
user users.AdmissionUser,
newNs *corev1.Namespace,
oldNs *corev1.Namespace,
_ admission.Decoder,
@@ -111,7 +111,7 @@ func (h *userMetadataHandler) OnUpdate(
}
}
if tnt.Spec.NamespaceOptions != nil {
if tnt.Spec.NamespaceOptions != nil && user.IsCapsule() {
labels, annotations, err := userMetadataForValidation(newNs, oldNs, tnt)
if err != nil {
return ad.ErroredResponse(err)