Files
capsule/e2e/rules_enforce_metadata_test.go
T
77d1810bb9 feat: add metadata enforcement (#1990)
* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: preserve ca-bundles injected from external providers

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

* feat: add metadata enforcement

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

* feat: add metadata enforcement

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

* fix: add resourcepoolclaim validation

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

* fix: add resourcepoolclaim validation

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

* fix: add resourcepoolclaim validation

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

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: add resourcepoolclaim validation

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

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-02 13:42:08 +02:00

2183 lines
45 KiB
Go

// Copyright 2020-2026 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"fmt"
"strings"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/utils/ptr"
"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/api/rules"
"github.com/projectcapsule/capsule/pkg/api/runtime"
"github.com/projectcapsule/capsule/pkg/runtime/events"
)
var _ = Describe("enforcing generic metadata namespace rules", Ordered, Label("tenant", "rules", "enforce", "metadata", "generic"), func() {
const ownerName = "e2e-rules-metadata"
var (
tnt *capsulev1beta2.Tenant
tenantRules []*rules.NamespaceRuleBodyTenant
)
metadataByExpression := func(expression string) runtime.ExpressionMatch {
return runtime.ExpressionMatch{
ExpressionRegex: runtime.ExpressionRegex{
Expression: expression,
},
}
}
metadataByNegatedExpression := func(expression string) runtime.ExpressionMatch {
return runtime.ExpressionMatch{
ExpressionRegex: runtime.ExpressionRegex{
Expression: expression,
Negate: true,
},
}
}
metadataByExact := func(exact ...string) runtime.ExpressionMatch {
return runtime.ExpressionMatch{
Exact: exact,
}
}
metadataByMatch := func(exact []string, expression string) runtime.ExpressionMatch {
return runtime.ExpressionMatch{
ExpressionRegex: runtime.ExpressionRegex{
Expression: expression,
},
Exact: exact,
}
}
metadataValueRule := func(required bool, values ...runtime.ExpressionMatch) rules.MetadataValueRule {
return rules.MetadataValueRule{
Required: required,
Values: values,
}
}
metadataRule := func(
action rules.ActionType,
apiVersion string,
kinds []string,
labels map[string]rules.MetadataValueRule,
annotations map[string]rules.MetadataValueRule,
) *rules.NamespaceRuleBodyTenant {
return &rules.NamespaceRuleBodyTenant{
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: action,
Metadata: []rules.MetadataRule{
{
VersionKinds: runtime.VersionKinds{
APIGroups: []string{apiVersion},
Kinds: kinds,
},
Labels: labels,
Annotations: annotations,
},
},
},
},
}
}
selectedRule := func(
selector map[string]string,
rule *rules.NamespaceRuleBodyTenant,
) *rules.NamespaceRuleBodyTenant {
rule.NamespaceSelector = &metav1.LabelSelector{
MatchLabels: selector,
}
return rule
}
baseTenantRules := func() []*rules.NamespaceRuleBodyTenant {
return []*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"",
[]string{
"ConfigMap",
"Service",
},
map[string]rules.MetadataValueRule{
"example.corp/tenant": metadataValueRule(
true,
metadataByExact("prod", "test"),
),
},
map[string]rules.MetadataValueRule{
"example.corp/cost-center": metadataValueRule(
false,
metadataByExpression("^INV-[0-9]{4}$"),
metadataByExact("prod", "test"),
),
},
),
metadataRule(
rules.ActionTypeAudit,
"*",
[]string{
"ConfigMap",
"Service",
},
map[string]rules.MetadataValueRule{
"example.corp/audit": metadataValueRule(
false,
metadataByExpression("^audit-.*"),
),
},
nil,
),
selectedRule(
map[string]string{
"environment": "prod",
},
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
nil,
map[string]rules.MetadataValueRule{
"example.corp/approval": metadataValueRule(
true,
metadataByExact("approved"),
),
},
),
),
}
}
newTenant := func() *capsulev1beta2.Tenant {
return &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-rule-metadata",
Labels: map[string]string{
"env": "e2e",
},
},
Spec: capsulev1beta2.TenantSpec{
Owners: rbac.OwnerListSpec{
{
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: ownerName,
Kind: "User",
},
},
},
},
Rules: tenantRules,
},
}
}
type expectedMetadataPolicy struct {
required bool
expressions []string
exact [][]string
negated []bool
}
type expectedMetadataStatusRule struct {
action rules.ActionType
apiGroups []string
kinds []string
labels map[string]expectedMetadataPolicy
annotations map[string]expectedMetadataPolicy
}
expectMetadataPolicy := func(g Gomega, got rules.MetadataValueRule, expected expectedMetadataPolicy) {
g.Expect(got.Required).To(Equal(expected.required))
wantValues := len(expected.expressions)
if len(expected.exact) > wantValues {
wantValues = len(expected.exact)
}
if len(expected.negated) > wantValues {
wantValues = len(expected.negated)
}
g.Expect(got.Values).To(HaveLen(wantValues))
for i := 0; i < wantValues; i++ {
value := got.Values[i]
if len(expected.expressions) > i {
g.Expect(value.Expression).To(Equal(expected.expressions[i]))
} else {
g.Expect(value.Expression).To(BeEmpty())
}
if len(expected.exact) > i {
g.Expect(value.Exact).To(Equal(expected.exact[i]))
} else {
g.Expect(value.Exact).To(BeEmpty())
}
if len(expected.negated) > i {
g.Expect(value.Negate).To(Equal(expected.negated[i]))
} else {
g.Expect(value.Negate).To(BeFalse())
}
}
}
expectNamespaceStatusRules := func(nsName string, want []expectedMetadataStatusRule) {
Eventually(func(g Gomega) {
nsStatus := &capsulev1beta2.RuleStatus{}
g.Expect(k8sClient.Get(
context.Background(),
client.ObjectKey{
Name: meta.NameForManagedRuleStatus(),
Namespace: nsName,
},
nsStatus,
)).To(Succeed())
g.Expect(nsStatus.Status.Rules).To(HaveLen(len(want)))
for i, expected := range want {
got := nsStatus.Status.Rules[i]
g.Expect(got).NotTo(BeNil())
g.Expect(got.Enforce).NotTo(BeNil())
g.Expect(got.Enforce.Action).To(Equal(expected.action))
g.Expect(got.Enforce.Metadata).To(HaveLen(1))
metadata := got.Enforce.Metadata[0]
if len(expected.apiGroups) == 0 {
g.Expect(metadata.APIGroups).To(BeEmpty())
} else {
g.Expect(metadata.APIGroups).To(Equal(expected.apiGroups))
}
g.Expect(metadata.Kinds).To(Equal(expected.kinds))
g.Expect(metadata.Labels).To(HaveLen(len(expected.labels)))
for key, policy := range expected.labels {
gotPolicy, ok := metadata.Labels[key]
g.Expect(ok).To(BeTrue(), "expected label policy %q", key)
expectMetadataPolicy(g, gotPolicy, policy)
}
g.Expect(metadata.Annotations).To(HaveLen(len(expected.annotations)))
for key, policy := range expected.annotations {
gotPolicy, ok := metadata.Annotations[key]
g.Expect(ok).To(BeTrue(), "expected annotation policy %q", key)
expectMetadataPolicy(g, gotPolicy, policy)
}
}
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
baseStatusRules := func() []expectedMetadataStatusRule {
return []expectedMetadataStatusRule{
{
action: rules.ActionTypeAllow,
apiGroups: []string{
"v1",
},
kinds: []string{
"ConfigMap",
"Service",
},
labels: map[string]expectedMetadataPolicy{
"example.corp/tenant": {
required: true,
exact: [][]string{
{
"prod",
"test",
},
},
},
},
annotations: map[string]expectedMetadataPolicy{
"example.corp/cost-center": {
required: false,
expressions: []string{
"^INV-[0-9]{4}$",
"",
},
exact: [][]string{
nil,
{
"prod",
"test",
},
},
},
},
},
{
action: rules.ActionTypeAudit,
apiGroups: []string{
"*",
},
kinds: []string{
"ConfigMap",
"Service",
},
labels: map[string]expectedMetadataPolicy{
"example.corp/audit": {
expressions: []string{
"^audit-.*",
},
},
},
},
}
}
updateTenantRules := func(next []*rules.NamespaceRuleBodyTenant) {
UpdateTenantEventually(tnt, func(current *capsulev1beta2.Tenant) {
current.Spec.Rules = next
})
tnt.Spec.Rules = next
}
createNamespace := func(labels map[string]string) *corev1.Namespace {
if labels == nil {
labels = map[string]string{}
}
labels[meta.TenantLabel] = tnt.GetName()
ns := NewNamespace("", labels)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
return ns
}
configMap := func(name string, labels map[string]string, annotations map[string]string) *corev1.ConfigMap {
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
Annotations: annotations,
},
Data: map[string]string{
"key": "value",
},
}
}
service := func(name string, labels map[string]string, annotations map[string]string) *corev1.Service {
return &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
Annotations: annotations,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Ports: []corev1.ServicePort{
{
Name: "http",
Port: 8080,
},
},
},
}
}
deployment := func(name string, labels map[string]string, annotations map[string]string) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
Annotations: annotations,
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": name,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx:1.25",
},
},
},
},
},
}
}
createConfigMapAndExpectAllowed := func(cs kubernetes.Interface, nsName string, cm *corev1.ConfigMap) {
EventuallyCreation(func() error {
_, err := cs.CoreV1().ConfigMaps(nsName).Create(context.Background(), cm, metav1.CreateOptions{})
return err
}).Should(Succeed())
}
createConfigMapAndExpectDenied := func(cs kubernetes.Interface, nsName string, cm *corev1.ConfigMap, substrings ...string) {
base := cm.DeepCopy()
baseName := base.Name
if baseName == "" {
baseName = "cm"
}
Eventually(func() error {
candidate := base.DeepCopy()
candidate.Name = fmt.Sprintf("%s-%d", baseName, time.Now().UnixNano()%1e6)
_, err := cs.CoreV1().ConfigMaps(nsName).Create(context.Background(), candidate, metav1.CreateOptions{})
if err == nil {
_ = cs.CoreV1().ConfigMaps(nsName).Delete(context.Background(), candidate.Name, metav1.DeleteOptions{})
return fmt.Errorf("expected configmap create to be denied, but it succeeded")
}
if apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unexpected AlreadyExists: %v", err)
}
msg := err.Error()
for _, substring := range substrings {
if !strings.Contains(msg, substring) {
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
updateConfigMapAndExpectDenied := func(
cs kubernetes.Interface,
nsName string,
cmName string,
mutate func(*corev1.ConfigMap),
substrings ...string,
) {
Eventually(func() error {
cm, err := cs.CoreV1().ConfigMaps(nsName).Get(context.Background(), cmName, metav1.GetOptions{})
if err != nil {
return err
}
mutate(cm)
_, err = cs.CoreV1().ConfigMaps(nsName).Update(context.Background(), cm, metav1.UpdateOptions{})
if err == nil {
return fmt.Errorf("expected configmap update to be denied, but it succeeded")
}
msg := err.Error()
for _, substring := range substrings {
if !strings.Contains(msg, substring) {
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
createServiceAndExpectAllowed := func(cs kubernetes.Interface, nsName string, svc *corev1.Service) {
EventuallyCreation(func() error {
_, err := cs.CoreV1().Services(nsName).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())
}
createServiceAndExpectDenied := func(cs kubernetes.Interface, nsName string, svc *corev1.Service, substrings ...string) {
base := svc.DeepCopy()
baseName := base.Name
if baseName == "" {
baseName = "svc"
}
Eventually(func() error {
candidate := base.DeepCopy()
candidate.Name = fmt.Sprintf("%s-%d", baseName, time.Now().UnixNano()%1e6)
_, err := cs.CoreV1().Services(nsName).Create(context.Background(), candidate, metav1.CreateOptions{})
if err == nil {
_ = cs.CoreV1().Services(nsName).Delete(context.Background(), candidate.Name, metav1.DeleteOptions{})
return fmt.Errorf("expected service create to be denied, but it succeeded")
}
if apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unexpected AlreadyExists: %v", err)
}
msg := err.Error()
for _, substring := range substrings {
if !strings.Contains(msg, substring) {
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
createDeploymentAndExpectAllowed := func(nsName string, deploy *appsv1.Deployment) {
EventuallyCreation(func() error {
return k8sClient.Create(context.Background(), deploy, &client.CreateOptions{})
}).Should(Succeed())
EventuallyDeletion(deploy)
}
createDeploymentAndExpectDenied := func(nsName string, deploy *appsv1.Deployment, substrings ...string) {
base := deploy.DeepCopy()
baseName := base.Name
if baseName == "" {
baseName = "deployment"
}
Eventually(func() error {
candidate := base.DeepCopy()
candidate.Name = fmt.Sprintf("%s-%d", baseName, time.Now().UnixNano()%1e6)
candidate.Namespace = nsName
candidate.Spec.Selector.MatchLabels["app"] = candidate.Name
candidate.Spec.Template.Labels["app"] = candidate.Name
err := k8sClient.Create(context.Background(), candidate, &client.CreateOptions{})
if err == nil {
_ = k8sClient.Delete(context.Background(), candidate)
return fmt.Errorf("expected deployment create to be denied, but it succeeded")
}
msg := err.Error()
for _, substring := range substrings {
if !strings.Contains(msg, substring) {
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
expectAuditEvent := func(
cs kubernetes.Interface,
namespace string,
kind string,
objectName string,
substrings ...string,
) {
Eventually(func() error {
evt, err := cs.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return err
}
for _, e := range evt.Items {
if e.Reason != events.ReasonNamespaceRuleAudit {
continue
}
if e.InvolvedObject.Kind != kind {
continue
}
eventObjectName := e.InvolvedObject.Name
if eventObjectName != objectName && !strings.HasPrefix(eventObjectName, objectName+"-") {
continue
}
message := e.Message
matched := true
for _, substring := range substrings {
if !strings.Contains(message, substring) {
matched = false
break
}
}
if matched {
return nil
}
}
return fmt.Errorf(
"expected audit event for %s %q containing %q",
kind,
objectName,
substrings,
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
BeforeEach(func() {
tenantRules = baseTenantRules()
})
JustBeforeEach(func() {
tnt = newTenant()
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)
})
JustAfterEach(func() {
EventuallyDeletion(tnt)
})
It("stores matching tenant metadata rules as independent status rule blocks", func() {
ns := createNamespace(nil)
expectNamespaceStatusRules(ns.GetName(), baseStatusRules())
})
It("stores namespace-selector matched metadata rules as additional independent status rule blocks", func() {
ns := createNamespace(map[string]string{
"environment": "prod",
})
want := baseStatusRules()
want = append(want, expectedMetadataStatusRule{
action: rules.ActionTypeAllow,
apiGroups: []string{
"*",
},
kinds: []string{
"ConfigMap",
},
annotations: map[string]expectedMetadataPolicy{
"example.corp/approval": {
required: true,
exact: [][]string{
{
"approved",
},
},
},
},
})
expectNamespaceStatusRules(ns.GetName(), want)
})
It("denies creation when a required label is missing", func() {
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap("required-label-missing", nil, nil),
"metadata",
"example.corp/tenant",
"required",
)
})
It("allows creation when a required label is present and exact value matches", func() {
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"required-label-allowed",
map[string]string{
"example.corp/tenant": "prod",
},
nil,
),
)
})
It("denies creation when a required label is present but value is not allowed", func() {
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"required-label-invalid",
map[string]string{
"example.corp/tenant": "stage",
},
nil,
),
"metadata",
"example.corp/tenant",
"stage",
"not allowed",
)
})
It("allows missing optional annotations but denies present non-matching annotation values", func() {
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"optional-annotation-missing",
map[string]string{
"example.corp/tenant": "prod",
},
nil,
),
)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"optional-annotation-invalid",
map[string]string{
"example.corp/tenant": "prod",
},
map[string]string{
"example.corp/cost-center": "BAD-1234",
},
),
"metadata",
"example.corp/cost-center",
"BAD-1234",
"not allowed",
)
})
It("allows optional annotation values matching regex, exact, or combined matchers", func() {
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"annotation-regex-allowed",
map[string]string{
"example.corp/tenant": "prod",
},
map[string]string{
"example.corp/cost-center": "INV-1234",
},
),
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"annotation-exact-allowed",
map[string]string{
"example.corp/tenant": "prod",
},
map[string]string{
"example.corp/cost-center": "test",
},
),
)
})
It("applies one metadata rule to multiple core kinds", func() {
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap("multi-kind-configmap-denied", nil, nil),
"metadata",
"example.corp/tenant",
"required",
)
createServiceAndExpectDenied(
cs,
ns.Name,
service("multi-kind-service-denied", nil, nil),
"metadata",
"example.corp/tenant",
"required",
)
createServiceAndExpectAllowed(
cs,
ns.Name,
service(
"multi-kind-service-allowed",
map[string]string{
"example.corp/tenant": "prod",
},
nil,
),
)
})
It("treats empty apiVersion as core v1 and does not match grouped resources", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"",
[]string{
"*",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap("core-v1-configmap-denied", nil, nil),
"metadata",
"env",
"required",
)
deploy := deployment("apps-deployment-not-matched", nil, nil)
deploy.Namespace = ns.Name
createDeploymentAndExpectAllowed(ns.Name, deploy)
})
It("matches grouped apiVersion and selected kinds", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"apps/v1",
[]string{
"Deployment",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
createDeploymentAndExpectDenied(
ns.Name,
deployment("apps-deployment-missing-env", nil, nil),
"metadata",
"env",
"required",
)
deploy := deployment(
"apps-deployment-env-allowed",
map[string]string{
"env": "prod",
},
nil,
)
deploy.Namespace = ns.Name
createDeploymentAndExpectAllowed(ns.Name, deploy)
})
It("denies a later matching deny rule after an earlier allow rule matched", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod", "test"),
),
},
nil,
),
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
false,
metadataByExact("test"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"later-deny-prod-allowed",
map[string]string{
"env": "prod",
},
nil,
),
)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"later-deny-test-denied",
map[string]string{
"env": "test",
},
nil,
),
"metadata",
"env",
"test",
"denied",
)
})
It("allows a later matching allow rule after an earlier deny rule did not match", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
false,
metadataByExact("blocked"),
),
},
nil,
),
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"later-allow-prod-allowed",
map[string]string{
"env": "prod",
},
nil,
),
)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"later-allow-blocked-denied",
map[string]string{
"env": "blocked",
},
nil,
),
"metadata",
"env",
"blocked",
"denied",
)
})
It("supports negated metadata matchers", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"team": metadataValueRule(
false,
metadataByNegatedExpression("^trusted-.*"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"negated-denied",
map[string]string{
"team": "untrusted",
},
nil,
),
"metadata",
"team",
"untrusted",
"denied",
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"negated-allowed",
map[string]string{
"team": "trusted-platform",
},
nil,
),
)
})
It("audits matching metadata but does not deny when a separate allow rule matches", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAudit,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"example.corp/audit": metadataValueRule(
false,
metadataByExpression("^audit-.*"),
),
},
nil,
),
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
cm := configMap(
"metadata-audit-allowed",
map[string]string{
"env": "prod",
"example.corp/audit": "audit-this",
},
nil,
)
createConfigMapAndExpectAllowed(cs, ns.Name, cm)
expectAuditEvent(
clusterAdminClient(),
ns.Name,
"ConfigMap",
cm.Name,
"metadata",
"example.corp/audit",
"audit-this",
)
})
It("applies namespace-selector matched required annotation only to selected namespaces", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
selectedRule(
map[string]string{
"environment": "prod",
},
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
nil,
map[string]rules.MetadataValueRule{
"example.corp/approval": metadataValueRule(
true,
metadataByExact("approved"),
),
},
),
),
})
devNS := createNamespace(map[string]string{
"environment": "dev",
})
prodNS := createNamespace(map[string]string{
"environment": "prod",
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
devNS.Name,
configMap("selected-rule-dev-allowed", nil, nil),
)
createConfigMapAndExpectDenied(
cs,
prodNS.Name,
configMap("selected-rule-prod-denied", nil, nil),
"metadata",
"example.corp/approval",
"required",
)
createConfigMapAndExpectAllowed(
cs,
prodNS.Name,
configMap(
"selected-rule-prod-allowed",
nil,
map[string]string{
"example.corp/approval": "approved",
},
),
)
})
It("denies an update when a metadata value becomes invalid", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod", "test"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
cm := configMap(
"metadata-update-denied",
map[string]string{
"env": "prod",
},
nil,
)
createConfigMapAndExpectAllowed(cs, ns.Name, cm)
updateConfigMapAndExpectDenied(
cs,
ns.Name,
cm.Name,
func(cm *corev1.ConfigMap) {
cm.Labels["env"] = "stage"
},
"metadata",
"env",
"stage",
"not allowed",
)
})
It("supports required metadata without value constraints", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"presence-only": metadataValueRule(true),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap("presence-only-missing", nil, nil),
"metadata",
"presence-only",
"required",
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"presence-only-present",
map[string]string{
"presence-only": "any-value",
},
nil,
),
)
})
It("does not enforce rules for Capsule managed metadata keys", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
meta.TenantLabel: metadataValueRule(
true,
metadataByExact("some-other-tenant"),
),
},
map[string]rules.MetadataValueRule{
meta.ReconcileAnnotation: metadataValueRule(
true,
metadataByExact("must-not-matter"),
),
},
),
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
meta.ManagedByCapsuleLabel: metadataValueRule(
false,
metadataByExact("blocked"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"managed-metadata-ignored",
map[string]string{
meta.ManagedByCapsuleLabel: "blocked",
},
nil,
),
)
})
It("skips generic metadata validation for controller-managed objects", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"controller-managed-skipped",
map[string]string{
meta.NewManagedByCapsuleLabel: meta.ValueController,
"env": "prod",
},
nil,
),
)
})
It("enforces non-skipped objects even when a similar managed-by value does not match the skip rule", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"managed-by-other-not-skipped",
map[string]string{
meta.ManagedByCapsuleLabel: "human",
},
nil,
),
"metadata",
"env",
"required",
)
})
It("supports combined exact and regex value matchers for metadata labels", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{
"ConfigMap",
},
map[string]rules.MetadataValueRule{
"release": metadataValueRule(
true,
metadataByMatch(
[]string{
"stable",
},
"^release-[0-9]+$",
),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"combined-exact-allowed",
map[string]string{
"release": "stable",
},
nil,
),
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"combined-regex-allowed",
map[string]string{
"release": "release-42",
},
nil,
),
)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"combined-denied",
map[string]string{
"release": "canary",
},
nil,
),
"metadata",
"release",
"canary",
"not allowed",
)
})
It("treats optional allow metadata as non-required but still validates it when present", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"optional-env": metadataValueRule(
false,
metadataByExact("prod", "test"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap("optional-label-missing-allowed", nil, nil),
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"optional-label-valid-allowed",
map[string]string{
"optional-env": "prod",
},
nil,
),
)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"optional-label-invalid-denied",
map[string]string{
"optional-env": "stage",
},
nil,
),
"metadata",
"optional-env",
"stage",
"not allowed",
)
})
It("does not treat required on deny rules as a presence requirement", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"blocked": metadataValueRule(
true,
metadataByExact("true"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap("deny-required-missing-allowed", nil, nil),
)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"deny-required-present-denied",
map[string]string{
"blocked": "true",
},
nil,
),
"metadata",
"blocked",
"true",
"denied",
)
})
It("does not treat required on audit rules as a presence requirement", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAudit,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"audited": metadataValueRule(
true,
metadataByExact("true"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap("audit-required-missing-allowed", nil, nil),
)
cm := configMap(
"audit-required-present-allowed",
map[string]string{
"audited": "true",
},
nil,
)
createConfigMapAndExpectAllowed(cs, ns.Name, cm)
expectAuditEvent(
clusterAdminClient(),
ns.Name,
"ConfigMap",
cm.Name,
"metadata",
"audited",
"true",
)
})
It("enforces multiple required metadata keys independently and reports the missing key", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
"team": metadataValueRule(
true,
metadataByExact("platform"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"multi-required-team-missing",
map[string]string{
"env": "prod",
},
nil,
),
"metadata",
"team",
"required",
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"multi-required-all-present",
map[string]string{
"env": "prod",
"team": "platform",
},
nil,
),
)
})
It("denies update when a required label is removed", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
cm := configMap(
"required-label-remove-denied",
map[string]string{
"env": "prod",
},
nil,
)
createConfigMapAndExpectAllowed(cs, ns.Name, cm)
updateConfigMapAndExpectDenied(
cs,
ns.Name,
cm.Name,
func(cm *corev1.ConfigMap) {
delete(cm.Labels, "env")
},
"metadata",
"env",
"required",
)
})
It("skips empty metadata values during value evaluation", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"empty-ok": metadataValueRule(
true,
metadataByExact(""),
),
"empty-not-ok": metadataValueRule(
false,
metadataByExact("prod"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"empty-label-exact-allowed",
map[string]string{
"empty-ok": "",
},
nil,
),
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"empty-label-invalid-skipped",
map[string]string{
"empty-ok": "",
"empty-not-ok": "",
},
nil,
),
)
})
It("matches apiVersion wildcard and kind wildcard across core and grouped resources", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"*"},
map[string]rules.MetadataValueRule{
"global-required": metadataValueRule(
true,
metadataByExact("yes"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap("wildcard-core-denied", nil, nil),
"metadata",
"global-required",
"required",
)
createDeploymentAndExpectDenied(
ns.Name,
deployment("wildcard-grouped-denied", nil, nil),
"metadata",
"global-required",
"required",
)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"wildcard-core-allowed",
map[string]string{
"global-required": "yes",
},
nil,
),
)
})
It("matches kind wildcard only inside default core v1 apiVersion", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"",
[]string{"*"},
map[string]rules.MetadataValueRule{
"core-required": metadataValueRule(
true,
metadataByExact("yes"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap("core-wildcard-configmap-denied", nil, nil),
"metadata",
"core-required",
"required",
)
deploy := deployment("core-wildcard-deployment-not-matched", nil, nil)
deploy.Namespace = ns.Name
createDeploymentAndExpectAllowed(ns.Name, deploy)
})
It("matches partial kind wildcard without matching unrelated kinds", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"v1",
[]string{"*Map"},
map[string]rules.MetadataValueRule{
"map-required": metadataValueRule(
true,
metadataByExact("yes"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap("partial-kind-configmap-denied", nil, nil),
"metadata",
"map-required",
"required",
)
createServiceAndExpectAllowed(
cs,
ns.Name,
service("partial-kind-service-not-matched", nil, nil),
)
})
It("does not let one optional matching key satisfy another required key", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"optional": metadataValueRule(
false,
metadataByExact("ok"),
),
"required": metadataValueRule(
true,
metadataByExact("ok"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"optional-does-not-satisfy-required",
map[string]string{
"optional": "ok",
},
nil,
),
"metadata",
"required",
"required",
)
})
It("does not let a matching annotation satisfy a required label with the same key", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"shared-key": metadataValueRule(
true,
metadataByExact("label-value"),
),
},
map[string]rules.MetadataValueRule{
"shared-key": metadataValueRule(
false,
metadataByExact("annotation-value"),
),
},
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"annotation-does-not-satisfy-label",
nil,
map[string]string{
"shared-key": "annotation-value",
},
),
"metadata",
"metadata.labels",
"shared-key",
"required",
)
})
It("ignores managed annotation prefixes when collecting controlled metadata entries", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
nil,
map[string]rules.MetadataValueRule{
meta.ResourceQuotaAnnotationPrefix + "cpu": metadataValueRule(
true,
metadataByExact("must-not-matter"),
),
meta.ResourceUsedAnnotationPrefix + "memory": metadataValueRule(
true,
metadataByExact("must-not-matter"),
),
},
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap("managed-prefix-annotations-ignored", nil, nil),
)
})
It("skips controller-managed objects even when a deny rule would otherwise match", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"blocked": metadataValueRule(
false,
metadataByExact("true"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"controller-managed-deny-skipped",
map[string]string{
meta.ManagedByCapsuleLabel: meta.ValueController,
"blocked": "false",
},
nil,
),
)
})
It("still enforces controller skip rule case-sensitively", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"blocked": metadataValueRule(
false,
metadataByExact("true"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"controller-managed-case-sensitive",
map[string]string{
meta.ManagedByCapsuleLabel: "Controller",
"blocked": "true",
},
nil,
),
"metadata",
"blocked",
"true",
"denied",
)
})
It("allows value after later allow overrides an earlier deny for the same key", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
false,
metadataByExact("test"),
),
},
nil,
),
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("test"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectAllowed(
cs,
ns.Name,
configMap(
"later-allow-overrides-earlier-deny",
map[string]string{
"env": "test",
},
nil,
),
)
})
It("denies value after later deny overrides an earlier allow for the same key", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
metadataRule(
rules.ActionTypeAllow,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
true,
metadataByExact("test"),
),
},
nil,
),
metadataRule(
rules.ActionTypeDeny,
"*",
[]string{"ConfigMap"},
map[string]rules.MetadataValueRule{
"env": metadataValueRule(
false,
metadataByExact("test"),
),
},
nil,
),
})
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createConfigMapAndExpectDenied(
cs,
ns.Name,
configMap(
"later-deny-overrides-earlier-allow",
map[string]string{
"env": "test",
},
nil,
),
"metadata",
"env",
"test",
"denied",
)
})
})