Add credential loading from env vars and 0600 env files
Credentials arrive only from BW_* process env or an env file parsed in pure Go (never sourced, never exec'd); files looser than 0600 are refused before a single byte is read, and errors carry line numbers and key names, never values. Env wins over file, per the porting-notes precedence.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeFile(t *testing.T, mode os.FileMode, content string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "env")
|
||||
if err := os.WriteFile(path, []byte(content), mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(path, mode); err != nil { // umask may have tightened
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func clearEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, k := range []string{KeyServerURL, KeyAccessToken, KeyClientID, KeyClientSecret} {
|
||||
t.Setenv(k, "")
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvFile(t *testing.T) {
|
||||
clearEnv(t)
|
||||
path := writeFile(t, 0o600, "# comment\nBW_SERVER_URL=https://vault.example.com\nBW_ACCESS_TOKEN='0.uuid.secret:key=='\n")
|
||||
cfg, _, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.ServerURL != "https://vault.example.com" {
|
||||
t.Fatalf("server url: %s", cfg.ServerURL)
|
||||
}
|
||||
if cfg.AccessToken != "0.uuid.secret:key==" {
|
||||
t.Fatalf("access token: %s", cfg.AccessToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRefusesLooseMode(t *testing.T) {
|
||||
clearEnv(t)
|
||||
path := writeFile(t, 0o644, "BW_ACCESS_TOKEN=x\n")
|
||||
_, _, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("loose file accepted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "insecure mode") {
|
||||
t.Fatalf("wrong error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRefusesEvenLooserBeforeReading(t *testing.T) {
|
||||
clearEnv(t)
|
||||
// 0644 file whose contents would break parsing: the mode check must
|
||||
// fire first (the parse error is never reached, contents never read).
|
||||
path := writeFile(t, 0o666, "this is not valid at all\n")
|
||||
_, _, err := Load(path)
|
||||
if err == nil || !strings.Contains(err.Error(), "insecure mode") {
|
||||
t.Fatalf("mode check did not fire first: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadStricterThan0600OK(t *testing.T) {
|
||||
clearEnv(t)
|
||||
path := writeFile(t, 0o400, "BW_CLIENTID=id\nBW_CLIENTSECRET=sec\n")
|
||||
cfg, _, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.ClientID != "id" || cfg.ClientSecret != "sec" {
|
||||
t.Fatal("split credentials not loaded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvWinsOverFile(t *testing.T) {
|
||||
clearEnv(t)
|
||||
path := writeFile(t, 0o600, "BW_ACCESS_TOKEN=file-token\n")
|
||||
t.Setenv("BW_ACCESS_TOKEN", "env-token")
|
||||
cfg, _, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.AccessToken != "env-token" {
|
||||
t.Fatal("env did not win over file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCredentialsIsTypedError(t *testing.T) {
|
||||
clearEnv(t)
|
||||
_, _, err := Load("")
|
||||
if err == nil {
|
||||
t.Fatal("missing credentials accepted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no credentials") {
|
||||
t.Fatalf("wrong error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingFileOK(t *testing.T) {
|
||||
clearEnv(t)
|
||||
// Env credentials plus an explicit-but-missing file path: the file is
|
||||
// simply not there to consult; env carries the login.
|
||||
t.Setenv("BW_CLIENTID", "id")
|
||||
t.Setenv("BW_CLIENTSECRET", "sec")
|
||||
path := writeFile(t, 0o600, "BW_CLIENTID=id\nBW_CLIENTSECRET=sec\n")
|
||||
cfg, _, err := Load(path + "-does-not-exist")
|
||||
if err != nil {
|
||||
t.Fatalf("explicit missing file: %v", err)
|
||||
}
|
||||
if cfg.ServerURL != DefaultServerURL {
|
||||
t.Fatalf("default server url: %s", cfg.ServerURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvFileDiscipline(t *testing.T) {
|
||||
vals, err := parseEnvFile([]byte("\n# c\nexport BW_A=1\nBW_B = spaced \nBW_C=\"quoted\"\nBW_C=later-wins\nBW_D=val # trailing comment\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
want := map[string]string{"BW_A": "1", "BW_B": "spaced", "BW_C": "later-wins", "BW_D": "val"}
|
||||
for k, v := range want {
|
||||
if vals[k] != v {
|
||||
t.Fatalf("%s: %q != %q", k, vals[k], v)
|
||||
}
|
||||
}
|
||||
_, err = parseEnvFile([]byte("BAD LINE WITHOUT EQUALS\n"))
|
||||
if err == nil || !strings.Contains(err.Error(), "line 1") {
|
||||
t.Fatalf("malformed line error: %v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "BAD LINE") {
|
||||
t.Fatalf("error echoes file contents: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user