kubernetes/test/integration/apiserver/cel/validatingadmissionpolicy_test.go
2023-03-08 15:17:58 -08:00

3025 lines
107 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 cel
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/rest"
featuregatetesting "k8s.io/component-base/featuregate/testing"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/authutil"
"k8s.io/kubernetes/test/integration/etcd"
"k8s.io/kubernetes/test/integration/framework"
"k8s.io/kubernetes/test/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
// Test_ValidateNamespace_NoParams tests a ValidatingAdmissionPolicy that validates creation of a Namespace with no params.
func Test_ValidateNamespace_NoParams(t *testing.T) {
forbiddenReason := metav1.StatusReasonForbidden
testcases := []struct {
name string
policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
policyBinding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding
namespace *v1.Namespace
err string
failureReason metav1.StatusReason
}{
{
name: "namespace name contains suffix enforced by validating admission policy, using object metadata fields",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.endsWith('k8s')",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "",
},
{
name: "namespace name does NOT contain suffix enforced by validating admission policyusing, object metadata fields",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.endsWith('k8s')",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-foobar",
},
},
err: "namespaces \"test-foobar\" is forbidden: ValidatingAdmissionPolicy 'validate-namespace-suffix' with binding 'validate-namespace-suffix-binding' denied request: failed expression: object.metadata.name.endsWith('k8s')",
failureReason: metav1.StatusReasonInvalid,
},
{
name: "namespace name does NOT contain suffix enforced by validating admission policy using object metadata fields, AND validating expression returns StatusReasonForbidden",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.endsWith('k8s')",
Reason: &forbiddenReason,
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "forbidden-test-foobar",
},
},
err: "namespaces \"forbidden-test-foobar\" is forbidden: ValidatingAdmissionPolicy 'validate-namespace-suffix' with binding 'validate-namespace-suffix-binding' denied request: failed expression: object.metadata.name.endsWith('k8s')",
failureReason: metav1.StatusReasonForbidden,
},
{
name: "namespace name contains suffix enforced by validating admission policy, using request field",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "request.name.endsWith('k8s')",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "",
},
{
name: "namespace name does NOT contains suffix enforced by validating admission policy, using request field",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "request.name.endsWith('k8s')",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "",
},
{
name: "runtime error when validating namespace, but failurePolicy=Ignore",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.nonExistentProperty == 'someval'",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Ignore, withNamespaceMatch(makePolicy("validate-namespace-suffix")))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "",
},
{
name: "runtime error when validating namespace, but failurePolicy=Fail",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.nonExistentProperty == 'someval'",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "namespaces \"test-k8s\" is forbidden: ValidatingAdmissionPolicy 'validate-namespace-suffix' with binding 'validate-namespace-suffix-binding' denied request: expression 'object.nonExistentProperty == 'someval'' resulted in error: no such key: nonExistentProperty",
failureReason: metav1.StatusReasonInvalid,
},
{
name: "runtime error due to unguarded params",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.startsWith(params.metadata.name)",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "namespaces \"test-k8s\" is forbidden: ValidatingAdmissionPolicy 'validate-namespace-suffix' with binding 'validate-namespace-suffix-binding' denied request: expression 'object.metadata.name.startsWith(params.metadata.name)' resulted in error: no such key: metadata",
failureReason: metav1.StatusReasonInvalid,
},
{
name: "with check against unguarded params using has()",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "has(params.metadata) && has(params.metadata.name) && object.metadata.name.endsWith(params.metadata.name)",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "namespaces \"test-k8s\" is forbidden: ValidatingAdmissionPolicy 'validate-namespace-suffix' with binding 'validate-namespace-suffix-binding' denied request: expression 'has(params.metadata) && has(params.metadata.name) && object.metadata.name.endsWith(params.metadata.name)' resulted in error: invalid type for field selection.",
failureReason: metav1.StatusReasonInvalid,
},
{
name: "with check against null params",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "(params != null && object.metadata.name.endsWith(params.metadata.name))",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "namespaces \"test-k8s\" is forbidden: ValidatingAdmissionPolicy 'validate-namespace-suffix' with binding 'validate-namespace-suffix-binding' denied request: failed expression: (params != null && object.metadata.name.endsWith(params.metadata.name))",
failureReason: metav1.StatusReasonInvalid,
},
{
name: "with check against unguarded params using has() and default check",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "(has(params.metadata) && has(params.metadata.name) && object.metadata.name.startsWith(params.metadata.name)) || object.metadata.name.endsWith('k8s')",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "",
},
{
name: "with check against null params and default check",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "(params != null && object.metadata.name.startsWith(params.metadata.name)) || object.metadata.name.endsWith('k8s')",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "",
},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withWaitReadyConstraintAndExpression(testcase.policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
if err := createAndWaitReady(t, client, testcase.policyBinding, nil); err != nil {
t.Fatal(err)
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), testcase.namespace, metav1.CreateOptions{})
checkExpectedError(t, err, testcase.err)
checkFailureReason(t, err, testcase.failureReason)
})
}
}
func Test_ValidateAnnotationsAndWarnings(t *testing.T) {
testcases := []struct {
name string
policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
policyBinding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding
object *v1.ConfigMap
err string
failureReason metav1.StatusReason
auditAnnotations map[string]string
warnings sets.Set[string]
}{
{
name: "with audit annotations",
policy: withAuditAnnotations([]admissionregistrationv1alpha1.AuditAnnotation{
{
Key: "example-key",
ValueExpression: "'object name: ' + object.metadata.name",
},
{
Key: "exclude-key",
ValueExpression: "null",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withConfigMapMatch(makePolicy("validate-audit-annotations"))))),
policyBinding: makeBinding("validate-audit-annotations-binding", "validate-audit-annotations", ""),
object: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test1-k8s",
},
},
err: "",
auditAnnotations: map[string]string{
"validate-audit-annotations/example-key": `object name: test1-k8s`,
},
},
{
name: "with audit annotations with invalid expression",
policy: withAuditAnnotations([]admissionregistrationv1alpha1.AuditAnnotation{
{
Key: "example-key",
ValueExpression: "string(params.metadata.name)", // runtime error, params is null
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withConfigMapMatch(makePolicy("validate-audit-annotations-invalid"))))),
policyBinding: makeBinding("validate-audit-annotations-invalid-binding", "validate-audit-annotations-invalid", ""),
object: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test2-k8s",
},
},
err: "configmaps \"test2-k8s\" is forbidden: ValidatingAdmissionPolicy 'validate-audit-annotations-invalid' with binding 'validate-audit-annotations-invalid-binding' denied request: expression 'string(params.metadata.name)' resulted in error: no such key: metadata",
failureReason: metav1.StatusReasonInvalid,
},
{
name: "with audit annotations with invalid expression and ignore failure policy",
policy: withAuditAnnotations([]admissionregistrationv1alpha1.AuditAnnotation{
{
Key: "example-key",
ValueExpression: "string(params.metadata.name)", // runtime error, params is null
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Ignore, withConfigMapMatch(makePolicy("validate-audit-annotations-invalid-ignore"))))),
policyBinding: makeBinding("validate-audit-annotations-invalid-ignore-binding", "validate-audit-annotations-invalid-ignore", ""),
object: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test3-k8s",
},
},
err: "",
},
{
name: "with warn validationActions",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.endsWith('k8s')",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withConfigMapMatch(makePolicy("validate-actions-warn"))))),
policyBinding: withValidationActions([]admissionregistrationv1alpha1.ValidationAction{admissionregistrationv1alpha1.Warn}, makeBinding("validate-actions-warn-binding", "validate-actions-warn", "")),
object: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test4-nope",
},
},
warnings: sets.New("Validation failed for ValidatingAdmissionPolicy 'validate-actions-warn' with binding 'validate-actions-warn-binding': failed expression: object.metadata.name.endsWith('k8s')"),
},
{
name: "with audit validationActions",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.endsWith('k8s')",
},
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1alpha1.Fail, withConfigMapMatch(makePolicy("validate-actions-audit"))))),
policyBinding: withValidationActions([]admissionregistrationv1alpha1.ValidationAction{admissionregistrationv1alpha1.Deny, admissionregistrationv1alpha1.Audit}, makeBinding("validate-actions-audit-binding", "validate-actions-audit", "")),
object: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test5-nope",
},
},
err: "configmaps \"test5-nope\" is forbidden: ValidatingAdmissionPolicy 'validate-actions-audit' with binding 'validate-actions-audit-binding' denied request: failed expression: object.metadata.name.endsWith('k8s')",
failureReason: metav1.StatusReasonInvalid,
auditAnnotations: map[string]string{
"validation.policy.admission.k8s.io/validation_failure": `[{"message":"failed expression: object.metadata.name.endsWith('k8s')","policy":"validate-actions-audit","binding":"validate-actions-audit-binding","expressionIndex":1,"validationActions":["Deny","Audit"]}]`,
},
},
}
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
// prepare audit policy file
policyFile, err := os.CreateTemp("", "audit-policy.yaml")
if err != nil {
t.Fatalf("Failed to create audit policy file: %v", err)
}
defer os.Remove(policyFile.Name())
if _, err := policyFile.Write([]byte(auditPolicy)); err != nil {
t.Fatalf("Failed to write audit policy file: %v", err)
}
if err := policyFile.Close(); err != nil {
t.Fatalf("Failed to close audit policy file: %v", err)
}
// prepare audit log file
logFile, err := os.CreateTemp("", "audit.log")
if err != nil {
t.Fatalf("Failed to create audit log file: %v", err)
}
defer os.Remove(logFile.Name())
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
"--audit-policy-file", policyFile.Name(),
"--audit-log-version", "audit.k8s.io/v1",
"--audit-log-mode", "blocking",
"--audit-log-path", logFile.Name(),
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
warnHandler := newWarningHandler()
config.WarningHandler = warnHandler
config.Impersonate.UserName = testReinvocationClientUsername
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
for i, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
testCaseID := strconv.Itoa(i)
ns := "auditannotations-" + testCaseID
_, err = client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
policy := withWaitReadyConstraintAndExpression(testcase.policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
if err := createAndWaitReadyNamespacedWithWarnHandler(t, client, withMatchNamespace(testcase.policyBinding, ns), nil, ns, warnHandler); err != nil {
t.Fatal(err)
}
warnHandler.reset()
testcase.object.Namespace = ns
_, err = client.CoreV1().ConfigMaps(ns).Create(context.TODO(), testcase.object, metav1.CreateOptions{})
code := int32(201)
if testcase.err != "" {
code = 422
}
auditAnnotationFilter := func(key, val string) bool {
_, ok := testcase.auditAnnotations[key]
return ok
}
checkExpectedError(t, err, testcase.err)
checkFailureReason(t, err, testcase.failureReason)
checkExpectedWarnings(t, warnHandler, testcase.warnings)
checkAuditEvents(t, logFile, expectedAuditEvents(testcase.auditAnnotations, ns, code), auditAnnotationFilter)
})
}
}
// Test_ValidateNamespace_WithConfigMapParams tests a ValidatingAdmissionPolicy that validates creation of a Namespace,
// using ConfigMap as a param reference.
func Test_ValidateNamespace_WithConfigMapParams(t *testing.T) {
testcases := []struct {
name string
policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
policyBinding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding
configMap *v1.ConfigMap
namespace *v1.Namespace
err string
failureReason metav1.StatusReason
}{
{
name: "namespace name contains suffix enforced by validating admission policy",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.endsWith(params.data.namespaceSuffix)",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withParams(configParamKind(), withNamespaceMatch(makePolicy("validate-namespace-suffix"))))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", "validate-namespace-suffix-param"),
configMap: makeConfigParams("validate-namespace-suffix-param", map[string]string{
"namespaceSuffix": "k8s",
}),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-k8s",
},
},
err: "",
},
{
name: "namespace name does NOT contain suffix enforced by validating admission policy",
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.endsWith(params.data.namespaceSuffix)",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withParams(configParamKind(), withNamespaceMatch(makePolicy("validate-namespace-suffix"))))),
policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", "validate-namespace-suffix-param"),
configMap: makeConfigParams("validate-namespace-suffix-param", map[string]string{
"namespaceSuffix": "k8s",
}),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-foo",
},
},
err: "namespaces \"test-foo\" is forbidden: ValidatingAdmissionPolicy 'validate-namespace-suffix' with binding 'validate-namespace-suffix-binding' denied request: failed expression: object.metadata.name.endsWith(params.data.namespaceSuffix)",
failureReason: metav1.StatusReasonInvalid,
},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), testcase.configMap, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
policy := withWaitReadyConstraintAndExpression(testcase.policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
if err := createAndWaitReady(t, client, testcase.policyBinding, nil); err != nil {
t.Fatal(err)
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), testcase.namespace, metav1.CreateOptions{})
checkExpectedError(t, err, testcase.err)
checkFailureReason(t, err, testcase.failureReason)
})
}
}
func TestMultiplePolicyBindings(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
paramKind := &admissionregistrationv1alpha1.ParamKind{
APIVersion: "v1",
Kind: "ConfigMap",
}
policy := withPolicyExistsLabels([]string{"paramIdent"}, withParams(paramKind, withPolicyMatch("secrets", withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("test-policy")))))
policy.Spec.Validations = []admissionregistrationv1alpha1.Validation{
{
Expression: "params.data.autofail != 'true' && (params.data.conditional == 'false' || object.metadata.name.startsWith(params.data.check))",
},
}
policy = withWaitReadyConstraintAndExpression(policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
autoFailParams := makeConfigParams("autofail-params", map[string]string{
"autofail": "true",
})
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), autoFailParams, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
autofailBinding := withBindingExistsLabels([]string{"autofail-binding-label"}, policy, makeBinding("autofail-binding", "test-policy", "autofail-params"))
if err := createAndWaitReady(t, client, autofailBinding, map[string]string{"paramIdent": "true", "autofail-binding-label": "true"}); err != nil {
t.Fatal(err)
}
autoPassParams := makeConfigParams("autopass-params", map[string]string{
"autofail": "false",
"conditional": "false",
})
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), autoPassParams, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
autopassBinding := withBindingExistsLabels([]string{"autopass-binding-label"}, policy, makeBinding("autopass-binding", "test-policy", "autopass-params"))
if err := createAndWaitReady(t, client, autopassBinding, map[string]string{"paramIdent": "true", "autopass-binding-label": "true"}); err != nil {
t.Fatal(err)
}
condpassParams := makeConfigParams("condpass-params", map[string]string{
"autofail": "false",
"conditional": "true",
"check": "prefix-",
})
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), condpassParams, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
condpassBinding := withBindingExistsLabels([]string{"condpass-binding-label"}, policy, makeBinding("condpass-binding", "test-policy", "condpass-params"))
if err := createAndWaitReady(t, client, condpassBinding, map[string]string{"paramIdent": "true", "condpass-binding-label": "true"}); err != nil {
t.Fatal(err)
}
autofailingSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "autofailing-secret",
Labels: map[string]string{
"paramIdent": "someVal",
"autofail-binding-label": "true",
},
},
}
_, err = client.CoreV1().Secrets("default").Create(context.TODO(), autofailingSecret, metav1.CreateOptions{})
if err == nil {
t.Fatal("expected secret creation to fail due to autofail-binding")
}
checkForFailedRule(t, err)
checkFailureReason(t, err, metav1.StatusReasonInvalid)
autopassingSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "autopassing-secret",
Labels: map[string]string{
"paramIdent": "someVal",
"autopass-binding-label": "true",
},
},
}
if _, err := client.CoreV1().Secrets("default").Create(context.TODO(), autopassingSecret, metav1.CreateOptions{}); err != nil {
t.Fatalf("expected secret creation to succeed, got: %s", err)
}
condpassingSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "prefix-condpassing-secret",
Labels: map[string]string{
"paramIdent": "someVal",
"condpass-binding-label": "true",
},
},
}
if _, err := client.CoreV1().Secrets("default").Create(context.TODO(), condpassingSecret, metav1.CreateOptions{}); err != nil {
t.Fatalf("expected secret creation to succeed, got: %s", err)
}
condfailingSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "condfailing-secret",
Labels: map[string]string{
"paramIdent": "someVal",
"condpass-binding-label": "true",
},
},
}
_, err = client.CoreV1().Secrets("default").Create(context.TODO(), condfailingSecret, metav1.CreateOptions{})
if err == nil {
t.Fatal("expected secret creation to fail due to autofail-binding")
}
checkForFailedRule(t, err)
checkFailureReason(t, err, metav1.StatusReasonInvalid)
}
// Test_PolicyExemption tests that ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding resources
// are exempt from policy rules.
func Test_PolicyExemption(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := makePolicy("test-policy")
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{
"*",
},
APIVersions: []string{
"*",
},
Resources: []string{
"*",
},
},
},
},
},
}
policy.Spec.Validations = []admissionregistrationv1alpha1.Validation{{
Expression: "false",
Message: "marker denied; policy is ready",
}}
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
policyBinding := makeBinding("test-policy-binding", "test-policy", "")
if err := createAndWaitReady(t, client, policyBinding, nil); err != nil {
t.Fatal(err)
}
// validate that operations to ValidatingAdmissionPolicy are exempt from an existing policy that catches all resources
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(context.TODO(), policy.Name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
ignoreFailurePolicy := admissionregistrationv1alpha1.Ignore
policy.Spec.FailurePolicy = &ignoreFailurePolicy
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Update(context.TODO(), policy, metav1.UpdateOptions{})
if err != nil {
t.Error(err)
}
policyBinding, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Get(context.TODO(), policyBinding.Name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// validate that operations to ValidatingAdmissionPolicyBindings are exempt from an existing policy that catches all resources
policyBindingCopy := policyBinding.DeepCopy()
policyBindingCopy.Spec.PolicyName = "different-binding"
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Update(context.TODO(), policyBindingCopy, metav1.UpdateOptions{})
if err != nil {
t.Error(err)
}
}
// Test_ValidatingAdmissionPolicy_UpdateParamKind validates the behavior of ValidatingAdmissionPolicy when
// only the ParamKind is updated. This test creates a policy where namespaces must have a prefix that matches
// the ParamKind set in the policy. Switching the ParamKind should result in only namespaces with prefixes matching
// the new ParamKind to be allowed. For example, when Paramkind is v1/ConfigMap, only namespaces prefixed with "configmap"
// is allowed and when ParamKind is updated to v1/Secret, only namespaces prefixed with "secret" is allowed, etc.
func Test_ValidatingAdmissionPolicy_UpdateParamKind(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
allowedPrefixesParamsConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed-prefixes",
},
}
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), allowedPrefixesParamsConfigMap, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
allowedPrefixesParamSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed-prefixes",
},
}
if _, err := client.CoreV1().Secrets("default").Create(context.TODO(), allowedPrefixesParamSecret, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
paramKind := &admissionregistrationv1alpha1.ParamKind{
APIVersion: "v1",
Kind: "ConfigMap",
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.startsWith(params.kind.lowerAscii())",
Message: "wrong paramKind",
},
}, withParams(paramKind, withNamespaceMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("allowed-prefixes")))))
policy = withWaitReadyConstraintAndExpression(policy)
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
allowedPrefixesBinding := makeBinding("allowed-prefixes-binding", "allowed-prefixes", "allowed-prefixes")
if err := createAndWaitReady(t, client, allowedPrefixesBinding, nil); err != nil {
t.Fatal(err)
}
// validate that namespaces starting with "configmap-" are allowed
// and namespaces starting with "secret-" are disallowed
allowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "configmap-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "secret-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
t.Error("unexpected nil error")
}
if !strings.Contains(err.Error(), "wrong paramKind") {
t.Errorf("unexpected error message: %v", err)
}
checkFailureReason(t, err, metav1.StatusReasonInvalid)
// update the policy ParamKind to reference a Secret
paramKind = &admissionregistrationv1alpha1.ParamKind{
APIVersion: "v1",
Kind: "Secret",
}
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(context.TODO(), policy.Name, metav1.GetOptions{})
if err != nil {
t.Error(err)
}
policy.Spec.ParamKind = paramKind
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Update(context.TODO(), policy, metav1.UpdateOptions{})
if err != nil {
t.Error(err)
}
// validate that namespaces starting with "secret-" are allowed
// and namespaces starting with "configmap-" are disallowed
// wait loop is required here since ConfigMaps were previousy allowed and we need to wait for the new policy
// to be enforced
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace = &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "configmap-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong paramKind") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
allowedNamespace = &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "secret-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
}
// Test_ValidatingAdmissionPolicy_UpdateParamRef validates the behavior of ValidatingAdmissionPolicy when
// only the ParamRef in the binding is updated. This test creates a policy where namespaces must have a prefix that matches
// the ParamRef set in the policy binding. The paramRef in the binding is then updated to a different object.
func Test_ValidatingAdmissionPolicy_UpdateParamRef(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
allowedPrefixesParamsConfigMap1 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-1",
Namespace: "default",
},
}
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), allowedPrefixesParamsConfigMap1, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
allowedPrefixesParamsConfigMap2 := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-2",
Namespace: "default",
},
}
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), allowedPrefixesParamsConfigMap2, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.startsWith(params.metadata.name)",
Message: "wrong paramRef",
},
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("allowed-prefixes")))))
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// validate that namespaces starting with "test-1" are allowed
// and namespaces starting with "test-2-" are disallowed
allowedPrefixesBinding := makeBinding("allowed-prefixes-binding", "allowed-prefixes", "test-1")
if err := createAndWaitReady(t, client, allowedPrefixesBinding, nil); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-2-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong paramRef") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
allowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-1-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
// Update the paramRef in the policy binding to use the test-2 ConfigMap
policyBinding, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Get(context.TODO(), allowedPrefixesBinding.Name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
policyBindingCopy := policyBinding.DeepCopy()
policyBindingCopy.Spec.ParamRef = &admissionregistrationv1alpha1.ParamRef{
Name: "test-2",
Namespace: "default",
}
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Update(context.TODO(), policyBindingCopy, metav1.UpdateOptions{})
if err != nil {
t.Error(err)
}
// validate that namespaces starting with "test-2" are allowed
// and namespaces starting with "test-1" are disallowed
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-1-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong paramRef") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
allowedNamespace = &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-2-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
}
// Test_ValidatingAdmissionPolicy_UpdateParamResource validates behavior of a policy after updates to the param resource.
func Test_ValidatingAdmissionPolicy_UpdateParamResource(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
paramConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed-prefix",
Namespace: "default",
},
Data: map[string]string{
"prefix": "test-1",
},
}
paramConfigMap, err = client.CoreV1().ConfigMaps(paramConfigMap.Namespace).Create(context.TODO(), paramConfigMap, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.startsWith(params.data['prefix'])",
Message: "wrong prefix",
},
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("allowed-prefixes")))))
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// validate that namespaces starting with "test-1" are allowed
// and namespaces starting with "test-2-" are disallowed
allowedPrefixesBinding := makeBinding("allowed-prefixes-binding", "allowed-prefixes", "allowed-prefix")
if err := createAndWaitReady(t, client, allowedPrefixesBinding, nil); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-2-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
allowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-1-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
// Update the param resource to use "test-2" as the new allwoed prefix
paramConfigMapCopy := paramConfigMap.DeepCopy()
paramConfigMapCopy.Data = map[string]string{
"prefix": "test-2",
}
_, err = client.CoreV1().ConfigMaps(paramConfigMapCopy.Namespace).Update(context.TODO(), paramConfigMapCopy, metav1.UpdateOptions{})
if err != nil {
t.Fatal(err)
}
// validate that namespaces starting with "test-2" are allowed
// and namespaces starting with "test-1" are disallowed
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-1-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
allowedNamespace = &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-2-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
}
func Test_ValidatingAdmissionPolicy_MatchByObjectSelector(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
labelSelector := &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "false",
Message: "matched by object selector!",
},
}, withConfigMapMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("match-by-object-selector"))))
policy = withObjectSelector(labelSelector, policy)
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
policyBinding := makeBinding("match-by-object-selector-binding", "match-by-object-selector", "")
if err := createAndWaitReady(t, client, policyBinding, map[string]string{"foo": "bar"}); err != nil {
t.Fatal(err)
}
matchedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "denied",
Namespace: "default",
Labels: map[string]string{
"foo": "bar",
},
},
}
_, err = client.CoreV1().ConfigMaps(matchedConfigMap.Namespace).Create(context.TODO(), matchedConfigMap, metav1.CreateOptions{})
if !strings.Contains(err.Error(), "matched by object selector!") {
t.Errorf("unexpected error: %v", err)
}
allowedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed",
Namespace: "default",
},
}
if _, err := client.CoreV1().ConfigMaps(allowedConfigMap.Namespace).Create(context.TODO(), allowedConfigMap, metav1.CreateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func Test_ValidatingAdmissionPolicy_MatchByNamespaceSelector(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
// only configmaps in default will be allowed.
labelSelector := &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "kubernetes.io/metadata.name",
Operator: "NotIn",
Values: []string{"default"},
},
},
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "false",
Message: "matched by namespace selector!",
},
}, withConfigMapMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("match-by-namespace-selector"))))
policy = withNamespaceSelector(labelSelector, policy)
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
policyBinding := makeBinding("match-by-namespace-selector-binding", "match-by-namespace-selector", "")
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), policyBinding, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
namespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "not-default",
},
}
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
matchedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "denied-",
Namespace: "not-default",
},
}
_, err := client.CoreV1().ConfigMaps(matchedConfigMap.Namespace).Create(context.TODO(), matchedConfigMap, metav1.CreateOptions{})
// policy not enforced yet, try again
if err == nil {
return false, nil
}
if !strings.Contains(err.Error(), "matched by namespace selector!") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", waitErr)
}
allowedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed",
Namespace: "default",
},
}
if _, err := client.CoreV1().ConfigMaps(allowedConfigMap.Namespace).Create(context.TODO(), allowedConfigMap, metav1.CreateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func Test_ValidatingAdmissionPolicy_MatchByResourceNames(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "false",
Message: "matched by resource names!",
},
}, withConfigMapMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("match-by-resource-names"))))
policy.Spec.MatchConstraints.ResourceRules[0].ResourceNames = []string{"matched-by-resource-name"}
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
policyBinding := makeBinding("match-by-resource-names-binding", "match-by-resource-names", "")
if err := createAndWaitReady(t, client, policyBinding, nil); err != nil {
t.Fatal(err)
}
matchedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "matched-by-resource-name",
Namespace: "default",
},
}
_, err = client.CoreV1().ConfigMaps(matchedConfigMap.Namespace).Create(context.TODO(), matchedConfigMap, metav1.CreateOptions{})
if !strings.Contains(err.Error(), "matched by resource names!") {
t.Errorf("unexpected error: %v", err)
}
allowedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "not-matched-by-resource-name",
Namespace: "default",
},
}
if _, err := client.CoreV1().ConfigMaps(allowedConfigMap.Namespace).Create(context.TODO(), allowedConfigMap, metav1.CreateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func Test_ValidatingAdmissionPolicy_MatchWithExcludeResources(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "false",
Message: "not matched by exclude resources!",
},
}, withPolicyMatch("*", withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("match-by-resource-names"))))
policy = withExcludePolicyMatch("configmaps", policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
policyBinding := makeBinding("match-by-resource-names-binding", "match-by-resource-names", "")
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), policyBinding, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-matched-by-exclude-resources",
Namespace: "default",
},
}
_, err := client.CoreV1().Secrets(secret.Namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
// policy not enforced yet, try again
if err == nil {
return false, nil
}
if !strings.Contains(err.Error(), "not matched by exclude resources!") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", waitErr)
}
allowedConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "matched-by-exclude-resources",
Namespace: "default",
},
}
if _, err := client.CoreV1().ConfigMaps(allowedConfigMap.Namespace).Create(context.TODO(), allowedConfigMap, metav1.CreateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func Test_ValidatingAdmissionPolicy_MatchWithMatchPolicyEquivalent(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, versionedCustomResourceDefinition())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "false",
Message: "matched by equivalent match policy!",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("match-by-match-policy-equivalent")))
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{
"awesome.bears.com",
},
APIVersions: []string{
"v1",
},
Resources: []string{
"pandas",
},
},
},
},
},
}
policy = withWaitReadyConstraintAndExpression(policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
policyBinding := makeBinding("match-by-match-policy-equivalent-binding", "match-by-match-policy-equivalent", "")
if err := createAndWaitReady(t, client, policyBinding, nil); err != nil {
t.Fatal(err)
}
v1Resource := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "awesome.bears.com" + "/" + "v1",
"kind": "Panda",
"metadata": map[string]interface{}{
"name": "v1-bears",
},
},
}
v2Resource := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "awesome.bears.com" + "/" + "v2",
"kind": "Panda",
"metadata": map[string]interface{}{
"name": "v2-bears",
},
},
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v1", Resource: "pandas"}).Create(context.TODO(), v1Resource, metav1.CreateOptions{})
if !strings.Contains(err.Error(), "matched by equivalent match policy!") {
t.Errorf("v1 panadas did not match against policy, err: %v", err)
}
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v2", Resource: "pandas"}).Create(context.TODO(), v2Resource, metav1.CreateOptions{})
if !strings.Contains(err.Error(), "matched by equivalent match policy!") {
t.Errorf("v2 panadas did not match against policy, err: %v", err)
}
}
func Test_ValidatingAdmissionPolicy_MatchWithMatchPolicyExact(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, versionedCustomResourceDefinition())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "false",
Message: "matched by exact match policy!",
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("match-by-match-policy-exact")))
matchPolicyExact := admissionregistrationv1alpha1.Exact
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
MatchPolicy: &matchPolicyExact,
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{
"awesome.bears.com",
},
APIVersions: []string{
"v1",
},
Resources: []string{
"pandas",
},
},
},
},
},
}
policy = withWaitReadyConstraintAndExpression(policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
policyBinding := makeBinding("match-by-match-policy-exact-binding", "match-by-match-policy-exact", "")
if err := createAndWaitReady(t, client, policyBinding, nil); err != nil {
t.Fatal(err)
}
v1Resource := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "awesome.bears.com" + "/" + "v1",
"kind": "Panda",
"metadata": map[string]interface{}{
"name": "v1-bears",
},
},
}
v2Resource := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "awesome.bears.com" + "/" + "v2",
"kind": "Panda",
"metadata": map[string]interface{}{
"name": "v2-bears",
},
},
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v1", Resource: "pandas"}).Create(context.TODO(), v1Resource, metav1.CreateOptions{})
if !strings.Contains(err.Error(), "matched by exact match policy!") {
t.Errorf("v1 panadas did not match against policy, err: %v", err)
}
// v2 panadas is allowed since policy specificed match policy Exact and only matched against v1
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v2", Resource: "pandas"}).Create(context.TODO(), v2Resource, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
}
// Test_ValidatingAdmissionPolicy_PolicyDeletedThenRecreated validates that deleting a ValidatingAdmissionPolicy
// removes the policy from the apiserver admission chain and recreating it re-enables it.
func Test_ValidatingAdmissionPolicy_PolicyDeletedThenRecreated(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.startsWith('test')",
Message: "wrong prefix",
},
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("allowed-prefixes")))))
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// validate that namespaces starting with "test" are allowed
policyBinding := makeBinding("allowed-prefixes-binding", "allowed-prefixes", "")
if err := createAndWaitReady(t, client, policyBinding, nil); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
// delete the binding object and validate that policy is not enforced
if err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Delete(context.TODO(), "allowed-prefixes", metav1.DeleteOptions{}); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
allowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err == nil {
return true, nil
}
// old policy is still enforced, try again
if strings.Contains(err.Error(), "wrong prefix") {
return false, nil
}
return false, err
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
}
// Test_ValidatingAdmissionPolicy_BindingDeletedThenRecreated validates that deleting a ValidatingAdmissionPolicyBinding
// removes the policy from the apiserver admission chain and recreating it re-enables it.
func Test_ValidatingAdmissionPolicy_BindingDeletedThenRecreated(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.startsWith('test')",
Message: "wrong prefix",
},
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("allowed-prefixes")))))
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// validate that namespaces starting with "test" are allowed
policyBinding := makeBinding("allowed-prefixes-binding", "allowed-prefixes", "")
if err := createAndWaitReady(t, client, policyBinding, nil); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
// delete the binding object and validate that policy is not enforced
if err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Delete(context.TODO(), "allowed-prefixes-binding", metav1.DeleteOptions{}); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
allowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
if err == nil {
return true, nil
}
// old policy is still enforced, try again
if strings.Contains(err.Error(), "wrong prefix") {
return false, nil
}
return false, err
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
// recreate the policy binding and test that policy is enforced again
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), policyBinding, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
}
// Test_ValidatingAdmissionPolicy_ParamResourceDeletedThenRecreated validates that deleting a param resource referenced
// by a binding renders the policy as invalid. Recreating the param resource re-enables the policy.
func Test_ValidatingAdmissionPolicy_ParamResourceDeletedThenRecreated(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
param := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
}
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), param, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.metadata.name.startsWith(params.metadata.name)",
Message: "wrong prefix",
},
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("allowed-prefixes")))))
policy = withWaitReadyConstraintAndExpression(policy)
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// validate that namespaces starting with "test" are allowed
policyBinding := makeBinding("allowed-prefixes-binding", "allowed-prefixes", "test")
if err := createAndWaitReady(t, client, policyBinding, nil); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
if err == nil {
return false, nil
}
if strings.Contains(err.Error(), "not yet synced to use for admission") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
// delete param object and validate that policy is invalid
if err := client.CoreV1().ConfigMaps("default").Delete(context.TODO(), "test", metav1.DeleteOptions{}); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
allowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), allowedNamespace, metav1.CreateOptions{})
// old policy is still enforced, try again
if strings.Contains(err.Error(), "wrong prefix") {
return false, nil
}
if !strings.Contains(err.Error(), "failed to configure binding: test not found") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
// recreate the param resource and validate namespace is disallowed again
if _, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), param, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
disallowedNamespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "not-test-",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), disallowedNamespace, metav1.CreateOptions{})
// cache not synced with new object yet, try again
if strings.Contains(err.Error(), "failed to configure binding: test not found") {
return false, nil
}
if !strings.Contains(err.Error(), "wrong prefix") {
return false, err
}
return true, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", err)
}
}
// TestCRDParams tests that a CustomResource can be used as a param resource for a ValidatingAdmissionPolicy.
func TestCRDParams(t *testing.T) {
testcases := []struct {
name string
resource *unstructured.Unstructured
policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
policyBinding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding
namespace *v1.Namespace
err string
failureReason metav1.StatusReason
}{
{
name: "a rule that uses data from a CRD param resource does NOT pass",
resource: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "awesome.bears.com/v1",
"kind": "Panda",
"metadata": map[string]interface{}{
"name": "config-obj",
},
"spec": map[string]interface{}{
"nameCheck": "crd-test-k8s",
},
}},
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "params.spec.nameCheck == object.metadata.name",
},
}, withNamespaceMatch(withParams(withCRDParamKind("Panda", "awesome.bears.com", "v1"), withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("test-policy"))))),
policyBinding: makeBinding("crd-policy-binding", "test-policy", "config-obj"),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "incorrect-name",
},
},
err: `namespaces "incorrect-name" is forbidden: ValidatingAdmissionPolicy 'test-policy' with binding 'crd-policy-binding' denied request: failed expression: params.spec.nameCheck == object.metadata.name`,
failureReason: metav1.StatusReasonInvalid,
},
{
name: "a rule that uses data from a CRD param resource that does pass",
resource: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "awesome.bears.com/v1",
"kind": "Panda",
"metadata": map[string]interface{}{
"name": "config-obj",
},
"spec": map[string]interface{}{
"nameCheck": "crd-test-k8s",
},
}},
policy: withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "params.spec.nameCheck == object.metadata.name",
},
}, withNamespaceMatch(withParams(withCRDParamKind("Panda", "awesome.bears.com", "v1"), withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("test-policy"))))),
policyBinding: makeBinding("crd-policy-binding", "test-policy", "config-obj"),
namespace: &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "crd-test-k8s",
},
},
err: ``,
},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
crd := versionedCustomResourceDefinition()
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, crd)
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
crClient := dynamicClient.Resource(gvr)
_, err = crClient.Create(context.TODO(), testcase.resource, metav1.CreateOptions{})
if err != nil {
t.Fatalf("error creating %s: %s", gvr, err)
}
policy := withWaitReadyConstraintAndExpression(testcase.policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
// remove default namespace since the CRD is cluster-scoped
testcase.policyBinding.Spec.ParamRef.Namespace = ""
if err := createAndWaitReady(t, client, testcase.policyBinding, nil); err != nil {
t.Fatal(err)
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), testcase.namespace, metav1.CreateOptions{})
checkExpectedError(t, err, testcase.err)
checkFailureReason(t, err, testcase.failureReason)
})
}
}
func TestBindingRemoval(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
policy := withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "false",
Message: "policy still in effect",
},
}, withNamespaceMatch(withFailurePolicy(admissionregistrationv1alpha1.Fail, makePolicy("test-policy"))))
policy = withWaitReadyConstraintAndExpression(policy)
if _, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
binding := makeBinding("test-binding", "test-policy", "test-params")
if err := createAndWaitReady(t, client, binding, nil); err != nil {
t.Fatal(err)
}
// check that the policy is active
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
namespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "check-namespace",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{})
if err != nil {
if strings.Contains(err.Error(), "policy still in effect") {
return true, nil
} else {
// unexpected error while attempting namespace creation
return true, err
}
}
return false, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", waitErr)
}
if err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Delete(context.TODO(), "test-binding", metav1.DeleteOptions{}); err != nil {
t.Fatal(err)
}
// wait for binding to be deleted
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
_, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Get(context.TODO(), "test-binding", metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return true, nil
} else {
return true, err
}
}
return false, nil
}); waitErr != nil {
t.Errorf("timed out waiting: %v", waitErr)
}
// policy should be considered in an invalid state and namespace creation should be allowed
if waitErr := wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
namespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-namespace",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{})
if err != nil {
t.Logf("namespace creation failed: %s", err)
return false, nil
}
return true, nil
}); waitErr != nil {
t.Errorf("expected namespace creation to succeed but timed out waiting: %v", waitErr)
}
}
// Test_ValidateSecondaryAuthorization tests a ValidatingAdmissionPolicy that performs secondary authorization checks
// for both users and service accounts.
func Test_ValidateSecondaryAuthorization(t *testing.T) {
testcases := []struct {
name string
rbac *rbacv1.PolicyRule
expression string
allowed bool
extraAccountFn func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset
extraAccountRbac *rbacv1.PolicyRule
}{
{
name: "principal is allowed to create a specific deployment",
rbac: &rbacv1.PolicyRule{
Verbs: []string{"create"},
APIGroups: []string{"apps"},
Resources: []string{"deployments/status"},
ResourceNames: []string{"charmander"},
},
expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('default').namespace('default').name('charmander').check('create').allowed()",
allowed: true,
},
{
name: "principal is not allowed to create a specific deployment",
expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('default').name('charmander').check('create').allowed()",
allowed: false,
},
{
name: "principal is authorized for custom verb on current resource",
rbac: &rbacv1.PolicyRule{
Verbs: []string{"anthropomorphize"},
APIGroups: []string{""},
Resources: []string{"namespaces"},
},
expression: "authorizer.requestResource.check('anthropomorphize').allowed()",
allowed: true,
},
{
name: "principal is not authorized for custom verb on current resource",
expression: "authorizer.requestResource.check('anthropomorphize').allowed()",
allowed: false,
},
{
name: "serviceaccount is authorized for custom verb on current resource",
extraAccountFn: serviceAccountClient("default", "extra-acct"),
extraAccountRbac: &rbacv1.PolicyRule{
Verbs: []string{"anthropomorphize"},
APIGroups: []string{""},
Resources: []string{"pods"},
},
expression: "authorizer.serviceAccount('default', 'extra-acct').group('').resource('pods').check('anthropomorphize').allowed()",
allowed: true,
},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
clients := map[string]func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset{
"user": secondaryAuthorizationUserClient,
"serviceaccount": secondaryAuthorizationServiceAccountClient,
}
for clientName, clientFn := range clients {
t.Run(clientName, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
"--authorization-mode=RBAC",
"--anonymous-auth",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
// For test set up such as creating policies, bindings and RBAC rules.
adminClient := clientset.NewForConfigOrDie(server.ClientConfig)
// Principal is always allowed to create and update namespaces so that the admission requests to test
// authorization expressions can be sent by the principal.
rules := []rbacv1.PolicyRule{{
Verbs: []string{"create", "update"},
APIGroups: []string{""},
Resources: []string{"namespaces"},
}}
if testcase.rbac != nil {
rules = append(rules, *testcase.rbac)
}
client := clientFn(t, adminClient, server.ClientConfig, rules)
if testcase.extraAccountFn != nil {
var extraRules []rbacv1.PolicyRule
if testcase.extraAccountRbac != nil {
extraRules = append(rules, *testcase.extraAccountRbac)
}
testcase.extraAccountFn(t, adminClient, server.ClientConfig, extraRules)
}
policy := withWaitReadyConstraintAndExpression(withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: testcase.expression,
},
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-authz")))))
if _, err := adminClient.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
if err := createAndWaitReady(t, adminClient, makeBinding("validate-authz-binding", "validate-authz", ""), nil); err != nil {
t.Fatal(err)
}
ns := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-authz",
},
}
_, err = client.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
var expected metav1.StatusReason = ""
if !testcase.allowed {
expected = metav1.StatusReasonInvalid
}
checkFailureReason(t, err, expected)
})
}
})
}
}
type clientFn func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset
func secondaryAuthorizationUserClient(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset {
clientConfig = rest.CopyConfig(clientConfig)
clientConfig.Impersonate = rest.ImpersonationConfig{
UserName: "alice",
UID: "1234",
}
client := clientset.NewForConfigOrDie(clientConfig)
for _, rule := range rules {
authutil.GrantUserAuthorization(t, context.TODO(), adminClient, "alice", rule)
}
return client
}
func secondaryAuthorizationServiceAccountClient(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset {
return serviceAccountClient("default", "test-service-acct")(t, adminClient, clientConfig, rules)
}
func serviceAccountClient(namespace, name string) clientFn {
return func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset {
clientConfig = rest.CopyConfig(clientConfig)
sa, err := adminClient.CoreV1().ServiceAccounts(namespace).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: name}}, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
uid := sa.UID
clientConfig.Impersonate = rest.ImpersonationConfig{
UserName: "system:serviceaccount:" + namespace + ":" + name,
UID: string(uid),
}
client := clientset.NewForConfigOrDie(clientConfig)
for _, rule := range rules {
authutil.GrantServiceAccountAuthorization(t, context.TODO(), adminClient, name, namespace, rule)
}
return client
}
}
func withWaitReadyConstraintAndExpression(policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy = policy.DeepCopy()
policy.Spec.MatchConstraints.ResourceRules = append(policy.Spec.MatchConstraints.ResourceRules, admissionregistrationv1alpha1.NamedRuleWithOperations{
ResourceNames: []string{"test-marker"},
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"UPDATE",
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{
"",
},
APIVersions: []string{
"v1",
},
Resources: []string{
"endpoints",
},
},
},
})
policy.Spec.Validations = append([]admissionregistrationv1alpha1.Validation{{
Expression: "object.metadata.name != 'test-marker'",
Message: "marker denied; policy is ready",
}}, policy.Spec.Validations...)
return policy
}
func createAndWaitReady(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string) error {
return createAndWaitReadyNamespaced(t, client, binding, matchLabels, "default")
}
func createAndWaitReadyNamespaced(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string) error {
return createAndWaitReadyNamespacedWithWarnHandler(t, client, binding, matchLabels, ns, newWarningHandler())
}
func createAndWaitReadyNamespacedWithWarnHandler(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string, handler *warningHandler) error {
marker := &v1.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "test-marker", Namespace: ns, Labels: matchLabels}}
defer func() {
err := client.CoreV1().Endpoints(ns).Delete(context.TODO(), marker.Name, metav1.DeleteOptions{})
if err != nil {
t.Logf("error deleting marker: %v", err)
}
}()
marker, err := client.CoreV1().Endpoints(ns).Create(context.TODO(), marker, metav1.CreateOptions{})
if err != nil {
return err
}
_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), binding, metav1.CreateOptions{})
if err != nil {
return err
}
if waitErr := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
handler.reset()
_, err := client.CoreV1().Endpoints(ns).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
if handler.hasObservedMarker() {
return true, nil
}
if err != nil && strings.Contains(err.Error(), "marker denied; policy is ready") {
return true, nil
} else if err != nil && strings.Contains(err.Error(), "not yet synced to use for admission") {
t.Logf("waiting for policy to be ready. Marker: %v. Admission not synced yet: %v", marker, err)
return false, nil
} else {
t.Logf("waiting for policy to be ready. Marker: %v, Last marker patch response: %v", marker, err)
return false, err
}
}); waitErr != nil {
return waitErr
}
t.Logf("Marker ready: %v", marker)
handler.reset()
return nil
}
func withMatchNamespace(binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, ns string) *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding {
binding.Spec.MatchResources = &admissionregistrationv1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "kubernetes.io/metadata.name",
Operator: metav1.LabelSelectorOpIn,
Values: []string{ns},
},
},
},
}
return binding
}
func makePolicy(name string) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
return &admissionregistrationv1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
}
func withParams(params *admissionregistrationv1alpha1.ParamKind, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.ParamKind = params
return policy
}
func configParamKind() *admissionregistrationv1alpha1.ParamKind {
return &admissionregistrationv1alpha1.ParamKind{
APIVersion: "v1",
Kind: "ConfigMap",
}
}
func withFailurePolicy(failure admissionregistrationv1alpha1.FailurePolicyType, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.FailurePolicy = &failure
return policy
}
func withNamespaceMatch(policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
return withPolicyMatch("namespaces", policy)
}
func withConfigMapMatch(policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
return withPolicyMatch("configmaps", policy)
}
func withObjectSelector(labelSelector *metav1.LabelSelector, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.MatchConstraints.ObjectSelector = labelSelector
return policy
}
func withNamespaceSelector(labelSelector *metav1.LabelSelector, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.MatchConstraints.NamespaceSelector = labelSelector
return policy
}
func withPolicyMatch(resource string, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{
"",
},
APIVersions: []string{
"*",
},
Resources: []string{
resource,
},
},
},
},
},
}
return policy
}
func withExcludePolicyMatch(resource string, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.MatchConstraints.ExcludeResourceRules = []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{
"",
},
APIVersions: []string{
"*",
},
Resources: []string{
resource,
},
},
},
},
}
return policy
}
func withPolicyExistsLabels(labels []string, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
if policy.Spec.MatchConstraints == nil {
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{}
}
matchExprs := buildExistsSelector(labels)
policy.Spec.MatchConstraints.ObjectSelector = &metav1.LabelSelector{
MatchExpressions: matchExprs,
}
return policy
}
func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
"*",
},
Rule: admissionregistrationv1.Rule{
APIGroups: groups,
APIVersions: versions,
Resources: resources,
},
},
},
},
}
return policy
}
func withValidations(validations []admissionregistrationv1alpha1.Validation, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.Validations = validations
return policy
}
func withAuditAnnotations(auditAnnotations []admissionregistrationv1alpha1.AuditAnnotation, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy.Spec.AuditAnnotations = auditAnnotations
return policy
}
func makeBinding(name, policyName, paramName string) *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding {
var paramRef *admissionregistrationv1alpha1.ParamRef
if paramName != "" {
paramRef = &admissionregistrationv1alpha1.ParamRef{
Name: paramName,
Namespace: "default",
}
}
return &admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding{
ObjectMeta: metav1.ObjectMeta{Name: name},
Spec: admissionregistrationv1alpha1.ValidatingAdmissionPolicyBindingSpec{
PolicyName: policyName,
ParamRef: paramRef,
ValidationActions: []admissionregistrationv1alpha1.ValidationAction{admissionregistrationv1alpha1.Deny},
},
}
}
func withValidationActions(validationActions []admissionregistrationv1alpha1.ValidationAction, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding) *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding {
binding.Spec.ValidationActions = validationActions
return binding
}
func withBindingExistsLabels(labels []string, policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding) *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding {
if policy != nil {
// shallow copy
constraintsCopy := *policy.Spec.MatchConstraints
binding.Spec.MatchResources = &constraintsCopy
}
matchExprs := buildExistsSelector(labels)
binding.Spec.MatchResources.ObjectSelector = &metav1.LabelSelector{
MatchExpressions: matchExprs,
}
return binding
}
func buildExistsSelector(labels []string) []metav1.LabelSelectorRequirement {
matchExprs := make([]metav1.LabelSelectorRequirement, len(labels))
for i := 0; i < len(labels); i++ {
matchExprs[i].Key = labels[i]
matchExprs[i].Operator = metav1.LabelSelectorOpExists
}
return matchExprs
}
func makeConfigParams(name string, data map[string]string) *v1.ConfigMap {
return &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: name},
Data: data,
}
}
func checkForFailedRule(t *testing.T, err error) {
if !strings.Contains(err.Error(), "failed expression") {
t.Fatalf("unexpected error (expected to find \"failed expression\"): %s", err)
}
if strings.Contains(err.Error(), "evaluation error") {
t.Fatalf("CEL rule evaluation failed: %s", err)
}
}
func checkFailureReason(t *testing.T, err error, expectedReason metav1.StatusReason) {
if err == nil && expectedReason == "" {
// no reason was given, no error was passed - early exit
return
}
switch e := err.(type) {
case apierrors.APIStatus:
reason := e.Status().Reason
if reason != expectedReason {
t.Logf("actual error reason: %v", reason)
t.Logf("expected failure reason: %v", expectedReason)
t.Error("Unexpected error reason")
}
default:
t.Errorf("Unexpected error: %v", err)
}
}
func checkExpectedWarnings(t *testing.T, recordedWarnings *warningHandler, expectedWarnings sets.Set[string]) {
if !recordedWarnings.equals(expectedWarnings) {
t.Errorf("Expected warnings '%v' but got '%v", expectedWarnings, recordedWarnings)
}
}
func checkAuditEvents(t *testing.T, logFile *os.File, auditEvents []utils.AuditEvent, filter utils.AuditAnnotationsFilter) {
stream, err := os.OpenFile(logFile.Name(), os.O_RDWR, 0600)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer stream.Close()
if auditEvents != nil {
missing, err := utils.CheckAuditLinesFiltered(stream, auditEvents, auditv1.SchemeGroupVersion, filter)
if err != nil {
t.Errorf("unexpected error checking audit lines: %v", err)
}
if len(missing.MissingEvents) > 0 {
t.Errorf("failed to get expected events -- missing: %s", missing)
}
}
if err := stream.Truncate(0); err != nil {
t.Errorf("unexpected error truncate file: %v", err)
}
if _, err := stream.Seek(0, 0); err != nil {
t.Errorf("unexpected error reset offset: %v", err)
}
}
func withCRDParamKind(kind, crdGroup, crdVersion string) *admissionregistrationv1alpha1.ParamKind {
return &admissionregistrationv1alpha1.ParamKind{
APIVersion: crdGroup + "/" + crdVersion,
Kind: kind,
}
}
func checkExpectedError(t *testing.T, err error, expectedErr string) {
if err == nil && expectedErr == "" {
return
}
if err == nil && expectedErr != "" {
t.Logf("actual error: %v", err)
t.Logf("expected error: %v", expectedErr)
t.Fatal("got nil error but expected an error")
}
if err != nil && expectedErr == "" {
t.Logf("actual error: %v", err)
t.Logf("expected error: %v", expectedErr)
t.Fatal("got error but expected none")
}
if err.Error() != expectedErr {
t.Logf("actual validation error: %v", err)
t.Logf("expected validation error: %v", expectedErr)
t.Error("unexpected validation error")
}
}
// Copied from etcd.GetCustomResourceDefinitionData
func versionedCustomResourceDefinition() *apiextensionsv1.CustomResourceDefinition {
return &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "pandas.awesome.bears.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "awesome.bears.com",
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1",
Served: true,
Storage: true,
Schema: fixtures.AllowAllSchema(),
Subresources: &apiextensionsv1.CustomResourceSubresources{
Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(),
},
},
},
{
Name: "v2",
Served: true,
Storage: false,
Schema: fixtures.AllowAllSchema(),
Subresources: &apiextensionsv1.CustomResourceSubresources{
Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(),
},
},
},
},
Scope: apiextensionsv1.ClusterScoped,
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: "pandas",
Kind: "Panda",
},
},
}
}
type warningHandler struct {
lock sync.Mutex
warnings sets.Set[string]
observedMarker bool
}
func newWarningHandler() *warningHandler {
return &warningHandler{warnings: sets.New[string]()}
}
func (w *warningHandler) reset() {
w.lock.Lock()
defer w.lock.Unlock()
w.warnings = sets.New[string]()
w.observedMarker = false
}
func (w *warningHandler) equals(s sets.Set[string]) bool {
w.lock.Lock()
defer w.lock.Unlock()
return w.warnings.Equal(s)
}
func (w *warningHandler) hasObservedMarker() bool {
w.lock.Lock()
defer w.lock.Unlock()
return w.observedMarker
}
func (w *warningHandler) HandleWarningHeader(code int, _ string, message string) {
if strings.HasSuffix(message, "marker denied; policy is ready") {
func() {
w.lock.Lock()
defer w.lock.Unlock()
w.observedMarker = true
}()
}
if code != 299 || len(message) == 0 {
return
}
w.lock.Lock()
defer w.lock.Unlock()
w.warnings.Insert(message)
}
func expectedAuditEvents(auditAnnotations map[string]string, ns string, code int32) []utils.AuditEvent {
return []utils.AuditEvent{
{
Level: auditinternal.LevelRequest,
Stage: auditinternal.StageResponseComplete,
RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", ns),
Verb: "create",
Code: code,
User: "system:apiserver",
ImpersonatedUser: testReinvocationClientUsername,
ImpersonatedGroups: "system:authenticated",
Resource: "configmaps",
Namespace: ns,
AuthorizeDecision: "allow",
RequestObject: true,
ResponseObject: false,
CustomAuditAnnotations: auditAnnotations,
},
}
}
const (
testReinvocationClientUsername = "webhook-reinvocation-integration-client"
auditPolicy = `
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Request
resources:
- group: "" # core
resources: ["configmaps"]
`
)
func TestValidatingAdmissionPolicyTypeChecking(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
server, err := apiservertesting.StartTestServer(t, nil, []string{
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy
assertFieldRef func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) // warning.fieldRef
assertWarnings func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) // warning.warning
}{
{
name: "deployment with correct expression",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.replicas > 1",
},
}, makePolicy("replicated-deployment"))),
assertFieldRef: toHasLengthOf(0),
assertWarnings: toHasLengthOf(0),
},
{
name: "deployment with type confusion",
policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1alpha1.Validation{
{
Expression: "object.spec.replicas < 100", // this one passes
},
{
Expression: "object.spec.replicas > '1'", // '1' should be int
},
}, makePolicy("confused-deployment"))),
assertFieldRef: toBe("spec.validations[1].expression"),
assertWarnings: toHasSubstring(`found no matching overload for '_>_' applied to '(int, string)'`),
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
policy, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(ctx, tc.policy, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
defer client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Delete(context.Background(), policy.Name, metav1.DeleteOptions{})
err = wait.PollImmediateWithContext(ctx, time.Second, time.Minute, func(ctx context.Context) (done bool, err error) {
name := policy.Name
// wait until the typeChecking is set, which means the type checking
// is complete.
updated, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{})
if err != nil {
return false, err
}
if updated.Status.TypeChecking != nil {
policy = updated
return true, nil
}
return false, nil
})
if err != nil {
t.Fatal(err)
}
tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t)
tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t)
})
}
}
func toBe(expected ...string) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if len(expected) != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings))
}
for i := range expected {
if expected[i] != warnings[i].FieldRef {
t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef)
}
}
}
}
func toHasSubstring(substrings ...string) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if len(substrings) != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
}
for i := range substrings {
if !strings.Contains(warnings[i].Warning, substrings[i]) {
t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
}
}
}
}
func toHasLengthOf(n int) func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
return func(warnings []admissionregistrationv1alpha1.ExpressionWarning, t *testing.T) {
if n != len(warnings) {
t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings))
}
}
}