From 1fdababf7c519722280ff2cd43eefece70fc2158 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 2 Jul 2022 19:13:13 +0200 Subject: [PATCH 01/14] Implement frontend part Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- options/locale/locale_en-US.ini | 9 ++++ templates/repo/settings/webhook/gitea.tmpl | 44 ++++++++++++++++ web_src/js/features/comp/WebHookEditor.js | 58 ++++++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index eb7ae4774313b..47ea245e55732 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1922,6 +1922,15 @@ settings.webhook.payload = Content settings.webhook.body = Body settings.webhook.replay.description = Replay this webhook. settings.webhook.delivery.success = An event has been added to the delivery queue. It may take few seconds before it shows up in the delivery history. +settings.webhook.auth_header.section = Authorization Header +settings.webhook.auth_header.description = Add authorization header to webhook delivery. +settings.webhook.auth_header.name = Header name +settings.webhook.auth_header.type = Header content type +settings.webhook.auth_header.type_basic = Basic authorization +settings.webhook.auth_header.type_token = Token authorization +settings.webhook.auth_header.username = Username +settings.webhook.auth_header.password = Password +settings.webhook.auth_header.token = Token settings.githooks_desc = "Git Hooks are powered by Git itself. You can edit hook files below to set up custom operations." settings.githook_edit_desc = If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook. settings.githook_name = Hook Name diff --git a/templates/repo/settings/webhook/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl index 062948b6abefd..703af4030a7e9 100644 --- a/templates/repo/settings/webhook/gitea.tmpl +++ b/templates/repo/settings/webhook/gitea.tmpl @@ -35,6 +35,50 @@ + +
+ +
+

{{.locale.Tr "repo.settings.webhook.auth_header.section"}}

+
+
+ + + {{.locale.Tr "repo.settings.webhook.auth_header.description"}} +
+
+ + + + + +
+ +
+ {{template "repo/settings/webhook/settings" .}} {{end}} diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js index 85a4f92809fa1..7cce08a1445d4 100644 --- a/web_src/js/features/comp/WebHookEditor.js +++ b/web_src/js/features/comp/WebHookEditor.js @@ -1,11 +1,69 @@ import $ from 'jquery'; const {csrfToken} = window.config; +const initAuthenticationHeaderSection = function() { + const $authHeaderSection = $('.auth-headers'); + + if ($authHeaderSection.length === 0) { + return; + } + + const $checkbox = $authHeaderSection.find('.checkbox input'); + + const updateHeaderContentType = function() { + const isBasicAuth = $authHeaderSection.find('#auth_header_type').val() === 'basic'; + const $basicAuthFields = $authHeaderSection.find('.basic-auth'); + const $tokenAuthFields = $authHeaderSection.find('.token-auth'); + + if (isBasicAuth) { + $basicAuthFields.addClass("required"); + $basicAuthFields.find('input').attr("required", ""); + $basicAuthFields.show(); + + $tokenAuthFields.removeClass("required"); + $tokenAuthFields.find('input').removeAttr("required"); + $tokenAuthFields.hide(); + } else { + $basicAuthFields.removeClass("required"); + $basicAuthFields.find('input').removeAttr("required"); + $basicAuthFields.hide(); + + $tokenAuthFields.addClass("required"); + $tokenAuthFields.find('input').attr("required", ""); + $tokenAuthFields.show(); + } + }; + + const updateHeaderCheckbox = function() { + if ($checkbox.is(':checked')) { + const $headerName = $authHeaderSection.find('#auth_header_name'); + $headerName.attr("required", ""); + $headerName.parent().addClass("required"); + $headerName.parent().show(); + $authHeaderSection.find('#auth_header_type').parent().parent().show(); + updateHeaderContentType(); + } else { + $authHeaderSection.find('.auth-header').hide(); + $authHeaderSection.find('.auth-header').removeClass("required"); + $authHeaderSection.find('.auth-header input').removeAttr("required"); + } + }; + + if ($checkbox.is(':checked')) { + updateHeaderCheckbox(); + } + + $checkbox.on('change', updateHeaderCheckbox); + $authHeaderSection.find('#auth_header_type').on('change', updateHeaderContentType); +}; + export function initCompWebHookEditor() { if ($('.new.webhook').length === 0) { return; } + initAuthenticationHeaderSection(); + $('.events.checkbox input').on('change', function () { if ($(this).is(':checked')) { $('.events.fields').show(); From 08b43177e5c3a3c8c329e8532711956551e96a90 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 3 Jul 2022 13:55:30 +0200 Subject: [PATCH 02/14] Persist auth header configuration Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- routers/web/repo/webhook.go | 47 ++++++++++++++++++++++ services/forms/repo_form.go | 14 +++++-- services/webhook/gitea.go | 44 ++++++++++++++++++++ templates/repo/settings/webhook/gitea.tmpl | 12 +++--- 4 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 services/webhook/gitea.go diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index a9b14ee21f453..a81909736bb0a 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -211,6 +212,27 @@ func GiteaHooksNewPost(ctx *context.Context) { contentType = webhook.ContentTypeForm } + meta, err := json.Marshal(&webhook_service.GiteaMeta{ + AuthHeader: webhook_service.GiteaAuthHeaderMeta{ + Active: form.AuthHeaderActive, + Name: form.AuthHeaderName, + Type: form.AuthHeaderType, + Username: form.AuthHeaderUsername, + Password: form.AuthHeaderPassword, + Token: form.AuthHeaderToken, + }, + }) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + encryptedMeta, err := secret.EncryptSecret(setting.SecretKey, string(meta)) + if err != nil { + ctx.ServerError("Encrypt", err) + return + } + w := &webhook.Webhook{ RepoID: orCtx.RepoID, URL: form.PayloadURL, @@ -220,6 +242,7 @@ func GiteaHooksNewPost(ctx *context.Context) { HookEvent: ParseHookEvent(form.WebhookForm), IsActive: form.Active, Type: webhook.GITEA, + Meta: encryptedMeta, OrgID: orCtx.OrgID, IsSystemWebhook: orCtx.IsSystemWebhook, } @@ -761,6 +784,8 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) { ctx.Data["HookType"] = w.Type switch w.Type { + case webhook.GITEA: + ctx.Data["GiteaHook"] = webhook_service.GetGiteaHook(w) case webhook.SLACK: ctx.Data["SlackHook"] = webhook_service.GetSlackHook(w) case webhook.DISCORD: @@ -818,10 +843,32 @@ func WebHooksEditPost(ctx *context.Context) { contentType = webhook.ContentTypeForm } + meta, err := json.Marshal(&webhook_service.GiteaMeta{ + AuthHeader: webhook_service.GiteaAuthHeaderMeta{ + Active: form.AuthHeaderActive, + Name: form.AuthHeaderName, + Type: form.AuthHeaderType, + Username: form.AuthHeaderUsername, + Password: form.AuthHeaderPassword, + Token: form.AuthHeaderToken, + }, + }) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + encryptedMeta, err := secret.EncryptSecret(setting.SecretKey, string(meta)) + if err != nil { + ctx.ServerError("Encrypt", err) + return + } + w.URL = form.PayloadURL w.ContentType = contentType w.Secret = form.Secret w.HookEvent = ParseHookEvent(form.WebhookForm) + w.Meta = encryptedMeta w.IsActive = form.Active w.HTTPMethod = form.HTTPMethod if err := w.UpdateEvent(); err != nil { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index c9327bbd9b0f8..1703620af210b 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -264,10 +264,16 @@ func (f WebhookForm) ChooseEvents() bool { // NewWebhookForm form for creating web hook type NewWebhookForm struct { - PayloadURL string `binding:"Required;ValidUrl"` - HTTPMethod string `binding:"Required;In(POST,GET)"` - ContentType int `binding:"Required"` - Secret string + PayloadURL string `binding:"Required;ValidUrl"` + HTTPMethod string `binding:"Required;In(POST,GET)"` + ContentType int `binding:"Required"` + Secret string + AuthHeaderActive bool + AuthHeaderName string + AuthHeaderType string `binding:"In(basic,token)"` + AuthHeaderUsername string + AuthHeaderPassword string + AuthHeaderToken string WebhookForm } diff --git a/services/webhook/gitea.go b/services/webhook/gitea.go new file mode 100644 index 0000000000000..2e26c92cdd602 --- /dev/null +++ b/services/webhook/gitea.go @@ -0,0 +1,44 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/secret" + "code.gitea.io/gitea/modules/setting" +) + +type ( + // GiteaAuthHeaderMeta contains the authentication header metadata + GiteaAuthHeaderMeta struct { + Active bool `json:"active"` + Name string `json:"name"` + Type string `json:"type"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + } + + // GiteaMeta contains the gitea webhook metadata + GiteaMeta struct { + AuthHeader GiteaAuthHeaderMeta `json:"authHeader"` + } +) + +// GetGiteaHook returns gitea metadata +func GetGiteaHook(w *webhook_model.Webhook) *GiteaMeta { + meta, err := secret.DecryptSecret(setting.SecretKey, w.Meta) + if err != nil { + log.Error("webhook.GetGiteaHook(%d): %v", w.ID, err) + } + + s := &GiteaMeta{} + if err := json.Unmarshal([]byte(meta), s); err != nil { + log.Error("webhook.GetGiteaHook(%d): %v", w.ID, err) + } + return s +} diff --git a/templates/repo/settings/webhook/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl index 703af4030a7e9..a4c8302ac5397 100644 --- a/templates/repo/settings/webhook/gitea.tmpl +++ b/templates/repo/settings/webhook/gitea.tmpl @@ -42,19 +42,19 @@

{{.locale.Tr "repo.settings.webhook.auth_header.section"}}

- + {{.locale.Tr "repo.settings.webhook.auth_header.description"}}
From fcfc79f8f48cc39f821e065722fc4f776d0054ad Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 3 Jul 2022 19:05:11 +0200 Subject: [PATCH 07/14] Apply code formatting Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- services/webhook/gitea.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/webhook/gitea.go b/services/webhook/gitea.go index 16727e5ec2d19..2417408ea4f21 100644 --- a/services/webhook/gitea.go +++ b/services/webhook/gitea.go @@ -25,8 +25,8 @@ type ( // GiteaMeta contains the gitea webhook metadata GiteaMeta struct { - AuthHeaderEnabled bool `json:"auth_header_enabled"` - AuthHeaderData string `json:"auth_header,omitempty"` + AuthHeaderEnabled bool `json:"auth_header_enabled"` + AuthHeaderData string `json:"auth_header,omitempty"` AuthHeader GiteaAuthHeaderMeta `json:"-"` } ) From 8e210e674a72aa8f0170b838b229b049c857b557 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:53:22 +0200 Subject: [PATCH 08/14] Pass encrypt and decrypt functions as parameters Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- modules/secret/secret.go | 5 +++++ routers/web/repo/webhook.go | 7 ++++--- services/webhook/deliver.go | 3 ++- services/webhook/gitea.go | 8 ++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/modules/secret/secret.go b/modules/secret/secret.go index e7edc7a95e528..3ef1b30c994c3 100644 --- a/modules/secret/secret.go +++ b/modules/secret/secret.go @@ -15,6 +15,11 @@ import ( "io" ) +type ( + EncryptSecretCallable func(key, str string) (string, error) + DecryptSecretCallable func(key, cipherhex string) (string, error) +) + // AesEncrypt encrypts text and given key with AES. func AesEncrypt(key, text []byte) ([]byte, error) { block, err := aes.NewCipher(key) diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index 651eeb3e26f5c..e8af050eb4d0d 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -211,7 +212,7 @@ func GiteaHooksNewPost(ctx *context.Context) { contentType = webhook.ContentTypeForm } - meta, err := webhook_service.CreateGiteaHook(form) + meta, err := webhook_service.CreateGiteaHook(form, secret.EncryptSecret) if err != nil { ctx.ServerError("Meta", err) return @@ -769,7 +770,7 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) { ctx.Data["HookType"] = w.Type switch w.Type { case webhook.GITEA: - ctx.Data["GiteaHook"] = webhook_service.GetGiteaHook(w) + ctx.Data["GiteaHook"] = webhook_service.GetGiteaHook(w, secret.DecryptSecret) case webhook.SLACK: ctx.Data["SlackHook"] = webhook_service.GetSlackHook(w) case webhook.DISCORD: @@ -827,7 +828,7 @@ func WebHooksEditPost(ctx *context.Context) { contentType = webhook.ContentTypeForm } - meta, err := webhook_service.CreateGiteaHook(form) + meta, err := webhook_service.CreateGiteaHook(form, secret.EncryptSecret) if err != nil { ctx.ServerError("Meta", err) return diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index cfbea0fd42c7f..d0ccb4251d85b 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "github.com/gobwas/glob" @@ -147,7 +148,7 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { } if w.Type == webhook_model.GITEA { - meta := GetGiteaHook(w) + meta := GetGiteaHook(w, secret.DecryptSecret) if meta.AuthHeaderEnabled { var content string switch meta.AuthHeader.Type { diff --git a/services/webhook/gitea.go b/services/webhook/gitea.go index 2417408ea4f21..606a79e73cac1 100644 --- a/services/webhook/gitea.go +++ b/services/webhook/gitea.go @@ -32,7 +32,7 @@ type ( ) // GetGiteaHook returns decrypted gitea metadata -func GetGiteaHook(w *webhook_model.Webhook) *GiteaMeta { +func GetGiteaHook(w *webhook_model.Webhook, decryptFn secret.DecryptSecretCallable) *GiteaMeta { s := &GiteaMeta{} // Legacy webhook configuration has no stored metadata @@ -48,7 +48,7 @@ func GetGiteaHook(w *webhook_model.Webhook) *GiteaMeta { return s } - headerData, err := secret.DecryptSecret(setting.SecretKey, s.AuthHeaderData) + headerData, err := decryptFn(setting.SecretKey, s.AuthHeaderData) if err != nil { log.Error("webhook.GetGiteaHook(%d): %v", w.ID, err) } @@ -67,7 +67,7 @@ func GetGiteaHook(w *webhook_model.Webhook) *GiteaMeta { // CreateGiteaHook creates an gitea metadata string with encrypted auth header data, // while it ensures to store the least necessary data in the database. -func CreateGiteaHook(form *forms.NewWebhookForm) (string, error) { +func CreateGiteaHook(form *forms.NewWebhookForm, encryptFn secret.EncryptSecretCallable) (string, error) { metaObject := &GiteaMeta{ AuthHeaderEnabled: form.AuthHeaderActive, } @@ -91,7 +91,7 @@ func CreateGiteaHook(form *forms.NewWebhookForm) (string, error) { return "", err } - encryptedHeaderData, err := secret.EncryptSecret(setting.SecretKey, string(headerData)) + encryptedHeaderData, err := encryptFn(setting.SecretKey, string(headerData)) if err != nil { return "", err } From 88b734e56f9db1d21062645efdb261195371fb32 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 4 Jul 2022 19:08:22 +0200 Subject: [PATCH 09/14] Add early exit on error Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- services/webhook/gitea.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/webhook/gitea.go b/services/webhook/gitea.go index 606a79e73cac1..176b6a9c80824 100644 --- a/services/webhook/gitea.go +++ b/services/webhook/gitea.go @@ -42,6 +42,7 @@ func GetGiteaHook(w *webhook_model.Webhook, decryptFn secret.DecryptSecretCallab if err := json.Unmarshal([]byte(w.Meta), s); err != nil { log.Error("webhook.GetGiteaHook(%d): %v", w.ID, err) + return nil } if !s.AuthHeaderEnabled { @@ -51,11 +52,13 @@ func GetGiteaHook(w *webhook_model.Webhook, decryptFn secret.DecryptSecretCallab headerData, err := decryptFn(setting.SecretKey, s.AuthHeaderData) if err != nil { log.Error("webhook.GetGiteaHook(%d): %v", w.ID, err) + return nil } h := GiteaAuthHeaderMeta{} if err := json.Unmarshal([]byte(headerData), &h); err != nil { log.Error("webhook.GetGiteaHook(%d): %v", w.ID, err) + return nil } // Replace encrypted content with decrypted settings From 3f33b2df3d68ab3967460719c385d77ad033c1d1 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 4 Jul 2022 19:30:36 +0200 Subject: [PATCH 10/14] Add GiteaHook unit tests Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- services/webhook/gitea.go | 2 +- services/webhook/gitea_test.go | 208 +++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 services/webhook/gitea_test.go diff --git a/services/webhook/gitea.go b/services/webhook/gitea.go index 176b6a9c80824..827e9d85aaf91 100644 --- a/services/webhook/gitea.go +++ b/services/webhook/gitea.go @@ -68,7 +68,7 @@ func GetGiteaHook(w *webhook_model.Webhook, decryptFn secret.DecryptSecretCallab return s } -// CreateGiteaHook creates an gitea metadata string with encrypted auth header data, +// CreateGiteaHook creates a gitea metadata string with encrypted auth header data, // while it ensures to store the least necessary data in the database. func CreateGiteaHook(form *forms.NewWebhookForm, encryptFn secret.EncryptSecretCallable) (string, error) { metaObject := &GiteaMeta{ diff --git a/services/webhook/gitea_test.go b/services/webhook/gitea_test.go new file mode 100644 index 0000000000000..1f0445db9189b --- /dev/null +++ b/services/webhook/gitea_test.go @@ -0,0 +1,208 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "fmt" + "testing" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/services/forms" + + "github.com/stretchr/testify/require" +) + +type GiteaSecretModuleMock struct { + DecryptCalled bool + EncryptCalled bool + SimulateError bool +} + +func (m *GiteaSecretModuleMock) DecryptSecret(key string, cipherhex string) (string, error) { + m.DecryptCalled = true + + if m.SimulateError { + return "", fmt.Errorf("Simulated error") + } + + return cipherhex, nil +} + +func (m *GiteaSecretModuleMock) EncryptSecret(key, str string) (string, error) { + m.EncryptCalled = true + + if m.SimulateError { + return "", fmt.Errorf("Simulated error") + } + + return str, nil +} + +func TestGetGiteaHook(t *testing.T) { + t.Run("Legacy configuration", func(t *testing.T) { + s := &webhook_model.Webhook{ + Type: webhook_model.GITEA, + Meta: "", + } + + m := GiteaSecretModuleMock{} + + actual := GetGiteaHook(s, m.DecryptSecret) + + require.NotNil(t, actual) + require.IsType(t, &GiteaMeta{}, actual) + require.False(t, actual.AuthHeaderEnabled) + require.False(t, m.DecryptCalled, "Decrypt function unexpectedly called") + }) + + t.Run("Disabled auth headers", func(t *testing.T) { + s := &webhook_model.Webhook{ + Type: webhook_model.GITEA, + Meta: `{"auth_header_enabled": false}`, + } + + m := GiteaSecretModuleMock{} + + actual := GetGiteaHook(s, m.DecryptSecret) + + require.NotNil(t, actual) + require.IsType(t, &GiteaMeta{}, actual) + require.False(t, actual.AuthHeaderEnabled) + require.False(t, m.DecryptCalled, "Decrypt function unexpectedly called") + }) + + t.Run("Enabled auth headers", func(t *testing.T) { + s := &webhook_model.Webhook{ + Type: webhook_model.GITEA, + Meta: `{"auth_header_enabled": true, "auth_header": "{\"name\": \"X-Test-Authorization\", \"type\": \"basic\", \"username\": \"test-user\", \"password\":\"test-password\"}"}`, + } + + m := GiteaSecretModuleMock{} + + actual := GetGiteaHook(s, m.DecryptSecret) + + require.NotNil(t, actual) + require.IsType(t, &GiteaMeta{}, actual) + require.True(t, actual.AuthHeaderEnabled) + require.True(t, m.DecryptCalled, "Decrypt function was not called") + + require.Equal(t, "X-Test-Authorization", actual.AuthHeader.Name) + require.Empty(t, actual.AuthHeaderData) + }) + + t.Run("Metadata parse error", func(t *testing.T) { + s := &webhook_model.Webhook{ + Type: webhook_model.GITEA, + Meta: `{"`, + } + + m := GiteaSecretModuleMock{} + + actual := GetGiteaHook(s, m.DecryptSecret) + + require.Nil(t, actual) + require.False(t, m.DecryptCalled, "Decrypt function unexpectedly called") + }) + + t.Run("AuthHeaderData parse error", func(t *testing.T) { + s := &webhook_model.Webhook{ + Type: webhook_model.GITEA, + Meta: `{"auth_header_enabled": true, "auth_header": "{\""}`, + } + + m := GiteaSecretModuleMock{} + + actual := GetGiteaHook(s, m.DecryptSecret) + + require.Nil(t, actual) + require.True(t, m.DecryptCalled, "Decrypt function was not called") + }) + + t.Run("Decryption error", func(t *testing.T) { + s := &webhook_model.Webhook{ + Type: webhook_model.GITEA, + Meta: `{"auth_header_enabled": true, "auth_header": "{\"name\": \"X-Test-Authorization\", \"type\": \"basic\", \"username\": \"test-user\", \"password\":\"test-password\"}"}`, + } + + m := GiteaSecretModuleMock{SimulateError: true} + + actual := GetGiteaHook(s, m.DecryptSecret) + + require.Nil(t, actual) + require.True(t, m.DecryptCalled, "Decrypt function was not called") + }) +} + +func TestCreateGiteaHook(t *testing.T) { + t.Run("Disabled auth headers", func(t *testing.T) { + m := GiteaSecretModuleMock{} + + form := &forms.NewWebhookForm{ + AuthHeaderActive: false, + } + + actual, err := CreateGiteaHook(form, m.EncryptSecret) + expected := `{"auth_header_enabled":false}` + + require.Nil(t, err) + require.Equal(t, expected, actual) + require.False(t, m.EncryptCalled, "Encrypt function unexpectedly called") + }) + + t.Run("Enabled auth headers (basic auth)", func(t *testing.T) { + m := GiteaSecretModuleMock{} + + form := &forms.NewWebhookForm{ + AuthHeaderActive: true, + AuthHeaderName: "Authorization", + AuthHeaderType: webhook_model.BASICAUTH, + AuthHeaderUsername: "test-user", + AuthHeaderPassword: "test-password", + } + + actual, err := CreateGiteaHook(form, m.EncryptSecret) + expected := `{"auth_header_enabled":true,"auth_header":"{\"name\":\"Authorization\",\"type\":\"basic\",\"username\":\"test-user\",\"password\":\"test-password\"}"}` + + require.Nil(t, err) + require.Equal(t, expected, actual) + require.True(t, m.EncryptCalled, "Encrypt function was not called") + }) + + t.Run("Enabled auth headers (token auth)", func(t *testing.T) { + m := GiteaSecretModuleMock{} + + form := &forms.NewWebhookForm{ + AuthHeaderActive: true, + AuthHeaderName: "Authorization", + AuthHeaderType: webhook_model.TOKENAUTH, + AuthHeaderToken: "test-token", + } + + actual, err := CreateGiteaHook(form, m.EncryptSecret) + expected := `{"auth_header_enabled":true,"auth_header":"{\"name\":\"Authorization\",\"type\":\"token\",\"token\":\"test-token\"}"}` + + require.Nil(t, err) + require.Equal(t, expected, actual) + require.True(t, m.EncryptCalled, "Encrypt function was not called") + }) + + t.Run("Encyption error", func(t *testing.T) { + m := GiteaSecretModuleMock{SimulateError: true} + + form := &forms.NewWebhookForm{ + AuthHeaderActive: true, + AuthHeaderName: "Authorization", + AuthHeaderType: webhook_model.TOKENAUTH, + AuthHeaderToken: "test-token", + } + + actual, err := CreateGiteaHook(form, m.EncryptSecret) + + require.NotNil(t, err) + require.Errorf(t, err, "Simulated error") + require.Empty(t, actual) + require.True(t, m.EncryptCalled, "Encrypt function was not called") + }) +} From 4c13cdc93ad7938eba2dad95352797b0815b17df Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:00:11 +0200 Subject: [PATCH 11/14] Apply code formatting Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- services/webhook/gitea_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/webhook/gitea_test.go b/services/webhook/gitea_test.go index 1f0445db9189b..9d5653a9d8d3d 100644 --- a/services/webhook/gitea_test.go +++ b/services/webhook/gitea_test.go @@ -20,7 +20,7 @@ type GiteaSecretModuleMock struct { SimulateError bool } -func (m *GiteaSecretModuleMock) DecryptSecret(key string, cipherhex string) (string, error) { +func (m *GiteaSecretModuleMock) DecryptSecret(key, cipherhex string) (string, error) { m.DecryptCalled = true if m.SimulateError { From b69dd1595045644e353095e18ee8112c976f1975 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:25:09 +0200 Subject: [PATCH 12/14] Reword header content types Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- options/locale/locale_en-US.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9afdbc45e2131..2c72ed6666b51 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1926,8 +1926,8 @@ settings.webhook.auth_header.section = Authorization Header settings.webhook.auth_header.description = Add authorization header to webhook delivery. settings.webhook.auth_header.name = Header name settings.webhook.auth_header.type = Header content type -settings.webhook.auth_header.type_basic = Basic authorization -settings.webhook.auth_header.type_token = Token authorization +settings.webhook.auth_header.type_basic = Basic authentication +settings.webhook.auth_header.type_token = Token authentication settings.webhook.auth_header.username = Username settings.webhook.auth_header.password = Password settings.webhook.auth_header.token = Token From a4ad516496edc26257c845af8071ebfed93536fb Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:31:27 +0200 Subject: [PATCH 13/14] Add section to docs Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- docs/content/doc/features/webhooks.en-us.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/content/doc/features/webhooks.en-us.md b/docs/content/doc/features/webhooks.en-us.md index 2dba7b7f83c74..fdceb3d326570 100644 --- a/docs/content/doc/features/webhooks.en-us.md +++ b/docs/content/doc/features/webhooks.en-us.md @@ -188,3 +188,7 @@ if (json_last_error() !== JSON_ERROR_NONE) { ``` There is a Test Delivery button in the webhook settings that allows to test the configuration as well as a list of the most Recent Deliveries. + +### Authorization header (Gitea hook only) + +**With 1.18**, Gitea hooks can be configured to send an [authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) to the webhook target. Supported header types are _Basic Authentication_ and _Token Authentication_. The header key can be changed in case the webhook target requires a different one. The key defaults to `Authorization`. From 96c608cb576fa795a4f3d8e9102136f0a7be57cc Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:21:05 +0200 Subject: [PATCH 14/14] Apply frontend formatting Signed-off-by: justusbunsi <61625851+justusbunsi@users.noreply.github.com> --- web_src/js/features/comp/WebHookEditor.js | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js index 7cce08a1445d4..760c119b5c2ec 100644 --- a/web_src/js/features/comp/WebHookEditor.js +++ b/web_src/js/features/comp/WebHookEditor.js @@ -16,20 +16,20 @@ const initAuthenticationHeaderSection = function() { const $tokenAuthFields = $authHeaderSection.find('.token-auth'); if (isBasicAuth) { - $basicAuthFields.addClass("required"); - $basicAuthFields.find('input').attr("required", ""); + $basicAuthFields.addClass('required'); + $basicAuthFields.find('input').attr('required', ''); $basicAuthFields.show(); - $tokenAuthFields.removeClass("required"); - $tokenAuthFields.find('input').removeAttr("required"); + $tokenAuthFields.removeClass('required'); + $tokenAuthFields.find('input').removeAttr('required'); $tokenAuthFields.hide(); } else { - $basicAuthFields.removeClass("required"); - $basicAuthFields.find('input').removeAttr("required"); + $basicAuthFields.removeClass('required'); + $basicAuthFields.find('input').removeAttr('required'); $basicAuthFields.hide(); - $tokenAuthFields.addClass("required"); - $tokenAuthFields.find('input').attr("required", ""); + $tokenAuthFields.addClass('required'); + $tokenAuthFields.find('input').attr('required', ''); $tokenAuthFields.show(); } }; @@ -37,15 +37,15 @@ const initAuthenticationHeaderSection = function() { const updateHeaderCheckbox = function() { if ($checkbox.is(':checked')) { const $headerName = $authHeaderSection.find('#auth_header_name'); - $headerName.attr("required", ""); - $headerName.parent().addClass("required"); + $headerName.attr('required', ''); + $headerName.parent().addClass('required'); $headerName.parent().show(); $authHeaderSection.find('#auth_header_type').parent().parent().show(); updateHeaderContentType(); } else { $authHeaderSection.find('.auth-header').hide(); - $authHeaderSection.find('.auth-header').removeClass("required"); - $authHeaderSection.find('.auth-header input').removeAttr("required"); + $authHeaderSection.find('.auth-header').removeClass('required'); + $authHeaderSection.find('.auth-header input').removeAttr('required'); } };