From 23efc511bcf9537587e9ef982810aa9fbe7bac57 Mon Sep 17 00:00:00 2001 From: William Walter <53450727+whwalter@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:24:42 -0500 Subject: [PATCH] Feature require auth (#24) Co-authored-by: Yuzhou Liu <89029096+yuzhouliu9@users.noreply.github.com> --- examples/testers/demo-secret.yaml | 8 + examples/testers/es-github.yaml | 17 ++- examples/testers/es-jira.yaml | 3 + examples/testers/es-officeips.yaml | 4 + internal/admission/eventsource/handler.go | 68 +++++++++ .../admission/eventsource/handler_test.go | 141 ++++++++++++++++++ 6 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 examples/testers/demo-secret.yaml diff --git a/examples/testers/demo-secret.yaml b/examples/testers/demo-secret.yaml new file mode 100644 index 0000000..ca32436 --- /dev/null +++ b/examples/testers/demo-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +data: + token: dGhpc2lzYW5leGFtcGxl +kind: Secret +metadata: + name: argoslower-examples + namespace: devops +type: Opaque diff --git a/examples/testers/es-github.yaml b/examples/testers/es-github.yaml index dcc04c3..985a0fc 100644 --- a/examples/testers/es-github.yaml +++ b/examples/testers/es-github.yaml @@ -11,8 +11,17 @@ spec: ports: - port: 80 targetPort: 8080 - webhook: + github: example: - endpoint: /example - method: POST - port: "8080" + webhook: + endpoint: /example + method: POST + port: "8080" + repositories: + - owner: kanopy-platform + names: + - argoslower + webhookSecret: + name: argoslower-examples + key: token + optional: true diff --git a/examples/testers/es-jira.yaml b/examples/testers/es-jira.yaml index b9a013a..2cbca63 100644 --- a/examples/testers/es-jira.yaml +++ b/examples/testers/es-jira.yaml @@ -16,3 +16,6 @@ spec: endpoint: /example method: POST port: "8080" + authSecret: + name: argoslower-examples + key: token diff --git a/examples/testers/es-officeips.yaml b/examples/testers/es-officeips.yaml index d122ee2..8d8b840 100644 --- a/examples/testers/es-officeips.yaml +++ b/examples/testers/es-officeips.yaml @@ -16,3 +16,7 @@ spec: endpoint: /example method: POST port: "8080" + authSecret: + name: argoslower-examples + key: token + optional: true diff --git a/internal/admission/eventsource/handler.go b/internal/admission/eventsource/handler.go index 81431c3..92f5d10 100644 --- a/internal/admission/eventsource/handler.go +++ b/internal/admission/eventsource/handler.go @@ -3,6 +3,7 @@ package eventsource import ( "context" "encoding/json" + "errors" "fmt" "net/http" @@ -11,6 +12,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + perrs "github.com/kanopy-platform/argoslower/pkg/errors" + common "github.com/argoproj/argo-events/pkg/apis/common" esv1alpha1 "github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1" ) @@ -76,6 +79,11 @@ func (h *Handler) Handle(ctx context.Context, req admission.Request) admission.R return admission.Denied(fmt.Sprintf("Namespace %s is not opted into the mesh. Please contact your cluster administrator and try again", out.Namespace)) } + err = ValidateEventSource(out) + if err != nil { + return admission.Denied(err.Error()) + } + out.Spec.Template = setIstioLabel(out.Spec.Template) bytes, err := json.Marshal(out) @@ -110,3 +118,63 @@ func setIstioLabel(in *esv1alpha1.Template) *esv1alpha1.Template { type MeshChecker interface { OnMesh(namespace string) (bool, error) } + +func ValidateEventSource(es *esv1alpha1.EventSource) error { + + if len(es.Spec.Webhook) == 0 && len(es.Spec.Github) == 0 { + return perrs.NewUnretryableError(fmt.Errorf("EventSource %s/%s has no supported webhook configuration", es.Namespace, es.Name)) + } + + if es.Spec.Webhook != nil { + var err error + + for hook, spec := range es.Spec.Webhook { + e := validateWebhookEventSource(&spec) + if e != nil { + err = perrs.NewUnretryableError(errors.Join(err, fmt.Errorf("Webhook %s misconfigured: %w", hook, e))) + } + } + + if err != nil { + return err + } + } + + if es.Spec.Github != nil { + var err error + + for hook, spec := range es.Spec.Github { + e := validateGithubEventSource(&spec) + if e != nil { + err = perrs.NewUnretryableError(errors.Join(err, fmt.Errorf("Github webhook %s misconfigured: %w", hook, e))) + } + } + + if err != nil { + return err + } + + } + + return nil +} + +func validateWebhookEventSource(spec *esv1alpha1.WebhookEventSource) error { + // This is Bearer token authentication provided by argo-events + if spec.AuthSecret == nil { + return perrs.NewUnretryableError(fmt.Errorf("Webhook EventSources require auth tokens. Ensure an authSecret secret selector configured.")) + } + + return nil +} + +func validateGithubEventSource(spec *esv1alpha1.GithubEventSource) error { + // Github webhooks provide signed messages for validation of the message payload. + // verification is implemented by argo-events + // https://github.com/argoproj/argo-events/blob/e948d7337aec619dd48fb2a065126b025ed9281d/eventsources/sources/github/start.go#L383 + if spec.WebhookSecret == nil { + return perrs.NewUnretryableError(fmt.Errorf("Github webhook EventSources require HMAC signing validation for ingress. Ensure a webhookSecret secret selector is provided.")) + } + + return nil +} diff --git a/internal/admission/eventsource/handler_test.go b/internal/admission/eventsource/handler_test.go index 2664025..50d4cc1 100644 --- a/internal/admission/eventsource/handler_test.go +++ b/internal/admission/eventsource/handler_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -65,6 +66,11 @@ func TestEventSourceHandler(t *testing.T) { }, }, Spec: esv1alpha1.EventSourceSpec{ + Github: map[string]esv1alpha1.GithubEventSource{ + "ghs": esv1alpha1.GithubEventSource{ + WebhookSecret: &corev1.SecretKeySelector{}, + }, + }, Template: &esv1alpha1.Template{ Metadata: &common.Metadata{ Labels: map[string]string{ @@ -83,6 +89,13 @@ func TestEventSourceHandler(t *testing.T) { eventsource.DefaultAnnotationKey: "false", }, }, + Spec: esv1alpha1.EventSourceSpec{ + Github: map[string]esv1alpha1.GithubEventSource{ + "ghs": esv1alpha1.GithubEventSource{ + WebhookSecret: &corev1.SecretKeySelector{}, + }, + }, + }, }, }, { @@ -155,3 +168,131 @@ func TestEventSourceHandler(t *testing.T) { } } + +func TestValidateEventSource(t *testing.T) { + + tests := map[string]struct { + spec *esv1alpha1.EventSource + err bool + }{ + + "no sources": { + spec: &esv1alpha1.EventSource{ + ObjectMeta: v1.ObjectMeta{ + Name: "empty", + Namespace: "testing", + }, + }, + err: true, + }, + "github no secret": { + spec: &esv1alpha1.EventSource{ + ObjectMeta: v1.ObjectMeta{ + Name: "nosecret", + Namespace: "testing", + }, + Spec: esv1alpha1.EventSourceSpec{ + Github: map[string]esv1alpha1.GithubEventSource{}, + }, + }, + err: true, + }, + "github secret": { + spec: &esv1alpha1.EventSource{ + ObjectMeta: v1.ObjectMeta{ + Name: "valid", + Namespace: "testing", + }, + Spec: esv1alpha1.EventSourceSpec{ + Github: map[string]esv1alpha1.GithubEventSource{ + "ghs": esv1alpha1.GithubEventSource{ + WebhookSecret: &corev1.SecretKeySelector{}, + }, + }, + }, + }, + }, + "github mixed": { + spec: &esv1alpha1.EventSource{ + ObjectMeta: v1.ObjectMeta{ + Name: "nosecret", + Namespace: "testing", + }, + Spec: esv1alpha1.EventSourceSpec{ + Github: map[string]esv1alpha1.GithubEventSource{ + "ghs": esv1alpha1.GithubEventSource{ + WebhookSecret: &corev1.SecretKeySelector{}, + }, + "nos": esv1alpha1.GithubEventSource{}, + }, + }, + }, + err: true, + }, + "webhook no secret": { + spec: &esv1alpha1.EventSource{ + ObjectMeta: v1.ObjectMeta{ + Name: "nosecret", + Namespace: "testing", + }, + Spec: esv1alpha1.EventSourceSpec{ + Webhook: map[string]esv1alpha1.WebhookEventSource{ + "nos": esv1alpha1.WebhookEventSource{ + WebhookContext: esv1alpha1.WebhookContext{}, + }, + }, + }, + }, + err: true, + }, + "webhook valid": { + spec: &esv1alpha1.EventSource{ + ObjectMeta: v1.ObjectMeta{ + Name: "valid", + Namespace: "testing", + }, + Spec: esv1alpha1.EventSourceSpec{ + Webhook: map[string]esv1alpha1.WebhookEventSource{ + "ws": esv1alpha1.WebhookEventSource{ + WebhookContext: esv1alpha1.WebhookContext{ + AuthSecret: &corev1.SecretKeySelector{}, + }, + }, + }, + }, + }, + }, + "webhook mixed": { + spec: &esv1alpha1.EventSource{ + ObjectMeta: v1.ObjectMeta{ + Name: "nosecret", + Namespace: "testing", + }, + Spec: esv1alpha1.EventSourceSpec{ + Webhook: map[string]esv1alpha1.WebhookEventSource{ + "ws": esv1alpha1.WebhookEventSource{ + WebhookContext: esv1alpha1.WebhookContext{ + AuthSecret: &corev1.SecretKeySelector{}, + }, + }, + "nos": esv1alpha1.WebhookEventSource{ + WebhookContext: esv1alpha1.WebhookContext{}, + }, + }, + }, + }, + err: true, + }, + } + + for name, test := range tests { + e := eventsource.ValidateEventSource(test.spec) + + if test.err { + assert.Error(t, e, name) + } else { + assert.NoError(t, e, name) + } + } + +}