From b40bc9d112891d5297aa5e686c23c87496c6b313 Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Tue, 19 Sep 2017 13:08:07 +0200 Subject: [PATCH] Basic audit extended test --- test/extended/cluster/audit.go | 150 +++++++++++++++++++++++++++++++++ test/extended/setup.sh | 7 +- 2 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 test/extended/cluster/audit.go diff --git a/test/extended/cluster/audit.go b/test/extended/cluster/audit.go new file mode 100644 index 000000000000..c634c191ded0 --- /dev/null +++ b/test/extended/cluster/audit.go @@ -0,0 +1,150 @@ +package cluster + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + + g "github.com/onsi/ginkgo" + o "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiv1 "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/test/e2e/framework" +) + +var _ = g.Describe("[Feature:Audit] Basic audit", func() { + f := framework.NewDefaultFramework("audit") + + g.It("should audit API calls", func() { + namespace := f.Namespace.Name + + // Create & Delete pod + pod := &apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "audit-pod", + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{{ + Name: "pause", + Image: framework.GetPauseImageName(f.ClientSet), + }}, + }, + } + 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"), + }, + } + _, 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") + + // /version should not be audited + _, err = f.ClientSet.Core().RESTClient().Get().AbsPath("/version").DoRaw() + framework.ExpectNoError(err, "failed to query version") + + 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 +} + +// Search the audit log for the expected audit lines. +func expectAuditLines(f *framework.Framework, expected []auditEvent) { + expectations := map[auditEvent]bool{} + for _, event := range expected { + expectations[event] = false + } + + stream, err := os.Open(filepath.Join(os.Getenv("LOG_DIR"), "audit.log")) + defer stream.Close() + framework.ExpectNoError(err, "error opening audit log") + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + line := scanner.Text() + event, err := parseAuditLine(line) + framework.ExpectNoError(err) + + // If the event was expected, mark it as found. + if _, found := expectations[event]; found { + expectations[event] = true + } + } + framework.ExpectNoError(scanner.Err(), "error reading audit log") + + for event, found := range expectations { + o.Expect(found).To(o.BeTrue(), "Event %#v not found!", event) + } +} + +func parseAuditLine(line string) (auditEvent, error) { + fields := strings.Fields(line) + if len(fields) < 3 { + return auditEvent{}, fmt.Errorf("could not parse audit line: %s", line) + } + // Ignore first field (timestamp) + if fields[1] != "AUDIT:" { + return auditEvent{}, fmt.Errorf("unexpected audit line format: %s", line) + } + 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 + } + } + return event, nil +} diff --git a/test/extended/setup.sh b/test/extended/setup.sh index 69ab25a1cc54..2b3618bad148 100644 --- a/test/extended/setup.sh +++ b/test/extended/setup.sh @@ -16,7 +16,7 @@ function os::test::extended::focus () { TEST_REPORT_FILE_NAME=focus_parallel TEST_PARALLEL="${PARALLEL_NODES:-5}" os::test::extended::run -- -ginkgo.skip "\[Serial\]" -test.timeout 6h ${TEST_EXTENDED_ARGS-} || exitstatus=$? # Then run everything that requires serial and matches the $FOCUS, serially. - # there is bit of overlap here because not all serial tests declare [Serial], so they might have run in the + # there is bit of overlap here because not all serial tests declare [Serial], so they might have run in the # parallel section above. Hopefully your focus was precise enough to exclude them, and we should be adding # the [Serial] tag to them as needed. os::log::info "" @@ -110,11 +110,12 @@ function os::test::extended::setup () { CONFIG_VERSION="${CONTROLLER_VERSION}" fi os::start::configure_server "${CONFIG_VERSION}" - #turn on audit logging for extended tests ... mimic what is done in os::start::configure_server, but don't + # turn on audit logging for extended tests ... mimic what is done in os::start::configure_server, but don't # put change there - only want this for extended tests os::log::info "Turn on audit logging" cp "${SERVER_CONFIG_DIR}/master/master-config.yaml" "${SERVER_CONFIG_DIR}/master/master-config.orig2.yaml" - openshift ex config patch "${SERVER_CONFIG_DIR}/master/master-config.orig2.yaml" --patch="{\"auditConfig\": {\"enabled\": true}}" > "${SERVER_CONFIG_DIR}/master/master-config.yaml" + openshift ex config patch "${SERVER_CONFIG_DIR}/master/master-config.orig2.yaml" --patch="{\"auditConfig\": {\"enabled\": true, \"auditFilePath\": \"${LOG_DIR}/audit.log\"}}" > "${SERVER_CONFIG_DIR}/master/master-config.yaml" + exit 1 cp "${SERVER_CONFIG_DIR}/master/master-config.yaml" "${SERVER_CONFIG_DIR}/master/master-config.orig2.yaml" openshift ex config patch "${SERVER_CONFIG_DIR}/master/master-config.orig2.yaml" --patch="{\"templateServiceBrokerConfig\": {\"templateNamespaces\": [\"openshift\"]}}" > "${SERVER_CONFIG_DIR}/master/master-config.yaml"