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>
pull/32619/head
Shashank Varma 1 week ago
parent fa11636b01
commit 60b508843d

@ -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"
) )
@ -67,6 +70,7 @@ func funcMap() template.FuncMap {
"mustToJson": mustToJSON, "mustToJson": mustToJSON,
"fromJson": fromJSON, "fromJson": fromJSON,
"fromJsonArray": fromJSONArray, "fromJsonArray": fromJSONArray,
"htpasswd": htpasswd,
// Duration helpers // Duration helpers
"mustToDuration": mustToDuration, "mustToDuration": mustToDuration,
@ -99,6 +103,34 @@ func funcMap() template.FuncMap {
return f 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 // 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).
// //

@ -17,6 +17,8 @@ limitations under the License.
package engine package engine
import ( import (
"crypto/sha1"
"encoding/base64"
"math" "math"
"strings" "strings"
"testing" "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) { func TestDurationHelpers(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

Loading…
Cancel
Save