Merge pull request #53119 from loburm/audit-e2e

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Improve e2e tests of audit logging.

Now test includes:
 * Verbs: create, list, watch, delete, get, update, patch.
 * Resources: pods, deployments, secrets, config maps, custom resource
 definition.
 * More fields: user, resource, level, stage, presence of request and
 response objects.

Fixes #49653
This commit is contained in:
Kubernetes Submit Queue 2017-10-12 07:36:44 -07:00 committed by GitHub
commit 0b1da1fb37
2 changed files with 618 additions and 89 deletions

View File

@ -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",

View File

@ -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
}