From 60b508843dbccfcb00cef8fe6bc0052462e507f1 Mon Sep 17 00:00:00 2001 From: Shashank Varma <324153016+shashankvarma499@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:26:03 +0000 Subject: [PATCH] Add optional hashAlgorithm argument to htpasswd template function The htpasswd template function (inherited from sprig) only supports bcrypt and accepts two arguments. This adds an optional third hashAlgorithm argument supporting "sha"/"sha1" (Apache {SHA} format) in addition to the default "bcrypt". Fixes #31924 Signed-off-by: Shashank Varma <324153016+shashankvarma499@users.noreply.github.com> --- pkg/engine/funcs.go | 32 +++++++++++++++++++++++ pkg/engine/funcs_test.go | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index e876df6c0..890dd99b1 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -18,6 +18,8 @@ package engine import ( "bytes" + "crypto/sha1" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -31,6 +33,7 @@ import ( "github.com/BurntSushi/toml" "github.com/Masterminds/sprig/v3" + "golang.org/x/crypto/bcrypt" "sigs.k8s.io/yaml" goYaml "sigs.k8s.io/yaml/goyaml.v3" ) @@ -67,6 +70,7 @@ func funcMap() template.FuncMap { "mustToJson": mustToJSON, "fromJson": fromJSON, "fromJsonArray": fromJSONArray, + "htpasswd": htpasswd, // Duration helpers "mustToDuration": mustToDuration, @@ -99,6 +103,34 @@ func funcMap() template.FuncMap { return f } +// htpasswd generates an Apache htpasswd entry for username and password. +// algorithm is optional and defaults to bcrypt. Supported values are +// "bcrypt", "sha", and "sha1". +func htpasswd(username, password string, algorithm ...string) string { + if strings.Contains(username, ":") { + return "invalid username: " + username + } + + algo := "bcrypt" + if len(algorithm) > 0 && algorithm[0] != "" { + algo = algorithm[0] + } + + switch algo { + case "bcrypt": + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "failed to encrypt string with bcrypt: " + err.Error() + } + return username + ":" + string(hash) + case "sha", "sha1": + sum := sha1.Sum([]byte(password)) + return username + ":{SHA}" + base64.StdEncoding.EncodeToString(sum[:]) + default: + return "invalid algorithm: " + algo + } +} + // toYAML takes an interface, marshals it to yaml, and returns a string. It will // always return a string, even on marshal error (empty string). // diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 03ed64153..b903ed626 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -17,6 +17,8 @@ limitations under the License. package engine import ( + "crypto/sha1" + "encoding/base64" "math" "strings" "testing" @@ -195,6 +197,59 @@ keyInElement1 = "valueInElement1"`, } } +func TestHtpasswd(t *testing.T) { + const username = "testuser" + const password = "testpassword" + + shaSum := sha1.Sum([]byte(password)) + shaExpected := username + ":{SHA}" + base64.StdEncoding.EncodeToString(shaSum[:]) + + tests := []struct { + name string + tpl string + expect string + bcrypt bool + }{{ + name: "two-arg defaults to bcrypt", + tpl: `{{ htpasswd "testuser" "testpassword" }}`, + bcrypt: true, + }, { + name: "three-arg bcrypt", + tpl: `{{ htpasswd "testuser" "testpassword" "bcrypt" }}`, + bcrypt: true, + }, { + name: "three-arg sha", + tpl: `{{ htpasswd "testuser" "testpassword" "sha" }}`, + expect: shaExpected, + }, { + name: "three-arg sha1", + tpl: `{{ htpasswd "testuser" "testpassword" "sha1" }}`, + expect: shaExpected, + }, { + name: "invalid algorithm", + tpl: `{{ htpasswd "testuser" "testpassword" "md5" }}`, + expect: "invalid algorithm: md5", + }, { + name: "username containing colon", + tpl: `{{ htpasswd "user:name" "testpassword" }}`, + expect: "invalid username: user:name", + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b strings.Builder + require.NoError(t, template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, nil), tt.tpl) + got := b.String() + if tt.bcrypt { + assert.True(t, strings.HasPrefix(got, username+":$")) + assert.NotContains(t, got, "{SHA}") + return + } + assert.Equal(t, tt.expect, got, tt.tpl) + }) + } +} + func TestDurationHelpers(t *testing.T) { tests := []struct { name string