mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-18 20:17:04 +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>
1135 lines
32 KiB
Go
1135 lines
32 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"
|
|
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"
|
|
"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 (
|
|
targetContainers = []rules.WorkloadValidationTarget{
|
|
"pod/containers",
|
|
}
|
|
targetEphemeralContainers = []rules.WorkloadValidationTarget{
|
|
"pod/ephemeralcontainers",
|
|
}
|
|
targetInitContainers = []rules.WorkloadValidationTarget{
|
|
"pod/initcontainers",
|
|
}
|
|
targetVolumes = []rules.WorkloadValidationTarget{
|
|
"pod/volumes",
|
|
}
|
|
)
|
|
|
|
var _ = Describe("enforcing container registry namespace rules", Ordered, Label("tenant", "rules", "images", "registry"), func() {
|
|
const ownerName = "e2e-rules-registry"
|
|
|
|
var tnt *capsulev1beta2.Tenant
|
|
|
|
registryByExpression := func(expression string) rules.OCIRegistry {
|
|
return rules.OCIRegistry{
|
|
ExpressionMatch: runtime.ExpressionMatch{
|
|
ExpressionRegex: runtime.ExpressionRegex{
|
|
Expression: expression,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
registryByNegatedExpression := func(expression string) rules.OCIRegistry {
|
|
return rules.OCIRegistry{
|
|
ExpressionMatch: runtime.ExpressionMatch{
|
|
ExpressionRegex: runtime.ExpressionRegex{
|
|
Expression: expression,
|
|
Negate: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
registryByExact := func(exact ...string) rules.OCIRegistry {
|
|
return rules.OCIRegistry{
|
|
ExpressionMatch: runtime.ExpressionMatch{
|
|
Exact: exact,
|
|
},
|
|
}
|
|
}
|
|
|
|
registryByMatch := func(exact []string, expression string) rules.OCIRegistry {
|
|
return rules.OCIRegistry{
|
|
ExpressionMatch: runtime.ExpressionMatch{
|
|
ExpressionRegex: runtime.ExpressionRegex{
|
|
Expression: expression,
|
|
},
|
|
Exact: exact,
|
|
},
|
|
}
|
|
}
|
|
|
|
newTenant := func() *capsulev1beta2.Tenant {
|
|
return &capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "e2e-rule-registry",
|
|
Labels: map[string]string{
|
|
"env": "e2e",
|
|
},
|
|
},
|
|
Spec: capsulev1beta2.TenantSpec{
|
|
Owners: rbac.OwnerListSpec{
|
|
{
|
|
CoreOwnerSpec: rbac.CoreOwnerSpec{
|
|
UserSpec: rbac.UserSpec{
|
|
Name: ownerName,
|
|
Kind: "User",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Rules: []*rules.NamespaceRuleBodyTenant{
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("harbor/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeDeny,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("harbor/customer/containers/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeDeny,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetInitContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("harbor/customer/init/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeDeny,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetEphemeralContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("harbor/customer/debug/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeDeny,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetVolumes,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("harbor/customer/volume/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAudit,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("audit/containers/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"allow-audit-containers": "true",
|
|
},
|
|
},
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("audit/containers/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAudit,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetVolumes,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("audit/volumes/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"allow-audit-volumes": "true",
|
|
},
|
|
},
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetVolumes,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("audit/volumes/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExact(
|
|
"exact/containers/app:1",
|
|
"exact/containers/app:2",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeDeny,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExact("exact/containers/blocked:1"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByMatch(
|
|
[]string{"combined/exact/app:1"},
|
|
"combined/regex/.*",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
{
|
|
ExpressionMatch: runtime.ExpressionMatch{
|
|
ExpressionRegex: runtime.ExpressionRegex{
|
|
Expression: "policy/.*",
|
|
},
|
|
},
|
|
Policy: []corev1.PullPolicy{
|
|
corev1.PullNever,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"environment": "prod",
|
|
},
|
|
},
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("harbor/customer/containers/prod/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"negate": "true",
|
|
},
|
|
},
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeDeny,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByNegatedExpression("trusted/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
NamespaceSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"negate": "true",
|
|
},
|
|
},
|
|
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
|
Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Action: rules.ActionTypeAllow,
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: targetContainers,
|
|
Registries: []rules.OCIRegistry{
|
|
registryByExpression("trusted/.*"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
type expectedStatusRule struct {
|
|
action rules.ActionType
|
|
targets []rules.WorkloadValidationTarget
|
|
expressions []string
|
|
exact [][]string
|
|
negated []bool
|
|
}
|
|
|
|
expectNamespaceStatusRules := func(nsName string, want []expectedStatusRule) {
|
|
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))
|
|
|
|
if len(expected.targets) == 0 {
|
|
g.Expect(got.Enforce.Workloads.Targets).To(BeEmpty())
|
|
} else {
|
|
g.Expect(got.Enforce.Workloads.Targets).To(Equal(expected.targets))
|
|
}
|
|
|
|
wantRegistries := len(expected.expressions)
|
|
if len(expected.exact) > wantRegistries {
|
|
wantRegistries = len(expected.exact)
|
|
}
|
|
|
|
g.Expect(got.Enforce.Workloads.Registries).To(HaveLen(wantRegistries))
|
|
|
|
for j := 0; j < wantRegistries; j++ {
|
|
match := got.Enforce.Workloads.Registries[j].ExpressionMatch
|
|
|
|
if len(expected.expressions) > j {
|
|
g.Expect(match.Expression).To(Equal(expected.expressions[j]))
|
|
} else {
|
|
g.Expect(match.Expression).To(BeEmpty())
|
|
}
|
|
|
|
if len(expected.exact) > j {
|
|
g.Expect(match.Exact).To(Equal(expected.exact[j]))
|
|
} else {
|
|
g.Expect(match.Exact).To(BeEmpty())
|
|
}
|
|
|
|
if len(expected.negated) > j {
|
|
g.Expect(match.Negate).To(Equal(expected.negated[j]))
|
|
} else {
|
|
g.Expect(match.Negate).To(BeFalse())
|
|
}
|
|
}
|
|
}
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
}
|
|
|
|
baseStatusRules := func() []expectedStatusRule {
|
|
return []expectedStatusRule{
|
|
{
|
|
action: rules.ActionTypeAllow,
|
|
expressions: []string{"harbor/.*"},
|
|
},
|
|
{
|
|
action: rules.ActionTypeDeny,
|
|
targets: targetContainers,
|
|
expressions: []string{"harbor/customer/containers/.*"},
|
|
},
|
|
{
|
|
action: rules.ActionTypeDeny,
|
|
targets: targetInitContainers,
|
|
expressions: []string{"harbor/customer/init/.*"},
|
|
},
|
|
{
|
|
action: rules.ActionTypeDeny,
|
|
targets: targetEphemeralContainers,
|
|
expressions: []string{"harbor/customer/debug/.*"},
|
|
},
|
|
{
|
|
action: rules.ActionTypeDeny,
|
|
targets: targetVolumes,
|
|
expressions: []string{"harbor/customer/volume/.*"},
|
|
},
|
|
{
|
|
action: rules.ActionTypeAudit,
|
|
targets: targetContainers,
|
|
expressions: []string{"audit/containers/.*"},
|
|
},
|
|
{
|
|
action: rules.ActionTypeAudit,
|
|
targets: targetVolumes,
|
|
expressions: []string{"audit/volumes/.*"},
|
|
},
|
|
{
|
|
action: rules.ActionTypeAllow,
|
|
targets: targetContainers,
|
|
exact: [][]string{
|
|
{
|
|
"exact/containers/app:1",
|
|
"exact/containers/app:2",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
action: rules.ActionTypeDeny,
|
|
targets: targetContainers,
|
|
exact: [][]string{
|
|
{
|
|
"exact/containers/blocked:1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
action: rules.ActionTypeAllow,
|
|
targets: targetContainers,
|
|
expressions: []string{"combined/regex/.*"},
|
|
exact: [][]string{
|
|
{
|
|
"combined/exact/app:1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
action: rules.ActionTypeAllow,
|
|
targets: targetContainers,
|
|
expressions: []string{"policy/.*"},
|
|
},
|
|
}
|
|
}
|
|
|
|
createPodAndExpectDenied := func(cs kubernetes.Interface, nsName string, pod *corev1.Pod, substrings ...string) {
|
|
base := pod.DeepCopy()
|
|
baseName := base.Name
|
|
if baseName == "" {
|
|
baseName = "pod"
|
|
}
|
|
|
|
Eventually(func() error {
|
|
p := base.DeepCopy()
|
|
p.Name = fmt.Sprintf("%s-%d", baseName, time.Now().UnixNano()%1e6)
|
|
|
|
_, err := cs.CoreV1().Pods(nsName).Create(context.Background(), p, metav1.CreateOptions{})
|
|
if err == nil {
|
|
_ = cs.CoreV1().Pods(nsName).Delete(context.Background(), p.Name, metav1.DeleteOptions{})
|
|
|
|
return fmt.Errorf("expected 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())
|
|
}
|
|
|
|
createPodAndExpectAllowed := func(cs kubernetes.Interface, nsName string, pod *corev1.Pod) {
|
|
EventuallyCreation(func() error {
|
|
_, err := cs.CoreV1().Pods(nsName).Create(context.Background(), pod, metav1.CreateOptions{})
|
|
|
|
return err
|
|
}).Should(Succeed())
|
|
}
|
|
|
|
updatePodAndExpectDenied := func(cs kubernetes.Interface, nsName string, podName string, mutate func(*corev1.Pod), substrings ...string) {
|
|
Eventually(func() error {
|
|
pod, err := cs.CoreV1().Pods(nsName).Get(context.Background(), podName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mutate(pod)
|
|
|
|
_, err = cs.CoreV1().Pods(nsName).Update(context.Background(), pod, metav1.UpdateOptions{})
|
|
if err == nil {
|
|
return fmt.Errorf("expected 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())
|
|
}
|
|
|
|
restrictedPod := func(name string, image string, pullPolicy corev1.PullPolicy) *corev1.Pod {
|
|
return &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
SecurityContext: nobodyPodSecurityContext(),
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "c",
|
|
Image: image,
|
|
ImagePullPolicy: pullPolicy,
|
|
SecurityContext: restrictedContainerSecurityContext(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
expectAuditEvent := func(
|
|
cs kubernetes.Interface,
|
|
namespace string,
|
|
podName 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 != "Pod" {
|
|
continue
|
|
}
|
|
|
|
eventPodName := e.InvolvedObject.Name
|
|
if eventPodName != podName && !strings.HasPrefix(eventPodName, podName+"-") {
|
|
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 pod %q containing %q",
|
|
podName,
|
|
substrings,
|
|
)
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
}
|
|
|
|
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("audits a matching image but still denies when no allow rule matches", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("audit-denied", "audit/containers/team/app:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectDenied(clusterAdminClient(), ns.Name, pod,
|
|
"containers[0]",
|
|
"audit/containers/team/app:1",
|
|
"not allowed",
|
|
"namespace rule",
|
|
)
|
|
|
|
expectAuditEvent(clusterAdminClient(), ns.Name, pod.Name,
|
|
"matched audit registry rule",
|
|
"audit/containers/.*",
|
|
)
|
|
})
|
|
|
|
It("stores matching tenant rules as independent status rule blocks", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
expectNamespaceStatusRules(ns.GetName(), baseStatusRules())
|
|
})
|
|
|
|
It("stores namespace-selector matched rules as additional independent status rule blocks", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
"environment": "prod",
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
want := baseStatusRules()
|
|
want = append(want, expectedStatusRule{
|
|
action: rules.ActionTypeAllow,
|
|
targets: targetContainers,
|
|
expressions: []string{"harbor/customer/containers/prod/.*"},
|
|
})
|
|
|
|
expectNamespaceStatusRules(ns.GetName(), want)
|
|
})
|
|
|
|
It("stores namespace-selector matched negated regex and explicit allow rules as independent status rule blocks", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
"negate": "true",
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
want := baseStatusRules()
|
|
want = append(want,
|
|
expectedStatusRule{
|
|
action: rules.ActionTypeDeny,
|
|
targets: targetContainers,
|
|
expressions: []string{"trusted/.*"},
|
|
negated: []bool{true},
|
|
},
|
|
expectedStatusRule{
|
|
action: rules.ActionTypeAllow,
|
|
targets: targetContainers,
|
|
expressions: []string{"trusted/.*"},
|
|
},
|
|
)
|
|
|
|
expectNamespaceStatusRules(ns.GetName(), want)
|
|
})
|
|
|
|
It("allows a broad matching allow rule", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("harbor-allowed", "harbor/platform/app:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectAllowed(cs, ns.Name, pod)
|
|
})
|
|
|
|
It("allows an exact array match", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("exact-array-allowed", "exact/containers/app:2", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectAllowed(cs, ns.Name, pod)
|
|
})
|
|
|
|
It("denies a later exact array deny rule", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("exact-array-denied", "exact/containers/blocked:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectDenied(cs, ns.Name, pod,
|
|
"containers[0]",
|
|
"exact/containers/blocked:1",
|
|
"denied",
|
|
)
|
|
})
|
|
|
|
It("allows a combined exact and regex matcher through the exact branch", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("combined-exact-allowed", "combined/exact/app:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectAllowed(cs, ns.Name, pod)
|
|
})
|
|
|
|
It("allows a combined exact and regex matcher through the regex branch", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("combined-regex-allowed", "combined/regex/team/app:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectAllowed(cs, ns.Name, pod)
|
|
})
|
|
|
|
It("denies a later more specific deny rule even when an earlier broad allow rule matched", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("customer-denied", "harbor/customer/containers/app:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectDenied(cs, ns.Name, pod,
|
|
"containers[0]",
|
|
"harbor/customer/containers/app:1",
|
|
"denied",
|
|
"harbor/customer/containers/.*",
|
|
)
|
|
})
|
|
|
|
It("denies an update when the new image matches a later specific deny rule", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("update-to-denied", "harbor/platform/app:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectAllowed(cs, ns.Name, pod)
|
|
|
|
updatePodAndExpectDenied(cs, ns.Name, pod.Name, func(pod *corev1.Pod) {
|
|
pod.Spec.Containers[0].Image = "harbor/customer/containers/app:1"
|
|
},
|
|
"containers[0]",
|
|
"harbor/customer/containers/app:1",
|
|
"denied",
|
|
"harbor/customer/containers/.*",
|
|
)
|
|
})
|
|
|
|
It("allows a later more specific allow rule to override an earlier deny rule in a selected namespace", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
"environment": "prod",
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
denied := restrictedPod("prod-customer-denied", "harbor/customer/containers/other/app:1", corev1.PullIfNotPresent)
|
|
createPodAndExpectDenied(cs, ns.Name, denied,
|
|
"containers[0]",
|
|
"harbor/customer/containers/other/app:1",
|
|
"denied",
|
|
"harbor/customer/containers/.*",
|
|
)
|
|
|
|
allowed := restrictedPod("prod-customer-allowed", "harbor/customer/containers/prod/app:1", corev1.PullIfNotPresent)
|
|
createPodAndExpectAllowed(cs, ns.Name, allowed)
|
|
})
|
|
|
|
It("evaluates init containers with the same multi-rule action semantics", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "init-denied",
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
SecurityContext: nobodyPodSecurityContext(),
|
|
InitContainers: []corev1.Container{
|
|
{
|
|
Name: "init",
|
|
Image: "harbor/customer/init/app:1",
|
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
|
SecurityContext: restrictedContainerSecurityContext(),
|
|
},
|
|
},
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "c",
|
|
Image: "harbor/platform/app:1",
|
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
|
SecurityContext: restrictedContainerSecurityContext(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
createPodAndExpectDenied(cs, ns.Name, pod,
|
|
"initContainers[0]",
|
|
"harbor/customer/init/app:1",
|
|
"denied",
|
|
"harbor/customer/init/.*",
|
|
)
|
|
})
|
|
|
|
It("evaluates image volumes with the same multi-rule action semantics", Label("skip-on-openshift"), func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "volume-denied",
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
SecurityContext: nobodyPodSecurityContext(),
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "c",
|
|
Image: "harbor/platform/app:1",
|
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
|
SecurityContext: restrictedContainerSecurityContext(),
|
|
},
|
|
},
|
|
Volumes: []corev1.Volume{
|
|
{
|
|
Name: "imgvol",
|
|
VolumeSource: corev1.VolumeSource{
|
|
Image: &corev1.ImageVolumeSource{
|
|
Reference: "harbor/customer/volume/app:1",
|
|
PullPolicy: corev1.PullIfNotPresent,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
createPodAndExpectDenied(cs, ns.Name, pod,
|
|
"volumes[0](imgvol)",
|
|
"harbor/customer/volume/app:1",
|
|
"denied",
|
|
"harbor/customer/volume/.*",
|
|
)
|
|
})
|
|
|
|
It("audits image volumes independently but still denies when no allow rule matches", Label("skip-on-openshift"), func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "volume-audit-denied",
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
SecurityContext: nobodyPodSecurityContext(),
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "c",
|
|
Image: "harbor/platform/app:1",
|
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
|
SecurityContext: restrictedContainerSecurityContext(),
|
|
},
|
|
},
|
|
Volumes: []corev1.Volume{
|
|
{
|
|
Name: "imgvol",
|
|
VolumeSource: corev1.VolumeSource{
|
|
Image: &corev1.ImageVolumeSource{
|
|
Reference: "audit/volumes/team/app:1",
|
|
PullPolicy: corev1.PullIfNotPresent,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
createPodAndExpectDenied(cs, ns.Name, pod,
|
|
"volumes[0](imgvol)",
|
|
"audit/volumes/team/app:1",
|
|
"not allowed",
|
|
"namespace rule",
|
|
)
|
|
|
|
expectAuditEvent(clusterAdminClient(), ns.Name, pod.Name,
|
|
"matched audit registry rule",
|
|
"audit/volumes/.*",
|
|
)
|
|
})
|
|
|
|
It("denies adding an ephemeral container when it matches the later specific deny rule", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
cleanupRBAC := GrantEphemeralContainersUpdate(ns.Name, tnt.Spec.Owners[0].UserSpec.Name)
|
|
defer cleanupRBAC()
|
|
|
|
pod := restrictedPod("base", "harbor/platform/app:1", corev1.PullIfNotPresent)
|
|
createPodAndExpectAllowed(cs, ns.Name, pod)
|
|
|
|
ephemeral := corev1.EphemeralContainer{
|
|
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
|
Name: "debug",
|
|
Image: "harbor/customer/debug/app:1",
|
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
|
SecurityContext: restrictedContainerSecurityContext(),
|
|
},
|
|
}
|
|
|
|
Eventually(func() error {
|
|
current, err := cs.CoreV1().Pods(ns.Name).Get(context.Background(), pod.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
current.Spec.EphemeralContainers = []corev1.EphemeralContainer{ephemeral}
|
|
|
|
_, err = cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(
|
|
context.Background(),
|
|
current.Name,
|
|
current,
|
|
metav1.UpdateOptions{},
|
|
)
|
|
if err == nil {
|
|
return fmt.Errorf("expected ephemeral container update to be denied, but it succeeded")
|
|
}
|
|
|
|
msg := err.Error()
|
|
for _, substring := range []string{
|
|
"ephemeralContainers[0]",
|
|
"harbor/customer/debug/app:1",
|
|
"denied",
|
|
"harbor/customer/debug/.*",
|
|
} {
|
|
if !strings.Contains(msg, substring) {
|
|
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
})
|
|
|
|
It("allows an allowed registry reference when its pull policy is permitted", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("policy-allowed", "policy/team/app:1", corev1.PullNever)
|
|
|
|
createPodAndExpectAllowed(cs, ns.Name, pod)
|
|
})
|
|
|
|
It("denies an allowed registry reference when its pull policy is not permitted", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
pod := restrictedPod("policy-denied", "policy/team/app:1", corev1.PullIfNotPresent)
|
|
|
|
createPodAndExpectDenied(cs, ns.Name, pod,
|
|
"containers[0]",
|
|
"policy/team/app:1",
|
|
"pullPolicy=IfNotPresent",
|
|
"allowed: Never",
|
|
)
|
|
})
|
|
|
|
It("applies namespace-selector matched negated regex rules after the base rules", func() {
|
|
ns := NewNamespace("", map[string]string{
|
|
"negate": "true",
|
|
meta.TenantLabel: tnt.GetName(),
|
|
})
|
|
|
|
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
|
|
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
|
|
|
|
denied := restrictedPod("negated-denied", "harbor/platform/app:1", corev1.PullIfNotPresent)
|
|
createPodAndExpectDenied(cs, ns.Name, denied,
|
|
"containers[0]",
|
|
"harbor/platform/app:1",
|
|
"denied",
|
|
"trusted/.*",
|
|
)
|
|
|
|
allowed := restrictedPod("negated-allowed", "trusted/platform/app:1", corev1.PullIfNotPresent)
|
|
createPodAndExpectAllowed(cs, ns.Name, allowed)
|
|
})
|
|
})
|