Files
capsule/e2e/config_namespace_tenant_assignment_test.go
T
Oliver BählerandGitHub e8ac32d364 feat: add administrator capabilities (#2049)
* feat: implement performance optimizations

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add administrator capabilities

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add administrator capabilities

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore

* feat: add administrator capabilities

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-23 00:09:23 +02:00

291 lines
9.9 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
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/utils"
)
var _ = Describe("Capsule administrators changing existing Namespace tenant assignment", Ordered, Serial,
Label("config", "namespace", "administrators", "assignment", "rolebindings"), func() {
administrator := rbac.UserSpec{
Name: "e2e-namespace-assignment-administrator",
Kind: rbac.UserOwner,
}
unprivilegedAdmin := rbac.UserSpec{
Name: "admin",
Kind: rbac.UserOwner,
}
ownerA := rbac.OwnerSpec{CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: "e2e-namespace-assignment-owner-a",
Kind: rbac.UserOwner,
},
ClusterRoles: []string{"admin"},
}}
ownerB := rbac.OwnerSpec{CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: "e2e-namespace-assignment-owner-b",
Kind: rbac.UserOwner,
},
ClusterRoles: []string{"view"},
}}
tenantA := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-namespace-assignment-a",
Labels: map[string]string{"env": "e2e"},
},
Spec: capsulev1beta2.TenantSpec{Owners: rbac.OwnerListSpec{ownerA}},
}
tenantB := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-namespace-assignment-b",
Labels: map[string]string{"env": "e2e"},
},
Spec: capsulev1beta2.TenantSpec{Owners: rbac.OwnerListSpec{ownerB}},
}
bindingA := ownerA.CoreOwnerSpec.ToAdditionalRolebindings()[0]
bindingB := ownerB.CoreOwnerSpec.ToAdditionalRolebindings()[0]
var originalConfigurationSpec *capsulev1beta2.CapsuleConfigurationSpec
BeforeAll(func() {
configuration := &capsulev1beta2.CapsuleConfiguration{}
Expect(k8sClient.Get(
context.Background(),
client.ObjectKey{Name: defaultConfigurationName},
configuration,
)).To(Succeed())
originalConfigurationSpec = configuration.Spec.DeepCopy()
})
JustBeforeEach(func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.Administrators = rbac.UserListSpec{administrator}
})
for _, tnt := range []*capsulev1beta2.Tenant{tenantA, tenantB} {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.Background(), tnt)
}).Should(Succeed())
TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)
}
})
JustAfterEach(func() {
EventuallyDeletion(tenantA)
EventuallyDeletion(tenantB)
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec = *originalConfigurationSpec
})
})
newUnassignedNamespace := func() *corev1.Namespace {
ns := NewNamespace("")
NamespaceCreation(ns, administrator, defaultTimeoutInterval).Should(Succeed())
DeferCleanup(func() { EventuallyDeletion(ns) })
ExpectNamespaceNotAssignedToTenant(context.Background(), ns.Name)
return ns
}
patchAssignment := func(
ns *corev1.Namespace,
tnt *capsulev1beta2.Tenant,
cs kubernetes.Interface,
) error {
return PatchTenantAssignmentForNamespace(tnt, ns, cs)
}
patchDetachment := func(ns *corev1.Namespace, cs kubernetes.Interface) error {
return PatchNamespace(ns, cs, map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
meta.TenantLabel: nil,
},
"ownerReferences": []interface{}{},
},
})
}
expectAssignment := func(
ns *corev1.Namespace,
assigned, unassigned *capsulev1beta2.Tenant,
) {
Eventually(func(g Gomega) {
current := &corev1.Namespace{}
g.Expect(k8sClient.Get(
context.Background(),
types.NamespacedName{Name: ns.Name},
current,
)).To(Succeed())
g.Expect(current.Labels).To(HaveKeyWithValue(meta.TenantLabel, assigned.Name))
g.Expect(tenantOwnerReferences(current)).To(Equal([]string{assigned.Name}))
g.Expect(hasTenantOwnerReferenceByNameAndUID(current, assigned.Name, assigned.UID)).To(BeTrue())
g.Expect(hasTenantOwnerReferenceByNameAndUID(current, unassigned.Name, unassigned.UID)).To(BeFalse())
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
NamespaceIsPartOfTenant(assigned, ns).Should(Succeed())
NamespaceIsNotPartOfTenant(unassigned, ns).Should(Succeed())
}
expectRoleBindings := func(
ns *corev1.Namespace,
present map[*capsulev1beta2.Tenant]rbac.AdditionalRoleBindingsSpec,
absent ...rbac.AdditionalRoleBindingsSpec,
) {
Eventually(func(g Gomega) {
for tnt, binding := range present {
name := meta.NameForManagedRoleBindings(utils.RoleBindingHashFunc(binding))
roleBinding := &rbacv1.RoleBinding{}
err := k8sClient.Get(context.Background(), client.ObjectKey{
Namespace: ns.Name,
Name: name,
}, roleBinding)
g.Expect(err).NotTo(HaveOccurred(),
"expected managed RoleBinding %s/%s for Tenant %s", ns.Name, name, tnt.Name)
g.Expect(roleBinding.RoleRef).To(Equal(rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: binding.ClusterRoleName,
}))
g.Expect(roleBinding.Subjects).To(ConsistOf(binding.Subjects))
g.Expect(roleBinding.Labels).To(HaveKeyWithValue(meta.NewTenantLabel, tnt.Name))
g.Expect(roleBinding.Labels).To(HaveKeyWithValue(
meta.NewManagedByCapsuleLabel,
meta.ValueController,
))
}
for _, binding := range absent {
name := meta.NameForManagedRoleBindings(utils.RoleBindingHashFunc(binding))
roleBinding := &rbacv1.RoleBinding{}
err := k8sClient.Get(context.Background(), client.ObjectKey{
Namespace: ns.Name,
Name: name,
}, roleBinding)
g.Expect(apierrors.IsNotFound(err)).To(BeTrue(),
"expected managed RoleBinding %s/%s to be absent, got %v", ns.Name, name, err)
}
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
expectUnassigned := func(ns *corev1.Namespace) {
ExpectNamespaceNotAssignedToTenant(context.Background(), ns.Name)
NamespaceIsNotPartOfTenant(tenantA, ns).Should(Succeed())
NamespaceIsNotPartOfTenant(tenantB, ns).Should(Succeed())
}
It("allows a configured administrator to join, migrate, and unjoin an existing Namespace", func() {
ns := newUnassignedNamespace()
adminClient := ownerClient(administrator)
By("starting without either Tenant's managed RoleBinding")
expectRoleBindings(ns, nil, bindingA, bindingB)
By("joining the existing Namespace to the first Tenant")
Eventually(func() error {
return patchAssignment(ns, tenantA, adminClient)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
expectAssignment(ns, tenantA, tenantB)
expectRoleBindings(ns, map[*capsulev1beta2.Tenant]rbac.AdditionalRoleBindingsSpec{
tenantA: bindingA,
}, bindingB)
By("migrating the Namespace from the first Tenant to the second Tenant")
Eventually(func() error {
return patchAssignment(ns, tenantB, adminClient)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
expectAssignment(ns, tenantB, tenantA)
expectRoleBindings(ns, map[*capsulev1beta2.Tenant]rbac.AdditionalRoleBindingsSpec{
tenantB: bindingB,
}, bindingA)
By("unjoining the Namespace from the second Tenant")
Eventually(func() error {
return patchDetachment(ns, adminClient)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
expectUnassigned(ns)
expectRoleBindings(ns, nil, bindingA, bindingB)
})
It("rejects join, migration, and unjoin attempts from other users", func() {
actors := []struct {
name string
user rbac.UserSpec
}{
{name: "an unconfigured admin user", user: unprivilegedAdmin},
{name: "a Tenant owner", user: ownerA.UserSpec},
}
for _, actor := range actors {
By(fmt.Sprintf("checking all assignment transitions for %s", actor.name))
ns := newUnassignedNamespace()
actorClient := ownerClient(actor.user)
adminClient := ownerClient(administrator)
expectRoleBindings(ns, nil, bindingA, bindingB)
By(fmt.Sprintf("rejecting a join by %s", actor.name))
Expect(patchAssignment(ns, tenantA, actorClient)).To(HaveOccurred())
expectUnassigned(ns)
expectRoleBindings(ns, nil, bindingA, bindingB)
By("preparing an administrator-owned transition baseline")
Eventually(func() error {
return patchAssignment(ns, tenantA, adminClient)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
expectAssignment(ns, tenantA, tenantB)
expectRoleBindings(ns, map[*capsulev1beta2.Tenant]rbac.AdditionalRoleBindingsSpec{
tenantA: bindingA,
}, bindingB)
By(fmt.Sprintf("rejecting a migration by %s", actor.name))
Expect(patchAssignment(ns, tenantB, actorClient)).To(HaveOccurred())
expectAssignment(ns, tenantA, tenantB)
expectRoleBindings(ns, map[*capsulev1beta2.Tenant]rbac.AdditionalRoleBindingsSpec{
tenantA: bindingA,
}, bindingB)
By(fmt.Sprintf("rejecting an unjoin by %s", actor.name))
Expect(patchDetachment(ns, actorClient)).To(HaveOccurred())
expectAssignment(ns, tenantA, tenantB)
expectRoleBindings(ns, map[*capsulev1beta2.Tenant]rbac.AdditionalRoleBindingsSpec{
tenantA: bindingA,
}, bindingB)
By("detaching with the configured administrator before cleanup")
Eventually(func() error {
return patchDetachment(ns, adminClient)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
expectUnassigned(ns)
expectRoleBindings(ns, nil, bindingA, bindingB)
}
})
})