mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
* 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>
1614 lines
45 KiB
Go
1614 lines
45 KiB
Go
// Copyright 2020-2023 Project Capsule Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
|
|
. "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/apiserver/pkg/authentication/serviceaccount"
|
|
"k8s.io/utils/ptr"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
|
clt "github.com/projectcapsule/capsule/pkg/runtime/client"
|
|
"github.com/projectcapsule/capsule/pkg/tenant"
|
|
)
|
|
|
|
var _ = Describe("creating several Namespaces for a Tenant", Ordered, Label("config", "namespace", "hijack"), func() {
|
|
administrator := rbac.UserSpec{
|
|
Name: "e2e-namespace-hijacking-admin",
|
|
Kind: rbac.UserOwner,
|
|
}
|
|
|
|
var originalConfigurationSpec *capsulev1beta2.CapsuleConfigurationSpec
|
|
|
|
t1 := &capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "e2e-ns-attack-1",
|
|
Labels: map[string]string{
|
|
"env": "e2e",
|
|
},
|
|
},
|
|
Spec: capsulev1beta2.TenantSpec{
|
|
Owners: rbac.OwnerListSpec{
|
|
{
|
|
CoreOwnerSpec: rbac.CoreOwnerSpec{
|
|
UserSpec: rbac.UserSpec{
|
|
Name: "gatsby",
|
|
Kind: "User",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
CoreOwnerSpec: rbac.CoreOwnerSpec{
|
|
UserSpec: rbac.UserSpec{
|
|
Name: "oidc:group",
|
|
Kind: "Group",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
CoreOwnerSpec: rbac.CoreOwnerSpec{
|
|
UserSpec: rbac.UserSpec{
|
|
Kind: "ServiceAccount",
|
|
Name: "system:serviceaccount:attacker-system:attacker",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t2 := &capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "e2e-ns-attack-2",
|
|
Labels: map[string]string{
|
|
"env": "e2e",
|
|
},
|
|
},
|
|
Spec: capsulev1beta2.TenantSpec{
|
|
Owners: rbac.OwnerListSpec{
|
|
{
|
|
CoreOwnerSpec: rbac.CoreOwnerSpec{
|
|
UserSpec: rbac.UserSpec{
|
|
Name: "gatsby",
|
|
Kind: "User",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t3 := &capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "e2e-ns-attack-3",
|
|
Labels: map[string]string{"env": "e2e"},
|
|
},
|
|
Spec: capsulev1beta2.TenantSpec{
|
|
Owners: rbac.OwnerListSpec{
|
|
{
|
|
CoreOwnerSpec: rbac.CoreOwnerSpec{
|
|
UserSpec: rbac.UserSpec{
|
|
Name: "different-owner",
|
|
Kind: "User",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
grantNamespaceSubresourceUpdate := func(name string, subject rbacv1.Subject) {
|
|
clusterRole := &rbacv1.ClusterRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Rules: []rbacv1.PolicyRule{
|
|
{
|
|
APIGroups: []string{""},
|
|
Resources: []string{
|
|
"namespaces/status",
|
|
"namespaces/finalize",
|
|
},
|
|
Verbs: []string{
|
|
"get",
|
|
"update",
|
|
"patch",
|
|
},
|
|
},
|
|
{
|
|
APIGroups: []string{""},
|
|
Resources: []string{
|
|
"namespaces",
|
|
},
|
|
Verbs: []string{
|
|
"get",
|
|
"update",
|
|
"patch",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Subjects: []rbacv1.Subject{
|
|
subject,
|
|
},
|
|
RoleRef: rbacv1.RoleRef{
|
|
APIGroup: rbacv1.GroupName,
|
|
Kind: "ClusterRole",
|
|
Name: name,
|
|
},
|
|
}
|
|
|
|
EventuallyCreation(func() error {
|
|
return k8sClient.Create(context.TODO(), clusterRole)
|
|
}).Should(Succeed())
|
|
|
|
EventuallyCreation(func() error {
|
|
return k8sClient.Create(context.TODO(), clusterRoleBinding)
|
|
}).Should(Succeed())
|
|
}
|
|
|
|
cleanupNamespaceSubresourceGrant := func(name string) {
|
|
Eventually(func() error {
|
|
return k8sClient.Delete(context.TODO(), &rbacv1.ClusterRoleBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
})
|
|
}).Should(SatisfyAny(Succeed(), WithTransform(apierrors.IsNotFound, BeTrue())))
|
|
|
|
Eventually(func() error {
|
|
return k8sClient.Delete(context.TODO(), &rbacv1.ClusterRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
})
|
|
}).Should(SatisfyAny(Succeed(), WithTransform(apierrors.IsNotFound, BeTrue())))
|
|
}
|
|
|
|
expectNoTenantHijackPersisted := func(nsName string, originalTenant, attackerTenant *capsulev1beta2.Tenant) {
|
|
Eventually(func(g Gomega) {
|
|
current := &corev1.Namespace{}
|
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: nsName}, current)).Should(Succeed())
|
|
|
|
g.Expect(current.Labels).To(HaveKeyWithValue(meta.TenantLabel, originalTenant.GetName()))
|
|
g.Expect(current.Labels).NotTo(HaveKeyWithValue(meta.TenantLabel, attackerTenant.GetName()))
|
|
|
|
g.Expect(hasTenantOwnerReferenceByNameAndUID(current, originalTenant.GetName(), originalTenant.GetUID())).
|
|
To(BeTrue(), "namespace should keep original tenant ownerReference")
|
|
|
|
g.Expect(hasTenantOwnerReferenceByNameAndUID(current, attackerTenant.GetName(), attackerTenant.GetUID())).
|
|
To(BeFalse(), "namespace must not gain attacker tenant ownerReference")
|
|
}).Should(Succeed())
|
|
}
|
|
|
|
kubeSystem := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "kube-system",
|
|
},
|
|
}
|
|
|
|
getTenant := func(name string) *capsulev1beta2.Tenant {
|
|
tenant := &capsulev1beta2.Tenant{}
|
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: name}, tenant)).Should(Succeed())
|
|
|
|
return tenant
|
|
}
|
|
|
|
getNamespace := func(name string) *corev1.Namespace {
|
|
ns := &corev1.Namespace{}
|
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: name}, ns)).Should(Succeed())
|
|
|
|
return ns
|
|
}
|
|
|
|
hasTenantOwnerReference := func(ns *corev1.Namespace, tenant *capsulev1beta2.Tenant) bool {
|
|
for _, ownerRef := range ns.OwnerReferences {
|
|
if ownerRef.APIVersion == capsulev1beta2.GroupVersion.String() &&
|
|
ownerRef.Kind == "Tenant" &&
|
|
ownerRef.Name == tenant.GetName() &&
|
|
ownerRef.UID == tenant.GetUID() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
hasTenantOwnerReferenceByNameAndUID := func(ns *corev1.Namespace, name string, uid types.UID) bool {
|
|
for _, ownerRef := range ns.OwnerReferences {
|
|
if ownerRef.APIVersion == capsulev1beta2.GroupVersion.String() &&
|
|
ownerRef.Kind == "Tenant" &&
|
|
ownerRef.Name == name &&
|
|
ownerRef.UID == uid {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
expectOriginalTenantOwnership := func(nsName string, tenant *capsulev1beta2.Tenant) {
|
|
retrievedNs := getNamespace(nsName)
|
|
|
|
Expect(retrievedNs.Labels).To(HaveKeyWithValue(meta.TenantLabel, tenant.GetName()))
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenant)).To(BeTrue(), "Namespace should keep original Tenant ownerReference")
|
|
}
|
|
|
|
expectNoTenantOwnership := func(nsName string, tenant *capsulev1beta2.Tenant) {
|
|
retrievedNs := getNamespace(nsName)
|
|
|
|
Expect(retrievedNs.Labels).NotTo(HaveKeyWithValue(meta.TenantLabel, tenant.GetName()))
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenant)).To(BeFalse(), "Namespace should not have Tenant ownerReference")
|
|
}
|
|
|
|
randomTenantReference := func() (string, types.UID) {
|
|
return fmt.Sprintf("random-tenant-%d", rand.Int()), types.UID(fmt.Sprintf("%d", rand.Int()))
|
|
}
|
|
|
|
createUnmanagedNamespace := func() *corev1.Namespace {
|
|
ns := NewNamespace("")
|
|
Expect(k8sClient.Create(context.TODO(), ns)).To(Succeed())
|
|
DeferCleanup(func() { EventuallyDeletion(ns) })
|
|
|
|
return ns
|
|
}
|
|
|
|
waitForTenantNamespacesDeletion := func(tenantNames ...string) {
|
|
names := make(map[string]struct{}, len(tenantNames))
|
|
for _, name := range tenantNames {
|
|
names[name] = struct{}{}
|
|
}
|
|
|
|
Eventually(func(g Gomega) {
|
|
list := &corev1.NamespaceList{}
|
|
g.Expect(k8sClient.List(context.TODO(), list)).To(Succeed())
|
|
|
|
for i := range list.Items {
|
|
for _, ref := range tenant.TenantOwnerReferences(&list.Items[i]) {
|
|
_, belongsToTestTenant := names[ref.Name]
|
|
g.Expect(belongsToTestTenant).To(
|
|
BeFalse(),
|
|
"namespace %q still references a previous incarnation of Tenant %q (UID %q)",
|
|
list.Items[i].Name,
|
|
ref.Name,
|
|
ref.UID,
|
|
)
|
|
}
|
|
}
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
}
|
|
|
|
BeforeAll(func() {
|
|
configuration := &capsulev1beta2.CapsuleConfiguration{}
|
|
Expect(k8sClient.Get(
|
|
context.Background(),
|
|
types.NamespacedName{Name: defaultConfigurationName},
|
|
configuration,
|
|
)).To(Succeed())
|
|
|
|
originalConfigurationSpec = configuration.Spec.DeepCopy()
|
|
|
|
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
|
|
configuration.Spec.Administrators = append(configuration.Spec.Administrators, administrator)
|
|
})
|
|
})
|
|
|
|
AfterAll(func() {
|
|
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
|
|
configuration.Spec = *originalConfigurationSpec
|
|
})
|
|
})
|
|
|
|
JustBeforeEach(func() {
|
|
waitForTenantNamespacesDeletion(t1.Name, t2.Name, t3.Name)
|
|
|
|
EventuallyCreation(func() error {
|
|
t1.ResourceVersion = ""
|
|
|
|
return k8sClient.Create(context.TODO(), t1)
|
|
}).Should(Succeed())
|
|
TenantReady(t1, metav1.ConditionTrue, defaultTimeoutInterval)
|
|
|
|
EventuallyCreation(func() error {
|
|
t2.ResourceVersion = ""
|
|
|
|
return k8sClient.Create(context.TODO(), t2)
|
|
}).Should(Succeed())
|
|
TenantReady(t2, metav1.ConditionTrue, defaultTimeoutInterval)
|
|
|
|
EventuallyCreation(func() error {
|
|
t3.ResourceVersion = ""
|
|
|
|
return k8sClient.Create(context.TODO(), t3)
|
|
}).Should(Succeed())
|
|
TenantReady(t3, metav1.ConditionTrue, defaultTimeoutInterval)
|
|
|
|
})
|
|
|
|
JustAfterEach(func() {
|
|
EventuallyDeletion(t1)
|
|
EventuallyDeletion(t2)
|
|
EventuallyDeletion(t3)
|
|
waitForTenantNamespacesDeletion(t1.Name, t2.Name, t3.Name)
|
|
})
|
|
|
|
It("Owners can not hijack Tenant ownership through namespaces/status", func() {
|
|
tenantA := getTenant(t1.Name)
|
|
tenantB := getTenant(t2.Name)
|
|
|
|
owner := t1.Spec.Owners[0].UserSpec
|
|
cs := ownerClient(owner)
|
|
|
|
grantName := "e2e-ns-status-hijack"
|
|
grantNamespaceSubresourceUpdate(grantName, rbacv1.Subject{
|
|
APIGroup: rbacv1.GroupName,
|
|
Kind: rbacv1.UserKind,
|
|
Name: owner.Name,
|
|
})
|
|
DeferCleanup(func() {
|
|
cleanupNamespaceSubresourceGrant(grantName)
|
|
})
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenantA.GetName(),
|
|
})
|
|
|
|
NamespaceCreation(ns, owner, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
current, err := cs.CoreV1().Namespaces().Get(
|
|
context.TODO(),
|
|
ns.Name,
|
|
metav1.GetOptions{},
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
hijacked := current.DeepCopy()
|
|
|
|
if hijacked.Labels == nil {
|
|
hijacked.Labels = map[string]string{}
|
|
}
|
|
|
|
hijacked.Labels[meta.TenantLabel] = tenantB.GetName()
|
|
|
|
replaced := false
|
|
for i := range hijacked.OwnerReferences {
|
|
ref := &hijacked.OwnerReferences[i]
|
|
|
|
if ref.APIVersion != capsulev1beta2.GroupVersion.String() {
|
|
continue
|
|
}
|
|
|
|
if ref.Kind != "Tenant" {
|
|
continue
|
|
}
|
|
|
|
if ref.Name != tenantA.GetName() {
|
|
continue
|
|
}
|
|
|
|
if ref.UID != tenantA.GetUID() {
|
|
continue
|
|
}
|
|
|
|
ref.Name = tenantB.GetName()
|
|
ref.UID = tenantB.GetUID()
|
|
replaced = true
|
|
|
|
break
|
|
}
|
|
|
|
Expect(replaced).To(BeTrue(), "expected test namespace to contain Tenant ownerReference for %q", tenantA.GetName())
|
|
|
|
Expect(hasTenantOwnerReferenceByNameAndUID(hijacked, tenantA.GetName(), tenantA.GetUID())).
|
|
To(BeFalse(), "hijack payload should no longer contain original Tenant ownerReference")
|
|
Expect(hasTenantOwnerReferenceByNameAndUID(hijacked, tenantB.GetName(), tenantB.GetUID())).
|
|
To(BeTrue(), "hijack payload should contain attacker Tenant ownerReference")
|
|
|
|
_, err = cs.CoreV1().Namespaces().UpdateStatus(
|
|
context.TODO(),
|
|
hijacked,
|
|
metav1.UpdateOptions{},
|
|
)
|
|
|
|
if err != nil {
|
|
By(fmt.Sprintf("namespaces/status hijack attempt was rejected: %v", err))
|
|
}
|
|
|
|
expectNoTenantHijackPersisted(ns.Name, tenantA, tenantB)
|
|
})
|
|
It("Owners can not hijack Tenant ownership through namespaces/finalize", func() {
|
|
tenantA := getTenant(t1.Name)
|
|
tenantB := getTenant(t2.Name)
|
|
|
|
owner := t1.Spec.Owners[0].UserSpec
|
|
cs := ownerClient(owner)
|
|
|
|
grantName := "e2e-ns-finalize-hijack"
|
|
grantNamespaceSubresourceUpdate(grantName, rbacv1.Subject{
|
|
APIGroup: rbacv1.GroupName,
|
|
Kind: rbacv1.UserKind,
|
|
Name: owner.Name,
|
|
})
|
|
DeferCleanup(func() {
|
|
cleanupNamespaceSubresourceGrant(grantName)
|
|
})
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenantA.GetName(),
|
|
})
|
|
|
|
NamespaceCreation(ns, owner, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
current, err := cs.CoreV1().Namespaces().Get(
|
|
context.TODO(),
|
|
ns.Name,
|
|
metav1.GetOptions{},
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
hijacked := current.DeepCopy()
|
|
|
|
if hijacked.Labels == nil {
|
|
hijacked.Labels = map[string]string{}
|
|
}
|
|
|
|
hijacked.Labels[meta.TenantLabel] = tenantB.GetName()
|
|
|
|
replaced := false
|
|
for i := range hijacked.OwnerReferences {
|
|
ref := &hijacked.OwnerReferences[i]
|
|
|
|
if ref.APIVersion != capsulev1beta2.GroupVersion.String() {
|
|
continue
|
|
}
|
|
|
|
if ref.Kind != "Tenant" {
|
|
continue
|
|
}
|
|
|
|
if ref.Name != tenantA.GetName() {
|
|
continue
|
|
}
|
|
|
|
if ref.UID != tenantA.GetUID() {
|
|
continue
|
|
}
|
|
|
|
ref.Name = tenantB.GetName()
|
|
ref.UID = tenantB.GetUID()
|
|
replaced = true
|
|
|
|
break
|
|
}
|
|
|
|
Expect(replaced).To(BeTrue(), "expected test namespace to contain Tenant ownerReference for %q", tenantA.GetName())
|
|
|
|
Expect(hasTenantOwnerReferenceByNameAndUID(hijacked, tenantA.GetName(), tenantA.GetUID())).
|
|
To(BeFalse(), "hijack payload should no longer contain original Tenant ownerReference")
|
|
Expect(hasTenantOwnerReferenceByNameAndUID(hijacked, tenantB.GetName(), tenantB.GetUID())).
|
|
To(BeTrue(), "hijack payload should contain attacker Tenant ownerReference")
|
|
|
|
_, err = cs.CoreV1().Namespaces().Finalize(
|
|
context.TODO(),
|
|
hijacked,
|
|
metav1.UpdateOptions{},
|
|
)
|
|
|
|
if err != nil {
|
|
By(fmt.Sprintf("namespaces/finalize hijack attempt was rejected: %v", err))
|
|
}
|
|
|
|
expectNoTenantHijackPersisted(ns.Name, tenantA, tenantB)
|
|
})
|
|
|
|
It("Owners can not add a second Tenant ownerReference to a managed namespace", func() {
|
|
tenantA := getTenant(t1.Name)
|
|
tenantB := getTenant(t2.Name)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenantA.GetName(),
|
|
})
|
|
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
current, err := cs.CoreV1().Namespaces().Get(
|
|
context.TODO(),
|
|
ns.Name,
|
|
metav1.GetOptions{},
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
current.OwnerReferences = append(current.OwnerReferences, metav1.OwnerReference{
|
|
APIVersion: capsulev1beta2.GroupVersion.String(),
|
|
Kind: "Tenant",
|
|
Name: tenantB.GetName(),
|
|
UID: tenantB.GetUID(),
|
|
})
|
|
|
|
_, err = cs.CoreV1().Namespaces().Update(
|
|
context.TODO(),
|
|
current,
|
|
metav1.UpdateOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
Eventually(func(g Gomega) {
|
|
updated := &corev1.Namespace{}
|
|
|
|
err := k8sClient.Get(
|
|
context.TODO(),
|
|
types.NamespacedName{Name: ns.Name},
|
|
updated,
|
|
)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
g.Expect(tenantOwnerReferences(updated)).To(Equal([]string{tenantA.GetName()}))
|
|
g.Expect(updated.Labels).To(HaveKeyWithValue(meta.TenantLabel, tenantA.GetName()))
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
}
|
|
})
|
|
|
|
It("Owners can not hijack unmanaged namespaces with multiple Tenant ownerReferences", func() {
|
|
tenantA := getTenant(t1.Name)
|
|
tenantB := getTenant(t2.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(`{
|
|
"metadata": {
|
|
"ownerReferences": [
|
|
{
|
|
"apiVersion": "%s",
|
|
"kind": "Tenant",
|
|
"name": "%s",
|
|
"uid": "%s"
|
|
},
|
|
{
|
|
"apiVersion": "%s",
|
|
"kind": "Tenant",
|
|
"name": "%s",
|
|
"uid": "%s"
|
|
}
|
|
]
|
|
}
|
|
}`,
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenantA.GetName(),
|
|
tenantA.GetUID(),
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenantB.GetName(),
|
|
tenantB.GetUID(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
Eventually(func(g Gomega) {
|
|
updated := &corev1.Namespace{}
|
|
|
|
err := k8sClient.Get(
|
|
context.TODO(),
|
|
types.NamespacedName{Name: unmanaged.Name},
|
|
updated,
|
|
)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
g.Expect(tenantOwnerReferences(updated)).To(BeEmpty())
|
|
g.Expect(updated.Labels).ToNot(HaveKey(meta.TenantLabel))
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
}
|
|
})
|
|
|
|
It("Tenant A owners can not adopt unmanaged namespaces into Tenant B", func() {
|
|
tenantB := getTenant(t3.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"},"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
meta.TenantLabel,
|
|
tenantB.GetName(),
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenantB.GetName(),
|
|
tenantB.GetUID(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenantB)
|
|
}
|
|
})
|
|
|
|
It("Owners can not hijack unmanaged namespaces with controller ownerReference flags", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(`{
|
|
"metadata":{
|
|
"ownerReferences":[{
|
|
"apiVersion":"%s",
|
|
"kind":"Tenant",
|
|
"name":"%s",
|
|
"uid":"%s",
|
|
"controller":true,
|
|
"blockOwnerDeletion":true
|
|
}]
|
|
}
|
|
}`, capsulev1beta2.GroupVersion.String(), tenant.GetName(), tenant.GetUID()))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not smuggle Tenant ownerReference beside unrelated ownerReferences", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(`{
|
|
"metadata":{
|
|
"ownerReferences":[
|
|
{"apiVersion":"v1","kind":"ConfigMap","name":"dummy","uid":"%s"},
|
|
{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}
|
|
]
|
|
}
|
|
}`, types.UID("12345"), capsulev1beta2.GroupVersion.String(), tenant.GetName(), tenant.GetUID()))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not create an ownership gap then patch managed namespace metadata", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{meta.TenantLabel: tenant.GetName()})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
remove := []byte(fmt.Sprintf(`{"metadata":{"labels":{"%s":null}}}`, meta.TenantLabel))
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
ns.Name,
|
|
types.StrategicMergePatchType,
|
|
remove,
|
|
metav1.PatchOptions{},
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
|
|
patch := []byte(`{"metadata":{"labels":{"attacker.example.com/touched":"true"}}}`)
|
|
_, err = cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
ns.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Can't hijack offlimits namespace (Ownerreferences)", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: kubeSystem.GetName()}, kubeSystem)).Should(Succeed())
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenant.GetName(),
|
|
tenant.GetUID(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), kubeSystem.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
|
|
Expect(err).To(HaveOccurred())
|
|
}
|
|
})
|
|
|
|
It("Can't hijack offlimits namespace (Labels)", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: kubeSystem.GetName()}, kubeSystem)).Should(Succeed())
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"}}}`,
|
|
meta.TenantLabel,
|
|
tenant.GetName(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), kubeSystem.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
|
|
Expect(err).To(HaveOccurred())
|
|
}
|
|
})
|
|
|
|
It("Can't hijack offlimits namespace (Annotations)", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: kubeSystem.GetName()}, kubeSystem)).Should(Succeed())
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"annotations":{"%s":"%s"}}}`,
|
|
meta.TenantLabel,
|
|
tenant.GetName(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), kubeSystem.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
|
|
Expect(err).To(HaveOccurred())
|
|
}
|
|
})
|
|
|
|
It("Owners can not hijack unmanaged namespaces using JSONPatch add label", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(`[
|
|
{"op":"add","path":"/metadata/labels","value":{}},
|
|
{"op":"add","path":"/metadata/labels/%s","value":"%s"}
|
|
]`, clt.EscapeJSONPointer(meta.TenantLabel), tenant.GetName()))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.JSONPatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not hijack unmanaged namespaces using JSONPatch add ownerReference", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(`[
|
|
{"op":"add","path":"/metadata/ownerReferences","value":[
|
|
{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}
|
|
]}
|
|
]`, capsulev1beta2.GroupVersion.String(), tenant.GetName(), tenant.GetUID()))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.JSONPatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not hijack unmanaged namespaces with matching label and forged ownerReference UID", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
_, fakeUID := randomTenantReference()
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"},"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
meta.TenantLabel,
|
|
tenant.GetName(),
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenant.GetName(),
|
|
fakeUID,
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not hijack unmanaged namespaces using server-side apply", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
apply := []byte(fmt.Sprintf(`{
|
|
"apiVersion":"v1",
|
|
"kind":"Namespace",
|
|
"metadata":{
|
|
"name":"%s",
|
|
"labels":{"%s":"%s"},
|
|
"ownerReferences":[{
|
|
"apiVersion":"%s",
|
|
"kind":"Tenant",
|
|
"name":"%s",
|
|
"uid":"%s"
|
|
}]
|
|
}
|
|
}`,
|
|
unmanaged.Name,
|
|
meta.TenantLabel,
|
|
tenant.GetName(),
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenant.GetName(),
|
|
tenant.GetUID(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.ApplyPatchType,
|
|
apply,
|
|
metav1.PatchOptions{
|
|
FieldManager: "attacker",
|
|
Force: ptr.To(true),
|
|
},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not combine status and metadata patches to adopt unmanaged namespaces", func() {
|
|
tenant := getTenant(t1.Name)
|
|
createNamespaceStatusRBACForOwner(tenant)
|
|
DeferCleanup(func(tnt *capsulev1beta2.Tenant) {
|
|
deleteNamespaceStatusRBACForOwner(tnt)
|
|
}, tenant)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
statusNs, err := cs.CoreV1().Namespaces().Get(context.TODO(), unmanaged.Name, metav1.GetOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
if statusNs.Labels == nil {
|
|
statusNs.Labels = map[string]string{}
|
|
}
|
|
|
|
statusNs.Labels[meta.TenantLabel] = tenant.GetName()
|
|
statusNs.OwnerReferences = []metav1.OwnerReference{{
|
|
APIVersion: capsulev1beta2.GroupVersion.String(),
|
|
Kind: "Tenant",
|
|
Name: tenant.GetName(),
|
|
UID: tenant.GetUID(),
|
|
}}
|
|
|
|
_, err = cs.CoreV1().Namespaces().UpdateStatus(context.TODO(), statusNs, metav1.UpdateOptions{})
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(apierrors.IsForbidden(err)).To(BeTrue())
|
|
Expect(err).To(MatchError(ContainSubstring("namespace can not be patched into a tenant")))
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"}}}`,
|
|
meta.TenantLabel,
|
|
tenant.GetName(),
|
|
))
|
|
|
|
_, err = cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not hijack unmanaged namespaces with valid ownerReference and mismatching label", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"not-%s"},"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
meta.TenantLabel,
|
|
tenant.GetName(),
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenant.GetName(),
|
|
tenant.GetUID(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
unmanaged.Name,
|
|
types.StrategicMergePatchType,
|
|
patch,
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can patch managed namespaces but ownerReference changes should be reverted", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenant.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
randomName, randomUID := randomTenantReference()
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
capsulev1beta2.GroupVersion.String(),
|
|
randomName,
|
|
randomUID,
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
retrievedNs := getNamespace(ns.Name)
|
|
|
|
Expect(hasTenantOwnerReferenceByNameAndUID(retrievedNs, randomName, randomUID)).To(BeFalse(), "Namespace should not keep patched Tenant ownerReference")
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenant)).To(BeTrue(), "Namespace should keep original Tenant ownerReference")
|
|
}
|
|
})
|
|
|
|
It("Owners can patch managed namespaces but tenant label changes should be reverted", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenant.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
randomName, _ := randomTenantReference()
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"}}}`,
|
|
meta.TenantLabel,
|
|
randomName,
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can patch managed namespaces but combined ownership changes should be reverted", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenant.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
randomName, randomUID := randomTenantReference()
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"},"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
meta.TenantLabel,
|
|
randomName,
|
|
capsulev1beta2.GroupVersion.String(),
|
|
randomName,
|
|
randomUID,
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
retrievedNs := getNamespace(ns.Name)
|
|
|
|
Expect(retrievedNs.Labels).To(HaveKeyWithValue(meta.TenantLabel, tenant.GetName()))
|
|
Expect(hasTenantOwnerReferenceByNameAndUID(retrievedNs, randomName, randomUID)).To(BeFalse())
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenant)).To(BeTrue())
|
|
}
|
|
})
|
|
|
|
It("Administrators can migrate managed namespaces to another Tenant", func() {
|
|
tenantA := getTenant(t1.Name)
|
|
tenantB := getTenant(t2.Name)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenantA.GetName(),
|
|
})
|
|
NamespaceCreation(ns, t1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
DeferCleanup(func() { EventuallyDeletion(ns) })
|
|
|
|
ref, err := GetTenantOwnerReferenceAsPatch(tenantB)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
patch := map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"labels": map[string]interface{}{
|
|
meta.TenantLabel: tenantB.GetName(),
|
|
},
|
|
"ownerReferences": []map[string]interface{}{ref},
|
|
},
|
|
}
|
|
|
|
Eventually(func() error {
|
|
return PatchNamespace(ns, ownerClient(administrator), patch)
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
|
|
Eventually(func(g Gomega) {
|
|
current := getNamespace(ns.Name)
|
|
|
|
g.Expect(current.Labels).To(HaveKeyWithValue(meta.TenantLabel, tenantB.GetName()))
|
|
g.Expect(tenantOwnerReferences(current)).To(Equal([]string{tenantB.GetName()}))
|
|
g.Expect(hasTenantOwnerReference(current, tenantB)).To(BeTrue())
|
|
g.Expect(hasTenantOwnerReference(current, tenantA)).To(BeFalse())
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
|
|
NamespaceIsNotPartOfTenant(t1, ns).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t2, ns).Should(Succeed())
|
|
})
|
|
|
|
It("Administrators can remove complete Tenant ownership while partial removal is reverted", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenant.GetName(),
|
|
})
|
|
NamespaceCreation(ns, t1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
DeferCleanup(func() { EventuallyDeletion(ns) })
|
|
|
|
adminClient := ownerClient(administrator)
|
|
Eventually(func() error {
|
|
_, err := adminClient.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
ns.Name,
|
|
types.MergePatchType,
|
|
[]byte(`{"metadata":{"annotations":{"e2e.capsule.clastix.io/admin":"true"}}}`),
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
return err
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
|
|
Eventually(func() error {
|
|
_, err := adminClient.CoreV1().Namespaces().Patch(
|
|
context.TODO(),
|
|
ns.Name,
|
|
types.MergePatchType,
|
|
[]byte(fmt.Sprintf(`{"metadata":{"labels":{"%s":null}}}`, meta.TenantLabel)),
|
|
metav1.PatchOptions{},
|
|
)
|
|
|
|
return err
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
|
|
patch := map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"labels": map[string]interface{}{
|
|
meta.TenantLabel: nil,
|
|
},
|
|
"ownerReferences": []interface{}{},
|
|
},
|
|
}
|
|
Expect(PatchNamespace(ns, adminClient, patch)).To(Succeed())
|
|
|
|
ExpectNamespaceNotAssignedToTenant(context.TODO(), ns.Name)
|
|
NamespaceIsNotPartOfTenant(t1, ns).Should(Succeed())
|
|
})
|
|
|
|
It("Owners can not migrate managed namespaces to another Tenant", func() {
|
|
tenantA := getTenant(t1.Name)
|
|
tenantB := getTenant(t2.Name)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenantA.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
patch := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"},"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
meta.TenantLabel,
|
|
tenantB.GetName(),
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenantB.GetName(),
|
|
tenantB.GetUID(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
retrievedNs := getNamespace(ns.Name)
|
|
|
|
Expect(retrievedNs.Labels).To(HaveKeyWithValue(meta.TenantLabel, tenantA.GetName()))
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenantA)).To(BeTrue())
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenantB)).To(BeFalse())
|
|
}
|
|
})
|
|
|
|
It("Owners can not remove tenant ownership from managed namespaces", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenant.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
patchRemoveOwnerReferences := []byte(`{"metadata":{"ownerReferences":[]}}`)
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patchRemoveOwnerReferences, metav1.PatchOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
|
|
patchRemoveTenantLabel := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":null}}}`,
|
|
meta.TenantLabel,
|
|
))
|
|
|
|
_, err = cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patchRemoveTenantLabel, metav1.PatchOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
|
|
patchRemoveOwnership := map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"labels": map[string]interface{}{
|
|
meta.TenantLabel: nil,
|
|
},
|
|
"ownerReferences": []interface{}{},
|
|
},
|
|
}
|
|
|
|
Expect(PatchNamespace(ns, cs, patchRemoveOwnership)).ToNot(Succeed())
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Owners can not patch unmanaged namespaces into a Tenant", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
patchLabel := []byte(fmt.Sprintf(
|
|
`{"metadata":{"labels":{"%s":"%s"}}}`,
|
|
meta.TenantLabel,
|
|
tenant.GetName(),
|
|
))
|
|
|
|
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), unmanaged.Name, types.StrategicMergePatchType, patchLabel, metav1.PatchOptions{})
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
patchOwnerReference := []byte(fmt.Sprintf(
|
|
`{"metadata":{"ownerReferences":[{"apiVersion":"%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`,
|
|
capsulev1beta2.GroupVersion.String(),
|
|
tenant.GetName(),
|
|
tenant.GetUID(),
|
|
))
|
|
|
|
_, err = cs.CoreV1().Namespaces().Patch(context.TODO(), unmanaged.Name, types.StrategicMergePatchType, patchOwnerReference, metav1.PatchOptions{})
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Namespace status updates by owners can not change tenant ownerReferences", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
createNamespaceStatusRBACForOwner(tenant)
|
|
DeferCleanup(func(tnt *capsulev1beta2.Tenant) {
|
|
deleteNamespaceStatusRBACForOwner(tnt)
|
|
}, tenant)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenant.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
randomName, randomUID := randomTenantReference()
|
|
|
|
statusNs, err := cs.CoreV1().Namespaces().Get(context.TODO(), ns.Name, metav1.GetOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
statusNs.OwnerReferences = []metav1.OwnerReference{
|
|
{
|
|
APIVersion: capsulev1beta2.GroupVersion.String(),
|
|
Kind: "Tenant",
|
|
Name: randomName,
|
|
UID: randomUID,
|
|
},
|
|
}
|
|
|
|
_, err = cs.CoreV1().Namespaces().UpdateStatus(context.TODO(), statusNs, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
|
|
continue
|
|
}
|
|
|
|
retrievedNs := getNamespace(ns.Name)
|
|
|
|
Expect(hasTenantOwnerReferenceByNameAndUID(retrievedNs, randomName, randomUID)).To(BeFalse(), "Namespace status update must not change Tenant ownerReference")
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenant)).To(BeTrue(), "Namespace should keep original Tenant ownerReference")
|
|
}
|
|
})
|
|
|
|
It("Namespace status updates by owners can not change tenant labels", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
createNamespaceStatusRBACForOwner(tenant)
|
|
DeferCleanup(func(tnt *capsulev1beta2.Tenant) {
|
|
deleteNamespaceStatusRBACForOwner(tnt)
|
|
}, tenant)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenant.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
randomName, _ := randomTenantReference()
|
|
|
|
statusNs, err := cs.CoreV1().Namespaces().Get(context.TODO(), ns.Name, metav1.GetOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
if statusNs.Labels == nil {
|
|
statusNs.Labels = map[string]string{}
|
|
}
|
|
|
|
statusNs.Labels[meta.TenantLabel] = randomName
|
|
|
|
_, err = cs.CoreV1().Namespaces().UpdateStatus(context.TODO(), statusNs, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
|
|
continue
|
|
}
|
|
|
|
expectOriginalTenantOwnership(ns.Name, tenant)
|
|
}
|
|
})
|
|
|
|
It("Namespace status updates by owners can not migrate namespaces to another Tenant", func() {
|
|
tenantA := getTenant(t1.Name)
|
|
tenantB := getTenant(t2.Name)
|
|
|
|
createNamespaceStatusRBACForOwner(tenantA)
|
|
DeferCleanup(func(tnt *capsulev1beta2.Tenant) {
|
|
deleteNamespaceStatusRBACForOwner(tnt)
|
|
}, tenantA)
|
|
|
|
createNamespaceStatusRBACForOwner(tenantB)
|
|
DeferCleanup(func(tnt *capsulev1beta2.Tenant) {
|
|
deleteNamespaceStatusRBACForOwner(tnt)
|
|
}, tenantB)
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tenantA.GetName(),
|
|
})
|
|
NamespaceCreation(ns, owner.UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(t1, ns).Should(Succeed())
|
|
|
|
statusNs, err := cs.CoreV1().Namespaces().Get(context.TODO(), ns.Name, metav1.GetOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
if statusNs.Labels == nil {
|
|
statusNs.Labels = map[string]string{}
|
|
}
|
|
|
|
statusNs.Labels[meta.TenantLabel] = tenantB.GetName()
|
|
statusNs.OwnerReferences = []metav1.OwnerReference{
|
|
{
|
|
APIVersion: capsulev1beta2.GroupVersion.String(),
|
|
Kind: "Tenant",
|
|
Name: tenantB.GetName(),
|
|
UID: tenantB.GetUID(),
|
|
},
|
|
}
|
|
|
|
_, err = cs.CoreV1().Namespaces().UpdateStatus(context.TODO(), statusNs, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
retrievedNs := getNamespace(ns.Name)
|
|
|
|
Expect(retrievedNs.Labels).To(HaveKeyWithValue(meta.TenantLabel, tenantA.GetName()))
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenantA)).To(BeTrue())
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenantB)).To(BeFalse())
|
|
|
|
continue
|
|
}
|
|
|
|
retrievedNs := getNamespace(ns.Name)
|
|
|
|
Expect(retrievedNs.Labels).To(HaveKeyWithValue(meta.TenantLabel, tenantA.GetName()))
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenantA)).To(BeTrue())
|
|
Expect(hasTenantOwnerReference(retrievedNs, tenantB)).To(BeFalse())
|
|
}
|
|
})
|
|
|
|
It("Namespace status updates by owners can not patch unmanaged namespaces into a Tenant", func() {
|
|
tenant := getTenant(t1.Name)
|
|
|
|
createNamespaceStatusRBACForOwner(tenant)
|
|
DeferCleanup(func(tnt *capsulev1beta2.Tenant) {
|
|
deleteNamespaceStatusRBACForOwner(tnt)
|
|
}, tenant)
|
|
|
|
unmanaged := createUnmanagedNamespace()
|
|
|
|
for _, owner := range t1.Spec.Owners {
|
|
cs := ownerClient(owner.UserSpec)
|
|
|
|
statusNs, err := cs.CoreV1().Namespaces().Get(context.TODO(), unmanaged.Name, metav1.GetOptions{})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
if statusNs.Labels == nil {
|
|
statusNs.Labels = map[string]string{}
|
|
}
|
|
|
|
statusNs.Labels[meta.TenantLabel] = tenant.GetName()
|
|
statusNs.OwnerReferences = []metav1.OwnerReference{
|
|
{
|
|
APIVersion: capsulev1beta2.GroupVersion.String(),
|
|
Kind: "Tenant",
|
|
Name: tenant.GetName(),
|
|
UID: tenant.GetUID(),
|
|
},
|
|
}
|
|
|
|
_, err = cs.CoreV1().Namespaces().UpdateStatus(context.TODO(), statusNs, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
|
|
continue
|
|
}
|
|
|
|
expectNoTenantOwnership(unmanaged.Name, tenant)
|
|
}
|
|
|
|
Expect(k8sClient.Delete(context.TODO(), unmanaged)).To(Succeed())
|
|
Eventually(func() bool {
|
|
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: unmanaged.Name}, &corev1.Namespace{})
|
|
|
|
return apierrors.IsNotFound(err)
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
|
})
|
|
})
|
|
|
|
func createNamespaceStatusRBACForOwner(tnt *capsulev1beta2.Tenant) {
|
|
name := "namespace-status-patch-" + tnt.GetName()
|
|
|
|
clusterRole := &rbacv1.ClusterRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Rules: []rbacv1.PolicyRule{
|
|
{
|
|
APIGroups: []string{""},
|
|
Resources: []string{
|
|
"namespaces",
|
|
"namespaces/status",
|
|
},
|
|
Verbs: []string{
|
|
"get",
|
|
"patch",
|
|
"update",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := k8sClient.Create(context.TODO(), clusterRole)
|
|
if err != nil && !apierrors.IsAlreadyExists(err) {
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|
|
|
|
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
RoleRef: rbacv1.RoleRef{
|
|
APIGroup: rbacv1.GroupName,
|
|
Kind: "ClusterRole",
|
|
Name: name,
|
|
},
|
|
}
|
|
|
|
for _, sub := range tnt.Spec.Owners {
|
|
if sub.Kind != rbac.ServiceAccountOwner {
|
|
clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{
|
|
Kind: string(sub.Kind),
|
|
Name: sub.Name,
|
|
})
|
|
} else {
|
|
namespace, name, err := serviceaccount.SplitUsername(sub.Name)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{
|
|
Kind: string(sub.Kind),
|
|
Name: name,
|
|
Namespace: namespace,
|
|
})
|
|
}
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), clusterRoleBinding)
|
|
if err != nil && !apierrors.IsAlreadyExists(err) {
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|
|
}
|
|
|
|
func deleteNamespaceStatusRBACForOwner(tnt *capsulev1beta2.Tenant) {
|
|
name := "namespace-status-patch-" + tnt.GetName()
|
|
|
|
err := k8sClient.Delete(context.TODO(), &rbacv1.ClusterRoleBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
})
|
|
if err != nil && !apierrors.IsNotFound(err) {
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|
|
|
|
err = k8sClient.Delete(context.TODO(), &rbacv1.ClusterRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
})
|
|
if err != nil && !apierrors.IsNotFound(err) {
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|
|
}
|
|
|
|
func tenantOwnerReferences(ns *corev1.Namespace) []string {
|
|
var refs []string
|
|
|
|
for _, ref := range ns.GetOwnerReferences() {
|
|
if tenant.IsTenantOwnerReference(ref) {
|
|
refs = append(refs, ref.Name)
|
|
}
|
|
}
|
|
|
|
sort.Strings(refs)
|
|
|
|
return refs
|
|
}
|