Files
ConfigKit/secrets_test.go

378 lines
14 KiB
Go

package configkit
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
func TestLoadSecretsDirLoadsCanonicalSortedValues(t *testing.T) {
directory := t.TempDir()
writeSecretTestFile(t, directory, "CONFIGKIT_SECRET_Z", "last\n\n")
writeSecretTestFile(t, directory, "CONFIGKIT_SECRET_A", "first\r\n")
writeSecretTestFile(t, directory, "CONFIGKIT_SECRET_EMPTY", "")
selected := directory
if runtime.GOOS != "windows" {
selected = filepath.Join(t.TempDir(), "selected")
if err := os.Symlink(directory, selected); err != nil {
t.Fatalf("Symlink() error = %v", err)
}
}
for _, name := range []string{"CONFIGKIT_SECRET_A", "CONFIGKIT_SECRET_EMPTY", "CONFIGKIT_SECRET_Z"} {
t.Setenv(name, "temporary")
if err := os.Unsetenv(name); err != nil {
t.Fatalf("Unsetenv(%q) error = %v", name, err)
}
}
report, err := LoadSecretsDir(selected)
if err != nil {
t.Fatalf("LoadSecretsDir() error = %v", err)
}
if got := os.Getenv("CONFIGKIT_SECRET_A"); got != "first" {
t.Fatalf("LF value = %q, want first", got)
}
if got := os.Getenv("CONFIGKIT_SECRET_Z"); got != "last\n" {
t.Fatalf("two-LF value = %q, want one retained LF", got)
}
if value, present := os.LookupEnv("CONFIGKIT_SECRET_EMPTY"); !present || value != "" {
t.Fatalf("empty value = %q, %v; want present empty", value, present)
}
canonical := canonicalTestPath(t, directory)
want := []SecretFileResult{
{Name: "CONFIGKIT_SECRET_A", Path: filepath.Join(canonical, "CONFIGKIT_SECRET_A"), Status: SecretFileLoaded},
{Name: "CONFIGKIT_SECRET_EMPTY", Path: filepath.Join(canonical, "CONFIGKIT_SECRET_EMPTY"), Status: SecretFileLoaded},
{Name: "CONFIGKIT_SECRET_Z", Path: filepath.Join(canonical, "CONFIGKIT_SECRET_Z"), Status: SecretFileLoaded},
}
if got := report.Entries(); !reflect.DeepEqual(got, want) {
t.Fatalf("Entries() = %#v, want %#v", got, want)
}
entries := report.Entries()
entries[0].Name = "mutated"
if report.Entries()[0].Name != want[0].Name {
t.Fatal("Entries() did not return a detached slice")
}
}
func TestLoadSecretsDirPreservesExistingWithoutReading(t *testing.T) {
directory := t.TempDir()
writeSecretTestFile(t, directory, "EMPTY_WINNER", strings.Repeat("CANARY", int(secretFileByteLimit)))
writeSecretTestFile(t, directory, "VALUE_WINNER", "CANARY-CONTENT")
environment := map[string]string{"EMPTY_WINNER": "", "VALUE_WINNER": "existing"}
opened := 0
access := testSecretsAccess()
access.openFile = func(string) (*os.File, error) {
opened++
return nil, errors.New("must not open")
}
report, err := loadSecretsDir(directory, mapSecretsEnvironment(environment), access)
if err != nil {
t.Fatalf("loadSecretsDir() error = %v", err)
}
if opened != 0 {
t.Fatalf("opened skipped files %d times", opened)
}
want := []SecretFileResult{
{Name: "EMPTY_WINNER", Path: filepath.Join(canonicalTestPath(t, directory), "EMPTY_WINNER"), Status: SecretFileSkippedExisting, ExistingEmpty: true},
{Name: "VALUE_WINNER", Path: filepath.Join(canonicalTestPath(t, directory), "VALUE_WINNER"), Status: SecretFileSkippedExisting},
}
if got := report.Entries(); !reflect.DeepEqual(got, want) {
t.Fatalf("Entries() = %#v, want %#v", got, want)
}
}
func TestLoadSecretsDirValidatesAllMetadataBeforeReadingOrWriting(t *testing.T) {
for _, test := range []struct {
name string
make func(*testing.T, string)
code Code
}{
{name: "invalid name", make: func(t *testing.T, directory string) { writeSecretTestFile(t, directory, "1INVALID", "CANARY") }, code: CodeSecretsInvalidName},
{name: "directory", make: func(t *testing.T, directory string) {
if err := os.Mkdir(filepath.Join(directory, "BROKEN"), 0o700); err != nil {
t.Fatal(err)
}
}, code: CodeSecretsNotRegular},
{name: "symlink", make: func(t *testing.T, directory string) {
if runtime.GOOS == "windows" {
t.Skip("symlink setup differs on Windows")
}
writeSecretTestFile(t, directory, "TARGET", "CANARY")
if err := os.Symlink(filepath.Join(directory, "TARGET"), filepath.Join(directory, "BROKEN")); err != nil {
t.Fatal(err)
}
}, code: CodeSecretsNotRegular},
} {
t.Run(test.name, func(t *testing.T) {
directory := t.TempDir()
writeSecretTestFile(t, directory, "A_VALID", "first")
test.make(t, directory)
writes := 0
access := testSecretsAccess()
opened := 0
access.openFile = func(path string) (*os.File, error) { opened++; return os.Open(path) }
report, err := loadSecretsDir(directory, secretsEnvironment{
lookup: func(string) (string, bool) { return "", false },
set: func(string, string) error { writes++; return nil },
}, access)
requireSecretsCode(t, err, test.code)
if writes != 0 || opened != 0 {
t.Fatalf("writes = %d, opens = %d; want no preflight activity", writes, opened)
}
if report == nil || len(report.Entries()) < 2 {
t.Fatalf("report = %#v, want enumerated entries", report)
}
})
}
}
func TestLoadSecretsDirPreflightFailuresDoNotWrite(t *testing.T) {
for _, test := range []struct {
name string
body string
code Code
}{
{name: "file limit", body: strings.Repeat("x", int(secretFileByteLimit)+1), code: CodeSecretsLimitExceeded},
{name: "NUL", body: "before\x00CANARY", code: CodeSecretsInvalidValue},
} {
t.Run(test.name, func(t *testing.T) {
directory := t.TempDir()
writeSecretTestFile(t, directory, "A_FIRST", "first")
writeSecretTestFile(t, directory, "Z_FAILED", test.body)
writes := 0
report, err := loadSecretsDir(directory, secretsEnvironment{
lookup: func(string) (string, bool) { return "", false },
set: func(string, string) error { writes++; return nil },
}, testSecretsAccess())
requireSecretsCode(t, err, test.code)
if writes != 0 {
t.Fatalf("writes = %d, want zero", writes)
}
entries := report.Entries()
if entries[0].Status != SecretFileNotApplied || entries[1].Status != SecretFileFailed {
t.Fatalf("statuses = %#v", entries)
}
})
}
}
func TestLoadSecretsDirClassifiesReadFailuresWithoutLeakingThem(t *testing.T) {
for _, test := range []struct {
name string
err error
code Code
}{
{name: "permission", err: fs.ErrPermission, code: CodeSecretsPermission},
{name: "missing", err: fs.ErrNotExist, code: CodeSecretsMissing},
{name: "other", err: errors.New("READ-ERROR-CANARY"), code: CodeSecretsRead},
} {
t.Run(test.name, func(t *testing.T) {
directory := t.TempDir()
writeSecretTestFile(t, directory, "SECRET", "CREDENTIAL-CANARY")
access := testSecretsAccess()
access.openFile = func(string) (*os.File, error) { return nil, test.err }
writes := 0
report, err := loadSecretsDir(directory, secretsEnvironment{
lookup: func(string) (string, bool) { return "", false },
set: func(string, string) error { writes++; return nil },
}, access)
requireSecretsCode(t, err, test.code)
if writes != 0 || report.Entries()[0].Status != SecretFileFailed {
t.Fatalf("writes = %d, entries = %#v", writes, report.Entries())
}
if strings.Contains(fmt.Sprintf("%v %#v", err, err), "CANARY") {
t.Fatalf("error leaked underlying failure: %v %#v", err, err)
}
})
}
}
func TestLoadSecretsDirEnforcesEnumerationAndTotalBounds(t *testing.T) {
t.Run("entries", func(t *testing.T) {
directory := t.TempDir()
for index := 0; index <= secretsEntryLimit; index++ {
writeSecretTestFile(t, directory, fmt.Sprintf("SECRET_%04d", index), "")
}
report, err := loadSecretsDir(directory, mapSecretsEnvironment(map[string]string{}), testSecretsAccess())
requireSecretsCode(t, err, CodeSecretsLimitExceeded)
if len(report.Entries()) != secretsEntryLimit+1 {
t.Fatalf("entries = %d, want %d bounded enumerated entries", len(report.Entries()), secretsEntryLimit+1)
}
})
t.Run("total bytes", func(t *testing.T) {
directory := t.TempDir()
for index := 0; index < 9; index++ {
writeSecretTestFile(t, directory, fmt.Sprintf("SECRET_%02d", index), strings.Repeat("x", int(secretFileByteLimit)))
}
writes := 0
report, err := loadSecretsDir(directory, secretsEnvironment{
lookup: func(string) (string, bool) { return "", false },
set: func(string, string) error { writes++; return nil },
}, testSecretsAccess())
requireSecretsCode(t, err, CodeSecretsLimitExceeded)
if writes != 0 || report.Entries()[8].Status != SecretFileFailed {
t.Fatalf("writes = %d, final status = %q", writes, report.Entries()[8].Status)
}
})
}
func TestLoadSecretsDirApplyReportsPartialFailureAndPresenceRace(t *testing.T) {
directory := t.TempDir()
for _, name := range []string{"A_LOADED", "B_RACED", "C_FAILED", "D_WAITING"} {
writeSecretTestFile(t, directory, name, "CANARY-"+name)
}
values := map[string]string{}
lookups := map[string]int{}
environment := secretsEnvironment{
lookup: func(name string) (string, bool) {
lookups[name]++
if name == "B_RACED" && lookups[name] == 2 {
values[name] = ""
}
value, present := values[name]
return value, present
},
set: func(name, value string) error {
if name == "C_FAILED" {
return errors.New("CANARY set error")
}
values[name] = value
return nil
},
}
report, err := loadSecretsDir(directory, environment, testSecretsAccess())
requireSecretsCode(t, err, CodeSecretsApplyFailed)
entries := report.Entries()
wantStatuses := []SecretFileStatus{SecretFileLoaded, SecretFileSkippedExisting, SecretFileFailed, SecretFileNotApplied}
for index, want := range wantStatuses {
if entries[index].Status != want {
t.Fatalf("entry %d status = %q, want %q", index, entries[index].Status, want)
}
}
if !entries[1].ExistingEmpty || values["A_LOADED"] != "CANARY-A_LOADED" {
t.Fatalf("partial report/state = %#v, %#v", entries, values)
}
if _, present := values["D_WAITING"]; present {
t.Fatal("entry after apply failure was written")
}
}
func TestLoadSecretsDirFailuresAreSafeAndDetached(t *testing.T) {
directory := t.TempDir()
writeSecretTestFile(t, directory, "SECRET", "CREDENTIAL-CANARY\x00")
report, err := loadSecretsDir(directory, mapSecretsEnvironment(map[string]string{}), testSecretsAccess())
secretsErr := requireSecretsCode(t, err, CodeSecretsInvalidValue)
diagnostics := secretsErr.Diagnostics()
diagnostics[0].Message = "mutated"
if secretsErr.Diagnostics()[0].Message == "mutated" {
t.Fatal("Diagnostics() did not return a detached slice")
}
var rendered strings.Builder
if _, writeErr := report.WriteTo(&rendered); writeErr != nil {
t.Fatal(writeErr)
}
jsonReport, marshalErr := json.Marshal(report)
if marshalErr != nil {
t.Fatal(marshalErr)
}
outputs := []string{err.Error(), fmt.Sprintf("%v %#v", err, err), fmt.Sprintf("%v %#v", report, report), rendered.String(), string(jsonReport)}
for _, output := range outputs {
if strings.Contains(output, "CREDENTIAL-CANARY") {
t.Fatalf("safe output leaked content: %q", output)
}
}
if secretsErr.Diagnostics()[0].Primary != nil || secretsErr.Diagnostics()[0].Path.String() != "" {
t.Fatalf("bootstrap diagnostic fabricated source location/path: %#v", secretsErr.Diagnostics()[0])
}
}
func TestSecretsReportWriteToPropagatesWriterFailures(t *testing.T) {
report := &SecretsReport{entries: []SecretFileResult{{Name: "A\nB", Path: "/tmp/A\nB", Status: SecretFileLoaded}}}
var output strings.Builder
written, err := report.WriteTo(&output)
if err != nil || written != int64(output.Len()) || strings.Contains(output.String(), "A\nB") || !strings.Contains(output.String(), `A\nB`) {
t.Fatalf("WriteTo() = (%d, %v), output %q", written, err, output.String())
}
failure := errors.New("writer CANARY")
written, err = report.WriteTo(errorAfterWriter{limit: 5, err: failure})
if written != 5 || !errors.Is(err, failure) {
t.Fatalf("failing WriteTo() = (%d, %v)", written, err)
}
written, err = report.WriteTo(shortWriter{})
if written == 0 || !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("short WriteTo() = (%d, %v)", written, err)
}
}
func TestLoadSecretsDirRejectsMissingAndInvalidDirectory(t *testing.T) {
report, err := LoadSecretsDir("")
requireSecretsCode(t, err, CodeSecretsInvalidPath)
if report == nil || len(report.Entries()) != 0 {
t.Fatalf("empty path report = %#v", report)
}
report, err = LoadSecretsDir(filepath.Join(t.TempDir(), "missing"))
requireSecretsCode(t, err, CodeSecretsMissing)
if report == nil || len(report.Entries()) != 0 {
t.Fatalf("missing path report = %#v", report)
}
file := filepath.Join(t.TempDir(), "FILE")
if err := os.WriteFile(file, nil, 0o600); err != nil {
t.Fatal(err)
}
_, err = LoadSecretsDir(file)
requireSecretsCode(t, err, CodeSecretsInvalidPath)
}
func testSecretsAccess() secretsAccess {
return secretsAccess{openDirectory: os.Open, lstat: os.Lstat, openFile: os.Open}
}
func mapSecretsEnvironment(values map[string]string) secretsEnvironment {
return secretsEnvironment{
lookup: func(name string) (string, bool) { value, present := values[name]; return value, present },
set: func(name, value string) error { values[name] = value; return nil },
}
}
func writeSecretTestFile(t *testing.T, directory, name, value string) {
t.Helper()
if err := os.WriteFile(filepath.Join(directory, name), []byte(value), 0o600); err != nil {
t.Fatalf("WriteFile(%q) error = %v", name, err)
}
}
func requireSecretsCode(t *testing.T, err error, code Code) *SecretsError {
t.Helper()
var secretsErr *SecretsError
if !errors.As(err, &secretsErr) {
t.Fatalf("error = %T %v, want *SecretsError", err, err)
}
diagnostics := secretsErr.Diagnostics()
if len(diagnostics) != 1 || diagnostics[0].Code != code {
t.Fatalf("diagnostics = %#v, want code %q", diagnostics, code)
}
return secretsErr
}
type errorAfterWriter struct {
limit int
err error
}
func (w errorAfterWriter) Write(data []byte) (int, error) {
return min(w.limit, len(data)), w.err
}