feat(engine): allow htpasswd to accept optional hash algorithm

Override the sprig-provided htpasswd function so Helm templates can pass an optional third argument to select the hash algorithm (bcrypt or sha). Unsupported algorithms and invalid usernames return descriptive error strings instead of panicking.

Signed-off-by: Sakthi Harish <sakthi.harish@edgeverve.com>
pull/32647/head
Sakthi Harish 3 weeks ago
parent bfa3b6c325
commit b93b5f7e59

@ -18,6 +18,8 @@ package engine
import ( import (
"bytes" "bytes"
"crypto/sha1"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -31,6 +33,7 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/Masterminds/sprig/v3" "github.com/Masterminds/sprig/v3"
"golang.org/x/crypto/bcrypt"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
goYaml "sigs.k8s.io/yaml/goyaml.v3" goYaml "sigs.k8s.io/yaml/goyaml.v3"
) )
@ -53,6 +56,10 @@ func funcMap() template.FuncMap {
delete(f, "env") delete(f, "env")
delete(f, "expandenv") delete(f, "expandenv")
// Override the sprig "htpasswd" function so it accepts an optional
// third argument to select the hash algorithm.
f["htpasswd"] = htpasswd
// Add some extra functionality // Add some extra functionality
extra := template.FuncMap{ extra := template.FuncMap{
"toToml": toTOML, "toToml": toTOML,
@ -99,6 +106,38 @@ func funcMap() template.FuncMap {
return f return f
} }
// htpasswd takes a username and password and returns an htpasswd entry. It
// extends the sprig-provided function (see https://github.com/helm/helm/issues/31924)
// by accepting an optional third argument to select the hash algorithm.
//
// Supported algorithms are "bcrypt" (the default, with the same cost as
// sprig) and "sha" ({SHA} base64-encoded SHA-1). Unlike a panic, invalid
// input is reported by returning a descriptive error string.
func htpasswd(username, password string, algorithm ...string) string {
if strings.Contains(username, ":") {
return fmt.Sprintf("invalid username: %s", username)
}
alg := "bcrypt"
if len(algorithm) > 0 {
alg = algorithm[0]
}
switch alg {
case "bcrypt":
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Sprintf("failed to encrypt string with bcrypt: %s", err)
}
return fmt.Sprintf("%s:%s", username, hash)
case "sha":
h := sha1.Sum([]byte(password))
return fmt.Sprintf("%s:{SHA}%s", username, base64.StdEncoding.EncodeToString(h[:]))
default:
return fmt.Sprintf("invalid hash algorithm: %s", alg)
}
}
// toYAML takes an interface, marshals it to yaml, and returns a string. It will // toYAML takes an interface, marshals it to yaml, and returns a string. It will
// always return a string, even on marshal error (empty string). // always return a string, even on marshal error (empty string).
// //

@ -195,6 +195,54 @@ keyInElement1 = "valueInElement1"`,
} }
} }
func TestHtpasswd(t *testing.T) {
tests := []struct {
name string
tpl string
expect string
// if non-empty, the output must have this prefix instead of matching expect exactly
prefix string
}{
{
name: "defaults to bcrypt",
tpl: `{{ htpasswd "user" "pass" }}`,
prefix: "user:$2",
},
{
name: "explicit bcrypt",
tpl: `{{ htpasswd "user" "pass" "bcrypt" }}`,
prefix: "user:$2",
},
{
name: "sha produces {SHA} base64 hash",
tpl: `{{ htpasswd "user" "pass" "sha" }}`,
expect: "user:{SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=",
},
{
name: "username containing colon is rejected",
tpl: `{{ htpasswd "bad:user" "pass" }}`,
expect: "invalid username: bad:user",
},
{
name: "unsupported algorithm returns error string",
tpl: `{{ htpasswd "user" "pass" "md5" }}`,
expect: "invalid hash algorithm: md5",
},
}
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)
if tt.prefix != "" {
assert.Truef(t, strings.HasPrefix(b.String(), tt.prefix), "expected output to start with %q, got %q", tt.prefix, b.String())
return
}
assert.Equal(t, tt.expect, b.String(), tt.tpl)
})
}
}
func TestDurationHelpers(t *testing.T) { func TestDurationHelpers(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

Loading…
Cancel
Save