pull/32033/merge
Asish Kumar 7 hours ago committed by GitHub
commit d7eb92c4ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,13 +18,17 @@ package engine
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"maps"
"strings"
"text/template"
"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"
)
@ -49,6 +53,7 @@ func funcMap() template.FuncMap {
// Add some extra functionality
extra := template.FuncMap{
"htpasswd": htpasswd,
"toToml": toTOML,
"mustToToml": mustToTOML,
"fromToml": fromTOML,
@ -80,6 +85,46 @@ func funcMap() template.FuncMap {
return f
}
// htpasswd takes a username and password and returns an htpasswd entry.
// By default it uses bcrypt, matching Sprig's existing behavior.
// An optional third argument can explicitly select the hash algorithm.
func htpasswd(username, password string, hashAlgorithms ...string) (string, error) {
if strings.Contains(username, ":") {
return fmt.Sprintf("invalid username: %s", username), nil
}
if strings.ContainsAny(username, "\n\r") {
return "", fmt.Errorf("invalid username %q: must not contain newline characters", username)
}
if len(hashAlgorithms) > 1 {
return "", fmt.Errorf("wrong number of args for htpasswd: want 2 or 3 got %d", len(hashAlgorithms)+2)
}
algorithm := "bcrypt"
if len(hashAlgorithms) == 1 && hashAlgorithms[0] != "" {
algorithm = strings.ToLower(hashAlgorithms[0])
}
switch algorithm {
case "bcrypt":
return bcryptHtpasswd(username, password)
case "sha", "sha1":
sum := sha1.Sum([]byte(password))
return fmt.Sprintf("%s:{SHA}%s", username, base64.StdEncoding.EncodeToString(sum[:])), nil
default:
return "", fmt.Errorf("unsupported htpasswd hash algorithm %q", hashAlgorithms[0])
}
}
func bcryptHtpasswd(username, password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to encrypt password with bcrypt: %w", err)
}
return fmt.Sprintf("%s:%s", username, string(hash)), nil
}
// toYAML takes an interface, marshals it to yaml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//

@ -17,11 +17,14 @@ limitations under the License.
package engine
import (
"fmt"
"strings"
"testing"
"text/template"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
)
func TestFuncs(t *testing.T) {
@ -181,6 +184,89 @@ keyInElement1 = "valueInElement1"`,
}
}
func TestHtpasswd(t *testing.T) {
tests := []struct {
name string
tpl string
expect string
expectError string
validate func(t *testing.T, rendered string)
}{
{
name: "defaults to bcrypt",
tpl: `{{ htpasswd "testuser" "testpassword" }}`,
validate: func(t *testing.T, rendered string) {
t.Helper()
parts := strings.SplitN(rendered, ":", 2)
require.Len(t, parts, 2)
assert.Equal(t, "testuser", parts[0])
assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(parts[1]), []byte("testpassword")))
},
},
{
name: "supports explicit bcrypt algorithm",
tpl: `{{ htpasswd "testuser" "testpassword" "bcrypt" }}`,
validate: func(t *testing.T, rendered string) {
t.Helper()
parts := strings.SplitN(rendered, ":", 2)
require.Len(t, parts, 2)
assert.Equal(t, "testuser", parts[0])
assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(parts[1]), []byte("testpassword")))
},
},
{
name: "supports sha algorithm",
tpl: `{{ htpasswd "testuser" "testpassword" "sha" }}`,
expect: `testuser:{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0=`,
},
{
name: "preserves invalid username behavior",
tpl: `{{ htpasswd "bad:user" "testpassword" }}`,
expect: `invalid username: bad:user`,
},
{
name: "rejects username with newline",
tpl: "{{ htpasswd \"bad\\nuser\" \"testpassword\" }}",
expectError: `must not contain newline characters`,
},
{
name: "returns bcrypt errors",
tpl: fmt.Sprintf(`{{ htpasswd "testuser" %q }}`, strings.Repeat("x", 73)),
expectError: `password length exceeds 72 bytes`,
},
{
name: "rejects unsupported algorithms",
tpl: `{{ htpasswd "testuser" "testpassword" "md5" }}`,
expectError: `unsupported htpasswd hash algorithm "md5"`,
},
{
name: "rejects too many hash algorithm args",
tpl: `{{ htpasswd "testuser" "testpassword" "sha" "extra" }}`,
expectError: `wrong number of args for htpasswd: want 2 or 3 got 4`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b strings.Builder
err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, nil)
if tt.expectError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectError)
return
}
require.NoError(t, err)
if tt.validate != nil {
tt.validate(t, b.String())
return
}
assert.Equal(t, tt.expect, b.String())
})
}
}
// This test to check a function provided by sprig is due to a change in a
// dependency of sprig. mergo in v0.3.9 changed the way it merges and only does
// public fields (i.e. those starting with a capital letter). This test, from

Loading…
Cancel
Save