Files

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)
}
}