
snuck in there while I was working on the test, but is ultimately not necessary to test the functionality. skipping healthz check resulted in leaking goroutines from poststarthooks.
3228 lines
112 KiB
Go
3228 lines
112 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"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"text/template"
|
|
"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"
|
|
|
|
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
|
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
|
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
|
|
"k8s.io/kubernetes/test/integration/authutil"
|
|
"k8s.io/kubernetes/test/integration/etcd"
|
|
"k8s.io/kubernetes/test/integration/framework"
|
|
"k8s.io/kubernetes/test/utils"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
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"
|
|
|
|
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
|
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
|
authorizationv1 "k8s.io/api/authorization/v1"
|
|
v1 "k8s.io/api/core/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
)
|
|
|
|
// 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 *admissionregistrationv1beta1.ValidatingAdmissionPolicy
|
|
policyBinding *admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.endsWith('k8s')",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.endsWith('k8s')",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.endsWith('k8s')",
|
|
Reason: &forbiddenReason,
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "request.name.endsWith('k8s')",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "request.name.endsWith('k8s')",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.nonExistentProperty == 'someval'",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.nonExistentProperty == 'someval'",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith(params.metadata.name)",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "has(params.metadata) && has(params.metadata.name) && object.metadata.name.endsWith(params.metadata.name)",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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: has(params.metadata) && has(params.metadata.name) && object.metadata.name.endsWith(params.metadata.name)",
|
|
failureReason: metav1.StatusReasonInvalid,
|
|
},
|
|
{
|
|
name: "with check against null params",
|
|
policy: withValidations([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "(params != null && object.metadata.name.endsWith(params.metadata.name))",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "(has(params.metadata) && has(params.metadata.name) && object.metadata.name.startsWith(params.metadata.name)) || object.metadata.name.endsWith('k8s')",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "(params != null && object.metadata.name.startsWith(params.metadata.name)) || object.metadata.name.endsWith('k8s')",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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 namespaceObject",
|
|
policy: withValidations([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "namespaceObject == null", // because namespace itself is cluster-scoped.
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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.AdmissionregistrationV1beta1().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 *admissionregistrationv1beta1.ValidatingAdmissionPolicy
|
|
policyBinding *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding
|
|
object *v1.ConfigMap
|
|
err string
|
|
failureReason metav1.StatusReason
|
|
auditAnnotations map[string]string
|
|
warnings sets.Set[string]
|
|
}{
|
|
{
|
|
name: "with audit annotations",
|
|
policy: withAuditAnnotations([]admissionregistrationv1beta1.AuditAnnotation{
|
|
{
|
|
Key: "example-key",
|
|
ValueExpression: "'object name: ' + object.metadata.name",
|
|
},
|
|
{
|
|
Key: "exclude-key",
|
|
ValueExpression: "null",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.AuditAnnotation{
|
|
{
|
|
Key: "example-key",
|
|
ValueExpression: "string(params.metadata.name)", // runtime error, params is null
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.AuditAnnotation{
|
|
{
|
|
Key: "example-key",
|
|
ValueExpression: "string(params.metadata.name)", // runtime error, params is null
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.endsWith('k8s')",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.Fail, withConfigMapMatch(makePolicy("validate-actions-warn"))))),
|
|
policyBinding: withValidationActions([]admissionregistrationv1beta1.ValidationAction{admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.endsWith('k8s')",
|
|
},
|
|
}, withParams(configParamKind(), withFailurePolicy(admissionregistrationv1beta1.Fail, withConfigMapMatch(makePolicy("validate-actions-audit"))))),
|
|
policyBinding: withValidationActions([]admissionregistrationv1beta1.ValidationAction{admissionregistrationv1beta1.Deny, admissionregistrationv1beta1.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.AdmissionregistrationV1beta1().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 *admissionregistrationv1beta1.ValidatingAdmissionPolicy
|
|
policyBinding *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding
|
|
configMap *v1.ConfigMap
|
|
namespace *v1.Namespace
|
|
err string
|
|
failureReason metav1.StatusReason
|
|
}{
|
|
{
|
|
name: "namespace name contains suffix enforced by validating admission policy",
|
|
policy: withValidations([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.endsWith(params.data.namespaceSuffix)",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.endsWith(params.data.namespaceSuffix)",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.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.AdmissionregistrationV1beta1().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 := &admissionregistrationv1beta1.ParamKind{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
}
|
|
policy := withPolicyExistsLabels([]string{"paramIdent"}, withParams(paramKind, withPolicyMatch("secrets", withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("test-policy")))))
|
|
policy.Spec.Validations = []admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "params.data.autofail != 'true' && (params.data.conditional == 'false' || object.metadata.name.startsWith(params.data.check))",
|
|
},
|
|
}
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
if _, err := client.AdmissionregistrationV1beta1().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 = &admissionregistrationv1beta1.MatchResources{
|
|
ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
|
|
{
|
|
RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
|
|
Operations: []admissionregistrationv1.OperationType{
|
|
"*",
|
|
},
|
|
Rule: admissionregistrationv1.Rule{
|
|
APIGroups: []string{
|
|
"*",
|
|
},
|
|
APIVersions: []string{
|
|
"*",
|
|
},
|
|
Resources: []string{
|
|
"*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
policy.Spec.Validations = []admissionregistrationv1beta1.Validation{{
|
|
Expression: "false",
|
|
Message: "marker denied; policy is ready",
|
|
}}
|
|
|
|
policy, err = client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Get(context.TODO(), policy.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ignoreFailurePolicy := admissionregistrationv1beta1.Ignore
|
|
policy.Spec.FailurePolicy = &ignoreFailurePolicy
|
|
_, err = client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Update(context.TODO(), policy, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
policyBinding, err = client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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 := &admissionregistrationv1beta1.ParamKind{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
}
|
|
|
|
policy := withValidations([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith(params.kind.lowerAscii())",
|
|
Message: "wrong paramKind",
|
|
},
|
|
}, withParams(paramKind, withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("allowed-prefixes")))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
policy, err = client.AdmissionregistrationV1beta1().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 = &admissionregistrationv1beta1.ParamKind{
|
|
APIVersion: "v1",
|
|
Kind: "Secret",
|
|
}
|
|
policy, err = client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Get(context.TODO(), policy.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
policy.Spec.ParamKind = paramKind
|
|
_, err = client.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith(params.metadata.name)",
|
|
Message: "wrong paramRef",
|
|
},
|
|
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("allowed-prefixes")))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().ValidatingAdmissionPolicyBindings().Get(context.TODO(), allowedPrefixesBinding.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
denyAction := admissionregistrationv1beta1.DenyAction
|
|
policyBindingCopy := policyBinding.DeepCopy()
|
|
policyBindingCopy.Spec.ParamRef = &admissionregistrationv1beta1.ParamRef{
|
|
Name: "test-2",
|
|
Namespace: "default",
|
|
ParameterNotFoundAction: &denyAction,
|
|
}
|
|
_, err = client.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith(params.data['prefix'])",
|
|
Message: "wrong prefix",
|
|
},
|
|
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("allowed-prefixes")))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "false",
|
|
Message: "matched by object selector!",
|
|
},
|
|
}, withConfigMapMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("match-by-object-selector"))))
|
|
policy = withObjectSelector(labelSelector, policy)
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "false",
|
|
Message: "matched by namespace selector!",
|
|
},
|
|
}, withConfigMapMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("match-by-namespace-selector"))))
|
|
policy = withNamespaceSelector(labelSelector, policy)
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "false",
|
|
Message: "matched by resource names!",
|
|
},
|
|
}, withConfigMapMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("match-by-resource-names"))))
|
|
policy.Spec.MatchConstraints.ResourceRules[0].ResourceNames = []string{"matched-by-resource-name"}
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "false",
|
|
Message: "not matched by exclude resources!",
|
|
},
|
|
}, withPolicyMatch("*", withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("match-by-resource-names"))))
|
|
|
|
policy = withExcludePolicyMatch("configmaps", policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "false",
|
|
Message: "matched by equivalent match policy!",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("match-by-match-policy-equivalent")))
|
|
policy.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
|
|
ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
|
|
{
|
|
RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
|
|
Operations: []admissionregistrationv1.OperationType{
|
|
"*",
|
|
},
|
|
Rule: admissionregistrationv1.Rule{
|
|
APIGroups: []string{
|
|
"awesome.bears.com",
|
|
},
|
|
APIVersions: []string{
|
|
"v1",
|
|
},
|
|
Resources: []string{
|
|
"pandas",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
if _, err := client.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "false",
|
|
Message: "matched by exact match policy!",
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("match-by-match-policy-exact")))
|
|
matchPolicyExact := admissionregistrationv1beta1.Exact
|
|
policy.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
|
|
MatchPolicy: &matchPolicyExact,
|
|
ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
|
|
{
|
|
RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
|
|
Operations: []admissionregistrationv1.OperationType{
|
|
"*",
|
|
},
|
|
Rule: admissionregistrationv1.Rule{
|
|
APIGroups: []string{
|
|
"awesome.bears.com",
|
|
},
|
|
APIVersions: []string{
|
|
"v1",
|
|
},
|
|
Resources: []string{
|
|
"pandas",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
if _, err := client.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith('test')",
|
|
Message: "wrong prefix",
|
|
},
|
|
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("allowed-prefixes")))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith('test')",
|
|
Message: "wrong prefix",
|
|
},
|
|
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("allowed-prefixes")))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith(params.metadata.name)",
|
|
Message: "wrong prefix",
|
|
},
|
|
}, withParams(configParamKind(), withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("allowed-prefixes")))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err = client.AdmissionregistrationV1beta1().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: no params found for policy binding with `Deny` parameterNotFoundAction") {
|
|
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: no params found for policy binding with `Deny` parameterNotFoundAction") {
|
|
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 *admissionregistrationv1beta1.ValidatingAdmissionPolicy
|
|
policyBinding *admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "params.spec.nameCheck == object.metadata.name",
|
|
},
|
|
}, withNamespaceMatch(withParams(withCRDParamKind("Panda", "awesome.bears.com", "v1"), withFailurePolicy(admissionregistrationv1beta1.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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "params.spec.nameCheck == object.metadata.name",
|
|
},
|
|
}, withNamespaceMatch(withParams(withCRDParamKind("Panda", "awesome.bears.com", "v1"), withFailurePolicy(admissionregistrationv1beta1.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.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "false",
|
|
Message: "policy still in effect",
|
|
},
|
|
}, withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("test-policy"))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
if _, err := client.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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.AdmissionregistrationV1beta1().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([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: testcase.expression,
|
|
},
|
|
}, withFailurePolicy(admissionregistrationv1beta1.Fail, withNamespaceMatch(makePolicy("validate-authz")))))
|
|
if _, err := adminClient.AdmissionregistrationV1beta1().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)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCRDsOnStartup(t *testing.T) {
|
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
|
|
|
|
testContext, testCancel := context.WithCancel(context.Background())
|
|
defer testCancel()
|
|
|
|
// Start server and create CRD, and validatingadmission policy and binding
|
|
etcdConfig := framework.SharedEtcd()
|
|
server := apiservertesting.StartTestServerOrDie(t, nil, []string{
|
|
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
|
|
"--authorization-mode=RBAC",
|
|
"--anonymous-auth",
|
|
}, etcdConfig)
|
|
client := clientset.NewForConfigOrDie(server.ClientConfig)
|
|
dynamicClient := dynamic.NewForConfigOrDie(server.ClientConfig)
|
|
apiextclient := apiextensionsclientset.NewForConfigOrDie(server.ClientConfig)
|
|
myCRD := &apiextensionsv1.CustomResourceDefinition{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foos.cr.bar.com",
|
|
},
|
|
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
|
Group: "cr.bar.com",
|
|
Scope: apiextensionsv1.NamespaceScoped,
|
|
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
|
Plural: "foos",
|
|
Kind: "Foo",
|
|
},
|
|
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
|
|
{
|
|
Name: "v1",
|
|
Served: true,
|
|
Storage: true,
|
|
Schema: fixtures.AllowAllSchema(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create a bunch of fake CRDs to make the initial startup sync take a long time
|
|
for i := 0; i < 100; i++ {
|
|
crd := myCRD.DeepCopy()
|
|
crd.Name = fmt.Sprintf("foos%d.cr.bar.com", i)
|
|
crd.Spec.Names.Plural = fmt.Sprintf("foos%d", i)
|
|
crd.Spec.Names.Kind = fmt.Sprintf("Foo%d", i)
|
|
|
|
if _, err := apiextclient.ApiextensionsV1().CustomResourceDefinitions().Create(context.Background(), crd, metav1.CreateOptions{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
etcd.CreateTestCRDs(t, apiextclient, false, myCRD)
|
|
crdGVK := schema.GroupVersionKind{
|
|
Group: "cr.bar.com",
|
|
Version: "v1",
|
|
Kind: "Foo",
|
|
}
|
|
crdGVR := crdGVK.GroupVersion().WithResource("foos")
|
|
|
|
param := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"name": "test",
|
|
"namespace": "default",
|
|
},
|
|
"foo": "bar",
|
|
},
|
|
}
|
|
param.GetObjectKind().SetGroupVersionKind(crdGVK)
|
|
|
|
if _, err := dynamicClient.Resource(crdGVR).Namespace("default").Create(context.TODO(), param, metav1.CreateOptions{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
policy := withValidations([]admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "object.metadata.name.startsWith(params.metadata.name)",
|
|
Message: "wrong prefix",
|
|
},
|
|
}, withParams(withCRDParamKind(crdGVK.Kind, crdGVK.Group, crdGVK.Version), withNamespaceMatch(withFailurePolicy(admissionregistrationv1beta1.Fail, makePolicy("allowed-prefixes")))))
|
|
policy = withWaitReadyConstraintAndExpression(policy)
|
|
_, err := client.AdmissionregistrationV1beta1().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)
|
|
}
|
|
|
|
doCheck := func(client clientset.Interface) {
|
|
if waitErr := wait.PollUntilContextTimeout(testContext, time.Millisecond*100, 3*time.Minute, true, func(ctx context.Context) (bool, error) {
|
|
disallowedNamespace := &v1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "not-test-",
|
|
},
|
|
}
|
|
|
|
_, err = client.CoreV1().Namespaces().Create(testContext, 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(), "failed to find resource referenced by paramKind") {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Show that the policy & binding are correctly working before restarting
|
|
// to use the paramKind and deliver an error
|
|
doCheck(client)
|
|
server.TearDownFn()
|
|
|
|
// Start the server.
|
|
server = apiservertesting.StartTestServerOrDie(
|
|
t,
|
|
&apiservertesting.TestServerInstanceOptions{},
|
|
[]string{
|
|
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
|
|
"--authorization-mode=RBAC",
|
|
"--anonymous-auth",
|
|
},
|
|
etcdConfig)
|
|
defer server.TearDownFn()
|
|
|
|
// Now that the server is restarted, show again that the policy & binding are correctly working
|
|
client = clientset.NewForConfigOrDie(server.ClientConfig)
|
|
|
|
doCheck(client)
|
|
|
|
}
|
|
|
|
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 *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy = policy.DeepCopy()
|
|
policy.Spec.MatchConstraints.ResourceRules = append(policy.Spec.MatchConstraints.ResourceRules, admissionregistrationv1beta1.NamedRuleWithOperations{
|
|
ResourceNames: []string{"test-marker"},
|
|
RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
|
|
Operations: []admissionregistrationv1.OperationType{
|
|
"UPDATE",
|
|
},
|
|
Rule: admissionregistrationv1.Rule{
|
|
APIGroups: []string{
|
|
"",
|
|
},
|
|
APIVersions: []string{
|
|
"v1",
|
|
},
|
|
Resources: []string{
|
|
"endpoints",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
policy.Spec.Validations = append([]admissionregistrationv1beta1.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.Interface, binding *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string) error {
|
|
return createAndWaitReadyNamespaced(t, client, binding, matchLabels, "default")
|
|
}
|
|
|
|
func createAndWaitReadyNamespaced(t *testing.T, client clientset.Interface, binding *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string) error {
|
|
return createAndWaitReadyNamespacedWithWarnHandler(t, client, binding, matchLabels, ns, newWarningHandler())
|
|
}
|
|
|
|
func createAndWaitReadyNamespacedWithWarnHandler(t *testing.T, client clientset.Interface, binding *admissionregistrationv1beta1.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.AdmissionregistrationV1beta1().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 *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding, ns string) *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding {
|
|
binding.Spec.MatchResources = &admissionregistrationv1beta1.MatchResources{
|
|
NamespaceSelector: &metav1.LabelSelector{
|
|
MatchExpressions: []metav1.LabelSelectorRequirement{
|
|
{
|
|
Key: "kubernetes.io/metadata.name",
|
|
Operator: metav1.LabelSelectorOpIn,
|
|
Values: []string{ns},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return binding
|
|
}
|
|
|
|
func makePolicy(name string) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
return &admissionregistrationv1beta1.ValidatingAdmissionPolicy{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name},
|
|
}
|
|
}
|
|
|
|
func withParams(params *admissionregistrationv1beta1.ParamKind, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.ParamKind = params
|
|
return policy
|
|
}
|
|
|
|
func configParamKind() *admissionregistrationv1beta1.ParamKind {
|
|
return &admissionregistrationv1beta1.ParamKind{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
}
|
|
}
|
|
|
|
func withFailurePolicy(failure admissionregistrationv1beta1.FailurePolicyType, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.FailurePolicy = &failure
|
|
return policy
|
|
}
|
|
|
|
func withNamespaceMatch(policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
return withPolicyMatch("namespaces", policy)
|
|
}
|
|
|
|
func withConfigMapMatch(policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
return withPolicyMatch("configmaps", policy)
|
|
}
|
|
|
|
func withObjectSelector(labelSelector *metav1.LabelSelector, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.MatchConstraints.ObjectSelector = labelSelector
|
|
return policy
|
|
}
|
|
|
|
func withNamespaceSelector(labelSelector *metav1.LabelSelector, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.MatchConstraints.NamespaceSelector = labelSelector
|
|
return policy
|
|
}
|
|
|
|
func withPolicyMatch(resource string, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
|
|
ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
|
|
{
|
|
RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
|
|
Operations: []admissionregistrationv1.OperationType{
|
|
"*",
|
|
},
|
|
Rule: admissionregistrationv1.Rule{
|
|
APIGroups: []string{
|
|
"",
|
|
},
|
|
APIVersions: []string{
|
|
"*",
|
|
},
|
|
Resources: []string{
|
|
resource,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return policy
|
|
}
|
|
|
|
func withExcludePolicyMatch(resource string, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.MatchConstraints.ExcludeResourceRules = []admissionregistrationv1beta1.NamedRuleWithOperations{
|
|
{
|
|
RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
|
|
Operations: []admissionregistrationv1.OperationType{
|
|
"*",
|
|
},
|
|
Rule: admissionregistrationv1.Rule{
|
|
APIGroups: []string{
|
|
"",
|
|
},
|
|
APIVersions: []string{
|
|
"*",
|
|
},
|
|
Resources: []string{
|
|
resource,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return policy
|
|
}
|
|
|
|
func withPolicyExistsLabels(labels []string, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
if policy.Spec.MatchConstraints == nil {
|
|
policy.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{}
|
|
}
|
|
matchExprs := buildExistsSelector(labels)
|
|
policy.Spec.MatchConstraints.ObjectSelector = &metav1.LabelSelector{
|
|
MatchExpressions: matchExprs,
|
|
}
|
|
return policy
|
|
}
|
|
|
|
func withValidations(validations []admissionregistrationv1beta1.Validation, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.Validations = validations
|
|
return policy
|
|
}
|
|
|
|
func withAuditAnnotations(auditAnnotations []admissionregistrationv1beta1.AuditAnnotation, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
|
policy.Spec.AuditAnnotations = auditAnnotations
|
|
return policy
|
|
}
|
|
|
|
func makeBinding(name, policyName, paramName string) *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding {
|
|
var paramRef *admissionregistrationv1beta1.ParamRef
|
|
if paramName != "" {
|
|
denyAction := admissionregistrationv1beta1.DenyAction
|
|
paramRef = &admissionregistrationv1beta1.ParamRef{
|
|
Name: paramName,
|
|
Namespace: "default",
|
|
ParameterNotFoundAction: &denyAction,
|
|
}
|
|
}
|
|
return &admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name},
|
|
Spec: admissionregistrationv1beta1.ValidatingAdmissionPolicyBindingSpec{
|
|
PolicyName: policyName,
|
|
ParamRef: paramRef,
|
|
ValidationActions: []admissionregistrationv1beta1.ValidationAction{admissionregistrationv1beta1.Deny},
|
|
},
|
|
}
|
|
}
|
|
|
|
func withValidationActions(validationActions []admissionregistrationv1beta1.ValidationAction, binding *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding) *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding {
|
|
binding.Spec.ValidationActions = validationActions
|
|
return binding
|
|
}
|
|
|
|
func withBindingExistsLabels(labels []string, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy, binding *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding) *admissionregistrationv1beta1.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) *admissionregistrationv1beta1.ParamKind {
|
|
return &admissionregistrationv1beta1.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 TestAuthorizationDecisionCaching(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
validations []admissionregistrationv1beta1.Validation
|
|
}{
|
|
{
|
|
name: "hit",
|
|
validations: []admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "authorizer.requestResource.check('test').reason() == authorizer.requestResource.check('test').reason()",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "miss",
|
|
validations: []admissionregistrationv1beta1.Validation{
|
|
{
|
|
Expression: "authorizer.requestResource.subresource('a').check('test').reason() == '1'",
|
|
},
|
|
{
|
|
Expression: "authorizer.requestResource.subresource('b').check('test').reason() == '2'",
|
|
},
|
|
{
|
|
Expression: "authorizer.requestResource.subresource('c').check('test').reason() == '3'",
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
|
|
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
defer cancel()
|
|
|
|
var nChecks int
|
|
webhook := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var review authorizationv1.SubjectAccessReview
|
|
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
review.Status.Allowed = true
|
|
if review.Spec.ResourceAttributes.Verb == "test" {
|
|
nChecks++
|
|
review.Status.Reason = fmt.Sprintf("%d", nChecks)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(review); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}))
|
|
defer webhook.Close()
|
|
|
|
kcfd, err := os.CreateTemp("", "kubeconfig-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
func() {
|
|
defer kcfd.Close()
|
|
tmpl, err := template.New("kubeconfig").Parse(`
|
|
apiVersion: v1
|
|
kind: Config
|
|
clusters:
|
|
- name: test-authz-service
|
|
cluster:
|
|
server: {{ .Server }}
|
|
users:
|
|
- name: test-api-server
|
|
current-context: webhook
|
|
contexts:
|
|
- context:
|
|
cluster: test-authz-service
|
|
user: test-api-server
|
|
name: webhook
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = tmpl.Execute(kcfd, struct {
|
|
Server string
|
|
}{
|
|
Server: webhook.URL,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
client, config, teardown := framework.StartTestServer(ctx, t, framework.TestServerSetup{
|
|
ModifyServerRunOptions: func(options *options.ServerRunOptions) {
|
|
options.Admission.GenericAdmission.EnablePlugins = append(options.Admission.GenericAdmission.EnablePlugins, "ValidatingAdmissionPolicy")
|
|
options.APIEnablement.RuntimeConfig.Set("api/all=true")
|
|
|
|
options.Authorization.Modes = []string{authzmodes.ModeWebhook}
|
|
options.Authorization.WebhookConfigFile = kcfd.Name()
|
|
options.Authorization.WebhookVersion = "v1"
|
|
// Bypass webhook cache to observe the policy plugin's cache behavior.
|
|
options.Authorization.WebhookCacheAuthorizedTTL = 0
|
|
options.Authorization.WebhookCacheUnauthorizedTTL = 0
|
|
},
|
|
})
|
|
defer teardown()
|
|
|
|
policy := &admissionregistrationv1beta1.ValidatingAdmissionPolicy{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-authorization-decision-caching-policy",
|
|
},
|
|
Spec: admissionregistrationv1beta1.ValidatingAdmissionPolicySpec{
|
|
MatchConstraints: &admissionregistrationv1beta1.MatchResources{
|
|
ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
|
|
{
|
|
ResourceNames: []string{"test-authorization-decision-caching-namespace"},
|
|
RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
|
|
Operations: []admissionregistrationv1.OperationType{
|
|
admissionregistrationv1.Create,
|
|
},
|
|
Rule: admissionregistrationv1.Rule{
|
|
APIGroups: []string{""},
|
|
APIVersions: []string{"v1"},
|
|
Resources: []string{"namespaces"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Validations: tc.validations,
|
|
},
|
|
}
|
|
|
|
policy, err = client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Create(ctx, withWaitReadyConstraintAndExpression(policy), metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := createAndWaitReady(t, client, makeBinding(policy.Name+"-binding", policy.Name, ""), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config = rest.CopyConfig(config)
|
|
config.Impersonate = rest.ImpersonationConfig{
|
|
UserName: "alice",
|
|
UID: "1234",
|
|
}
|
|
client, err = clientset.NewForConfig(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := client.CoreV1().Namespaces().Create(
|
|
ctx,
|
|
&v1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-authorization-decision-caching-namespace",
|
|
},
|
|
},
|
|
metav1.CreateOptions{},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|