378 lines
14 KiB
Go
378 lines
14 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type secretReferenceConfig struct {
|
|
Required Secret `yaml:"required"`
|
|
Optional *Secret `yaml:"optional"`
|
|
Map map[string]Secret `yaml:"map"`
|
|
Pointers map[string]*Secret `yaml:"pointers"`
|
|
Sequence []Secret `yaml:"sequence"`
|
|
}
|
|
|
|
type resolutionQueryReader struct {
|
|
input *strings.Reader
|
|
reads int
|
|
}
|
|
|
|
func (reader *resolutionQueryReader) Read(data []byte) (int, error) {
|
|
reader.reads++
|
|
return reader.input.Read(data)
|
|
}
|
|
|
|
func TestSecretReferencesResolveTypedShapesOncePerName(t *testing.T) {
|
|
input := `required: SHARED_SECRET
|
|
optional: OPTIONAL_SECRET
|
|
map:
|
|
first: SHARED_SECRET
|
|
pointers:
|
|
nil: null
|
|
value: POINTER_SECRET
|
|
sequence: [SHARED_SECRET, SEQUENCE_SECRET]
|
|
`
|
|
values := map[string]string{
|
|
"SHARED_SECRET": "shared-value",
|
|
"OPTIONAL_SECRET": "optional-value",
|
|
"POINTER_SECRET": "pointer-value",
|
|
"SEQUENCE_SECRET": "sequence-value",
|
|
}
|
|
counts := map[string]int{}
|
|
lookup := func(name string) (string, bool) {
|
|
counts[name]++
|
|
value, present := values[name]
|
|
return value, present
|
|
}
|
|
validationCalled := false
|
|
result, err := loadWithSecretLookup(Bytes("secrets.yml", []byte(input)), lookup, Options[secretReferenceConfig]{
|
|
Definition: Definition[secretReferenceConfig]{Validate: func(configuration *secretReferenceConfig, validation *Validation) {
|
|
validationCalled = true
|
|
if configuration.Required.Value() != "shared-value" || !configuration.Required.IsResolved() ||
|
|
configuration.Optional.Value() != "optional-value" || configuration.Pointers["value"].Value() != "pointer-value" ||
|
|
configuration.Sequence[1].Value() != "sequence-value" {
|
|
validation.Error(Keys("required"), "references were not resolved before validation")
|
|
}
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("loadWithSecretLookup() error = %v", err)
|
|
}
|
|
if !validationCalled || result.Config.Optional == nil || result.Config.Pointers["nil"] != nil {
|
|
t.Fatalf("resolved config = %#v", result.Config)
|
|
}
|
|
if result.Config.Required.EnvName() != "SHARED_SECRET" || result.Config.Map["first"].Value() != "shared-value" {
|
|
t.Fatalf("reference names/values = %#v", result.Config)
|
|
}
|
|
if !reflect.DeepEqual(counts, map[string]int{"SHARED_SECRET": 1, "OPTIONAL_SECRET": 1, "POINTER_SECRET": 1, "SEQUENCE_SECRET": 1}) {
|
|
t.Fatalf("lookup counts = %#v", counts)
|
|
}
|
|
}
|
|
|
|
func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
input string
|
|
lookup secretLookup
|
|
code Code
|
|
}{
|
|
{name: "absent", input: "{}\n", code: CodeSecretRequired},
|
|
{name: "null", input: "required: null\n", code: CodeSecretRequired},
|
|
{name: "empty name", input: "required: ''\n", code: CodeSecretInvalidName},
|
|
{name: "invalid name", input: "required: INVALID-NAME\n", code: CodeSecretInvalidName},
|
|
{name: "unset", input: "required: MISSING_SECRET\n", code: CodeSecretUnset},
|
|
{name: "empty environment", input: "required: EMPTY_SECRET\n", lookup: func(string) (string, bool) { return "", true }, code: CodeSecretEmpty},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
lookup := test.lookup
|
|
if lookup == nil {
|
|
lookup = func(string) (string, bool) { return "", false }
|
|
}
|
|
_, err := loadWithSecretLookup(Bytes("secret-errors.yml", []byte(test.input)), lookup, Options[secretReferenceConfig]{})
|
|
loadError := requireSecretLoadCode(t, err, test.code)
|
|
diagnostic := loadError.Diagnostics()[0]
|
|
if diagnostic.Path.String() != "/required" || strings.Contains(fmt.Sprintf("%v %#v", err, err), "CREDENTIAL-CANARY") {
|
|
t.Fatalf("diagnostic/error = %#v / %v", diagnostic, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("non-nil optional is required to resolve", func(t *testing.T) {
|
|
type config struct {
|
|
Optional *Secret `yaml:"optional"`
|
|
}
|
|
_, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: ''\n")), func(string) (string, bool) { return "", false }, Options[config]{})
|
|
requireSecretLoadCode(t, err, CodeSecretInvalidName)
|
|
result, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: null\n")), func(string) (string, bool) { return "", false }, Options[config]{})
|
|
if err != nil || result.Config.Optional != nil {
|
|
t.Fatalf("nil optional result = %#v, %v", result, err)
|
|
}
|
|
})
|
|
|
|
t.Run("failed lookup is cached", func(t *testing.T) {
|
|
type config struct {
|
|
Secrets []Secret `yaml:"secrets"`
|
|
}
|
|
lookups := 0
|
|
_, err := loadWithSecretLookup(Bytes("missing-twice.yml", []byte("secrets: [MISSING_SHARED, MISSING_SHARED]\n")), func(string) (string, bool) {
|
|
lookups++
|
|
return "", false
|
|
}, Options[config]{})
|
|
loadError := requireSecretLoadCode(t, err, CodeSecretUnset)
|
|
if lookups != 1 || len(loadError.Diagnostics()) != 2 {
|
|
t.Fatalf("lookups = %d, diagnostics = %#v", lookups, loadError.Diagnostics())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResolutionSelectsEffectiveSecretNameBeforeLookup(t *testing.T) {
|
|
type config struct {
|
|
Name string `yaml:"name"`
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
lookedUp := []string{}
|
|
validated := ""
|
|
result, err := loadWithSecretLookup(Bytes("resolved-secret.yml", []byte("name: SELECTED_SECRET\nsecret: OLD_SECRET\n")), func(name string) (string, bool) {
|
|
lookedUp = append(lookedUp, name)
|
|
return "resolved-value", true
|
|
}, Options[config]{Definition: Definition[config]{
|
|
Resolve: func(configuration *config, resolution *Resolution) error {
|
|
Normalize(resolution, Keys("secret"), &configuration.Secret, SecretEnv(configuration.Name), "selected named credential", Keys("name"))
|
|
return nil
|
|
},
|
|
Validate: func(configuration *config, validation *Validation) {
|
|
validated = configuration.Secret.Value()
|
|
},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("loadWithSecretLookup() error = %v", err)
|
|
}
|
|
if !reflect.DeepEqual(lookedUp, []string{"SELECTED_SECRET"}) || validated != "resolved-value" || result.Config.Secret.EnvName() != "SELECTED_SECRET" {
|
|
t.Fatalf("lookups = %#v, validated = %q, config = %#v", lookedUp, validated, result.Config)
|
|
}
|
|
history := result.Metadata().History(Keys("secret"))
|
|
if len(history) != 3 || history[len(history)-1].Origin.Operation != ChangeNormalization {
|
|
t.Fatalf("secret history = %#v", history)
|
|
}
|
|
}
|
|
|
|
func TestResolutionInputQueriesDoNotReadOrResolveSecrets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
reader := &resolutionQueryReader{input: strings.NewReader("value: source\nsecret: QUERY_SECRET\n")}
|
|
lookups := 0
|
|
var callbackErr error
|
|
result, err := loadWithSecretLookup(Reader("query.yml", reader), func(name string) (string, bool) {
|
|
lookups++
|
|
if name != "QUERY_SECRET" {
|
|
t.Fatalf("secret lookup name = %q", name)
|
|
}
|
|
return "query-credential", true
|
|
}, Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
|
reads := reader.reads
|
|
value, valueFound := resolution.InputInfo(Keys("value"))
|
|
secret, secretFound := resolution.InputInfo(Keys("secret"))
|
|
if !valueFound || !secretFound || len(value.Origin.Locations) != 1 || len(secret.Origin.Locations) != 1 {
|
|
callbackErr = errors.New("entry metadata is incomplete")
|
|
return callbackErr
|
|
}
|
|
if source, found := resolution.Source(value.Origin.Locations[0].SourceID); !found || source.Path != "" {
|
|
callbackErr = errors.New("entry source is incomplete")
|
|
return callbackErr
|
|
}
|
|
if reader.reads != reads || lookups != 0 {
|
|
callbackErr = errors.New("entry query performed input or credential access")
|
|
}
|
|
return callbackErr
|
|
}}})
|
|
if callbackErr != nil {
|
|
t.Fatal(callbackErr)
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if lookups != 1 || result.Config.Secret.Value() != "query-credential" {
|
|
t.Fatalf("secret resolution = lookups %d config %#v", lookups, result.Config)
|
|
}
|
|
}
|
|
|
|
func TestValidationCannotReplaceResolvedSecretWithSameName(t *testing.T) {
|
|
type config struct {
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
_, err := loadWithSecretLookup(Bytes("validator.yml", []byte("secret: VALIDATOR_SECRET\n")), func(string) (string, bool) {
|
|
return "CREDENTIAL-CANARY", true
|
|
}, Options[config]{Definition: Definition[config]{Validate: func(configuration *config, validation *Validation) {
|
|
configuration.Secret = SecretEnv(configuration.Secret.EnvName())
|
|
}}})
|
|
loadError := requireSecretLoadCode(t, err, CodeValidateMutation)
|
|
if loadError.Diagnostics()[0].Path.String() != "/secret" || strings.Contains(fmt.Sprintf("%v %#v", err, err), "CREDENTIAL-CANARY") {
|
|
t.Fatalf("validator mutation error = %v %#v", err, err)
|
|
}
|
|
}
|
|
|
|
func TestResolvedDefaultsAreRebuiltAndLaterLoadsRelookup(t *testing.T) {
|
|
type config struct {
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
first, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "first-value", true }, Options[config]{
|
|
Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("DEFAULT_SECRET")} }},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "second-value", true }, Options[config]{
|
|
Definition: Definition[config]{Defaults: func() config { return first.Config }},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Config.Secret.Value() != "first-value" || second.Config.Secret.Value() != "second-value" {
|
|
t.Fatalf("resolved defaults retained stale state: %q, %q", first.Config.Secret.Value(), second.Config.Secret.Value())
|
|
}
|
|
|
|
t.Setenv("CONFIGKIT_RELOOKUP_SECRET", "one")
|
|
options := Options[config]{Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("CONFIGKIT_RELOOKUP_SECRET")} }}}
|
|
realFirst, err := Load(NoSource(), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Setenv("CONFIGKIT_RELOOKUP_SECRET", "two"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
realSecond, err := Load(NoSource(), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if realFirst.Config.Secret.Value() != "one" || realSecond.Config.Secret.Value() != "two" {
|
|
t.Fatalf("later loads = %q, %q", realFirst.Config.Secret.Value(), realSecond.Config.Secret.Value())
|
|
}
|
|
}
|
|
|
|
func TestSecretSafeFormattingJSONProjectionAndHistory(t *testing.T) {
|
|
const credential = "CREDENTIAL-CANARY"
|
|
secret := Secret{name: "SAFE_SECRET_NAME", value: credential, specified: true, resolved: true}
|
|
for _, format := range []string{"%v", "%+v", "%#v", "%s", "%q", "%x", "%d"} {
|
|
formatted := fmt.Sprintf(format, secret)
|
|
if strings.Contains(formatted, credential) || !strings.Contains(formatted, "SAFE_SECRET_NAME") {
|
|
t.Fatalf("format %q produced %q", format, formatted)
|
|
}
|
|
}
|
|
encoded, err := json.Marshal(secret)
|
|
if err != nil || string(encoded) != `"SAFE_SECRET_NAME"` || strings.Contains(string(encoded), credential) {
|
|
t.Fatalf("MarshalJSON() = %s, %v", encoded, err)
|
|
}
|
|
|
|
type config struct {
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
result, err := loadWithSecretLookup(Bytes("safe-secret.yml", []byte("secret: SAFE_SECRET_NAME\n")), func(string) (string, bool) {
|
|
return credential, true
|
|
}, Options[config]{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encodedConfig, err := json.Marshal(result.Config)
|
|
if err != nil || strings.Contains(string(encodedConfig), credential) || !strings.Contains(string(encodedConfig), "SAFE_SECRET_NAME") {
|
|
t.Fatalf("configuration JSON = %s, %v", encodedConfig, err)
|
|
}
|
|
var effective strings.Builder
|
|
if err := result.WriteEffective(&effective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outputs := []string{effective.String(), fmt.Sprintf("%#v", result.Metadata()), fmt.Sprintf("%#v", result.Metadata().History(Keys("secret"))), fmt.Sprintf("%#v", result.Config)}
|
|
for _, output := range outputs {
|
|
if strings.Contains(output, credential) {
|
|
t.Fatalf("inspection leaked credential: %q", output)
|
|
}
|
|
}
|
|
if !strings.Contains(effective.String(), "SAFE_SECRET_NAME") {
|
|
t.Fatalf("effective reference name missing: %q", effective.String())
|
|
}
|
|
}
|
|
|
|
func TestBootstrapExistingEmptyWinsAndReferenceFails(t *testing.T) {
|
|
const name = "CONFIGKIT_BLOCKED_EMPTY_SECRET"
|
|
t.Setenv(name, "")
|
|
directory := t.TempDir()
|
|
writeSecretTestFile(t, directory, name, "directory-value")
|
|
report, err := LoadSecretsDir(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries := report.Entries()
|
|
if len(entries) != 1 || entries[0].Status != SecretFileSkippedExisting || !entries[0].ExistingEmpty {
|
|
t.Fatalf("bootstrap report = %#v", entries)
|
|
}
|
|
type config struct {
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
_, err = Load[config](Bytes("blocked.yml", []byte("secret: "+name+"\n")))
|
|
requireSecretLoadCode(t, err, CodeSecretEmpty)
|
|
if value, present := os.LookupEnv(name); !present || value != "" {
|
|
t.Fatalf("environment changed to %q, %v", value, present)
|
|
}
|
|
}
|
|
|
|
func TestDynamicValuesDoNotEstablishSecretReferences(t *testing.T) {
|
|
type dynamic struct {
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
type config struct {
|
|
Dynamic any `yaml:"dynamic"`
|
|
}
|
|
schema := mustSchemaFor[config](t)
|
|
for _, value := range []any{SecretEnv("DYNAMIC_SECRET"), dynamic{Secret: SecretEnv("DYNAMIC_SECRET")}} {
|
|
_, issue := projectConfiguration(schema, reflect.ValueOf(config{Dynamic: value}), defaultProjectionLimits())
|
|
if issue == nil || issue.code != CodeSchemaDynamicType {
|
|
t.Fatalf("dynamic Secret projection issue = %#v", issue)
|
|
}
|
|
}
|
|
lookups := 0
|
|
result, err := loadWithSecretLookup(Bytes("dynamic.yml", []byte("dynamic: ORDINARY_STRING\n")), func(string) (string, bool) {
|
|
lookups++
|
|
return "unexpected", true
|
|
}, Options[config]{})
|
|
if err != nil || result.Config.Dynamic != "ORDINARY_STRING" || lookups != 0 {
|
|
t.Fatalf("dynamic string result = %#v, lookups = %d, error = %v", result, lookups, err)
|
|
}
|
|
}
|
|
|
|
func TestLoadDoesNotMutateSecretEnvironment(t *testing.T) {
|
|
const name = "CONFIGKIT_UNCHANGED_SECRET"
|
|
t.Setenv(name, "original")
|
|
type config struct {
|
|
Secret Secret `yaml:"secret"`
|
|
}
|
|
result, err := Load[config](Bytes("unchanged.yml", []byte("secret: "+name+"\n")))
|
|
if err != nil || result.Config.Secret.Value() != "original" {
|
|
t.Fatalf("Load() = %#v, %v", result, err)
|
|
}
|
|
if got := os.Getenv(name); got != "original" {
|
|
t.Fatalf("environment = %q", got)
|
|
}
|
|
}
|
|
|
|
func requireSecretLoadCode(t *testing.T, err error, code Code) *LoadError {
|
|
t.Helper()
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("error = %T %v, want *LoadError", err, err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
if len(diagnostics) == 0 || diagnostics[0].Code != code {
|
|
t.Fatalf("diagnostics = %#v, want first code %q", diagnostics, code)
|
|
}
|
|
return loadError
|
|
}
|