// 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)
	})
})
