158 lines
5.3 KiB
Go
158 lines
5.3 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const testSuperPassword = "Sup3r-Secret-Value"
|
|
|
|
func lookupMap(m map[string]string) func(string) (string, bool) {
|
|
return func(k string) (string, bool) {
|
|
v, ok := m[k]
|
|
return v, ok
|
|
}
|
|
}
|
|
|
|
func testPolicy(pw string) error {
|
|
if len(pw) < 8 || len(pw) > 256 {
|
|
return errors.New("password must be 8-256 characters")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeEnvFile(t *testing.T, body string, perm os.FileMode) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), ".env")
|
|
if err := os.WriteFile(path, []byte(body), perm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(path, perm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func assertNoSecret(t *testing.T, err error, secret string) {
|
|
t.Helper()
|
|
if err != nil && secret != "" && strings.Contains(err.Error(), secret) {
|
|
t.Fatalf("error leaks secret: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminValidateFailsClosed(t *testing.T) {
|
|
cases := []struct {
|
|
name, owner, password, want string
|
|
}{
|
|
{"both absent", "", "", "SUPER_ADMIN_OWNER and SUPER_ADMIN_PASSWORD are required"},
|
|
{"owner only", "root", "", "SUPER_ADMIN_PASSWORD is missing"},
|
|
{"password only", "", testSuperPassword, "SUPER_ADMIN_OWNER is missing"},
|
|
{"short password", "root", "short", "SUPER_ADMIN_PASSWORD violates password policy"},
|
|
{"owner whitespace", " root", testSuperPassword, "surrounding whitespace"},
|
|
{"owner control", "ro\x01ot", testSuperPassword, "control characters"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
sa, err := LoadSuperAdmin(lookupMap(map[string]string{EnvSuperAdminOwner: tc.owner, EnvSuperAdminPassword: tc.password}), "", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = sa.Validate(testPolicy)
|
|
if !errors.Is(err, ErrSuperAdminConfig) || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("err=%v want %q", err, tc.want)
|
|
}
|
|
assertNoSecret(t, err, tc.password)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminLoadFromProcessEnvironment(t *testing.T) {
|
|
sa, err := LoadSuperAdmin(lookupMap(map[string]string{EnvSuperAdminOwner: "root", EnvSuperAdminPassword: testSuperPassword}), "", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sa.Owner != "root" || sa.Password() != testSuperPassword {
|
|
t.Fatalf("got owner=%q", sa.Owner)
|
|
}
|
|
if err := sa.Validate(testPolicy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminEnvFileFillsUnsetKeysOnly(t *testing.T) {
|
|
path := writeEnvFile(t, "# comment\nexport SUPER_ADMIN_OWNER=\"file-root\"\nSUPER_ADMIN_PASSWORD='"+testSuperPassword+"'\r\nnot a line\n", 0o600)
|
|
sa, err := LoadSuperAdmin(lookupMap(map[string]string{}), path, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sa.Owner != "file-root" || sa.Password() != testSuperPassword {
|
|
t.Fatalf("file values not loaded: owner=%q", sa.Owner)
|
|
}
|
|
sa, err = LoadSuperAdmin(lookupMap(map[string]string{EnvSuperAdminOwner: "env-root", EnvSuperAdminPassword: ""}), path, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sa.Owner != "env-root" || sa.Password() != testSuperPassword {
|
|
t.Fatalf("process environment must win, empty must fall back: owner=%q", sa.Owner)
|
|
}
|
|
sa, err = LoadSuperAdmin(lookupMap(map[string]string{EnvFileKey: path}), "", true)
|
|
if err != nil || sa.Owner != "file-root" {
|
|
t.Fatalf("AGENT_SYSTEM_ENV_FILE not honored: owner=%q err=%v", sa.Owner, err)
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminEnvFilePartialFailsValidation(t *testing.T) {
|
|
path := writeEnvFile(t, "SUPER_ADMIN_OWNER=root\n", 0o600)
|
|
sa, err := LoadSuperAdmin(lookupMap(map[string]string{}), path, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sa.Validate(testPolicy); err == nil || !strings.Contains(err.Error(), "SUPER_ADMIN_PASSWORD is missing") {
|
|
t.Fatalf("partial file accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminEnvFileMustBeProtected(t *testing.T) {
|
|
path := writeEnvFile(t, "SUPER_ADMIN_OWNER=root\nSUPER_ADMIN_PASSWORD="+testSuperPassword+"\n", 0o644)
|
|
_, err := LoadSuperAdmin(lookupMap(map[string]string{}), path, true)
|
|
if !errors.Is(err, ErrSuperAdminConfig) || !strings.Contains(err.Error(), "group/other") {
|
|
t.Fatalf("world-readable env file accepted: %v", err)
|
|
}
|
|
assertNoSecret(t, err, testSuperPassword)
|
|
_, err = LoadSuperAdmin(lookupMap(map[string]string{}), filepath.Join(t.TempDir(), "missing.env"), true)
|
|
if !errors.Is(err, ErrSuperAdminConfig) {
|
|
t.Fatalf("missing env file accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminNotRequiredWithoutAuthentication(t *testing.T) {
|
|
sa, err := LoadSuperAdmin(lookupMap(map[string]string{EnvSuperAdminOwner: "root"}), filepath.Join(t.TempDir(), "missing.env"), false)
|
|
if err != nil {
|
|
t.Fatalf("auth-disabled mode must not read or require credentials: %v", err)
|
|
}
|
|
if sa.Owner != "" || sa.Password() != "" {
|
|
t.Fatalf("auth-disabled mode loaded credentials: owner=%q", sa.Owner)
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminRedactsPassword(t *testing.T) {
|
|
sa := NewSuperAdmin("root", testSuperPassword)
|
|
for _, out := range []string{fmt.Sprint(sa), fmt.Sprintf("%v %+v %#v %s", sa, sa, sa, sa), fmt.Sprint(&sa)} {
|
|
if strings.Contains(out, testSuperPassword) {
|
|
t.Fatalf("formatted output leaks password: %s", out)
|
|
}
|
|
}
|
|
raw, err := json.Marshal(struct{ SuperAdmin SuperAdmin }{sa})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), testSuperPassword) || !strings.Contains(string(raw), `"passwordConfigured":true`) {
|
|
t.Fatalf("json leaks password or lost status: %s", raw)
|
|
}
|
|
}
|