From 2fe5d09b2ef0158820fd269bb904ca792a89a29a Mon Sep 17 00:00:00 2001
From: Raul Cabello Martin <raulcabm@gmail.com>
Date: Tue, 30 Jul 2024 18:54:53 +0200
Subject: [PATCH] feat: add azure basic auth verification (#191)

---
 azuredevops/azuredevops.go      | 46 +++++++++++++++++++++--
 azuredevops/azuredevops_test.go | 66 +++++++++++++++++++++++++++++++++
 2 files changed, 109 insertions(+), 3 deletions(-)

diff --git a/azuredevops/azuredevops.go b/azuredevops/azuredevops.go
index 8bb0ddf..460cae4 100644
--- a/azuredevops/azuredevops.go
+++ b/azuredevops/azuredevops.go
@@ -13,8 +13,9 @@ import (
 
 // parse errors
 var (
-	ErrInvalidHTTPMethod = errors.New("invalid HTTP Method")
-	ErrParsingPayload    = errors.New("error parsing payload")
+	ErrInvalidHTTPMethod           = errors.New("invalid HTTP Method")
+	ErrParsingPayload              = errors.New("error parsing payload")
+	ErrBasicAuthVerificationFailed = errors.New("basic auth verification failed")
 )
 
 // Event defines an Azure DevOps server hook event type
@@ -29,13 +30,38 @@ const (
 	GitPushEventType               Event = "git.push"
 )
 
+// Option is a configuration option for the webhook
+type Option func(*Webhook) error
+
+// Options is a namespace var for configuration options
+var Options = WebhookOptions{}
+
+// WebhookOptions is a namespace for configuration option methods
+type WebhookOptions struct{}
+
+// BasicAuth verifies payload using basic auth
+func (WebhookOptions) BasicAuth(username, password string) Option {
+	return func(hook *Webhook) error {
+		hook.username = username
+		hook.password = password
+		return nil
+	}
+}
+
 // Webhook instance contains all methods needed to process events
 type Webhook struct {
+	username string
+	password string
 }
 
 // New creates and returns a WebHook instance
-func New() (*Webhook, error) {
+func New(options ...Option) (*Webhook, error) {
 	hook := new(Webhook)
+	for _, opt := range options {
+		if err := opt(hook); err != nil {
+			return nil, errors.New("Error applying Option")
+		}
+	}
 	return hook, nil
 }
 
@@ -46,6 +72,10 @@ func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error)
 		_ = r.Body.Close()
 	}()
 
+	if !hook.verifyBasicAuth(r) {
+		return nil, ErrBasicAuthVerificationFailed
+	}
+
 	if r.Method != http.MethodPost {
 		return nil, ErrInvalidHTTPMethod
 	}
@@ -78,3 +108,13 @@ func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error)
 		return nil, fmt.Errorf("unknown event %s", pl.EventType)
 	}
 }
+
+func (hook Webhook) verifyBasicAuth(r *http.Request) bool {
+	// skip validation if username or password was not provided
+	if hook.username == "" && hook.password == "" {
+		return true
+	}
+	username, password, ok := r.BasicAuth()
+
+	return ok && username == hook.username && password == hook.password
+}
diff --git a/azuredevops/azuredevops_test.go b/azuredevops/azuredevops_test.go
index 1ca7591..2ff8f11 100644
--- a/azuredevops/azuredevops_test.go
+++ b/azuredevops/azuredevops_test.go
@@ -1,6 +1,9 @@
 package azuredevops
 
 import (
+	"bytes"
+	"fmt"
+	"github.com/stretchr/testify/assert"
 	"log"
 	"net/http"
 	"net/http/httptest"
@@ -117,3 +120,66 @@ func TestWebhooks(t *testing.T) {
 		})
 	}
 }
+
+func TestParseBasicAuth(t *testing.T) {
+	const validUser = "validUser"
+	const validPass = "pass123"
+	tests := []struct {
+		name        string
+		webhookUser string
+		webhookPass string
+		reqUser     string
+		reqPass     string
+		expectedErr error
+	}{
+		{
+			name:        "valid basic auth",
+			webhookUser: validUser,
+			webhookPass: validPass,
+			reqUser:     validUser,
+			reqPass:     validPass,
+			expectedErr: fmt.Errorf("unknown event "), // no event passed, so this is expected
+		},
+		{
+			name:        "no basic auth provided",
+			expectedErr: fmt.Errorf("unknown event "), // no event passed, so this is expected
+		},
+		{
+			name:        "invalid basic auth",
+			webhookUser: validUser,
+			webhookPass: validPass,
+			reqUser:     "fakeUser",
+			reqPass:     "fakePass",
+			expectedErr: ErrBasicAuthVerificationFailed,
+		},
+	}
+
+	for _, tt := range tests {
+		h := Webhook{
+			username: tt.webhookUser,
+			password: tt.webhookPass,
+		}
+		body := []byte(`{}`)
+		r, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer(body))
+		assert.NoError(t, err)
+		r.SetBasicAuth(tt.reqUser, tt.reqPass)
+
+		p, err := h.Parse(r)
+
+		assert.Equal(t, err, tt.expectedErr)
+		assert.Nil(t, p)
+	}
+}
+
+func TestBasicAuth(t *testing.T) {
+	const user = "user"
+	const pass = "pass123"
+
+	opt := Options.BasicAuth(user, pass)
+	h := &Webhook{}
+	err := opt(h)
+
+	assert.NoError(t, err)
+	assert.Equal(t, h.username, user)
+	assert.Equal(t, h.password, pass)
+}