mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
* 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>
2183 lines
45 KiB
Go
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",
|
|
)
|
|
})
|
|
})
|