2004 lines
64 KiB
Go
2004 lines
64 KiB
Go
/*
|
|
Copyright 2022 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package validating_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
|
v1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
utiljson "k8s.io/apimachinery/pkg/util/json"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apiserver/pkg/admission"
|
|
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
|
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
|
"k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
|
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
"k8s.io/apiserver/pkg/warning"
|
|
"k8s.io/client-go/kubernetes"
|
|
)
|
|
|
|
var (
|
|
clusterScopedParamsGVK schema.GroupVersionKind = schema.GroupVersionKind{
|
|
Group: "example.com",
|
|
Version: "v1",
|
|
Kind: "ClusterScopedParamsConfig",
|
|
}
|
|
|
|
paramsGVK schema.GroupVersionKind = schema.GroupVersionKind{
|
|
Group: "example.com",
|
|
Version: "v1",
|
|
Kind: "ParamsConfig",
|
|
}
|
|
|
|
// Common objects
|
|
denyPolicy *admissionregistrationv1.ValidatingAdmissionPolicy = &admissionregistrationv1.ValidatingAdmissionPolicy{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "denypolicy.example.com",
|
|
ResourceVersion: "1",
|
|
},
|
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{
|
|
ParamKind: &admissionregistrationv1.ParamKind{
|
|
APIVersion: paramsGVK.GroupVersion().String(),
|
|
Kind: paramsGVK.Kind,
|
|
},
|
|
FailurePolicy: ptrTo(admissionregistrationv1.Fail),
|
|
Validations: []admissionregistrationv1.Validation{
|
|
{
|
|
Expression: "messageId for deny policy",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
fakeParams *unstructured.Unstructured = &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": paramsGVK.GroupVersion().String(),
|
|
"kind": paramsGVK.Kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": "replicas-test.example.com",
|
|
"namespace": "default",
|
|
"resourceVersion": "1",
|
|
},
|
|
"maxReplicas": int64(3),
|
|
},
|
|
}
|
|
|
|
denyBinding *admissionregistrationv1.ValidatingAdmissionPolicyBinding = &admissionregistrationv1.ValidatingAdmissionPolicyBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "denybinding.example.com",
|
|
ResourceVersion: "1",
|
|
},
|
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{
|
|
PolicyName: denyPolicy.Name,
|
|
ParamRef: &admissionregistrationv1.ParamRef{
|
|
Name: fakeParams.GetName(),
|
|
Namespace: fakeParams.GetNamespace(),
|
|
// fake object tracker does not populate defaults
|
|
ParameterNotFoundAction: ptrTo(admissionregistrationv1.DenyAction),
|
|
},
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny},
|
|
},
|
|
}
|
|
denyBindingWithNoParamRef *admissionregistrationv1.ValidatingAdmissionPolicyBinding = &admissionregistrationv1.ValidatingAdmissionPolicyBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "denybinding.example.com",
|
|
ResourceVersion: "1",
|
|
},
|
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{
|
|
PolicyName: denyPolicy.Name,
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny},
|
|
},
|
|
}
|
|
|
|
denyBindingWithAudit = &admissionregistrationv1.ValidatingAdmissionPolicyBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "denybinding.example.com",
|
|
ResourceVersion: "1",
|
|
},
|
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{
|
|
PolicyName: denyPolicy.Name,
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Audit},
|
|
},
|
|
}
|
|
denyBindingWithWarn = &admissionregistrationv1.ValidatingAdmissionPolicyBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "denybinding.example.com",
|
|
ResourceVersion: "1",
|
|
},
|
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{
|
|
PolicyName: denyPolicy.Name,
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Warn},
|
|
},
|
|
}
|
|
denyBindingWithAll = &admissionregistrationv1.ValidatingAdmissionPolicyBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "denybinding.example.com",
|
|
ResourceVersion: "1",
|
|
},
|
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{
|
|
PolicyName: denyPolicy.Name,
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny, admissionregistrationv1.Warn, admissionregistrationv1.Audit},
|
|
},
|
|
}
|
|
)
|
|
|
|
func newParam(name, namespace string, labels map[string]string) *unstructured.Unstructured {
|
|
if len(namespace) == 0 {
|
|
namespace = metav1.NamespaceDefault
|
|
}
|
|
res := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": paramsGVK.GroupVersion().String(),
|
|
"kind": paramsGVK.Kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
"namespace": namespace,
|
|
"resourceVersion": "1",
|
|
},
|
|
},
|
|
}
|
|
res.SetLabels(labels)
|
|
return res
|
|
}
|
|
|
|
func newClusterScopedParam(name string, labels map[string]string) *unstructured.Unstructured {
|
|
res := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": clusterScopedParamsGVK.GroupVersion().String(),
|
|
"kind": clusterScopedParamsGVK.Kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
"resourceVersion": "1",
|
|
},
|
|
},
|
|
}
|
|
res.SetLabels(labels)
|
|
return res
|
|
}
|
|
|
|
var _ validating.Validator = validateFunc(nil)
|
|
|
|
type validateFunc func(
|
|
ctx context.Context,
|
|
matchResource schema.GroupVersionResource,
|
|
versionedAttr *admission.VersionedAttributes,
|
|
versionedParams runtime.Object,
|
|
namespace *v1.Namespace,
|
|
runtimeCELCostBudget int64,
|
|
authz authorizer.Authorizer) validating.ValidateResult
|
|
|
|
type fakeCompiler struct {
|
|
ValidateFuncs map[types.NamespacedName]validating.Validator
|
|
|
|
lock sync.Mutex
|
|
NumCompiles map[types.NamespacedName]int
|
|
}
|
|
|
|
func (f *fakeCompiler) getNumCompiles(p *validating.Policy) int {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
return f.NumCompiles[types.NamespacedName{
|
|
Name: p.Name,
|
|
Namespace: p.Namespace,
|
|
}]
|
|
}
|
|
|
|
func (f *fakeCompiler) RegisterDefinition(definition *validating.Policy, vf validateFunc) {
|
|
if f.ValidateFuncs == nil {
|
|
f.ValidateFuncs = make(map[types.NamespacedName]validating.Validator)
|
|
}
|
|
|
|
f.ValidateFuncs[types.NamespacedName{
|
|
Name: definition.Name,
|
|
Namespace: definition.Namespace,
|
|
}] = vf
|
|
}
|
|
|
|
func (f *fakeCompiler) CompilePolicy(policy *validating.Policy) validating.Validator {
|
|
nn := types.NamespacedName{
|
|
Name: policy.Name,
|
|
Namespace: policy.Namespace,
|
|
}
|
|
|
|
defer func() {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
if f.NumCompiles == nil {
|
|
f.NumCompiles = make(map[types.NamespacedName]int)
|
|
}
|
|
f.NumCompiles[nn]++
|
|
}()
|
|
return f.ValidateFuncs[nn]
|
|
}
|
|
|
|
func (f validateFunc) Validate(
|
|
ctx context.Context,
|
|
matchResource schema.GroupVersionResource,
|
|
versionedAttr *admission.VersionedAttributes,
|
|
versionedParams runtime.Object,
|
|
namespace *v1.Namespace,
|
|
runtimeCELCostBudget int64,
|
|
authz authorizer.Authorizer,
|
|
) validating.ValidateResult {
|
|
return f(
|
|
ctx,
|
|
matchResource,
|
|
versionedAttr,
|
|
versionedParams,
|
|
namespace,
|
|
runtimeCELCostBudget,
|
|
authz,
|
|
)
|
|
}
|
|
|
|
var _ generic.PolicyMatcher = &fakeMatcher{}
|
|
|
|
func (f *fakeMatcher) ValidateInitialization() error {
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMatcher) GetNamespace(name string) (*v1.Namespace, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type fakeMatcher struct {
|
|
DefaultMatch bool
|
|
DefinitionMatchFuncs map[types.NamespacedName]func(generic.PolicyAccessor, admission.Attributes) bool
|
|
BindingMatchFuncs map[types.NamespacedName]func(generic.BindingAccessor, admission.Attributes) bool
|
|
}
|
|
|
|
func (f *fakeMatcher) RegisterDefinition(definition *admissionregistrationv1.ValidatingAdmissionPolicy, matchFunc func(generic.PolicyAccessor, admission.Attributes) bool) {
|
|
namespace, name := definition.Namespace, definition.Name
|
|
key := types.NamespacedName{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
}
|
|
|
|
if matchFunc != nil {
|
|
if f.DefinitionMatchFuncs == nil {
|
|
f.DefinitionMatchFuncs = make(map[types.NamespacedName]func(generic.PolicyAccessor, admission.Attributes) bool)
|
|
}
|
|
f.DefinitionMatchFuncs[key] = matchFunc
|
|
}
|
|
}
|
|
|
|
func (f *fakeMatcher) RegisterBinding(binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding, matchFunc func(generic.BindingAccessor, admission.Attributes) bool) {
|
|
namespace, name := binding.Namespace, binding.Name
|
|
key := types.NamespacedName{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
}
|
|
|
|
if matchFunc != nil {
|
|
if f.BindingMatchFuncs == nil {
|
|
f.BindingMatchFuncs = make(map[types.NamespacedName]func(generic.BindingAccessor, admission.Attributes) bool)
|
|
}
|
|
f.BindingMatchFuncs[key] = matchFunc
|
|
}
|
|
}
|
|
|
|
// Matches says whether this policy definition matches the provided admission
|
|
// resource request
|
|
func (f *fakeMatcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition generic.PolicyAccessor) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
|
|
namespace, name := definition.GetNamespace(), definition.GetName()
|
|
key := types.NamespacedName{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
}
|
|
if fun, ok := f.DefinitionMatchFuncs[key]; ok {
|
|
return fun(definition, a), a.GetResource(), a.GetKind(), nil
|
|
}
|
|
|
|
// Default is match everything
|
|
return f.DefaultMatch, a.GetResource(), a.GetKind(), nil
|
|
}
|
|
|
|
// Matches says whether this policy definition matches the provided admission
|
|
// resource request
|
|
func (f *fakeMatcher) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding generic.BindingAccessor) (bool, error) {
|
|
namespace, name := binding.GetNamespace(), binding.GetName()
|
|
key := types.NamespacedName{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
}
|
|
if fun, ok := f.BindingMatchFuncs[key]; ok {
|
|
return fun(binding, a), nil
|
|
}
|
|
|
|
// Default is match everything
|
|
return f.DefaultMatch, nil
|
|
}
|
|
|
|
func setupFakeTest(t *testing.T, comp *fakeCompiler, match *fakeMatcher) *generic.PolicyTestContext[*validating.Policy, *validating.PolicyBinding, validating.Validator] {
|
|
return setupTestCommon(t, comp, match, true)
|
|
}
|
|
|
|
// Starts CEL admission controller and sets up a plugin configured with it as well
|
|
// as object trackers for manipulating the objects available to the system
|
|
//
|
|
// ParamTracker only knows the gvk `paramGVK`. If in the future we need to
|
|
// support multiple types of params this function needs to be augmented
|
|
//
|
|
// PolicyTracker expects FakePolicyDefinition and FakePolicyBinding types
|
|
// !TODO: refactor this test/framework to remove startInformers argument and
|
|
// clean up the return args, and in general make it more accessible.
|
|
func setupTestCommon(
|
|
t *testing.T,
|
|
compiler *fakeCompiler,
|
|
matcher generic.PolicyMatcher,
|
|
shouldStartInformers bool,
|
|
) *generic.PolicyTestContext[*validating.Policy, *validating.PolicyBinding, validating.Validator] {
|
|
testContext, testContextCancel, err := generic.NewPolicyTestContext(
|
|
t,
|
|
validating.NewValidatingAdmissionPolicyAccessor,
|
|
validating.NewValidatingAdmissionPolicyBindingAccessor,
|
|
func(p *validating.Policy) validating.Validator {
|
|
return compiler.CompilePolicy(p)
|
|
},
|
|
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[validating.PolicyHook] {
|
|
coolMatcher := matcher
|
|
if coolMatcher == nil {
|
|
coolMatcher = generic.NewPolicyMatcher(m)
|
|
}
|
|
return validating.NewDispatcher(a, coolMatcher)
|
|
},
|
|
nil,
|
|
[]meta.RESTMapping{
|
|
{
|
|
Resource: paramsGVK.GroupVersion().WithResource("paramsconfigs"),
|
|
GroupVersionKind: paramsGVK,
|
|
Scope: meta.RESTScopeNamespace,
|
|
},
|
|
{
|
|
Resource: clusterScopedParamsGVK.GroupVersion().WithResource("clusterscopedparamsconfigs"),
|
|
GroupVersionKind: clusterScopedParamsGVK,
|
|
Scope: meta.RESTScopeRoot,
|
|
},
|
|
{
|
|
Resource: schema.GroupVersionResource{Group: "admissionregistration.k8s.io", Version: "v1beta1", Resource: "validatingadmissionpolicies"},
|
|
GroupVersionKind: schema.GroupVersionKind{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingAdmissionPolicy"},
|
|
Scope: meta.RESTScopeRoot,
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(testContextCancel)
|
|
|
|
if shouldStartInformers {
|
|
require.NoError(t, testContext.Start())
|
|
}
|
|
|
|
return testContext
|
|
}
|
|
|
|
func attributeRecord(
|
|
old, new runtime.Object,
|
|
operation admission.Operation,
|
|
) *FakeAttributes {
|
|
if old == nil && new == nil {
|
|
panic("both `old` and `new` may not be nil")
|
|
}
|
|
|
|
// one of old/new may be nil, but not both
|
|
example := new
|
|
if example == nil {
|
|
example = old
|
|
}
|
|
|
|
accessor, err := meta.Accessor(example)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return &FakeAttributes{
|
|
Attributes: admission.NewAttributesRecord(
|
|
new,
|
|
old,
|
|
example.GetObjectKind().GroupVersionKind(),
|
|
accessor.GetNamespace(),
|
|
accessor.GetName(),
|
|
schema.GroupVersionResource{},
|
|
"",
|
|
operation,
|
|
nil,
|
|
false,
|
|
nil,
|
|
),
|
|
}
|
|
}
|
|
|
|
func ptrTo[T any](obj T) *T {
|
|
return &obj
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// Functionality Tests
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
func TestPluginNotReady(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
// Show that an unstarted informer (or one that has failed its listwatch)
|
|
// will show proper error from plugin
|
|
ctx := setupTestCommon(t, compiler, matcher, false)
|
|
err := ctx.Plugin.Dispatch(
|
|
context.Background(),
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns a denial
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, "not yet ready to handle request")
|
|
|
|
// Show that by now starting the informer, the error is dissipated
|
|
ctx = setupTestCommon(t, compiler, matcher, true)
|
|
err = ctx.Plugin.Dispatch(
|
|
context.Background(),
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns a denial
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestBasicPolicyDefinitionFailure(t *testing.T) {
|
|
datalock := sync.Mutex{}
|
|
numCompiles := 0
|
|
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding))
|
|
|
|
warningRecorder := newWarningRecorder()
|
|
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
|
|
attr := attributeRecord(nil, fakeParams, admission.Create)
|
|
err := testContext.Plugin.Dispatch(
|
|
warnCtx,
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns a denial
|
|
attr,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.Equal(t, 0, warningRecorder.len())
|
|
|
|
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
|
|
require.Empty(t, annotations)
|
|
|
|
require.ErrorContains(t, err, `Denied`)
|
|
}
|
|
|
|
// Shows that if a definition does not match the input, it will not be used.
|
|
// But with a different input it will be used.
|
|
func TestDefinitionDoesntMatch(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
datalock := sync.Mutex{}
|
|
passedParams := []*unstructured.Unstructured{}
|
|
numCompiles := 0
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
matcher.RegisterDefinition(denyPolicy, func(vap generic.PolicyAccessor, a admission.Attributes) bool {
|
|
// Match names with even-numbered length
|
|
obj := a.GetObject()
|
|
|
|
accessor, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return false
|
|
}
|
|
|
|
return len(accessor.GetName())%2 == 0
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding))
|
|
|
|
// Validate a non-matching input.
|
|
// Should pass validation with no error.
|
|
|
|
nonMatchingParams := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": paramsGVK.GroupVersion().String(),
|
|
"kind": paramsGVK.Kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": "oddlength",
|
|
"resourceVersion": "1",
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t,
|
|
testContext.Plugin.Dispatch(testContext,
|
|
attributeRecord(
|
|
nil, nonMatchingParams,
|
|
admission.Create), &admission.RuntimeObjectInterfaces{}))
|
|
require.Empty(t, passedParams)
|
|
|
|
// Validate a matching input.
|
|
// Should match and be denied.
|
|
matchingParams := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": paramsGVK.GroupVersion().String(),
|
|
"kind": paramsGVK.Kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": "evenlength",
|
|
"resourceVersion": "1",
|
|
},
|
|
},
|
|
}
|
|
require.ErrorContains(t,
|
|
testContext.Plugin.Dispatch(testContext,
|
|
attributeRecord(
|
|
nil, matchingParams,
|
|
admission.Create), &admission.RuntimeObjectInterfaces{}),
|
|
`Denied`)
|
|
require.Equal(t, 1, numCompiles)
|
|
}
|
|
|
|
func TestReconfigureBinding(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
datalock := sync.Mutex{}
|
|
numCompiles := 0
|
|
|
|
fakeParams2 := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": paramsGVK.GroupVersion().String(),
|
|
"kind": paramsGVK.Kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": "replicas-test2.example.com",
|
|
// fake object tracker does not populate missing namespace
|
|
"namespace": "default",
|
|
"resourceVersion": "2",
|
|
},
|
|
"maxReplicas": int64(35),
|
|
},
|
|
}
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
denyBinding2 := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "denybinding.example.com",
|
|
ResourceVersion: "2",
|
|
},
|
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{
|
|
PolicyName: denyPolicy.Name,
|
|
ParamRef: &admissionregistrationv1.ParamRef{
|
|
Name: fakeParams2.GetName(),
|
|
Namespace: fakeParams2.GetNamespace(),
|
|
ParameterNotFoundAction: ptrTo(admissionregistrationv1.DenyAction),
|
|
},
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding))
|
|
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
// Expect validation to fail for first time due to binding unconditionally
|
|
// failing
|
|
require.ErrorContains(t, err, `Denied`, "expect policy validation error")
|
|
|
|
// Expect `Compile` only called once
|
|
require.Equal(t, 1, numCompiles, "expect `Compile` to be called only once")
|
|
|
|
// Update the tracker to point at different params
|
|
require.NoError(t, testContext.UpdateAndWait(denyBinding2))
|
|
|
|
err = testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, "no params found for policy binding with `Deny` parameterNotFoundAction")
|
|
|
|
// Add the missing params
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams2))
|
|
|
|
// Expect validation to now fail again.
|
|
err = testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
// Expect validation to fail the third time due to validation failure
|
|
require.ErrorContains(t, err, `Denied`, "expected a true policy failure, not a configuration error")
|
|
// require.Equal(t, []*unstructured.Unstructured{fakeParams, fakeParams2}, passedParams, "expected call to `Validate` to cause call to evaluator")
|
|
require.Equal(t, 2, numCompiles, "expect changing binding causes a recompile")
|
|
}
|
|
|
|
// Shows that a policy which is in effect will stop being in effect when removed
|
|
func TestRemoveDefinition(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
datalock := sync.Mutex{}
|
|
numCompiles := 0
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding))
|
|
|
|
record := attributeRecord(nil, fakeParams, admission.Create)
|
|
require.ErrorContains(t,
|
|
testContext.Plugin.Dispatch(
|
|
testContext,
|
|
record,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
),
|
|
`Denied`)
|
|
|
|
require.NoError(t, testContext.DeleteAndWait(denyPolicy))
|
|
|
|
require.NoError(t, testContext.Plugin.Dispatch(
|
|
testContext,
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns a denial
|
|
record,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
))
|
|
}
|
|
|
|
// Shows that a binding which is in effect will stop being in effect when removed
|
|
func TestRemoveBinding(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
datalock := sync.Mutex{}
|
|
numCompiles := 0
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding))
|
|
|
|
record := attributeRecord(nil, fakeParams, admission.Create)
|
|
|
|
require.ErrorContains(t,
|
|
testContext.Plugin.Dispatch(
|
|
testContext,
|
|
record,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
),
|
|
`Denied`)
|
|
|
|
require.NoError(t, testContext.DeleteAndWait(denyBinding))
|
|
}
|
|
|
|
// Shows that an error is surfaced if a paramSource specified in a binding does
|
|
// not actually exist
|
|
func TestInvalidParamSourceGVK(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
passedParams := make(chan *unstructured.Unstructured)
|
|
|
|
badPolicy := *denyPolicy
|
|
badPolicy.Spec.ParamKind = &admissionregistrationv1.ParamKind{
|
|
APIVersion: paramsGVK.GroupVersion().String(),
|
|
Kind: "BadParamKind",
|
|
}
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&badPolicy, denyBinding))
|
|
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
// expect the specific error to be that the param was not found, not that CRD
|
|
// is not existing
|
|
require.ErrorContains(t, err,
|
|
`failed to configure policy: failed to find resource referenced by paramKind: 'example.com/v1, Kind=BadParamKind'`)
|
|
|
|
close(passedParams)
|
|
require.Empty(t, passedParams)
|
|
}
|
|
|
|
// Shows that an error is surfaced if a param specified in a binding does not
|
|
// actually exist
|
|
func TestInvalidParamSourceInstanceName(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
datalock := sync.Mutex{}
|
|
passedParams := []*unstructured.Unstructured{}
|
|
numCompiles := 0
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(denyPolicy, denyBinding))
|
|
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
// expect the specific error to be that the param was not found, not that CRD
|
|
// is not existing
|
|
require.ErrorContains(t, err,
|
|
"no params found for policy binding with `Deny` parameterNotFoundAction")
|
|
require.Empty(t, passedParams)
|
|
}
|
|
|
|
// Show that policy still gets evaluated with `nil` param if paramRef & namespaceParamRef
|
|
// are both unset
|
|
func TestEmptyParamRef(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
datalock := sync.Mutex{}
|
|
numCompiles := 0
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
|
|
// Versioned params must be nil to pass the test
|
|
if versionedParams != nil {
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionAdmit,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(denyPolicy, denyBindingWithNoParamRef))
|
|
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns a denial
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, `Denied`)
|
|
require.Equal(t, 1, numCompiles)
|
|
}
|
|
|
|
// Shows that a definition with no param source works just fine, and has
|
|
// nil params passed to its evaluator.
|
|
//
|
|
// Also shows that if binding has specified params in this instance then they
|
|
// are silently ignored.
|
|
func TestEmptyParamSource(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
datalock := sync.Mutex{}
|
|
numCompiles := 0
|
|
|
|
// Push some fake
|
|
noParamSourcePolicy := *denyPolicy
|
|
noParamSourcePolicy.Spec.ParamKind = nil
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
datalock.Lock()
|
|
numCompiles += 1
|
|
datalock.Unlock()
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithNoParamRef))
|
|
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns a denial
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, `Denied`)
|
|
require.Equal(t, 1, numCompiles)
|
|
}
|
|
|
|
// Shows what happens when multiple policies share one param type, then
|
|
// one policy stops using the param. The expectation is the second policy
|
|
// keeps behaving normally
|
|
func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
// Use ConfigMap native-typed param
|
|
policy1 := *denyPolicy
|
|
policy1.Name = "denypolicy1.example.com"
|
|
policy1.Spec = admissionregistrationv1.ValidatingAdmissionPolicySpec{
|
|
ParamKind: &admissionregistrationv1.ParamKind{
|
|
APIVersion: paramsGVK.GroupVersion().String(),
|
|
Kind: paramsGVK.Kind,
|
|
},
|
|
FailurePolicy: ptrTo(admissionregistrationv1.Fail),
|
|
Validations: []admissionregistrationv1.Validation{
|
|
{
|
|
Expression: "policy1",
|
|
},
|
|
},
|
|
}
|
|
|
|
policy2 := *denyPolicy
|
|
policy2.Name = "denypolicy2.example.com"
|
|
policy2.Spec = admissionregistrationv1.ValidatingAdmissionPolicySpec{
|
|
ParamKind: &admissionregistrationv1.ParamKind{
|
|
APIVersion: paramsGVK.GroupVersion().String(),
|
|
Kind: paramsGVK.Kind,
|
|
},
|
|
FailurePolicy: ptrTo(admissionregistrationv1.Fail),
|
|
Validations: []admissionregistrationv1.Validation{
|
|
{
|
|
Expression: "policy2",
|
|
},
|
|
},
|
|
}
|
|
|
|
binding1 := *denyBinding
|
|
binding2 := *denyBinding
|
|
|
|
binding1.Name = "denybinding1.example.com"
|
|
binding1.Spec.PolicyName = policy1.Name
|
|
binding2.Name = "denybinding2.example.com"
|
|
binding2.Spec.PolicyName = policy2.Name
|
|
|
|
evaluations1 := atomic.Int64{}
|
|
evaluations2 := atomic.Int64{}
|
|
|
|
compiler.RegisterDefinition(&policy1, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
evaluations1.Add(1)
|
|
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionAdmit,
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
compiler.RegisterDefinition(&policy2, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
evaluations2.Add(1)
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Policy2Denied",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams, &policy1, &binding1))
|
|
|
|
// Make sure policy 1 is created and bound to the params type first
|
|
require.NoError(t, testContext.UpdateAndWait(&policy2, &binding2))
|
|
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns admit meaning the params
|
|
// passed was a configmap
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, `Denied`)
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&policy1))
|
|
require.EqualValues(t, 1, evaluations1.Load())
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&policy2))
|
|
require.EqualValues(t, 1, evaluations2.Load())
|
|
|
|
// Remove param type from policy1
|
|
// Show that policy2 evaluator is still being passed the configmaps
|
|
policy1.Spec.ParamKind = nil
|
|
policy1.ResourceVersion = "2"
|
|
|
|
binding1.Spec.ParamRef = nil
|
|
binding1.ResourceVersion = "2"
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&policy1, &binding1))
|
|
|
|
err = testContext.Plugin.Dispatch(
|
|
testContext,
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns admit meaning the params
|
|
// passed was a configmap
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, `Policy2Denied`)
|
|
require.EqualValues(t, 2, compiler.getNumCompiles(&policy1))
|
|
require.EqualValues(t, 2, evaluations1.Load())
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&policy2))
|
|
require.EqualValues(t, 2, evaluations2.Load())
|
|
}
|
|
|
|
// Shows that we can refer to native-typed params just fine
|
|
// (as opposed to CRD params)
|
|
func TestNativeTypeParam(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
evaluations := atomic.Int64{}
|
|
|
|
// Use ConfigMap native-typed param
|
|
nativeTypeParamPolicy := *denyPolicy
|
|
nativeTypeParamPolicy.Spec.ParamKind = &admissionregistrationv1.ParamKind{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
}
|
|
|
|
compiler.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
evaluations.Add(1)
|
|
if _, ok := versionedParams.(*v1.ConfigMap); ok {
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "correct type",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Incorrect param type",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
configMapParam := &v1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "replicas-test.example.com",
|
|
Namespace: "default",
|
|
ResourceVersion: "1",
|
|
},
|
|
Data: map[string]string{
|
|
"coolkey": "coolvalue",
|
|
},
|
|
}
|
|
require.NoError(t, testContext.UpdateAndWait(&nativeTypeParamPolicy, denyBinding, configMapParam))
|
|
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed, and returns admit meaning the params
|
|
// passed was a configmap
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, "correct type")
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy))
|
|
require.EqualValues(t, 1, evaluations.Load())
|
|
}
|
|
|
|
func TestAuditValidationAction(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
// Push some fake
|
|
noParamSourcePolicy := *denyPolicy
|
|
noParamSourcePolicy.Spec.ParamKind = nil
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "I'm sorry Dave",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithAudit))
|
|
|
|
attr := attributeRecord(nil, fakeParams, admission.Create)
|
|
warningRecorder := newWarningRecorder()
|
|
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
|
|
err := testContext.Plugin.Dispatch(
|
|
warnCtx,
|
|
attr,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.Equal(t, 0, warningRecorder.len())
|
|
|
|
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
|
|
require.Len(t, annotations, 1)
|
|
valueJson, ok := annotations["validation.policy.admission.k8s.io/validation_failure"]
|
|
require.True(t, ok)
|
|
var value []validating.ValidationFailureValue
|
|
jsonErr := utiljson.Unmarshal([]byte(valueJson), &value)
|
|
require.NoError(t, jsonErr)
|
|
expected := []validating.ValidationFailureValue{{
|
|
ExpressionIndex: 0,
|
|
Message: "I'm sorry Dave",
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Audit},
|
|
Binding: "denybinding.example.com",
|
|
Policy: noParamSourcePolicy.Name,
|
|
}}
|
|
require.Equal(t, expected, value)
|
|
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestWarnValidationAction(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
// Push some fake
|
|
noParamSourcePolicy := *denyPolicy
|
|
noParamSourcePolicy.Spec.ParamKind = nil
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "I'm sorry Dave",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithWarn))
|
|
|
|
attr := attributeRecord(nil, fakeParams, admission.Create)
|
|
warningRecorder := newWarningRecorder()
|
|
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
|
|
err := testContext.Plugin.Dispatch(
|
|
warnCtx,
|
|
attr,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.Equal(t, 1, warningRecorder.len())
|
|
require.True(t, warningRecorder.hasWarning("Validation failed for ValidatingAdmissionPolicy 'denypolicy.example.com' with binding 'denybinding.example.com': I'm sorry Dave"))
|
|
|
|
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
|
|
require.Empty(t, annotations)
|
|
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAllValidationActions(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
// Push some fake
|
|
noParamSourcePolicy := *denyPolicy
|
|
noParamSourcePolicy.Spec.ParamKind = nil
|
|
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "I'm sorry Dave",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithAll))
|
|
|
|
attr := attributeRecord(nil, fakeParams, admission.Create)
|
|
warningRecorder := newWarningRecorder()
|
|
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
|
|
err := testContext.Plugin.Dispatch(
|
|
warnCtx,
|
|
attr,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.Equal(t, 1, warningRecorder.len())
|
|
require.True(t, warningRecorder.hasWarning("Validation failed for ValidatingAdmissionPolicy 'denypolicy.example.com' with binding 'denybinding.example.com': I'm sorry Dave"))
|
|
|
|
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
|
|
require.Len(t, annotations, 1)
|
|
valueJson, ok := annotations["validation.policy.admission.k8s.io/validation_failure"]
|
|
require.True(t, ok)
|
|
var value []validating.ValidationFailureValue
|
|
jsonErr := utiljson.Unmarshal([]byte(valueJson), &value)
|
|
require.NoError(t, jsonErr)
|
|
expected := []validating.ValidationFailureValue{{
|
|
ExpressionIndex: 0,
|
|
Message: "I'm sorry Dave",
|
|
ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny, admissionregistrationv1.Warn, admissionregistrationv1.Audit},
|
|
Binding: "denybinding.example.com",
|
|
Policy: noParamSourcePolicy.Name,
|
|
}}
|
|
require.Equal(t, expected, value)
|
|
|
|
require.ErrorContains(t, err, "I'm sorry Dave")
|
|
}
|
|
|
|
func TestNamespaceParamRefName(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
evaluations := atomic.Int64{}
|
|
|
|
// Use ConfigMap native-typed param
|
|
nativeTypeParamPolicy := *denyPolicy
|
|
nativeTypeParamPolicy.Spec.ParamKind = &admissionregistrationv1.ParamKind{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
}
|
|
|
|
namespaceParamBinding := *denyBinding
|
|
namespaceParamBinding.Spec.ParamRef = &admissionregistrationv1.ParamRef{
|
|
Name: "replicas-test.example.com",
|
|
}
|
|
lock := sync.Mutex{}
|
|
observedParamNamespaces := []string{}
|
|
compiler.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
evaluations.Add(1)
|
|
if p, ok := versionedParams.(*v1.ConfigMap); ok {
|
|
observedParamNamespaces = append(observedParamNamespaces, p.Namespace)
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "correct type",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Incorrect param type",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
configMapParam := &v1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "replicas-test.example.com",
|
|
Namespace: "default",
|
|
ResourceVersion: "1",
|
|
},
|
|
Data: map[string]string{
|
|
"coolkey": "default",
|
|
},
|
|
}
|
|
configMapParam2 := &v1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "replicas-test.example.com",
|
|
Namespace: "mynamespace",
|
|
ResourceVersion: "1",
|
|
},
|
|
Data: map[string]string{
|
|
"coolkey": "mynamespace",
|
|
},
|
|
}
|
|
configMapParam3 := &v1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "replicas-test.example.com",
|
|
Namespace: "othernamespace",
|
|
ResourceVersion: "1",
|
|
},
|
|
Data: map[string]string{
|
|
"coolkey": "othernamespace",
|
|
},
|
|
}
|
|
require.NoError(t, testContext.UpdateAndWait(&nativeTypeParamPolicy, &namespaceParamBinding, configMapParam, configMapParam2, configMapParam3))
|
|
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed with correct namespace, and returns admit
|
|
// meaning the params passed was a configmap
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, configMapParam, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
func() {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
require.ErrorContains(t, err, "correct type")
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy))
|
|
require.EqualValues(t, 1, evaluations.Load())
|
|
}()
|
|
|
|
err = testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, configMapParam2, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
func() {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
require.ErrorContains(t, err, "correct type")
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy))
|
|
require.EqualValues(t, 2, evaluations.Load())
|
|
}()
|
|
|
|
err = testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, configMapParam3, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
func() {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
require.ErrorContains(t, err, "correct type")
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy))
|
|
require.EqualValues(t, 3, evaluations.Load())
|
|
}()
|
|
|
|
err = testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, configMapParam, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
func() {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
require.ErrorContains(t, err, "correct type")
|
|
require.EqualValues(t, []string{"default", "mynamespace", "othernamespace", "default"}, observedParamNamespaces)
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy))
|
|
require.EqualValues(t, 4, evaluations.Load())
|
|
}()
|
|
}
|
|
|
|
func TestParamRef(t *testing.T) {
|
|
for _, paramIsClusterScoped := range []bool{false, true} {
|
|
for _, nameIsSet := range []bool{false, true} {
|
|
for _, namespaceIsSet := range []bool{false, true} {
|
|
if paramIsClusterScoped && namespaceIsSet {
|
|
// Skip invalid configuration
|
|
continue
|
|
}
|
|
|
|
for _, selectorIsSet := range []bool{false, true} {
|
|
if selectorIsSet && nameIsSet {
|
|
// SKip invalid configuration
|
|
continue
|
|
}
|
|
|
|
for _, denyNotFound := range []bool{false, true} {
|
|
|
|
name := "ParamRef"
|
|
|
|
if paramIsClusterScoped {
|
|
name = "ClusterScoped" + name
|
|
}
|
|
|
|
if nameIsSet {
|
|
name = name + "WithName"
|
|
} else if selectorIsSet {
|
|
name = name + "WithLabelSelector"
|
|
} else {
|
|
name = name + "WithEverythingSelector"
|
|
}
|
|
|
|
if namespaceIsSet {
|
|
name = name + "WithNamespace"
|
|
}
|
|
|
|
if denyNotFound {
|
|
name = name + "DenyNotFound"
|
|
} else {
|
|
name = name + "AllowNotFound"
|
|
}
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
// Test creating a policy with a cluster or namesapce-scoped param
|
|
// and binding with the provided configuration. Test will ensure
|
|
// that the provided configuration is capable of matching
|
|
// params as expected, and not matching params when not expected.
|
|
// Also ensures the NotFound setting works as expected with this particular
|
|
// configuration of ParamRef when all the previously
|
|
// matched params are deleted.
|
|
testParamRefCase(t, paramIsClusterScoped, nameIsSet, namespaceIsSet, selectorIsSet, denyNotFound)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// testParamRefCase constructs a ParamRef and policy with appropriate ParamKind
|
|
// for the given parameters, then constructs a scenario with several matching/non-matching params
|
|
// of varying names, namespaces, labels.
|
|
//
|
|
// Test then selects subset of params that should match provided configuration
|
|
// and ensuers those params are the only ones used.
|
|
//
|
|
// Also ensures NotFound action is enforced correctly by deleting all found
|
|
// params and ensuring the Action is used.
|
|
//
|
|
// This test is not meant to test every possible scenario of matching/not matching:
|
|
// only that each ParamRef CAN be evaluated correctly for both cluster scoped
|
|
// and namespace-scoped request kinds, and that the failure action is correctly
|
|
// applied.
|
|
func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIsSet, selectorIsSet, denyNotFound bool) {
|
|
// Create a cluster scoped and a namespace scoped CRD
|
|
policy := *denyPolicy
|
|
binding := *denyBinding
|
|
binding.Spec.ParamRef = &admissionregistrationv1.ParamRef{}
|
|
paramRef := binding.Spec.ParamRef
|
|
|
|
shouldErrorOnClusterScopedRequests := !namespaceIsSet && !paramIsClusterScoped
|
|
|
|
matchingParamName := "replicas-test.example.com"
|
|
matchingNamespace := "mynamespace"
|
|
nonMatchingNamespace := "othernamespace"
|
|
|
|
matchingLabels := labels.Set{"doesitmatch": "yes"}
|
|
nonmatchingLabels := labels.Set{"doesitmatch": "no"}
|
|
otherNonmatchingLabels := labels.Set{"notaffiliated": "no"}
|
|
|
|
if paramIsClusterScoped {
|
|
policy.Spec.ParamKind = &admissionregistrationv1.ParamKind{
|
|
APIVersion: clusterScopedParamsGVK.GroupVersion().String(),
|
|
Kind: clusterScopedParamsGVK.Kind,
|
|
}
|
|
} else {
|
|
policy.Spec.ParamKind = &admissionregistrationv1.ParamKind{
|
|
APIVersion: paramsGVK.GroupVersion().String(),
|
|
Kind: paramsGVK.Kind,
|
|
}
|
|
}
|
|
|
|
if nameIsSet {
|
|
paramRef.Name = matchingParamName
|
|
} else if selectorIsSet {
|
|
paramRef.Selector = metav1.SetAsLabelSelector(matchingLabels)
|
|
} else {
|
|
paramRef.Selector = &metav1.LabelSelector{}
|
|
}
|
|
|
|
if namespaceIsSet {
|
|
paramRef.Namespace = matchingNamespace
|
|
}
|
|
|
|
if denyNotFound {
|
|
paramRef.ParameterNotFoundAction = ptrTo(admissionregistrationv1.DenyAction)
|
|
} else {
|
|
paramRef.ParameterNotFoundAction = ptrTo(admissionregistrationv1.AllowAction)
|
|
}
|
|
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
|
|
var matchedParams []runtime.Object
|
|
paramLock := sync.Mutex{}
|
|
observeParam := func(p runtime.Object) {
|
|
paramLock.Lock()
|
|
defer paramLock.Unlock()
|
|
matchedParams = append(matchedParams, p)
|
|
}
|
|
getAndResetObservedParams := func() []runtime.Object {
|
|
paramLock.Lock()
|
|
defer paramLock.Unlock()
|
|
oldParams := matchedParams
|
|
matchedParams = nil
|
|
return oldParams
|
|
}
|
|
|
|
compiler.RegisterDefinition(&policy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
observeParam(versionedParams)
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: "Denied by policy",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
// Create library of params to try to fool the controller
|
|
params := []*unstructured.Unstructured{
|
|
newParam(matchingParamName, v1.NamespaceDefault, nonmatchingLabels),
|
|
newParam(matchingParamName, matchingNamespace, nonmatchingLabels),
|
|
newParam(matchingParamName, nonMatchingNamespace, nonmatchingLabels),
|
|
|
|
newParam(matchingParamName+"1", v1.NamespaceDefault, matchingLabels),
|
|
newParam(matchingParamName+"1", matchingNamespace, matchingLabels),
|
|
newParam(matchingParamName+"1", nonMatchingNamespace, matchingLabels),
|
|
|
|
newParam(matchingParamName+"2", v1.NamespaceDefault, otherNonmatchingLabels),
|
|
newParam(matchingParamName+"2", matchingNamespace, otherNonmatchingLabels),
|
|
newParam(matchingParamName+"2", nonMatchingNamespace, otherNonmatchingLabels),
|
|
|
|
newParam(matchingParamName+"3", v1.NamespaceDefault, otherNonmatchingLabels),
|
|
newParam(matchingParamName+"3", matchingNamespace, matchingLabels),
|
|
newParam(matchingParamName+"3", nonMatchingNamespace, matchingLabels),
|
|
|
|
newClusterScopedParam(matchingParamName, matchingLabels),
|
|
newClusterScopedParam(matchingParamName+"1", nonmatchingLabels),
|
|
newClusterScopedParam(matchingParamName+"2", otherNonmatchingLabels),
|
|
newClusterScopedParam(matchingParamName+"3", matchingLabels),
|
|
newClusterScopedParam(matchingParamName+"4", nonmatchingLabels),
|
|
newClusterScopedParam(matchingParamName+"5", otherNonmatchingLabels),
|
|
}
|
|
|
|
for _, p := range params {
|
|
// Don't wait for these sync the informers would not have been
|
|
// created unless bound to a policy
|
|
require.NoError(t, testContext.Update(p))
|
|
}
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&policy, &binding))
|
|
|
|
namespacedRequestObject := newParam("some param", nonMatchingNamespace, nil)
|
|
clusterScopedRequestObject := newClusterScopedParam("other param", nil)
|
|
|
|
// Validate a namespaced object, and verify that the params being validated
|
|
// are the ones we would expect
|
|
timeoutCtx, timeoutCancel := context.WithTimeout(testContext, 5*time.Second)
|
|
defer timeoutCancel()
|
|
var expectedParamsForNamespacedRequest []*unstructured.Unstructured
|
|
for _, p := range params {
|
|
if p.GetAPIVersion() != policy.Spec.ParamKind.APIVersion || p.GetKind() != policy.Spec.ParamKind.Kind {
|
|
continue
|
|
} else if len(paramRef.Name) > 0 && p.GetName() != paramRef.Name {
|
|
continue
|
|
} else if len(paramRef.Namespace) > 0 && p.GetNamespace() != paramRef.Namespace {
|
|
continue
|
|
}
|
|
|
|
if !paramIsClusterScoped {
|
|
// If the paramRef has empty namespace and the kind is
|
|
// namespaced-scoped, then it only matches params of the same
|
|
// namespace
|
|
if len(paramRef.Namespace) == 0 && p.GetNamespace() != namespacedRequestObject.GetNamespace() {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if paramRef.Selector != nil {
|
|
ls := p.GetLabels()
|
|
matched := true
|
|
|
|
for k, v := range paramRef.Selector.MatchLabels {
|
|
if l, hasLabel := ls[k]; !hasLabel {
|
|
matched = false
|
|
break
|
|
} else if l != v {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
|
|
// Empty selector matches everything
|
|
if len(paramRef.Selector.MatchExpressions) == 0 && len(paramRef.Selector.MatchLabels) == 0 {
|
|
matched = true
|
|
}
|
|
|
|
if !matched {
|
|
continue
|
|
}
|
|
}
|
|
|
|
expectedParamsForNamespacedRequest = append(expectedParamsForNamespacedRequest, p)
|
|
require.NoError(t, testContext.WaitForReconcile(timeoutCtx, p))
|
|
}
|
|
require.NotEmpty(t, expectedParamsForNamespacedRequest, "all test cases should match at least one param")
|
|
require.ErrorContains(t, testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, namespacedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}), "Denied by policy")
|
|
require.ElementsMatch(t, expectedParamsForNamespacedRequest, getAndResetObservedParams(), "should exactly match expected params")
|
|
|
|
// Validate a cluster-scoped object, and verify that the params being validated
|
|
// are the ones we would expect
|
|
var expectedParamsForClusterScopedRequest []*unstructured.Unstructured
|
|
timeoutCtx, timeoutCancel = context.WithTimeout(testContext, 5*time.Second)
|
|
defer timeoutCancel()
|
|
for _, p := range params {
|
|
if shouldErrorOnClusterScopedRequests {
|
|
continue
|
|
} else if p.GetAPIVersion() != policy.Spec.ParamKind.APIVersion || p.GetKind() != policy.Spec.ParamKind.Kind {
|
|
continue
|
|
} else if len(paramRef.Name) > 0 && p.GetName() != paramRef.Name {
|
|
continue
|
|
} else if len(paramRef.Namespace) > 0 && p.GetNamespace() != paramRef.Namespace {
|
|
continue
|
|
} else if !paramIsClusterScoped && len(paramRef.Namespace) == 0 && p.GetNamespace() != v1.NamespaceDefault {
|
|
continue
|
|
}
|
|
|
|
if paramRef.Selector != nil {
|
|
ls := p.GetLabels()
|
|
matched := true
|
|
for k, v := range paramRef.Selector.MatchLabels {
|
|
if l, hasLabel := ls[k]; !hasLabel {
|
|
matched = false
|
|
break
|
|
} else if l != v {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
|
|
// Empty selector matches everything
|
|
if len(paramRef.Selector.MatchExpressions) == 0 && len(paramRef.Selector.MatchLabels) == 0 {
|
|
matched = true
|
|
}
|
|
|
|
if !matched {
|
|
continue
|
|
}
|
|
}
|
|
|
|
expectedParamsForClusterScopedRequest = append(expectedParamsForClusterScopedRequest, p)
|
|
require.NoError(t, testContext.WaitForReconcile(timeoutCtx, p))
|
|
|
|
}
|
|
|
|
err := testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, clusterScopedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{})
|
|
if shouldErrorOnClusterScopedRequests {
|
|
// Cannot validate cliuster-scoped resources against a paramRef that sets namespace
|
|
require.ErrorContains(t, err, "failed to configure binding: cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
|
|
} else {
|
|
require.NotEmpty(t, expectedParamsForClusterScopedRequest, "all test cases should match at least one param")
|
|
require.ErrorContains(t, err, "Denied by policy")
|
|
}
|
|
require.ElementsMatch(t, expectedParamsForClusterScopedRequest, getAndResetObservedParams(), "should exactly match expected params")
|
|
|
|
// Remove all params matched by namespaced, and cluster-scoped validation.
|
|
// Validate again to make sure NotFoundAction is respected
|
|
var deleted []runtime.Object
|
|
for _, p := range expectedParamsForNamespacedRequest {
|
|
deleted = append(deleted, p)
|
|
}
|
|
|
|
for _, p := range expectedParamsForClusterScopedRequest {
|
|
deleted = append(deleted, p)
|
|
}
|
|
|
|
require.NoError(t, testContext.DeleteAndWait(deleted...))
|
|
|
|
// Check that NotFound is working correctly for both namespaeed & non-namespaced
|
|
// request object
|
|
err = testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, namespacedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{})
|
|
if denyNotFound {
|
|
require.ErrorContains(t, err, "no params found for policy binding with `Deny` parameterNotFoundAction")
|
|
} else {
|
|
require.NoError(t, err, "Allow not found expects no error when no params found. Policy should have been skipped")
|
|
}
|
|
require.Empty(t, getAndResetObservedParams(), "policy should not have been evaluated")
|
|
|
|
err = testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, clusterScopedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{})
|
|
if shouldErrorOnClusterScopedRequests {
|
|
require.ErrorContains(t, err, "failed to configure binding: cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
|
|
|
|
} else if denyNotFound {
|
|
require.ErrorContains(t, err, "no params found for policy binding with `Deny` parameterNotFoundAction")
|
|
} else {
|
|
require.NoError(t, err, "Allow not found expects no error when no params found. Policy should have been skipped")
|
|
}
|
|
require.Empty(t, getAndResetObservedParams(), "policy should not have been evaluated")
|
|
}
|
|
|
|
// If the ParamKind is ClusterScoped, and namespace param is used.
|
|
// This is a Configuration Error of the policy
|
|
func TestNamespaceParamRefClusterScopedParamError(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
evaluations := atomic.Int64{}
|
|
|
|
// Use ValidatingAdmissionPolicy for param type since it is cluster-scoped
|
|
nativeTypeParamPolicy := *denyPolicy
|
|
nativeTypeParamPolicy.Spec.ParamKind = &admissionregistrationv1.ParamKind{
|
|
APIVersion: "admissionregistration.k8s.io/v1beta1",
|
|
Kind: "ValidatingAdmissionPolicy",
|
|
}
|
|
|
|
namespaceParamBinding := *denyBinding
|
|
namespaceParamBinding.Spec.ParamRef = &admissionregistrationv1.ParamRef{
|
|
Name: "other-param-to-use-with-no-label.example.com",
|
|
Namespace: "mynamespace",
|
|
}
|
|
|
|
compiler.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
evaluations.Add(1)
|
|
if _, ok := versionedParams.(*admissionregistrationv1.ValidatingAdmissionPolicy); ok {
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionAdmit,
|
|
Message: "correct type",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return validating.ValidateResult{
|
|
Decisions: []validating.PolicyDecision{
|
|
{
|
|
Action: validating.ActionDeny,
|
|
Message: fmt.Sprintf("Incorrect param type %T", versionedParams),
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(&nativeTypeParamPolicy, &namespaceParamBinding))
|
|
|
|
// Object is irrelevant/unchecked for this test. Just test that
|
|
// the evaluator is executed with correct namespace, and returns admit
|
|
// meaning the params passed was a configmap
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attributeRecord(nil, fakeParams, admission.Create),
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
require.ErrorContains(t, err, "paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
|
|
require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy))
|
|
require.EqualValues(t, 0, evaluations.Load())
|
|
}
|
|
|
|
func TestAuditAnnotations(t *testing.T) {
|
|
compiler := &fakeCompiler{}
|
|
matcher := &fakeMatcher{
|
|
DefaultMatch: true,
|
|
}
|
|
testContext := setupFakeTest(t, compiler, matcher)
|
|
|
|
// Push some fake
|
|
policy := *denyPolicy
|
|
compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult {
|
|
o, err := meta.Accessor(versionedParams)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exampleValue := "normal-value"
|
|
if o.GetName() == "replicas-test2.example.com" {
|
|
exampleValue = "special-value"
|
|
}
|
|
return validating.ValidateResult{
|
|
AuditAnnotations: []validating.PolicyAuditAnnotation{
|
|
{
|
|
Key: "example-key",
|
|
Value: exampleValue,
|
|
Action: validating.AuditAnnotationActionPublish,
|
|
},
|
|
{
|
|
Key: "excluded-key",
|
|
Value: "excluded-value",
|
|
Action: validating.AuditAnnotationActionExclude,
|
|
},
|
|
{
|
|
Key: "error-key",
|
|
Action: validating.AuditAnnotationActionError,
|
|
Error: "example error",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
fakeParams2 := fakeParams.DeepCopy()
|
|
fakeParams2.SetName("replicas-test2.example.com")
|
|
denyBinding2 := denyBinding.DeepCopy()
|
|
denyBinding2.SetName("denybinding2.example.com")
|
|
denyBinding2.Spec.ParamRef.Name = fakeParams2.GetName()
|
|
|
|
fakeParams3 := fakeParams.DeepCopy()
|
|
fakeParams3.SetName("replicas-test3.example.com")
|
|
denyBinding3 := denyBinding.DeepCopy()
|
|
denyBinding3.SetName("denybinding3.example.com")
|
|
denyBinding3.Spec.ParamRef.Name = fakeParams3.GetName()
|
|
|
|
require.NoError(t, testContext.UpdateAndWait(fakeParams, fakeParams2, fakeParams3, &policy, denyBinding, denyBinding2, denyBinding3))
|
|
|
|
attr := attributeRecord(nil, fakeParams, admission.Create)
|
|
err := testContext.Plugin.Dispatch(
|
|
testContext,
|
|
attr,
|
|
&admission.RuntimeObjectInterfaces{},
|
|
)
|
|
|
|
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
|
|
require.Len(t, annotations, 1)
|
|
value := annotations[policy.Name+"/example-key"]
|
|
parts := strings.Split(value, ", ")
|
|
require.Len(t, parts, 2)
|
|
require.Contains(t, parts, "normal-value", "special-value")
|
|
|
|
require.ErrorContains(t, err, "example error")
|
|
}
|
|
|
|
// FakeAttributes decorates admission.Attributes. It's used to trace the added annotations.
|
|
type FakeAttributes struct {
|
|
admission.Attributes
|
|
annotations map[string]string
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
// AddAnnotation adds an annotation key value pair to FakeAttributes
|
|
func (f *FakeAttributes) AddAnnotation(k, v string) error {
|
|
return f.AddAnnotationWithLevel(k, v, auditinternal.LevelMetadata)
|
|
}
|
|
|
|
// AddAnnotationWithLevel adds an annotation key value pair to FakeAttributes
|
|
func (f *FakeAttributes) AddAnnotationWithLevel(k, v string, _ auditinternal.Level) error {
|
|
f.mutex.Lock()
|
|
defer f.mutex.Unlock()
|
|
if err := f.Attributes.AddAnnotation(k, v); err != nil {
|
|
return err
|
|
}
|
|
if f.annotations == nil {
|
|
f.annotations = make(map[string]string)
|
|
}
|
|
f.annotations[k] = v
|
|
return nil
|
|
}
|
|
|
|
// GetAnnotations reads annotations from FakeAttributes
|
|
func (f *FakeAttributes) GetAnnotations(_ auditinternal.Level) map[string]string {
|
|
f.mutex.Lock()
|
|
defer f.mutex.Unlock()
|
|
annotations := make(map[string]string, len(f.annotations))
|
|
for k, v := range f.annotations {
|
|
annotations[k] = v
|
|
}
|
|
return annotations
|
|
}
|
|
|
|
type warningRecorder struct {
|
|
sync.Mutex
|
|
warnings sets.Set[string]
|
|
}
|
|
|
|
func newWarningRecorder() *warningRecorder {
|
|
return &warningRecorder{warnings: sets.New[string]()}
|
|
}
|
|
|
|
func (r *warningRecorder) AddWarning(_, text string) {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
r.warnings.Insert(text)
|
|
return
|
|
}
|
|
|
|
func (r *warningRecorder) hasWarning(text string) bool {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
return r.warnings.Has(text)
|
|
}
|
|
|
|
func (r *warningRecorder) len() int {
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
return len(r.warnings)
|
|
}
|