From 08f8689102f7427d10cc07c925e864f126fe99c9 Mon Sep 17 00:00:00 2001 From: Ali Ok Date: Thu, 19 Sep 2024 14:16:15 +0300 Subject: [PATCH] E2E tests for backend security (#85) * Added e2e tests Signed-off-by: Ali Ok * Check in vendored files Signed-off-by: Ali Ok * Actually run the tests Signed-off-by: Ali Ok * goimports Signed-off-by: Ali Ok --------- Signed-off-by: Ali Ok --- backends/tests/e2e/integration_test.go | 66 +++++++- backends/tests/e2e/main_test.go | 41 +++++ go.mod | 2 +- hack/tools.go | 3 + test/config/eventmesh-backend-user.yaml | 72 ++++++++ .../pkg/eventshub/assert/doc.go | 20 +++ .../eventshub/assert/event_info_matchers.go | 133 +++++++++++++++ .../pkg/eventshub/assert/step.go | 156 ++++++++++++++++++ vendor/modules.txt | 1 + 9 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 backends/tests/e2e/main_test.go create mode 100644 test/config/eventmesh-backend-user.yaml create mode 100644 vendor/knative.dev/reconciler-test/pkg/eventshub/assert/doc.go create mode 100644 vendor/knative.dev/reconciler-test/pkg/eventshub/assert/event_info_matchers.go create mode 100644 vendor/knative.dev/reconciler-test/pkg/eventshub/assert/step.go diff --git a/backends/tests/e2e/integration_test.go b/backends/tests/e2e/integration_test.go index 4045b8c3..48c93bf4 100644 --- a/backends/tests/e2e/integration_test.go +++ b/backends/tests/e2e/integration_test.go @@ -4,9 +4,71 @@ package e2e import ( + "context" "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubeclient "knative.dev/pkg/client/injection/kube/client" + "knative.dev/reconciler-test/pkg/k8s" + "knative.dev/reconciler-test/pkg/knative" + + "knative.dev/reconciler-test/pkg/environment" + "knative.dev/reconciler-test/pkg/eventshub" + "knative.dev/reconciler-test/pkg/eventshub/assert" + "knative.dev/reconciler-test/pkg/feature" ) -func TestSample(t *testing.T) { - t.Log("All good") +func TestIntegration(t *testing.T) { + t.Parallel() + + ctx, env := global.Environment( + knative.WithKnativeNamespace("knative-eventing"), + knative.WithLoggingConfig, + knative.WithTracingConfig, + k8s.WithEventListener, + environment.Managed(t), + ) + env.Test(ctx, t, VerifyBackstageBackendAuthentication()) +} + +func VerifyBackstageBackendAuthentication() *feature.Feature { + + f := feature.NewFeature() + + authenticatedClientName := feature.MakeRandomK8sName("authenticated-client") + unauthenticatedClientName := feature.MakeRandomK8sName("unauthenticated-client") + SANamespace := "eventmesh-backend-user-namespace" + SecretName := "eventmesh-backend-user-secret" + + f.Setup("request with authenticated client", func(ctx context.Context, t feature.T) { + secret, err := kubeclient.Get(ctx).CoreV1().Secrets(SANamespace).Get(ctx, SecretName, metav1.GetOptions{}) + if err != nil { + t.Fatal("Failed to get secret", err) + } + + token := string(secret.Data["token"]) + + eventshub.Install(authenticatedClientName, + eventshub.StartSenderURL("http://eventmesh-backend.knative-eventing.svc.cluster.local:8080"), + eventshub.InputHeader("Authorization", "Bearer "+token), + eventshub.InputMethod("GET"), + )(ctx, t) + }) + f.Setup("request with unauthenticated client", eventshub.Install( + unauthenticatedClientName, + eventshub.StartSenderURL("http://eventmesh-backend.knative-eventing.svc.cluster.local:8080"), + eventshub.InputHeader("Foo", "Bar"), + eventshub.InputMethod("GET")), + ) + + f.Assert("assert response with authenticated client", assert.OnStore(authenticatedClientName). + Match(assert.MatchKind(eventshub.EventResponse)). + Match(assert.MatchStatusCode(200)). + AtLeast(1)) + f.Assert("assert response with unauthenticated client", assert.OnStore(unauthenticatedClientName). + Match(assert.MatchKind(eventshub.EventResponse)). + Match(assert.MatchStatusCode(401)). + AtLeast(1)) + + return f } diff --git a/backends/tests/e2e/main_test.go b/backends/tests/e2e/main_test.go new file mode 100644 index 00000000..896460b0 --- /dev/null +++ b/backends/tests/e2e/main_test.go @@ -0,0 +1,41 @@ +//go:build e2e +// +build e2e + +/* + * Copyright 2024 The Knative 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 e2e + +import ( + "os" + "testing" + + _ "knative.dev/pkg/system/testing" + "knative.dev/reconciler-test/pkg/environment" +) + +// global is the singleton instance of GlobalEnvironment. It is used to parse +// the testing config for the test run. The config will specify the cluster +// config as well as the parsing level and state flags. +var global environment.GlobalEnvironment + +// TestMain is the first entry point for `go test`. +func TestMain(m *testing.M) { + global = environment.NewStandardGlobalEnvironment() + + // Run the tests. + os.Exit(m.Run()) +} diff --git a/go.mod b/go.mod index 0acd69b2..25f2f9d7 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( knative.dev/eventing v0.39.1 knative.dev/hack v0.0.0-20231122182901-eb352426ecc1 knative.dev/pkg v0.0.0-20231204120332-9386ad6703ee + knative.dev/reconciler-test v0.0.0-20231024072442-5fb93a792b99 ) require ( @@ -103,7 +104,6 @@ require ( k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect - knative.dev/reconciler-test v0.0.0-20231024072442-5fb93a792b99 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/hack/tools.go b/hack/tools.go index 5dc20574..5ba8d145 100644 --- a/hack/tools.go +++ b/hack/tools.go @@ -23,4 +23,7 @@ import ( _ "knative.dev/eventing/hack" _ "knative.dev/hack" _ "knative.dev/pkg/hack" + + // eventshub is a cloudevents sender/receiver utility for e2e testing. + _ "knative.dev/reconciler-test/cmd/eventshub" ) diff --git a/test/config/eventmesh-backend-user.yaml b/test/config/eventmesh-backend-user.yaml new file mode 100644 index 00000000..1dc064d0 --- /dev/null +++ b/test/config/eventmesh-backend-user.yaml @@ -0,0 +1,72 @@ +# Copyright 2024 The Knative 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 +# +# https://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. + +apiVersion: v1 +kind: Namespace +metadata: + name: eventmesh-backend-user-namespace +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: eventmesh-backend-user-service-account + namespace: eventmesh-backend-user-namespace +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: eventmesh-backend-user-cluster-role +rules: + # permissions for eventtypes, brokers and triggers + - apiGroups: + - "eventing.knative.dev" + resources: + - brokers + - eventtypes + - triggers + verbs: + - get + - list + - watch + # permissions to get subscribers for triggers + # as subscribers can be any resource, we need to give access to all resources + # we fetch subscribers one by one, we only need `get` verb + - apiGroups: + - "*" + resources: + - "*" + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: eventmesh-backend-user-cluster-role-binding +subjects: + - kind: ServiceAccount + name: eventmesh-backend-user-service-account + namespace: eventmesh-backend-user-namespace +roleRef: + kind: ClusterRole + name: eventmesh-backend-user-cluster-role + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: Secret +metadata: + name: eventmesh-backend-user-secret + namespace: eventmesh-backend-user-namespace + annotations: + kubernetes.io/service-account.name: eventmesh-backend-user-service-account +type: kubernetes.io/service-account-token diff --git a/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/doc.go b/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/doc.go new file mode 100644 index 00000000..1f285282 --- /dev/null +++ b/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2021 The Knative 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 + + https://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. +*/ + +/* +assert contains the necessary matchers and steps to perform assertions on eventshub contents +*/ +package assert diff --git a/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/event_info_matchers.go b/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/event_info_matchers.go new file mode 100644 index 00000000..be484d1f --- /dev/null +++ b/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/event_info_matchers.go @@ -0,0 +1,133 @@ +/* +Copyright 2020 The Knative 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 + + https://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 assert + +import ( + "fmt" + "strings" + + cloudevents "github.com/cloudevents/sdk-go/v2" + cetest "github.com/cloudevents/sdk-go/v2/test" + + "knative.dev/reconciler-test/pkg/eventshub" +) + +// Matcher that never fails +func Any() eventshub.EventInfoMatcher { + return func(ei eventshub.EventInfo) error { + return nil + } +} + +// Matcher that fails if there is an error in the EventInfo +func NoError() eventshub.EventInfoMatcher { + return func(ei eventshub.EventInfo) error { + if ei.Error != "" { + return fmt.Errorf("not expecting an error in event info: %s", ei.Error) + } + return nil + } +} + +// Convert a matcher that checks valid messages to a function +// that checks EventInfo structures, returning an error for any that don't +// contain valid events. +func MatchEvent(evf ...cetest.EventMatcher) eventshub.EventInfoMatcher { + return func(ei eventshub.EventInfo) error { + if ei.Event == nil { + return fmt.Errorf("Saw nil event") + } else { + return cetest.AllOf(evf...)(*ei.Event) + } + } +} + +// Convert a matcher that checks valid messages to a function +// that checks EventInfo structures, returning an error for any that don't +// contain valid events. +func HasAdditionalHeader(key, value string) eventshub.EventInfoMatcher { + key = strings.ToLower(key) + return func(ei eventshub.EventInfo) error { + for k, v := range ei.HTTPHeaders { + if strings.ToLower(k) == key && v[0] == value { + return nil + } + } + return fmt.Errorf("cannot find header '%s' = '%s' between the headers", key, value) + } +} + +// Reexport kinds here to simplify the usage +const ( + EventReceived = eventshub.EventReceived + EventRejected = eventshub.EventRejected + + EventSent = eventshub.EventSent + EventResponse = eventshub.EventResponse +) + +// MatchKind matches the kind of EventInfo +func MatchKind(kind eventshub.EventKind) eventshub.EventInfoMatcher { + return func(info eventshub.EventInfo) error { + if kind != info.Kind { + return fmt.Errorf("event kind don't match. Expected: '%s', Actual: '%s'", kind, info.Kind) + } + return nil + } +} + +func OneOf(matchers ...eventshub.EventInfoMatcher) eventshub.EventInfoMatcher { + return func(info eventshub.EventInfo) error { + var lastErr error + for _, m := range matchers { + err := m(info) + if err == nil { + return nil + } + lastErr = err + } + return lastErr + } +} + +// MatchStatusCode matches the status code of EventInfo +func MatchStatusCode(statusCode int) eventshub.EventInfoMatcher { + return func(info eventshub.EventInfo) error { + if info.StatusCode != statusCode { + return fmt.Errorf("event status code don't match. Expected: '%d', Actual: '%d'", statusCode, info.StatusCode) + } + return nil + } +} + +// MatchHeartBeatsImageMessage matches that the data field of the event, in the format of the heartbeats image, contains the following msg field +func MatchHeartBeatsImageMessage(expectedMsg string) cetest.EventMatcher { + return cetest.AllOf( + cetest.HasDataContentType(cloudevents.ApplicationJSON), + func(have cloudevents.Event) error { + var m map[string]interface{} + err := have.DataAs(&m) + if err != nil { + return fmt.Errorf("cannot parse heartbeats message %s", err.Error()) + } + if m["msg"].(string) != expectedMsg { + return fmt.Errorf("heartbeats message don't match. Expected: '%s', Actual: '%s'", expectedMsg, m["msg"].(string)) + } + return nil + }, + ) +} diff --git a/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/step.go b/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/step.go new file mode 100644 index 00000000..2bbff311 --- /dev/null +++ b/vendor/knative.dev/reconciler-test/pkg/eventshub/assert/step.go @@ -0,0 +1,156 @@ +package assert + +import ( + "context" + "encoding/json" + "fmt" + + cetest "github.com/cloudevents/sdk-go/v2/test" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubeclient "knative.dev/pkg/client/injection/kube/client" + + "knative.dev/reconciler-test/pkg/eventshub" + "knative.dev/reconciler-test/pkg/feature" +) + +type MatchAssertionBuilder struct { + storeName string + matchers []eventshub.EventInfoMatcherCtx +} + +// OnStore creates an assertion builder starting from the name of the store +func OnStore(name string) MatchAssertionBuilder { + return MatchAssertionBuilder{ + storeName: name, + matchers: nil, + } +} + +// Match adds the provided matchers in this builder +func (m MatchAssertionBuilder) Match(matchers ...eventshub.EventInfoMatcher) MatchAssertionBuilder { + for _, matcher := range matchers { + m.matchers = append(m.matchers, matcher.WithContext()) + } + return m +} + +// MatchWithContext adds the provided matchers in this builder +func (m MatchAssertionBuilder) MatchWithContext(matchers ...eventshub.EventInfoMatcherCtx) MatchAssertionBuilder { + m.matchers = append(m.matchers, matchers...) + return m +} + +// MatchPeerCertificates adds the provided matchers in this builder +func (m MatchAssertionBuilder) MatchPeerCertificatesReceived(matchers ...eventshub.EventInfoMatcherCtx) MatchAssertionBuilder { + m.matchers = append(m.matchers, MatchKind(eventshub.PeerCertificatesReceived).WithContext()) + m.matchers = append(m.matchers, matchers...) + return m +} + +// MatchReceivedEvent is a shortcut for Match(MatchKind(eventshub.EventReceived), MatchEvent(matchers...)) +func (m MatchAssertionBuilder) MatchReceivedEvent(matchers ...cetest.EventMatcher) MatchAssertionBuilder { + m.matchers = append(m.matchers, MatchKind(eventshub.EventReceived).WithContext()) + m.matchers = append(m.matchers, MatchEvent(matchers...).WithContext()) + return m +} + +// MatchRejectedEvent is a shortcut for Match(MatchKind(eventshub.EventRejected), MatchEvent(matchers...)) +func (m MatchAssertionBuilder) MatchRejectedEvent(matchers ...cetest.EventMatcher) MatchAssertionBuilder { + m.matchers = append(m.matchers, MatchKind(eventshub.EventRejected).WithContext()) + m.matchers = append(m.matchers, MatchEvent(matchers...).WithContext()) + return m +} + +// MatchSentEvent is a shortcut for Match(MatchKind(eventshub.EventSent), MatchEvent(matchers...)) +func (m MatchAssertionBuilder) MatchSentEvent(matchers ...cetest.EventMatcher) MatchAssertionBuilder { + m.matchers = append(m.matchers, MatchKind(eventshub.EventSent).WithContext()) + m.matchers = append(m.matchers, MatchEvent(matchers...).WithContext()) + return m +} + +// MatchResponseEvent is a shortcut for Match(MatchKind(eventshub.EventResponse), MatchEvent(matchers...)) +func (m MatchAssertionBuilder) MatchResponseEvent(matchers ...cetest.EventMatcher) MatchAssertionBuilder { + m.matchers = append(m.matchers, MatchKind(eventshub.EventResponse).WithContext()) + m.matchers = append(m.matchers, MatchEvent(matchers...).WithContext()) + return m +} + +// MatchEvent is a shortcut for Match(MatchEvent(), OneOf(MatchKind(eventshub.EventReceived), MatchKind(eventshub.EventSent))) +func (m MatchAssertionBuilder) MatchEvent(matchers ...cetest.EventMatcher) MatchAssertionBuilder { + m.matchers = append(m.matchers, OneOf( + MatchKind(eventshub.EventReceived), + MatchKind(eventshub.EventSent), + ).WithContext()) + m.matchers = append(m.matchers, MatchEvent(matchers...).WithContext()) + return m +} + +// AtLeast builds the assertion feature.StepFn +// OnStore(store).Match(matchers).AtLeast(min) is equivalent to StoreFromContext(ctx, store).AssertAtLeast(min, matchers) +func (m MatchAssertionBuilder) AtLeast(min int) feature.StepFn { + return func(ctx context.Context, t feature.T) { + eventshub.StoreFromContext(ctx, m.storeName).AssertAtLeast(ctx, t, min, toFixedContextMatchers(ctx, m.matchers)...) + } +} + +// InRange builds the assertion feature.StepFn +// OnStore(store).Match(matchers).InRange(min, max) is equivalent to StoreFromContext(ctx, store).AssertInRange(min, max, matchers) +func (m MatchAssertionBuilder) InRange(min int, max int) feature.StepFn { + return func(ctx context.Context, t feature.T) { + eventshub.StoreFromContext(ctx, m.storeName).AssertInRange(ctx, t, min, max, toFixedContextMatchers(ctx, m.matchers)...) + } +} + +// Exact builds the assertion feature.StepFn +// OnStore(store).Match(matchers).Exact(n) is equivalent to StoreFromContext(ctx, store).AssertExact(n, matchers) +func (m MatchAssertionBuilder) Exact(n int) feature.StepFn { + return func(ctx context.Context, t feature.T) { + eventshub.StoreFromContext(ctx, m.storeName).AssertExact(ctx, t, n, toFixedContextMatchers(ctx, m.matchers)...) + } +} + +// Not builds the assertion feature.StepFn +// OnStore(store).Match(matchers).Not() is equivalent to StoreFromContext(ctx, store).AssertNot(matchers) +func (m MatchAssertionBuilder) Not() feature.StepFn { + return func(ctx context.Context, t feature.T) { + eventshub.StoreFromContext(ctx, m.storeName).AssertNot(t, toFixedContextMatchers(ctx, m.matchers)...) + } +} + +func toFixedContextMatchers(ctx context.Context, matchers []eventshub.EventInfoMatcherCtx) []eventshub.EventInfoMatcher { + out := make([]eventshub.EventInfoMatcher, 0, len(matchers)) + for _, matcher := range matchers { + out = append(out, matcher.WithContext(ctx)) + } + return out +} + +func MatchPeerCertificatesFromSecret(namespace, name string, key string) eventshub.EventInfoMatcherCtx { + return func(ctx context.Context, info eventshub.EventInfo) error { + secret, err := kubeclient.Get(ctx).CoreV1(). + Secrets(namespace). + Get(ctx, name, metav1.GetOptions{}) + + if err != nil { + return fmt.Errorf("failed to get secret: %w", err) + } + + value, ok := secret.Data[key] + if !ok { + return fmt.Errorf("failed to get value from secret %s/%s for key %s", secret.Namespace, secret.Name, key) + } + + if info.Connection == nil || info.Connection.TLS == nil { + return fmt.Errorf("failed to match peer certificates, connection is not TLS") + } + + for _, cert := range info.Connection.TLS.PemPeerCertificates { + if cert == string(value) { + return nil + } + } + + bytes, _ := json.MarshalIndent(info.Connection.TLS.PemPeerCertificates, "", " ") + return fmt.Errorf("failed to find peer certificate with value\n%s\nin:\n%s", string(value), string(bytes)) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 6695f562..59b036b6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1122,6 +1122,7 @@ knative.dev/pkg/tracker knative.dev/reconciler-test/cmd/eventshub knative.dev/reconciler-test/pkg/environment knative.dev/reconciler-test/pkg/eventshub +knative.dev/reconciler-test/pkg/eventshub/assert knative.dev/reconciler-test/pkg/eventshub/dropevents knative.dev/reconciler-test/pkg/eventshub/dropevents/dropeventsfibonacci knative.dev/reconciler-test/pkg/eventshub/dropevents/dropeventsfirst