123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- /*
- Copyright 2018 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 master
- import (
- "context"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "os"
- "strings"
- "testing"
- "time"
- "k8s.io/api/admission/v1beta1"
- admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
- apiv1 "k8s.io/api/core/v1"
- 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/wait"
- "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
- auditinternal "k8s.io/apiserver/pkg/apis/audit"
- auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
- auditv1beta1 "k8s.io/apiserver/pkg/apis/audit/v1beta1"
- "k8s.io/client-go/kubernetes"
- clientset "k8s.io/client-go/kubernetes"
- kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
- "k8s.io/kubernetes/test/integration/framework"
- "k8s.io/kubernetes/test/utils"
- jsonpatch "github.com/evanphx/json-patch"
- )
- const (
- testWebhookConfigurationName = "auditmutation.integration.test"
- testWebhookName = "auditmutation.integration.test"
- )
- var (
- auditPolicyPattern = `
- apiVersion: {version}
- kind: Policy
- rules:
- - level: {level}
- resources:
- - group: "" # core
- resources: ["configmaps"]
- `
- namespace = "default"
- watchTestTimeout int64 = 1
- watchOptions = metav1.ListOptions{TimeoutSeconds: &watchTestTimeout}
- patch, _ = json.Marshal(jsonpatch.Patch{})
- auditTestUser = "system:apiserver"
- versions = map[string]schema.GroupVersion{
- "audit.k8s.io/v1": auditv1.SchemeGroupVersion,
- "audit.k8s.io/v1beta1": auditv1beta1.SchemeGroupVersion,
- }
- expectedEvents = []utils.AuditEvent{
- {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", namespace),
- Verb: "create",
- Code: 201,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: true,
- ResponseObject: true,
- AuthorizeDecision: "allow",
- }, {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace),
- Verb: "get",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: false,
- ResponseObject: true,
- AuthorizeDecision: "allow",
- }, {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", namespace),
- Verb: "list",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: false,
- ResponseObject: true,
- AuthorizeDecision: "allow",
- }, {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseStarted,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps?timeout=%ds&timeoutSeconds=%d&watch=true", namespace, watchTestTimeout, watchTestTimeout),
- Verb: "watch",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: false,
- ResponseObject: false,
- AuthorizeDecision: "allow",
- }, {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps?timeout=%ds&timeoutSeconds=%d&watch=true", namespace, watchTestTimeout, watchTestTimeout),
- Verb: "watch",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: false,
- ResponseObject: false,
- AuthorizeDecision: "allow",
- }, {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace),
- Verb: "update",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: true,
- ResponseObject: true,
- AuthorizeDecision: "allow",
- }, {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace),
- Verb: "patch",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: true,
- ResponseObject: true,
- AuthorizeDecision: "allow",
- }, {
- Level: auditinternal.LevelRequestResponse,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace),
- Verb: "delete",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- RequestObject: true,
- ResponseObject: true,
- AuthorizeDecision: "allow",
- },
- }
- )
- // TestAudit ensures that both v1beta1 and v1 version audit api could work.
- func TestAudit(t *testing.T) {
- tcs := []struct {
- auditLevel auditinternal.Level
- enableMutatingWebhook bool
- }{
- {
- auditLevel: auditinternal.LevelRequestResponse,
- enableMutatingWebhook: false,
- },
- {
- auditLevel: auditinternal.LevelMetadata,
- enableMutatingWebhook: true,
- },
- {
- auditLevel: auditinternal.LevelRequest,
- enableMutatingWebhook: true,
- },
- {
- auditLevel: auditinternal.LevelRequestResponse,
- enableMutatingWebhook: true,
- },
- }
- for version := range versions {
- for _, tc := range tcs {
- t.Run(fmt.Sprintf("%s.%s.%t", version, tc.auditLevel, tc.enableMutatingWebhook), func(t *testing.T) {
- testAudit(t, version, tc.auditLevel, tc.enableMutatingWebhook)
- })
- }
- }
- }
- func testAudit(t *testing.T, version string, level auditinternal.Level, enableMutatingWebhook bool) {
- var url string
- var err error
- closeFunc := func() {}
- if enableMutatingWebhook {
- webhookMux := http.NewServeMux()
- webhookMux.Handle("/mutation", utils.AdmissionWebhookHandler(t, admitFunc))
- url, closeFunc, err = utils.NewAdmissionWebhookServer(webhookMux)
- }
- defer closeFunc()
- if err != nil {
- t.Fatalf("%v", err)
- }
- // prepare audit policy file
- auditPolicy := strings.Replace(auditPolicyPattern, "{version}", version, 1)
- auditPolicy = strings.Replace(auditPolicy, "{level}", string(level), 1)
- policyFile, err := ioutil.TempFile("", "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 := ioutil.TempFile("", "audit.log")
- if err != nil {
- t.Fatalf("Failed to create audit log file: %v", err)
- }
- defer os.Remove(logFile.Name())
- // start api server
- result := kubeapiservertesting.StartTestServerOrDie(t, nil,
- []string{
- "--audit-policy-file", policyFile.Name(),
- "--audit-log-version", version,
- "--audit-log-mode", "blocking",
- "--audit-log-path", logFile.Name()},
- framework.SharedEtcd())
- defer result.TearDownFn()
- kubeclient, err := kubernetes.NewForConfig(result.ClientConfig)
- if err != nil {
- t.Fatalf("Unexpected error: %v", err)
- }
- if enableMutatingWebhook {
- if err := createV1beta1MutationWebhook(kubeclient, url+"/mutation"); err != nil {
- t.Fatal(err)
- }
- }
- var lastMissingReport string
- if err := wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
- // perform configmap operations
- configMapOperations(t, kubeclient)
- // check for corresponding audit logs
- stream, err := os.Open(logFile.Name())
- if err != nil {
- return false, fmt.Errorf("unexpected error: %v", err)
- }
- defer stream.Close()
- missingReport, err := utils.CheckAuditLines(stream, getExpectedEvents(level, enableMutatingWebhook), versions[version])
- if err != nil {
- return false, fmt.Errorf("unexpected error: %v", err)
- }
- if len(missingReport.MissingEvents) > 0 {
- lastMissingReport = missingReport.String()
- return false, nil
- }
- return true, nil
- }); err != nil {
- t.Fatalf("failed to get expected events -- missingReport: %s, error: %v", lastMissingReport, err)
- }
- }
- func getExpectedEvents(level auditinternal.Level, enableMutatingWebhook bool) []utils.AuditEvent {
- if !enableMutatingWebhook {
- return expectedEvents
- }
- var webhookMutationAnnotations, webhookPatchAnnotations map[string]string
- var requestObject, responseObject bool
- if level.GreaterOrEqual(auditinternal.LevelMetadata) {
- // expect mutation existence annotation
- webhookMutationAnnotations = map[string]string{}
- webhookMutationAnnotations[mutating.MutationAuditAnnotationPrefix+"round_0_index_0"] = fmt.Sprintf(`{"configuration":"%s","webhook":"%s","mutated":%t}`, testWebhookConfigurationName, testWebhookName, true)
- }
- if level.GreaterOrEqual(auditinternal.LevelRequest) {
- // expect actual patch annotation
- webhookPatchAnnotations = map[string]string{}
- webhookPatchAnnotations[mutating.PatchAuditAnnotationPrefix+"round_0_index_0"] = strings.Replace(fmt.Sprintf(`{"configuration": "%s", "webhook": "%s", "patch": %s, "patchType": "JSONPatch"}`, testWebhookConfigurationName, testWebhookName, `[{"op":"add","path":"/data","value":{"test":"dummy"}}]`), " ", "", -1)
- // expect request object in audit log
- requestObject = true
- }
- if level.GreaterOrEqual(auditinternal.LevelRequestResponse) {
- // expect response obect in audit log
- responseObject = true
- }
- return []utils.AuditEvent{
- {
- // expect CREATE audit event with webhook in effect
- Level: level,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", namespace),
- Verb: "create",
- Code: 201,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- AuthorizeDecision: "allow",
- RequestObject: requestObject,
- ResponseObject: responseObject,
- AdmissionWebhookMutationAnnotations: webhookMutationAnnotations,
- AdmissionWebhookPatchAnnotations: webhookPatchAnnotations,
- }, {
- // expect UPDATE audit event with webhook in effect
- Level: level,
- Stage: auditinternal.StageResponseComplete,
- RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace),
- Verb: "update",
- Code: 200,
- User: auditTestUser,
- Resource: "configmaps",
- Namespace: namespace,
- AuthorizeDecision: "allow",
- RequestObject: requestObject,
- ResponseObject: responseObject,
- AdmissionWebhookMutationAnnotations: webhookMutationAnnotations,
- AdmissionWebhookPatchAnnotations: webhookPatchAnnotations,
- },
- }
- }
- // configMapOperations is a set of known operations performed on the configmap type
- // which correspond to the expected events.
- // This is shared by the dynamic test
- func configMapOperations(t *testing.T, kubeclient kubernetes.Interface) {
- // create, get, watch, update, patch, list and delete configmap.
- configMap := &apiv1.ConfigMap{
- ObjectMeta: metav1.ObjectMeta{
- Name: "audit-configmap",
- },
- Data: map[string]string{
- "map-key": "map-value",
- },
- }
- _, err := kubeclient.CoreV1().ConfigMaps(namespace).Create(context.TODO(), configMap, metav1.CreateOptions{})
- expectNoError(t, err, "failed to create audit-configmap")
- _, err = kubeclient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMap.Name, metav1.GetOptions{})
- expectNoError(t, err, "failed to get audit-configmap")
- configMapChan, err := kubeclient.CoreV1().ConfigMaps(namespace).Watch(context.TODO(), watchOptions)
- expectNoError(t, err, "failed to create watch for config maps")
- for range configMapChan.ResultChan() {
- // Block until watchOptions.TimeoutSeconds expires.
- // If the test finishes before watchOptions.TimeoutSeconds expires, the watch audit
- // event at stage ResponseComplete will not be generated.
- }
- _, err = kubeclient.CoreV1().ConfigMaps(namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{})
- expectNoError(t, err, "failed to update audit-configmap")
- _, err = kubeclient.CoreV1().ConfigMaps(namespace).Patch(context.TODO(), configMap.Name, types.JSONPatchType, patch, metav1.PatchOptions{})
- expectNoError(t, err, "failed to patch configmap")
- _, err = kubeclient.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
- expectNoError(t, err, "failed to list config maps")
- err = kubeclient.CoreV1().ConfigMaps(namespace).Delete(context.TODO(), configMap.Name, &metav1.DeleteOptions{})
- expectNoError(t, err, "failed to delete audit-configmap")
- }
- func expectNoError(t *testing.T, err error, msg string) {
- if err != nil {
- t.Fatalf("%s: %v", msg, err)
- }
- }
- func admitFunc(review *v1beta1.AdmissionReview) error {
- gvk := schema.GroupVersionKind{Group: "admission.k8s.io", Version: "v1beta1", Kind: "AdmissionReview"}
- if review.GetObjectKind().GroupVersionKind() != gvk {
- return fmt.Errorf("invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind())
- }
- if len(review.Request.Object.Raw) > 0 {
- u := &unstructured.Unstructured{Object: map[string]interface{}{}}
- if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil {
- return fmt.Errorf("failed to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err)
- }
- review.Request.Object.Object = u
- }
- if len(review.Request.OldObject.Raw) > 0 {
- u := &unstructured.Unstructured{Object: map[string]interface{}{}}
- if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil {
- return fmt.Errorf("failed to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err)
- }
- review.Request.OldObject.Object = u
- }
- review.Response = &v1beta1.AdmissionResponse{
- Allowed: true,
- UID: review.Request.UID,
- Result: &metav1.Status{Message: "admitted"},
- }
- review.Response.Patch = []byte(`[{"op":"add","path":"/data","value":{"test":"dummy"}}]`)
- jsonPatch := v1beta1.PatchTypeJSONPatch
- review.Response.PatchType = &jsonPatch
- return nil
- }
- func createV1beta1MutationWebhook(client clientset.Interface, endpoint string) error {
- fail := admissionv1beta1.Fail
- // Attaching Mutation webhook to API server
- _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1beta1.MutatingWebhookConfiguration{
- ObjectMeta: metav1.ObjectMeta{Name: testWebhookConfigurationName},
- Webhooks: []admissionv1beta1.MutatingWebhook{{
- Name: testWebhookName,
- ClientConfig: admissionv1beta1.WebhookClientConfig{
- URL: &endpoint,
- CABundle: utils.LocalhostCert,
- },
- Rules: []admissionv1beta1.RuleWithOperations{{
- Operations: []admissionv1beta1.OperationType{admissionv1beta1.Create, admissionv1beta1.Update},
- Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
- }},
- FailurePolicy: &fail,
- AdmissionReviewVersions: []string{"v1beta1"},
- }},
- }, metav1.CreateOptions{})
- return err
- }
|