diff --git a/config/settings.go b/config/settings.go index 0cea776c..f84b971d 100644 --- a/config/settings.go +++ b/config/settings.go @@ -151,10 +151,14 @@ func (s *WebSettingsBasicAuth) String(prefix string) (str string) { // CheckValues will ensure that the values are SHA256 hashed. func (ba *WebSettingsBasicAuth) CheckValues() { // Username - ba.UsernameHash = util.GetHash(ba.Username) + ba.UsernameHash = util.GetHash(util.EvalEnvVars(ba.Username)) // Password - ba.PasswordHash = util.GetHash(ba.Password) - ba.Password = util.FmtHash(ba.PasswordHash) + password := util.EvalEnvVars(ba.Password) + ba.PasswordHash = util.GetHash(password) + if password == ba.Password { + // If the password is not an environment variable, hash the stored value. + ba.Password = util.FmtHash(ba.PasswordHash) + } } // FaviconSettings contains the favicon override settings. diff --git a/config/settings_test.go b/config/settings_test.go index 63e4e212..4e50510a 100644 --- a/config/settings_test.go +++ b/config/settings_test.go @@ -29,6 +29,7 @@ import ( func TestSettingsBase_CheckValues(t *testing.T) { // GIVEN a Settings struct with some values set tests := map[string]struct { + env map[string]string had Settings want Settings wantUsernameHash string @@ -44,19 +45,23 @@ func TestSettingsBase_CheckValues(t *testing.T) { Web: WebSettings{ BasicAuth: nil}}}, }, - "BasicAuth - hashed Username and str Password": { + "BasicAuth - hashed Username and str env Password": { + env: map[string]string{ + "TESTSETTINGSBASE_CHECKVALUES_ONE": "ass"}, had: Settings{ SettingsBase: SettingsBase{ Web: WebSettings{ BasicAuth: &WebSettingsBasicAuth{ Username: util.FmtHash(util.GetHash("user")), - Password: "pass"}}}}, + Password: "p${TESTSETTINGSBASE_CHECKVALUES_ONE}"}}}}, want: Settings{ SettingsBase: SettingsBase{ Web: WebSettings{ BasicAuth: &WebSettingsBasicAuth{ Username: util.FmtHash(util.GetHash("user")), - Password: util.FmtHash(util.GetHash("pass"))}}}}, + Password: "p${TESTSETTINGSBASE_CHECKVALUES_ONE}"}}}}, + wantUsernameHash: util.FmtHash(util.GetHash("user")), + wantPasswordHash: util.FmtHash(util.GetHash("pass")), }, "Favicon - empty": { had: Settings{ @@ -88,6 +93,11 @@ func TestSettingsBase_CheckValues(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + for k, v := range tc.env { + os.Setenv(k, v) + defer os.Unsetenv(k) + } + // WHEN CheckValues is called on it tc.had.CheckValues() @@ -100,14 +110,20 @@ func TestSettingsBase_CheckValues(t *testing.T) { } // AND the BasicAuth username and password are hashed (if they exist) if tc.want.Web.BasicAuth != nil { - wantUsernameHash := util.GetHash(tc.want.Web.BasicAuth.Username) - if tc.had.Web.BasicAuth.UsernameHash != wantUsernameHash { - t.Errorf("want: %x\ngot: %x", + wantUsernameHash := util.FmtHash(util.GetHash(tc.want.Web.BasicAuth.Username)) + if tc.wantUsernameHash != "" { + wantUsernameHash = tc.wantUsernameHash + } + if util.FmtHash(tc.had.Web.BasicAuth.UsernameHash) != wantUsernameHash { + t.Errorf("want: %q\ngot: %q", wantUsernameHash, tc.had.Web.BasicAuth.UsernameHash) } - wantPasswordHash := util.GetHash(tc.want.Web.BasicAuth.Password) - if tc.had.Web.BasicAuth.PasswordHash != wantPasswordHash { - t.Errorf("want: %x\ngot: %x", + wantPasswordHash := util.FmtHash(util.GetHash(tc.want.Web.BasicAuth.Password)) + if tc.wantPasswordHash != "" { + wantPasswordHash = tc.wantPasswordHash + } + if util.FmtHash(tc.had.Web.BasicAuth.PasswordHash) != wantPasswordHash { + t.Errorf("want: %q\ngot: %q", wantPasswordHash, tc.had.Web.BasicAuth.PasswordHash) } } @@ -680,8 +696,10 @@ func TestSettings_WebBasicAuthPasswordHash(t *testing.T) { func TestWebSettings_CheckValues(t *testing.T) { // GIVEN a WebSettings struct with some values set tests := map[string]struct { - had WebSettings - want WebSettings + env map[string]string + had WebSettings + want WebSettings + wantUsernameHash string }{ "BasicAuth - empty": { had: WebSettings{ @@ -709,6 +727,34 @@ func TestWebSettings_CheckValues(t *testing.T) { Username: "user", Password: util.FmtHash(util.GetHash("pass"))}}, }, + "BasicAuth - Username and password from env vars": { + env: map[string]string{ + "TESTWEBSETTINGS_CHECKVALUES_ONE": "user", + "TESTWEBSETTINGS_CHECKVALUES_TWO": "pass"}, + had: WebSettings{ + BasicAuth: &WebSettingsBasicAuth{ + Username: "${TESTWEBSETTINGS_CHECKVALUES_ONE}", + Password: "${TESTWEBSETTINGS_CHECKVALUES_TWO}"}}, + want: WebSettings{ + BasicAuth: &WebSettingsBasicAuth{ + Username: "${TESTWEBSETTINGS_CHECKVALUES_ONE}", + Password: "${TESTWEBSETTINGS_CHECKVALUES_TWO}"}}, + wantUsernameHash: util.FmtHash(util.GetHash("user")), + }, + "BasicAuth - Username and password from env vars partial": { + env: map[string]string{ + "TESTWEBSETTINGS_CHECKVALUES_THREE": "er", + "TESTWEBSETTINGS_CHECKVALUES_FOUR": "ss"}, + had: WebSettings{ + BasicAuth: &WebSettingsBasicAuth{ + Username: "us${TESTWEBSETTINGS_CHECKVALUES_THREE}", + Password: "pa${TESTWEBSETTINGS_CHECKVALUES_FOUR}"}}, + want: WebSettings{ + BasicAuth: &WebSettingsBasicAuth{ + Username: "us${TESTWEBSETTINGS_CHECKVALUES_THREE}", + Password: "pa${TESTWEBSETTINGS_CHECKVALUES_FOUR}"}}, + wantUsernameHash: util.FmtHash(util.GetHash("user")), + }, "Favicon - empty": { had: WebSettings{ Favicon: &FaviconSettings{}}, @@ -747,6 +793,11 @@ func TestWebSettings_CheckValues(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + for k, v := range tc.env { + os.Setenv(k, v) + defer os.Unsetenv(k) + } + // WHEN CheckValues is called on it tc.had.CheckValues() @@ -757,6 +808,13 @@ func TestWebSettings_CheckValues(t *testing.T) { t.Errorf("want:\n%v\ngot:\n%v", wantStr, hadStr) } + if tc.wantUsernameHash != "" { + got := util.FmtHash(tc.had.BasicAuth.UsernameHash) + if got != tc.wantUsernameHash { + t.Errorf("Username hash\nwant: %q\ngot: %q", + tc.wantUsernameHash, got) + } + } }) } } @@ -764,8 +822,11 @@ func TestWebSettings_CheckValues(t *testing.T) { func TestWebSettingsBasicAuth_CheckValues(t *testing.T) { // GIVEN a WebSettingsBasicAuth struct with some values set tests := map[string]struct { - had WebSettingsBasicAuth - want WebSettingsBasicAuth + env map[string]string + had WebSettingsBasicAuth + want WebSettingsBasicAuth + wantUsernameHash string + wantPasswordHash string }{ "str Username": { had: WebSettingsBasicAuth{ @@ -789,6 +850,38 @@ func TestWebSettingsBasicAuth_CheckValues(t *testing.T) { Username: "user", Password: util.FmtHash(util.GetHash("pass"))}, }, + "str env Web.BasicAuth.Username and str env Web.BasicAuth.Password": { + env: map[string]string{ + "TESTWEBSETTINGSBASICAUTH_CHECKVALUES_ONE": "user", + "TESTWEBSETTINGSBASICAUTH_CHECKVALUES_TWO": "pass"}, + had: WebSettingsBasicAuth{ + Username: "${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_ONE}", + Password: "${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_TWO}"}, + want: WebSettingsBasicAuth{ + Username: "${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_ONE}", + Password: "${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_TWO}"}, + }, + "str env partial Web.BasicAuth.Username and str env partial Web.BasicAuth.Password": { + env: map[string]string{ + "TESTWEBSETTINGSBASICAUTH_CHECKVALUES_THREE": "user", + "TESTWEBSETTINGSBASICAUTH_CHECKVALUES_FOUR": "pass"}, + had: WebSettingsBasicAuth{ + Username: "a${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_THREE}", + Password: "b${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_FOUR}"}, + want: WebSettingsBasicAuth{ + Username: "a${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_THREE}", + Password: "b${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_FOUR}"}, + }, + "str env undefined Web.BasicAuth.Username and str env undefined Web.BasicAuth.Password": { + had: WebSettingsBasicAuth{ + Username: "a${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_UNDEFINED}", + Password: "b${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_UNDEFINED}"}, + want: WebSettingsBasicAuth{ + Username: "a${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_UNDEFINED}", + Password: util.FmtHash(util.GetHash("b${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_UNDEFINED}"))}, + wantUsernameHash: util.FmtHash(util.GetHash("a${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_UNDEFINED}")), + wantPasswordHash: util.FmtHash(util.GetHash("b${TESTWEBSETTINGSBASICAUTH_CHECKVALUES_UNDEFINED}")), + }, "str Web.BasicAuth.Username and Web.BasicAuth.Password already hashed": { had: WebSettingsBasicAuth{ Username: "user", @@ -819,6 +912,11 @@ func TestWebSettingsBasicAuth_CheckValues(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + for k, v := range tc.env { + os.Setenv(k, v) + defer os.Unsetenv(k) + } + // WHEN CheckValues is called on it tc.had.CheckValues() @@ -829,6 +927,28 @@ func TestWebSettingsBasicAuth_CheckValues(t *testing.T) { t.Errorf("want:\n%v\ngot:\n%v", wantStr, hadStr) } + // AND the UsernameHash is calculated correctly + want := util.FmtHash(util.GetHash( + util.EvalEnvVars(tc.want.Username))) + if tc.wantUsernameHash != "" { + want = tc.wantUsernameHash + } + got := util.FmtHash(tc.had.UsernameHash) + if got != want { + t.Errorf("Username Hash\nwant: %s\ngot: %s", + want, got) + } + // AND the PasswordHash is calculated correctly + want = util.FmtHash(util.GetHash( + util.EvalEnvVars(tc.want.Password))) + if tc.wantPasswordHash != "" { + want = tc.wantPasswordHash + } + got = util.FmtHash(tc.had.PasswordHash) + if got != want { + t.Errorf("Password Hash\nwant: %s\ngot: %s", + want, got) + } }) } } diff --git a/service/deployed_version/query_test.go b/service/deployed_version/query_test.go index 347258cc..e4c6e0db 100644 --- a/service/deployed_version/query_test.go +++ b/service/deployed_version/query_test.go @@ -116,23 +116,23 @@ func TestLookup_Query(t *testing.T) { wantVersion: "[0-9]{4}", errRegex: "^$", url: "https://release-argus.io", - regex: "([0-9]+) The Argus Developers", + regex: `([0-9]+)\s<[^>]+>The Argus Developers`, }, "url from env": { + env: map[string]string{"TESTLOOKUP_DV_QUERY_ONE": "https://release-argus.io"}, noSemanticVersioning: true, wantVersion: "[0-9]{4}", errRegex: "^$", - env: map[string]string{"TESTLOOKUP_DV_QUERY_ONE": "https://release-argus.io"}, url: "${TESTLOOKUP_DV_QUERY_ONE}", - regex: "([0-9]+) The Argus Developers", + regex: `([0-9]+)\s<[^>]+>The Argus Developers`, }, "url from env partial": { + env: map[string]string{"TESTLOOKUP_DV_QUERY_TWO": "release-argus"}, noSemanticVersioning: true, wantVersion: "[0-9]{4}", errRegex: "^$", - env: map[string]string{"TESTLOOKUP_DV_QUERY_TWO": "release-argus"}, url: "https://${TESTLOOKUP_DV_QUERY_TWO}.io", - regex: "([0-9]+) The Argus Developers", + regex: `([0-9]+)\s<[^>]+>The Argus Developers`, }, "passing regex with no capture group": { noSemanticVersioning: true, @@ -145,7 +145,7 @@ func TestLookup_Query(t *testing.T) { noSemanticVersioning: true, errRegex: "^$", url: "https://release-argus.io", - regex: "([0-9]+) (The) (Argus) (Developers)", + regex: `([0-9]+)\s<[^>]+>(The) (Argus) (Developers)`, regexTemplate: stringPtr("$2 $1 $4, $3"), wantVersion: "The [0-9]+ Developers, Argus", }, @@ -157,19 +157,19 @@ func TestLookup_Query(t *testing.T) { "handle non-semantic (only major) version": { noSemanticVersioning: false, url: "https://release-argus.io", - regex: "([0-9]+) The Argus Developers", + regex: `([0-9]+)\s<[^>]+>The Argus Developers`, }, "want semantic versioning but get non-semantic version": { noSemanticVersioning: false, errRegex: "failed converting .* to a semantic version", url: "https://release-argus.io", - regex: "([0-9]+ )The Argus Developers", + regex: `([0-9]+\s)<[^>]+>The Argus Developers`, }, "allow non-semantic versioning and get non-semantic version": { noSemanticVersioning: true, errRegex: "^$", url: "https://release-argus.io", - regex: "([0-9]+) The Argus Developers", + regex: `([0-9]+\s)<[^>]+>The Argus Developers`, }, "valid semantic version": { errRegex: "^$", @@ -196,6 +196,7 @@ func TestLookup_Query(t *testing.T) { for k, v := range tc.env { os.Setenv(k, v) + defer os.Unsetenv(k) } dvl := testLookup() dvl.URL = tc.url diff --git a/web/help_test.go b/web/help_test.go index 8485bb7f..fb871647 100644 --- a/web/help_test.go +++ b/web/help_test.go @@ -315,7 +315,7 @@ func testDeployedVersion() *deployedver.Lookup { var ( allowInvalidCerts = false json = "something" - regex = "([0-9]+) The Argus Developers" + regex = `([0-9]+)\s<[^>]+>The Argus Developers` regexTemplate = "v$1" url = "https://release-argus.io" ) diff --git a/webhook/headers.go b/webhook/headers.go index b7565471..fa07116c 100644 --- a/webhook/headers.go +++ b/webhook/headers.go @@ -45,7 +45,8 @@ func (w *WebHook) setCustomHeaders(req *http.Request) { ID: *w.ServiceStatus.ServiceID, LatestVersion: w.ServiceStatus.LatestVersion()} for _, header := range *customHeaders { - value := util.TemplateString(header.Value, serviceInfo) - req.Header[header.Key] = []string{value} + key := util.EvalEnvVars(header.Key) + value := util.TemplateString(util.EvalEnvVars(header.Value), serviceInfo) + req.Header[key] = []string{value} } } diff --git a/webhook/headers_test.go b/webhook/headers_test.go index f7b3f58d..73828645 100644 --- a/webhook/headers_test.go +++ b/webhook/headers_test.go @@ -19,6 +19,7 @@ package webhook import ( "net/http" "net/http/httptest" + "os" "testing" svcstatus "github.com/release-argus/Argus/service/status" @@ -29,6 +30,7 @@ func TestWebHook_SetCustomHeaders(t *testing.T) { latestVersion := "1.2.3" serviceID := "service" tests := map[string]struct { + env map[string]string root *Headers main *Headers dfault *Headers @@ -115,12 +117,27 @@ func TestWebHook_SetCustomHeaders(t *testing.T) { "X-Service": serviceID, "X-Version": latestVersion}, }, + "header with env var": { + env: map[string]string{"FOO": "bar"}, + root: &Headers{ + {Key: "X-Service", Value: "{{ service_id }}"}, + {Key: "X-Version", Value: "{{ version }}"}, + {Key: "X-Foo", Value: "b${FOO}r"}}, + want: map[string]string{ + "X-Service": serviceID, + "X-Version": latestVersion, + "X-Foo": "bbarr"}, + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { t.Parallel() + for k, v := range tc.env { + os.Setenv(k, v) + defer os.Unsetenv(k) + } req := httptest.NewRequest(http.MethodGet, "/approvals", nil) webhook := WebHook{ ServiceStatus: &svcstatus.Status{