diff --git a/test/e2e/auth/BUILD b/test/e2e/auth/BUILD index 894022940fc..d2a850585b9 100644 --- a/test/e2e/auth/BUILD +++ b/test/e2e/auth/BUILD @@ -19,12 +19,18 @@ go_library( "//plugin/pkg/admission/serviceaccount:go_default_library", "//test/e2e/framework:go_default_library", "//test/utils/image:go_default_library", + "//vendor/github.com/evanphx/json-patch:go_default_library", "//vendor/github.com/onsi/ginkgo:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library", "//vendor/k8s.io/api/certificates/v1beta1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", + "//vendor/k8s.io/api/extensions/v1beta1:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/test/integration/testserver:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/uuid:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/audit/v1beta1:go_default_library", diff --git a/test/e2e/auth/audit.go b/test/e2e/auth/audit.go index 2f609e39923..12fa3f336b6 100644 --- a/test/e2e/auth/audit.go +++ b/test/e2e/auth/audit.go @@ -23,83 +23,624 @@ import ( "strings" apiv1 "k8s.io/api/core/v1" + extensions "k8s.io/api/extensions/v1beta1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/test/integration/testserver" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/apis/audit/v1beta1" "k8s.io/kubernetes/test/e2e/framework" + imageutils "k8s.io/kubernetes/test/utils/image" + "github.com/evanphx/json-patch" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) +var ( + watchTestTimeout int64 = 1 + auditTestUser = "kubecfg" + + crd = testserver.NewRandomNameCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped) + crdName = strings.SplitN(crd.Name, ".", 2)[0] + crdNamespace = strings.SplitN(crd.Name, ".", 2)[1] + + watchOptions = metav1.ListOptions{TimeoutSeconds: &watchTestTimeout} + patch, _ = json.Marshal(jsonpatch.Patch{}) +) + var _ = SIGDescribe("Advanced Audit [Feature:Audit]", func() { f := framework.NewDefaultFramework("audit") It("should audit API calls", func() { namespace := f.Namespace.Name - // Create & Delete pod - pod := &apiv1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "audit-pod", + config, err := framework.LoadConfig() + framework.ExpectNoError(err, "failed to load config") + apiExtensionClient, err := clientset.NewForConfig(config) + framework.ExpectNoError(err, "failed to initialize apiExtensionClient") + + testCases := []struct { + action func() + events []auditEvent + }{ + // Create, get, update, patch, delete, list, watch pods. + { + func() { + pod := &apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "audit-pod", + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{{ + Name: "pause", + Image: framework.GetPauseImageName(f.ClientSet), + }}, + }, + } + updatePod := func(pod *apiv1.Pod) {} + + f.PodClient().CreateSync(pod) + + _, err := f.PodClient().Get(pod.Name, metav1.GetOptions{}) + framework.ExpectNoError(err, "failed to get audit-pod") + + podChan, err := f.PodClient().Watch(watchOptions) + framework.ExpectNoError(err, "failed to create watch for pods") + for range podChan.ResultChan() { + } + + f.PodClient().Update(pod.Name, updatePod) + + _, err = f.PodClient().List(metav1.ListOptions{}) + framework.ExpectNoError(err, "failed to list pods") + + _, err = f.PodClient().Patch(pod.Name, types.JSONPatchType, patch) + framework.ExpectNoError(err, "failed to patch pod") + + f.PodClient().DeleteSync(pod.Name, &metav1.DeleteOptions{}, framework.DefaultPodDeletionTimeout) + }, + []auditEvent{ + { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/pods", namespace), + "create", + 201, + auditTestUser, + "pods", + namespace, + true, + true, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/pods/audit-pod", namespace), + "get", + 200, + auditTestUser, + "pods", + namespace, + false, + false, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/pods", namespace), + "list", + 200, + auditTestUser, + "pods", + namespace, + false, + false, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseStarted, + fmt.Sprintf("/api/v1/namespaces/%s/pods?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "pods", + namespace, + false, + false, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/pods?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "pods", + namespace, + false, + false, + }, { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/pods/audit-pod", namespace), + "update", + 200, + auditTestUser, + "pods", + namespace, + true, + true, + }, { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/pods/audit-pod", namespace), + "patch", + 200, + auditTestUser, + "pods", + namespace, + true, + true, + }, { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/pods/audit-pod", namespace), + "delete", + 200, + auditTestUser, + "pods", + namespace, + true, + true, + }, + }, }, - Spec: apiv1.PodSpec{ - Containers: []apiv1.Container{{ - Name: "pause", - Image: framework.GetPauseImageName(f.ClientSet), - }}, + // Create, get, update, patch, delete, list, watch deployments. + { + func() { + podLabels := map[string]string{"name": "audit-deployment-pod"} + d := framework.NewDeployment("audit-deployment", int32(1), podLabels, "redis", imageutils.GetE2EImage(imageutils.Redis), extensions.RecreateDeploymentStrategyType) + + _, err := f.ClientSet.Extensions().Deployments(namespace).Create(d) + framework.ExpectNoError(err, "failed to create audit-deployment") + + _, err = f.ClientSet.Extensions().Deployments(namespace).Get(d.Name, metav1.GetOptions{}) + framework.ExpectNoError(err, "failed to get audit-deployment") + + deploymentChan, err := f.ClientSet.Extensions().Deployments(namespace).Watch(watchOptions) + framework.ExpectNoError(err, "failed to create watch for deployments") + for range deploymentChan.ResultChan() { + } + + _, err = f.ClientSet.Extensions().Deployments(namespace).Update(d) + framework.ExpectNoError(err, "failed to update audit-deployment") + + _, err = f.ClientSet.Extensions().Deployments(namespace).Patch(d.Name, types.JSONPatchType, patch) + framework.ExpectNoError(err, "failed to patch deployment") + + _, err = f.ClientSet.Extensions().Deployments(namespace).List(metav1.ListOptions{}) + framework.ExpectNoError(err, "failed to create list deployments") + + err = f.ClientSet.Extensions().Deployments(namespace).Delete("audit-deployment", &metav1.DeleteOptions{}) + framework.ExpectNoError(err, "failed to delete deployments") + }, + []auditEvent{ + { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments", namespace), + "create", + 201, + auditTestUser, + "deployments", + namespace, + true, + true, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseComplete, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments/audit-deployment", namespace), + "get", + 200, + auditTestUser, + "deployments", + namespace, + false, + false, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseComplete, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments", namespace), + "list", + 200, + auditTestUser, + "deployments", + namespace, + false, + false, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseStarted, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "deployments", + namespace, + false, + false, + }, { + v1beta1.LevelRequest, + v1beta1.StageResponseComplete, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "deployments", + namespace, + false, + false, + }, { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments/audit-deployment", namespace), + "update", + 200, + auditTestUser, + "deployments", + namespace, + true, + true, + }, { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments/audit-deployment", namespace), + "patch", + 200, + auditTestUser, + "deployments", + namespace, + true, + true, + }, { + v1beta1.LevelRequestResponse, + v1beta1.StageResponseComplete, + fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/deployments/audit-deployment", namespace), + "delete", + 200, + auditTestUser, + "deployments", + namespace, + true, + true, + }, + }, + }, + // Create, get, update, patch, delete, list, watch configmaps. + { + func() { + configMap := &apiv1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "audit-configmap", + }, + Data: map[string]string{ + "map-key": "map-value", + }, + } + + _, err := f.ClientSet.Core().ConfigMaps(namespace).Create(configMap) + framework.ExpectNoError(err, "failed to create audit-configmap") + + _, err = f.ClientSet.Core().ConfigMaps(namespace).Get(configMap.Name, metav1.GetOptions{}) + framework.ExpectNoError(err, "failed to get audit-configmap") + + configMapChan, err := f.ClientSet.Core().ConfigMaps(namespace).Watch(watchOptions) + framework.ExpectNoError(err, "failed to create watch for config maps") + for range configMapChan.ResultChan() { + } + + _, err = f.ClientSet.Core().ConfigMaps(namespace).Update(configMap) + framework.ExpectNoError(err, "failed to update audit-configmap") + + _, err = f.ClientSet.Core().ConfigMaps(namespace).Patch(configMap.Name, types.JSONPatchType, patch) + framework.ExpectNoError(err, "failed to patch configmap") + + _, err = f.ClientSet.Core().ConfigMaps(namespace).List(metav1.ListOptions{}) + framework.ExpectNoError(err, "failed to list config maps") + + err = f.ClientSet.Core().ConfigMaps(namespace).Delete(configMap.Name, &metav1.DeleteOptions{}) + framework.ExpectNoError(err, "failed to delete audit-configmap") + }, + []auditEvent{ + { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps", namespace), + "create", + 201, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace), + "get", + 200, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps", namespace), + "list", + 200, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseStarted, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace), + "update", + 200, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace), + "patch", + 200, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace), + "delete", + 200, + auditTestUser, + "configmaps", + namespace, + false, + false, + }, + }, + }, + // Create, get, update, patch, delete, list, watch secrets. + { + func() { + secret := &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "audit-secret", + }, + Data: map[string][]byte{ + "top-secret": []byte("foo-bar"), + }, + } + _, err := f.ClientSet.Core().Secrets(namespace).Create(secret) + framework.ExpectNoError(err, "failed to create audit-secret") + + _, err = f.ClientSet.Core().Secrets(namespace).Get(secret.Name, metav1.GetOptions{}) + framework.ExpectNoError(err, "failed to get audit-secret") + + secretChan, err := f.ClientSet.Core().Secrets(namespace).Watch(watchOptions) + framework.ExpectNoError(err, "failed to create watch for secrets") + for range secretChan.ResultChan() { + } + + _, err = f.ClientSet.Core().Secrets(namespace).Update(secret) + framework.ExpectNoError(err, "failed to update audit-secret") + + _, err = f.ClientSet.Core().Secrets(namespace).Patch(secret.Name, types.JSONPatchType, patch) + framework.ExpectNoError(err, "failed to patch secret") + + _, err = f.ClientSet.Core().Secrets(namespace).List(metav1.ListOptions{}) + framework.ExpectNoError(err, "failed to list secrets") + + err = f.ClientSet.Core().Secrets(namespace).Delete(secret.Name, &metav1.DeleteOptions{}) + framework.ExpectNoError(err, "failed to delete audit-secret") + }, + []auditEvent{ + { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/secrets", namespace), + "create", + 201, + auditTestUser, + "secrets", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/secrets/audit-secret", namespace), + "get", + 200, + auditTestUser, + "secrets", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/secrets", namespace), + "list", + 200, + auditTestUser, + "secrets", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseStarted, + fmt.Sprintf("/api/v1/namespaces/%s/secrets?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "secrets", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/secrets?timeoutSeconds=%d&watch=true", namespace, watchTestTimeout), + "watch", + 200, + auditTestUser, + "secrets", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/secrets/audit-secret", namespace), + "update", + 200, + auditTestUser, + "secrets", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/secrets/audit-secret", namespace), + "patch", + 200, + auditTestUser, + "secrets", + namespace, + false, + false, + }, { + v1beta1.LevelMetadata, + v1beta1.StageResponseComplete, + fmt.Sprintf("/api/v1/namespaces/%s/secrets/audit-secret", namespace), + "delete", + 200, + auditTestUser, + "secrets", + namespace, + false, + false, + }, + }, + }, + // Create and delete custom resource definition. + { + func() { + _, err = testserver.CreateNewCustomResourceDefinition(crd, apiExtensionClient, f.ClientPool) + framework.ExpectNoError(err, "failed to create custom resource definition") + testserver.DeleteCustomResourceDefinition(crd, apiExtensionClient) + }, + []auditEvent{ + { + level: v1beta1.LevelRequestResponse, + stage: v1beta1.StageResponseComplete, + requestURI: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", + verb: "create", + code: 201, + user: auditTestUser, + resource: "customresourcedefinitions", + requestObject: true, + responseObject: true, + }, { + level: v1beta1.LevelMetadata, + stage: v1beta1.StageResponseComplete, + requestURI: fmt.Sprintf("/apis/%s/v1beta1/%s", crdNamespace, crdName), + verb: "create", + code: 201, + user: auditTestUser, + resource: crdName, + requestObject: false, + responseObject: false, + }, { + level: v1beta1.LevelRequestResponse, + stage: v1beta1.StageResponseComplete, + requestURI: fmt.Sprintf("/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/%s", crd.Name), + verb: "delete", + code: 200, + user: auditTestUser, + resource: "customresourcedefinitions", + requestObject: false, + responseObject: true, + }, { + level: v1beta1.LevelMetadata, + stage: v1beta1.StageResponseComplete, + requestURI: fmt.Sprintf("/apis/%s/v1beta1/%s/setup-instance", crdNamespace, crdName), + verb: "delete", + code: 200, + user: auditTestUser, + resource: crdName, + requestObject: false, + responseObject: false, + }, + }, }, } - f.PodClient().CreateSync(pod) - f.PodClient().DeleteSync(pod.Name, &metav1.DeleteOptions{}, framework.DefaultPodDeletionTimeout) - // Create, Read, Delete secret - secret := &apiv1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "audit-secret", - }, - Data: map[string][]byte{ - "top-secret": []byte("foo-bar"), - }, + expectedEvents := []auditEvent{} + for _, t := range testCases { + t.action() + expectedEvents = append(expectedEvents, t.events...) } - _, err := f.ClientSet.Core().Secrets(f.Namespace.Name).Create(secret) - framework.ExpectNoError(err, "failed to create audit-secret") - _, err = f.ClientSet.Core().Secrets(f.Namespace.Name).Get(secret.Name, metav1.GetOptions{}) - framework.ExpectNoError(err, "failed to get audit-secret") - err = f.ClientSet.Core().Secrets(f.Namespace.Name).Delete(secret.Name, &metav1.DeleteOptions{}) - framework.ExpectNoError(err, "failed to delete audit-secret") - expectedEvents := []auditEvent{{ - method: "create", - namespace: namespace, - uri: fmt.Sprintf("/api/v1/namespaces/%s/pods", namespace), - response: "201", - }, { - method: "delete", - namespace: namespace, - uri: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, pod.Name), - response: "200", - }, { - method: "create", - namespace: namespace, - uri: fmt.Sprintf("/api/v1/namespaces/%s/secrets", namespace), - response: "201", - }, { - method: "get", - namespace: namespace, - uri: fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", namespace, secret.Name), - response: "200", - }, { - method: "delete", - namespace: namespace, - uri: fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", namespace, secret.Name), - response: "200", - }} expectAuditLines(f, expectedEvents) }) }) type auditEvent struct { - method, namespace, uri, response string + level v1beta1.Level + stage v1beta1.Stage + requestURI string + verb string + code int32 + user string + resource string + namespace string + requestObject bool + responseObject bool } // Search the audit log for the expected audit lines. @@ -134,46 +675,28 @@ func expectAuditLines(f *framework.Framework, expected []auditEvent) { func parseAuditLine(line string) (auditEvent, error) { var e v1beta1.Event - if err := json.Unmarshal([]byte(line), &e); err == nil { - event := auditEvent{ - method: e.Verb, - uri: e.RequestURI, - } - if e.ObjectRef != nil { - event.namespace = e.ObjectRef.Namespace - } - if e.ResponseStatus != nil { - event.response = fmt.Sprintf("%d", e.ResponseStatus.Code) - } - return event, nil + if err := json.Unmarshal([]byte(line), &e); err != nil { + return auditEvent{}, err } - - fields := strings.Fields(line) - if len(fields) < 3 { - return auditEvent{}, fmt.Errorf("could not parse audit line: %s", line) + event := auditEvent{ + level: e.Level, + stage: e.Stage, + requestURI: e.RequestURI, + verb: e.Verb, + user: e.User.Username, } - // Ignore first field (timestamp) - if fields[1] != "AUDIT:" { - return auditEvent{}, fmt.Errorf("unexpected audit line format: %s", line) + if e.ObjectRef != nil { + event.namespace = e.ObjectRef.Namespace + event.resource = e.ObjectRef.Resource } - fields = fields[2:] - event := auditEvent{} - for _, f := range fields { - parts := strings.SplitN(f, "=", 2) - if len(parts) != 2 { - return auditEvent{}, fmt.Errorf("could not parse audit line (part: %q): %s", f, line) - } - value := strings.Trim(parts[1], "\"") - switch parts[0] { - case "method": - event.method = value - case "namespace": - event.namespace = value - case "uri": - event.uri = value - case "response": - event.response = value - } + if e.ResponseStatus != nil { + event.code = e.ResponseStatus.Code + } + if e.ResponseObject != nil { + event.responseObject = true + } + if e.RequestObject != nil { + event.requestObject = true } return event, nil }