From 9c9b7588abfa310e3c73bf4e16ba4e81c667753e Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 20 Jun 2021 19:47:55 +0200 Subject: [PATCH] Read webhook secret from file into configuration Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 16 +++-- internal/settings/settings.go | 54 +++++++++++++++-- internal/settings/settings_test.go | 95 ++++++++++++++++++++++++++---- 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 629ecd4..606a906 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -5,15 +5,14 @@ gitea: # Created access token for the user that shall be used as bot account. # User needs "Read project" permissions with access to "Pull Requests" - token: <...> + token: "" # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. - webhookSecret: {} - # # either plain text - # value: <...> + webhookSecret: + value: "" # # or path to file containing the plain text secret # file: /path/to/gitea/webhook/secret @@ -35,18 +34,17 @@ sonarqube: # Created access token for the user that shall be used as bot account. # User needs "Browse on project" permissions - token: <...> + token: "" # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be # validated. - webhookSecret: {} - # # either plain text - # value: <...> + webhookSecret: + value: "" # # or path to file containing the plain text secret - # file: /path/to/gitea/webhook/secret + # file: /path/to/sonarqube/webhook/secret # List of project keys from inside SonarQube that should be handled. Webhooks containing other projects will be ignored. projects: diff --git a/internal/settings/settings.go b/internal/settings/settings.go index c7fde51..d8a36ec 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "io/ioutil" "strings" "github.com/spf13/viper" @@ -12,15 +13,28 @@ type GiteaRepository struct { Name string } +type WebhookSecret struct { + Value string + File string +} + type GiteaConfig struct { Url string Token string - WebhookSecret string `mapstructure:"webhookSecret"` + WebhookSecret WebhookSecret `mapstructure:"webhookSecret"` Repositories []GiteaRepository } +type SonarQubeConfig struct { + Url string + Token string + WebhookSecret WebhookSecret `mapstructure:"webhookSecret"` + Projects []string +} + var ( Gitea GiteaConfig + SonarQube SonarQubeConfig ) func init() { @@ -30,6 +44,30 @@ func init() { viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AllowEmptyEnv(true) viper.AutomaticEnv() + + ApplyConfigDefaults() +} + +func ApplyConfigDefaults() { + viper.SetDefault("gitea.url", "") + viper.SetDefault("gitea.token", "") + viper.SetDefault("gitea.webhookSecret.value", "") + viper.SetDefault("gitea.webhookSecret.file", "") + viper.SetDefault("gitea.repositories", []interface{}{}) + viper.SetDefault("sonarqube.url", "") + viper.SetDefault("sonarqube.token", "") + viper.SetDefault("sonarqube.webhookSecret.value", "") + viper.SetDefault("sonarqube.webhookSecret.file", "") + viper.SetDefault("sonarqube.projects", []string{}) +} + +func ReadSecretFile(file string) string { + content, err := ioutil.ReadFile(file) + if err != nil { + panic(fmt.Errorf("Cannot read '%s' or it is no regular file. %w", file, err)) + } + + return string(content) } func Load(configPath string) { @@ -40,12 +78,9 @@ func Load(configPath string) { panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) } - if viper.IsSet("gitea") == false { - panic("Gitea not configured") - } - var fullConfig struct { Gitea GiteaConfig + SonarQube SonarQubeConfig `mapstructure:"sonarqube"` } err = viper.Unmarshal(&fullConfig) @@ -54,4 +89,13 @@ func Load(configPath string) { } Gitea = fullConfig.Gitea + SonarQube = fullConfig.SonarQube + + if Gitea.WebhookSecret.File != "" { + Gitea.WebhookSecret.Value = ReadSecretFile(Gitea.WebhookSecret.File) + } + + if SonarQube.WebhookSecret.File != "" { + SonarQube.WebhookSecret.Value = ReadSecretFile(SonarQube.WebhookSecret.File) + } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index af2da01..342d26d 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -13,12 +13,25 @@ var defaultConfigInlineSecrets []byte = []byte( `gitea: url: https://example.com/gitea token: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 - webhookSecret: "haxxor" + webhookSecret: + value: haxxor-gitea-secret repositories: [] sonarqube: url: https://example.com/sonarqube token: a09eb5785b25bb2cbacf48808a677a0709f02d8e - webhookSecret: "haxxor" + webhookSecret: + value: haxxor-sonarqube-secret + projects: [] +`) + +var incompleteConfig []byte = []byte( +`gitea: + url: https://example.com/gitea + webhookSecret: + value: haxxor-gitea-secret +sonarqube: + url: https://example.com/sonarqube + token: a09eb5785b25bb2cbacf48808a677a0709f02d8e projects: [] `) @@ -43,12 +56,6 @@ func TestLoadWithExistingFile(t *testing.T) { assert.NotPanics(t, func() { Load(os.TempDir()) }, "Unexpected panic while reading existing file") } -func TestLoadWithMissingGiteaStructure(t *testing.T) { - WriteConfigFile(t, []byte(``)) - - assert.Panics(t, func() { Load(os.TempDir()) }, "No panic when Gitea is not configured") -} - func TestLoadGiteaStructure(t *testing.T) { WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) @@ -56,7 +63,9 @@ func TestLoadGiteaStructure(t *testing.T) { expected := GiteaConfig{ Url: "https://example.com/gitea", Token: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", - WebhookSecret: "haxxor", + WebhookSecret: WebhookSecret{ + Value: "haxxor-gitea-secret", + }, Repositories: []GiteaRepository{}, } @@ -64,7 +73,7 @@ func TestLoadGiteaStructure(t *testing.T) { } func TestLoadGiteaStructureWithEnvInjectedWebhookSecret(t *testing.T) { - os.Setenv("PRBOT_GITEA_WEBHOOKSECRET", "injected-secret") + os.Setenv("PRBOT_GITEA_WEBHOOKSECRET_VALUE", "injected-secret") os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) @@ -72,14 +81,76 @@ func TestLoadGiteaStructureWithEnvInjectedWebhookSecret(t *testing.T) { expected := GiteaConfig{ Url: "https://example.com/gitea", Token: "injected-token", - WebhookSecret: "injected-secret", + WebhookSecret: WebhookSecret{ + Value: "injected-secret", + }, Repositories: []GiteaRepository{}, } assert.EqualValues(t, expected, Gitea) t.Cleanup(func() { - os.Unsetenv("PRBOT_GITEA_WEBHOOKSECRET") + os.Unsetenv("PRBOT_GITEA_WEBHOOKSECRET_VALUE") os.Unsetenv("PRBOT_GITEA_TOKEN") }) } + +func TestLoadStructureWithResolvedWebhookFileFromEnvInjected(t *testing.T) { + secretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") + _ = ioutil.WriteFile(secretFile, []byte(`totally-secret`),0444) + + os.Setenv("PRBOT_GITEA_WEBHOOKSECRET_FILE", secretFile) + os.Setenv("PRBOT_SONARQUBE_WEBHOOKSECRET_FILE", secretFile) + os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") + + WriteConfigFile(t, incompleteConfig) + Load(os.TempDir()) + + expectedGitea := GiteaConfig{ + Url: "https://example.com/gitea", + Token: "injected-token", + WebhookSecret: WebhookSecret{ + Value: "totally-secret", + File: secretFile, + }, + Repositories: []GiteaRepository{}, + } + + expectedSonarQube := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + WebhookSecret: WebhookSecret{ + Value: "totally-secret", + File: secretFile, + }, + Projects: []string{}, + } + + assert.EqualValues(t, expectedGitea, Gitea) + assert.EqualValues(t, expectedSonarQube, SonarQube) + + t.Cleanup(func() { + os.Remove(secretFile) + os.Unsetenv("PRBOT_SONARQUBE_WEBHOOKSECRET_FILE") + os.Unsetenv("PRBOT_GITEA_TOKEN") + }) +} + +func TestReadSecretFileWhenDirectoryProvided(t *testing.T) { + assert.Panics(t, func() { ReadSecretFile(os.TempDir()) }, "No panic while trying to read content from directory") +} + +func TestReadSecretFileWhenMissingFileProvided(t *testing.T) { + assert.Panics(t, func() { ReadSecretFile(path.Join(os.TempDir(), "secret-file")) }, "No panic while trying to read missing file") +} + +func TestReadSecretFile(t *testing.T) { + secretFile := path.Join(os.TempDir(), "secret-file") + _ = ioutil.WriteFile(secretFile, []byte(`awesome-secret-content`),0444) + + assert.Equal(t, "awesome-secret-content", ReadSecretFile(secretFile)) + + t.Cleanup(func() { + os.Remove(secretFile) + }) +}