mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-23 22:46:59 +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>
1088 lines
24 KiB
Go
1088 lines
24 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package validation
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
admissionv1 "k8s.io/api/admission/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
k8sruntime "k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/internal/cache"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
apirules "github.com/projectcapsule/capsule/pkg/api/rules"
|
|
"github.com/projectcapsule/capsule/pkg/api/runtime"
|
|
"github.com/projectcapsule/capsule/pkg/ruleengine"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
)
|
|
|
|
func TestGenericRules(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := GenericRules(nil)
|
|
|
|
h, ok := got.(*genericRules)
|
|
if !ok {
|
|
t.Fatalf("expected *genericRules, got %T", got)
|
|
}
|
|
|
|
if h.regexCache == nil {
|
|
t.Fatalf("expected regex cache")
|
|
}
|
|
|
|
if len(h.rules) != 1 {
|
|
t.Fatalf("expected one generic validator, got %d", len(h.rules))
|
|
}
|
|
|
|
if !h.managedMetadata.HasLabel(meta.TenantLabel) {
|
|
t.Fatalf("expected default managed metadata to include %q", meta.TenantLabel)
|
|
}
|
|
|
|
if len(h.objectSkipRules) == 0 {
|
|
t.Fatalf("expected default object skip rules")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateGenericRules(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("nil object returns nil", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := evaluateGenericRules[runtime.ExpressionMatch](
|
|
nil,
|
|
[]*apirules.NamespaceRuleEnforceBody{
|
|
{
|
|
Action: apirules.ActionTypeAllow,
|
|
},
|
|
},
|
|
genericRuleSet[runtime.ExpressionMatch]{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("expected nil evaluation, got %#v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("empty enforce bodies returns nil", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := evaluateGenericRules[runtime.ExpressionMatch](
|
|
genericMetadataObject(nil, nil),
|
|
nil,
|
|
genericRuleSet[runtime.ExpressionMatch]{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("expected nil evaluation, got %#v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("delegates to ruleengine", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
set := genericRuleSet[runtime.ExpressionMatch]{
|
|
Name: "test",
|
|
EventReason: events.ReasonForbiddenMetadata,
|
|
AllowedDescription: "Allowed test values",
|
|
Values: func(obj genericObject) []ruleengine.Value {
|
|
return []ruleengine.Value{
|
|
{
|
|
Value: obj.GetLabels()["env"],
|
|
Path: `metadata.labels["env"]`,
|
|
},
|
|
}
|
|
},
|
|
Rules: func(enforce *apirules.NamespaceRuleEnforceBody) []runtime.ExpressionMatch {
|
|
return []runtime.ExpressionMatch{
|
|
{
|
|
Exact: []string{"prod"},
|
|
},
|
|
}
|
|
},
|
|
Matches: func(match runtime.ExpressionMatch, value ruleengine.Value) (ruleengine.Match, error) {
|
|
return ruleengine.Match{
|
|
Matched: value.Value == "prod",
|
|
}, nil
|
|
},
|
|
RuleDescription: func(match runtime.ExpressionMatch) string {
|
|
return runtime.DescribeExpressionMatch(match)
|
|
},
|
|
}
|
|
|
|
got, err := evaluateGenericRules(
|
|
genericMetadataObject(map[string]string{"env": "prod"}, nil),
|
|
[]*apirules.NamespaceRuleEnforceBody{
|
|
{
|
|
Action: apirules.ActionTypeAllow,
|
|
},
|
|
},
|
|
set,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatalf("expected evaluation")
|
|
}
|
|
if err := got.BlockingError(); err != nil {
|
|
t.Fatalf("expected no blocking error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateGenericRules(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
baseGVK := schema.GroupVersionKind{
|
|
Group: "",
|
|
Version: "v1",
|
|
Kind: "ConfigMap",
|
|
}
|
|
|
|
t.Run("nil object returns nil and does not call validators", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
t.Fatalf("validator must not be called")
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
nil,
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("sets gvk before running validators", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
obj := genericMetadataObject(nil, nil)
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(obj genericObject, got schema.GroupVersionKind, _ []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
if got != baseGVK {
|
|
t.Fatalf("expected gvk %s, got %s", baseGVK.String(), got.String())
|
|
}
|
|
|
|
if obj.GetObjectKind().GroupVersionKind() != baseGVK {
|
|
t.Fatalf(
|
|
"expected object gvk %s, got %s",
|
|
baseGVK.String(),
|
|
obj.GetObjectKind().GroupVersionKind().String(),
|
|
)
|
|
}
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
obj,
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("skips controller managed objects before validators", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
called := false
|
|
skipRules := meta.DefaultObjectSkipRules()
|
|
|
|
if len(skipRules) == 0 {
|
|
t.Fatalf("expected default object skip rules")
|
|
}
|
|
|
|
labels := map[string]string{}
|
|
for key, value := range skipRules[0].Labels {
|
|
labels[key] = value
|
|
}
|
|
|
|
if len(labels) == 0 {
|
|
t.Fatalf("expected default object skip rule to contain labels")
|
|
}
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(
|
|
genericObject,
|
|
schema.GroupVersionKind,
|
|
[]*apirules.NamespaceRuleEnforceBody,
|
|
) (*ruleengine.Evaluation, error) {
|
|
called = true
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
regexCache: cache.NewRegexCache(),
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: skipRules,
|
|
}
|
|
|
|
obj := genericMetadataObject(labels, nil)
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
obj,
|
|
coreGVK("ConfigMap"),
|
|
&capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "tenant-a",
|
|
},
|
|
},
|
|
testEventRecorder{},
|
|
[]*apirules.NamespaceRuleEnforceBody{
|
|
{
|
|
Action: apirules.ActionTypeAllow,
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
if called {
|
|
t.Fatalf("validator must not be called for skipped object")
|
|
}
|
|
})
|
|
|
|
t.Run("does not skip non matching controller managed label value", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
called := false
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
called = true
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
genericMetadataObject(map[string]string{
|
|
"managed-by": "human",
|
|
}, nil),
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if !called {
|
|
t.Fatalf("expected validator to be called")
|
|
}
|
|
})
|
|
|
|
t.Run("runs validators in order", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
calls := []string{}
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
calls = append(calls, "first")
|
|
|
|
return nil, nil
|
|
},
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
calls = append(calls, "second")
|
|
|
|
return &ruleengine.Evaluation{}, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
genericMetadataObject(nil, nil),
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
if got, want := strings.Join(calls, ","), "first,second"; got != want {
|
|
t.Fatalf("expected calls %q, got %q", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("passes enforce bodies to validators", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
enforceBodies := []*apirules.NamespaceRuleEnforceBody{
|
|
{
|
|
Action: apirules.ActionTypeAllow,
|
|
},
|
|
{
|
|
Action: apirules.ActionTypeDeny,
|
|
},
|
|
}
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(_ genericObject, _ schema.GroupVersionKind, got []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
if len(got) != len(enforceBodies) {
|
|
t.Fatalf("expected %d enforce bodies, got %d", len(enforceBodies), len(got))
|
|
}
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
genericMetadataObject(nil, nil),
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
enforceBodies,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("propagates validator error and stops", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expected := errors.New("boom")
|
|
secondCalled := false
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
return nil, expected
|
|
},
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
secondCalled = true
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
genericMetadataObject(nil, nil),
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
nil,
|
|
)
|
|
if !errors.Is(err, expected) {
|
|
t.Fatalf("expected %v, got %v", expected, err)
|
|
}
|
|
if secondCalled {
|
|
t.Fatalf("second validator must not be called")
|
|
}
|
|
})
|
|
|
|
t.Run("handles audit-only evaluation and continues", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
calls := 0
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
calls++
|
|
|
|
return &ruleengine.Evaluation{
|
|
Audits: []*ruleengine.Decision{
|
|
{
|
|
SetName: "metadata label",
|
|
EventReason: events.ReasonForbiddenMetadata,
|
|
Action: apirules.ActionTypeAudit,
|
|
Value: ruleengine.Value{
|
|
Value: "audit",
|
|
Path: `metadata.labels["audit"]`,
|
|
},
|
|
Message: "audit message",
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
calls++
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
genericMetadataObject(nil, nil),
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if calls != 2 {
|
|
t.Fatalf("expected two validators to be called, got %d", calls)
|
|
}
|
|
})
|
|
|
|
t.Run("returns blocking evaluation error and stops", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
secondCalled := false
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
return &ruleengine.Evaluation{
|
|
Blocking: &ruleengine.Decision{
|
|
SetName: "metadata label",
|
|
EventReason: events.ReasonForbiddenMetadata,
|
|
Action: apirules.ActionTypeDeny,
|
|
Value: ruleengine.Value{
|
|
Value: "stage",
|
|
Path: `metadata.labels["env"]`,
|
|
},
|
|
Message: "blocked",
|
|
},
|
|
}, nil
|
|
},
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
secondCalled = true
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
err := h.validateGenericRules(
|
|
context.Background(),
|
|
admission.Request{},
|
|
genericMetadataObject(nil, nil),
|
|
baseGVK,
|
|
testTenant(),
|
|
testEventRecorder{},
|
|
nil,
|
|
)
|
|
if err == nil {
|
|
t.Fatalf("expected blocking error")
|
|
}
|
|
|
|
var decisionErr *ruleengine.DecisionError
|
|
if !errors.As(err, &decisionErr) {
|
|
t.Fatalf("expected DecisionError, got %T: %v", err, err)
|
|
}
|
|
|
|
if decisionErr.Decision == nil || decisionErr.Decision.Message != "blocked" {
|
|
t.Fatalf("unexpected decision error: %#v", decisionErr.Decision)
|
|
}
|
|
|
|
if secondCalled {
|
|
t.Fatalf("second validator must not be called after blocking decision")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGenericRulesOnCreate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("allows when validators return nil", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
called := false
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
called = true
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
fn := h.OnCreate(
|
|
nil,
|
|
nil,
|
|
genericMetadataObject(nil, nil),
|
|
nil,
|
|
testEventRecorder{},
|
|
testTenant(),
|
|
[]*apirules.NamespaceRuleBodyNamespace{
|
|
{
|
|
Enforce: &apirules.NamespaceRuleEnforceBody{
|
|
Action: apirules.ActionTypeAllow,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
|
|
if resp != nil {
|
|
t.Fatalf("expected nil response, got %#v", resp)
|
|
}
|
|
if !called {
|
|
t.Fatalf("expected validator to be called")
|
|
}
|
|
})
|
|
|
|
t.Run("denies incomplete admission kind", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := &genericRules{
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
fn := h.OnCreate(
|
|
nil,
|
|
nil,
|
|
genericMetadataObject(nil, nil),
|
|
nil,
|
|
testEventRecorder{},
|
|
testTenant(),
|
|
nil,
|
|
)
|
|
|
|
resp := fn(context.Background(), admission.Request{
|
|
AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Kind: metav1.GroupVersionKind{
|
|
Version: "",
|
|
Kind: "ConfigMap",
|
|
},
|
|
},
|
|
})
|
|
if resp == nil {
|
|
t.Fatalf("expected denial response")
|
|
}
|
|
if resp.Allowed {
|
|
t.Fatalf("expected denied response")
|
|
}
|
|
if !strings.Contains(resp.Result.Message, "admission request kind is incomplete") {
|
|
t.Fatalf("expected incomplete kind error, got %q", resp.Result.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("denies validator error", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(genericObject, schema.GroupVersionKind, []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
return nil, errors.New("validator failed")
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
fn := h.OnCreate(
|
|
nil,
|
|
nil,
|
|
genericMetadataObject(nil, nil),
|
|
nil,
|
|
testEventRecorder{},
|
|
testTenant(),
|
|
nil,
|
|
)
|
|
|
|
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
|
|
if resp == nil {
|
|
t.Fatalf("expected denial response")
|
|
}
|
|
if resp.Allowed {
|
|
t.Fatalf("expected denied response")
|
|
}
|
|
if !strings.Contains(resp.Result.Message, "validator failed") {
|
|
t.Fatalf("expected validator error, got %q", resp.Result.Message)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGenericRulesOnUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("uses new object and allows when validators return nil", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
calledWithNewObject := false
|
|
|
|
oldObj := genericMetadataObject(map[string]string{"old": "true"}, nil)
|
|
newObj := genericMetadataObject(map[string]string{"new": "true"}, nil)
|
|
|
|
h := &genericRules{
|
|
rules: []genericRuleValidator{
|
|
func(obj genericObject, _ schema.GroupVersionKind, _ []*apirules.NamespaceRuleEnforceBody) (*ruleengine.Evaluation, error) {
|
|
calledWithNewObject = obj.GetLabels()["new"] == "true"
|
|
|
|
return nil, nil
|
|
},
|
|
},
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
fn := h.OnUpdate(
|
|
nil,
|
|
nil,
|
|
oldObj,
|
|
newObj,
|
|
nil,
|
|
testEventRecorder{},
|
|
testTenant(),
|
|
nil,
|
|
)
|
|
|
|
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
|
|
if resp != nil {
|
|
t.Fatalf("expected nil response, got %#v", resp)
|
|
}
|
|
if !calledWithNewObject {
|
|
t.Fatalf("expected validator to receive the new object")
|
|
}
|
|
})
|
|
|
|
t.Run("denies incomplete admission kind", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := &genericRules{
|
|
managedMetadata: meta.NewManagedMetadata(nil, nil),
|
|
objectSkipRules: meta.DefaultObjectSkipRules(),
|
|
}
|
|
|
|
fn := h.OnUpdate(
|
|
nil,
|
|
nil,
|
|
genericMetadataObject(nil, nil),
|
|
genericMetadataObject(nil, nil),
|
|
nil,
|
|
testEventRecorder{},
|
|
testTenant(),
|
|
nil,
|
|
)
|
|
|
|
resp := fn(context.Background(), admission.Request{
|
|
AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Kind: metav1.GroupVersionKind{
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "",
|
|
},
|
|
},
|
|
})
|
|
if resp == nil {
|
|
t.Fatalf("expected denial response")
|
|
}
|
|
if resp.Allowed {
|
|
t.Fatalf("expected denied response")
|
|
}
|
|
if !strings.Contains(resp.Result.Message, "admission request kind is incomplete") {
|
|
t.Fatalf("expected incomplete kind error, got %q", resp.Result.Message)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGenericRulesOnDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := &genericRules{}
|
|
|
|
fn := h.OnDelete(
|
|
nil,
|
|
nil,
|
|
genericMetadataObject(nil, nil),
|
|
nil,
|
|
testEventRecorder{},
|
|
testTenant(),
|
|
nil,
|
|
)
|
|
|
|
resp := fn(context.Background(), admissionRequest("v1", "ConfigMap"))
|
|
if resp != nil {
|
|
t.Fatalf("expected nil response, got %#v", resp)
|
|
}
|
|
}
|
|
|
|
func TestGroupVersionKind(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
req admission.Request
|
|
want schema.GroupVersionKind
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "core v1 kind",
|
|
req: admissionRequest("v1", "ConfigMap"),
|
|
want: schema.GroupVersionKind{
|
|
Group: "",
|
|
Version: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
},
|
|
{
|
|
name: "grouped kind",
|
|
req: admission.Request{
|
|
AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Kind: metav1.GroupVersionKind{
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
},
|
|
},
|
|
},
|
|
want: schema.GroupVersionKind{
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
},
|
|
},
|
|
{
|
|
name: "missing version",
|
|
req: admission.Request{
|
|
AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Kind: metav1.GroupVersionKind{
|
|
Group: "",
|
|
Version: "",
|
|
Kind: "ConfigMap",
|
|
},
|
|
},
|
|
},
|
|
wantErr: "admission request kind is incomplete",
|
|
},
|
|
{
|
|
name: "missing kind",
|
|
req: admission.Request{
|
|
AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Kind: metav1.GroupVersionKind{
|
|
Group: "",
|
|
Version: "v1",
|
|
Kind: "",
|
|
},
|
|
},
|
|
},
|
|
wantErr: "admission request kind is incomplete",
|
|
},
|
|
{
|
|
name: "empty request",
|
|
req: admission.Request{},
|
|
wantErr: "admission request kind is incomplete",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := groupVersionKind(tt.req)
|
|
if tt.wantErr != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("expected error containing %q, got %v", tt.wantErr, err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
if got != tt.want {
|
|
t.Fatalf("expected %s, got %s", tt.want.String(), got.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func genericMetadataObject(
|
|
labels map[string]string,
|
|
annotations map[string]string,
|
|
) genericObject {
|
|
return &metav1.PartialObjectMetadata{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "object",
|
|
Namespace: "tenant-a",
|
|
Labels: labels,
|
|
Annotations: annotations,
|
|
},
|
|
}
|
|
}
|
|
|
|
var (
|
|
_ events.EventRecorder = testEventRecorder{}
|
|
_ events.LabeledEvent = (*testLabeledEvent)(nil)
|
|
)
|
|
|
|
type testEventRecorder struct{}
|
|
|
|
func (testEventRecorder) Eventf(
|
|
k8sruntime.Object,
|
|
k8sruntime.Object,
|
|
string,
|
|
string,
|
|
string,
|
|
string,
|
|
...interface{},
|
|
) {
|
|
}
|
|
|
|
func (testEventRecorder) LabeledEvent(
|
|
regarding k8sruntime.Object,
|
|
eventType string,
|
|
reason string,
|
|
action string,
|
|
note string,
|
|
) events.LabeledEvent {
|
|
return &testLabeledEvent{
|
|
regarding: regarding,
|
|
eventType: eventType,
|
|
reason: reason,
|
|
action: action,
|
|
note: note,
|
|
labels: map[string]string{},
|
|
annotations: map[string]string{},
|
|
}
|
|
}
|
|
|
|
type testLabeledEvent struct {
|
|
regarding k8sruntime.Object
|
|
related k8sruntime.Object
|
|
|
|
eventType string
|
|
reason string
|
|
action string
|
|
note string
|
|
|
|
labels map[string]string
|
|
annotations map[string]string
|
|
}
|
|
|
|
func (*testLabeledEvent) Emit(context.Context) {}
|
|
|
|
func (e *testLabeledEvent) WithRelated(obj k8sruntime.Object) events.LabeledEvent {
|
|
e.related = obj
|
|
|
|
return e
|
|
}
|
|
|
|
func (e *testLabeledEvent) WithLabels(labels map[string]string) events.LabeledEvent {
|
|
for key, value := range labels {
|
|
e.labels[key] = value
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func (e *testLabeledEvent) WithAnnotations(annotations map[string]string) events.LabeledEvent {
|
|
for key, value := range annotations {
|
|
e.annotations[key] = value
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func (e *testLabeledEvent) WithTenantLabel(tnt *capsulev1beta2.Tenant) events.LabeledEvent {
|
|
if tnt != nil {
|
|
e.labels[meta.NewTenantLabel] = tnt.Name
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func (e *testLabeledEvent) WithRequestAnnotations(req admission.Request) events.LabeledEvent {
|
|
if req.UID != "" {
|
|
e.annotations[meta.AuditRequestUID] = string(req.UID)
|
|
}
|
|
|
|
if req.UserInfo.Username != "" {
|
|
e.annotations[meta.AuditUsername] = req.UserInfo.Username
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func (e *testLabeledEvent) Reason() string {
|
|
return e.reason
|
|
}
|
|
|
|
func (e *testLabeledEvent) Action() string {
|
|
return e.action
|
|
}
|
|
|
|
func (e *testLabeledEvent) Regarding() k8sruntime.Object {
|
|
return e.regarding
|
|
}
|
|
|
|
func (e *testLabeledEvent) Labels() map[string]string {
|
|
return e.labels
|
|
}
|
|
|
|
func (e *testLabeledEvent) Annotations() map[string]string {
|
|
return e.annotations
|
|
}
|
|
|
|
func (e *testLabeledEvent) Note() string {
|
|
return e.note
|
|
}
|
|
|
|
func (e *testLabeledEvent) EventType() string {
|
|
return e.eventType
|
|
}
|
|
|
|
func (e *testLabeledEvent) Related() k8sruntime.Object {
|
|
return e.related
|
|
}
|
|
|
|
func admissionRequest(
|
|
apiVersion string,
|
|
kind string,
|
|
) admission.Request {
|
|
gv, err := schema.ParseGroupVersion(apiVersion)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return admission.Request{
|
|
AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Kind: metav1.GroupVersionKind{
|
|
Group: gv.Group,
|
|
Version: gv.Version,
|
|
Kind: kind,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func testTenant() *capsulev1beta2.Tenant {
|
|
return &capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "tenant-a",
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGenericRulesWithRealMetadataValidatorSmoke(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := GenericRules(cache.NewRegexCache()).(*genericRules)
|
|
|
|
evaluation, err := h.validateMetadata(
|
|
genericMetadataObject(map[string]string{"env": "prod"}, nil),
|
|
schema.GroupVersionKind{
|
|
Group: "",
|
|
Version: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
[]*apirules.NamespaceRuleEnforceBody{
|
|
{
|
|
Action: apirules.ActionTypeAllow,
|
|
Metadata: []apirules.MetadataRule{
|
|
{
|
|
VersionKinds: gvkVersionKinds([]string{"*"}, "ConfigMap"),
|
|
Labels: map[string]apirules.MetadataValueRule{
|
|
"env": {
|
|
Required: true,
|
|
Values: []runtime.ExpressionMatch{
|
|
{
|
|
Exact: []string{"prod"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if evaluation == nil {
|
|
t.Fatalf("expected evaluation")
|
|
}
|
|
if err := evaluation.BlockingError(); err != nil {
|
|
t.Fatalf("expected no blocking error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func gvkVersionKinds(apiVersion []string, kinds ...string) runtime.VersionKinds {
|
|
return runtime.VersionKinds{
|
|
APIGroups: apiVersion,
|
|
Kinds: kinds,
|
|
}
|
|
}
|