359 lines
10 KiB
Go
359 lines
10 KiB
Go
package credentials
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMemoryStorePutIsIdempotentForUnchangedScope(t *testing.T) {
|
|
store, err := NewMemoryStore(2)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
value := Value{Username: "alice", Password: "secret-password"}
|
|
|
|
first, err := store.Put(context.Background(), "provider-a", value)
|
|
if err != nil {
|
|
t.Fatalf("Put(first): %v", err)
|
|
}
|
|
second, err := store.Put(context.Background(), "provider-a", value)
|
|
if err != nil {
|
|
t.Fatalf("Put(second): %v", err)
|
|
}
|
|
if first != second {
|
|
t.Fatalf("second reference = %#v, want %#v", second, first)
|
|
}
|
|
if first.SecretRef == "" || first.CredentialVersion != "v1" {
|
|
t.Fatalf("first reference = %#v, want opaque ref at v1", first)
|
|
}
|
|
for _, plaintext := range []string{"provider-a", value.Username, value.Password} {
|
|
if strings.Contains(first.SecretRef, plaintext) {
|
|
t.Fatalf("SecretRef %q contains plaintext %q", first.SecretRef, plaintext)
|
|
}
|
|
}
|
|
|
|
got, err := store.Resolve(context.Background(), first)
|
|
if err != nil {
|
|
t.Fatalf("Resolve(): %v", err)
|
|
}
|
|
if got != value {
|
|
t.Fatalf("Resolve() = %#v, want %#v", got, value)
|
|
}
|
|
}
|
|
|
|
func TestCredentialFormattingRedactsSensitiveMaterial(t *testing.T) {
|
|
value := Value{Username: "alice", Password: "secret-password"}
|
|
for _, formatted := range []string{
|
|
fmt.Sprintf("%v", value),
|
|
fmt.Sprintf("%+v", value),
|
|
fmt.Sprintf("%#v", value),
|
|
fmt.Sprintf("%s", value),
|
|
fmt.Sprintf("%q", value),
|
|
} {
|
|
if strings.Contains(formatted, value.Password) {
|
|
t.Fatalf("formatted Value exposes password: %s", formatted)
|
|
}
|
|
}
|
|
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
reference, err := store.Put(context.Background(), "provider-secret-scope", value)
|
|
if err != nil {
|
|
t.Fatalf("Put(): %v", err)
|
|
}
|
|
for _, formatted := range []string{
|
|
fmt.Sprintf("%v", store),
|
|
fmt.Sprintf("%+v", store),
|
|
fmt.Sprintf("%#v", store),
|
|
fmt.Sprintf("%s", store),
|
|
fmt.Sprintf("%q", store),
|
|
} {
|
|
for _, secret := range []string{
|
|
"provider-secret-scope",
|
|
value.Username,
|
|
value.Password,
|
|
reference.SecretRef,
|
|
} {
|
|
if strings.Contains(formatted, secret) {
|
|
t.Fatalf("formatted MemoryStore exposes sensitive material: %s", formatted)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMemoryStorePutIncrementsVersionAndRejectsStaleReference(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
oldReference, err := store.Put(context.Background(), "provider-a", Value{
|
|
Username: "alice",
|
|
Password: "old-password",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Put(old): %v", err)
|
|
}
|
|
want := Value{Username: "alice", Password: "new-password"}
|
|
newReference, err := store.Put(context.Background(), "provider-a", want)
|
|
if err != nil {
|
|
t.Fatalf("Put(new): %v", err)
|
|
}
|
|
if newReference.SecretRef != oldReference.SecretRef {
|
|
t.Fatalf("new SecretRef changed across versions")
|
|
}
|
|
if newReference.CredentialVersion != "v2" {
|
|
t.Fatalf("new CredentialVersion = %q, want v2", newReference.CredentialVersion)
|
|
}
|
|
if _, err := store.Resolve(context.Background(), oldReference); !errors.Is(err, ErrCredentialVersionMismatch) {
|
|
t.Fatalf("Resolve(stale) error = %v, want ErrCredentialVersionMismatch", err)
|
|
}
|
|
got, err := store.Resolve(context.Background(), newReference)
|
|
if err != nil {
|
|
t.Fatalf("Resolve(new): %v", err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("Resolve(new) returned unexpected credentials")
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreEnforcesCapacityWithoutChangingExistingCredentials(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
want := Value{Username: "alice", Password: "first-password"}
|
|
reference, err := store.Put(context.Background(), "provider-a", want)
|
|
if err != nil {
|
|
t.Fatalf("Put(first scope): %v", err)
|
|
}
|
|
if _, err := store.Put(context.Background(), "provider-b", Value{
|
|
Username: "bob",
|
|
Password: "second-password",
|
|
}); !errors.Is(err, ErrCapacityExceeded) {
|
|
t.Fatalf("Put(over capacity) error = %v, want ErrCapacityExceeded", err)
|
|
}
|
|
|
|
got, err := store.Resolve(context.Background(), reference)
|
|
if err != nil {
|
|
t.Fatalf("Resolve(existing): %v", err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("Resolve(existing) returned changed credentials")
|
|
}
|
|
if _, err := store.Put(context.Background(), "provider-a", want); err != nil {
|
|
t.Fatalf("Put(idempotent at capacity): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreStrictlyValidatesCredentialVersion(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
reference, err := store.Put(context.Background(), "provider-a", Value{Password: "password"})
|
|
if err != nil {
|
|
t.Fatalf("Put(): %v", err)
|
|
}
|
|
for _, version := range []string{"", "v0", "v01", "1", "latest", "v-1"} {
|
|
invalid := reference
|
|
invalid.CredentialVersion = version
|
|
if _, err := store.Resolve(context.Background(), invalid); !errors.Is(err, ErrInvalidReference) {
|
|
t.Fatalf("Resolve(version %q) error = %v, want ErrInvalidReference", version, err)
|
|
}
|
|
}
|
|
if _, err := store.Put(context.Background(), " \t\n", Value{}); !errors.Is(err, ErrInvalidScope) {
|
|
t.Fatalf("Put(blank scope) error = %v, want ErrInvalidScope", err)
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreValidatesCapacityAndHonorsCancellation(t *testing.T) {
|
|
if _, err := NewMemoryStore(0); !errors.Is(err, ErrInvalidCapacity) {
|
|
t.Fatalf("NewMemoryStore(0) error = %v, want ErrInvalidCapacity", err)
|
|
}
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(1): %v", err)
|
|
}
|
|
canceled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := store.Put(canceled, "provider-a", Value{}); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Put(canceled) error = %v, want context.Canceled", err)
|
|
}
|
|
if _, err := store.Resolve(canceled, Reference{
|
|
SecretRef: "cred_000000000000000000000000000000000000000000000000",
|
|
CredentialVersion: "v1",
|
|
}); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Resolve(canceled) error = %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreRejectsNilAndZeroValueStores(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
for _, store := range []*MemoryStore{nil, {}} {
|
|
if _, err := store.Put(ctx, "provider-a", Value{}); !errors.Is(err, ErrInvalidStore) {
|
|
t.Fatalf("Put() error = %v, want ErrInvalidStore", err)
|
|
}
|
|
if _, err := store.Resolve(ctx, Reference{
|
|
SecretRef: "cred_000000000000000000000000000000000000000000000000",
|
|
CredentialVersion: "v1",
|
|
}); !errors.Is(err, ErrInvalidStore) {
|
|
t.Fatalf("Resolve() error = %v, want ErrInvalidStore", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreIsConcurrencySafeAndIdempotent(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
want := Value{Username: "alice", Password: "shared-password"}
|
|
const workers = 100
|
|
references := make(chan Reference, workers)
|
|
errorsSeen := make(chan error, workers)
|
|
var wait sync.WaitGroup
|
|
for range workers {
|
|
wait.Add(1)
|
|
go func() {
|
|
defer wait.Done()
|
|
reference, err := store.Put(context.Background(), "provider-a", want)
|
|
if err != nil {
|
|
errorsSeen <- err
|
|
return
|
|
}
|
|
references <- reference
|
|
}()
|
|
}
|
|
wait.Wait()
|
|
close(references)
|
|
close(errorsSeen)
|
|
for err := range errorsSeen {
|
|
t.Errorf("concurrent Put(): %v", err)
|
|
}
|
|
var first Reference
|
|
for reference := range references {
|
|
if first == (Reference{}) {
|
|
first = reference
|
|
}
|
|
if reference != first {
|
|
t.Errorf("concurrent Put() reference differs from first")
|
|
}
|
|
}
|
|
if first.CredentialVersion != "v1" {
|
|
t.Fatalf("concurrent version = %q, want v1", first.CredentialVersion)
|
|
}
|
|
got, err := store.Resolve(context.Background(), first)
|
|
if err != nil {
|
|
t.Fatalf("Resolve(): %v", err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("Resolve() returned unexpected credentials")
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreSerializesConcurrentCredentialChanges(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
type result struct {
|
|
value Value
|
|
reference Reference
|
|
err error
|
|
}
|
|
const workers = 100
|
|
results := make(chan result, workers)
|
|
var wait sync.WaitGroup
|
|
for index := range workers {
|
|
wait.Add(1)
|
|
go func() {
|
|
defer wait.Done()
|
|
value := Value{Username: "alice", Password: "password-" + strconv.Itoa(index)}
|
|
reference, err := store.Put(context.Background(), "provider-a", value)
|
|
results <- result{value: value, reference: reference, err: err}
|
|
}()
|
|
}
|
|
wait.Wait()
|
|
close(results)
|
|
|
|
versions := make(map[string]struct{}, workers)
|
|
var secretRef string
|
|
var latest result
|
|
for current := range results {
|
|
if current.err != nil {
|
|
t.Fatalf("concurrent Put(): %v", current.err)
|
|
}
|
|
if secretRef == "" {
|
|
secretRef = current.reference.SecretRef
|
|
}
|
|
if current.reference.SecretRef != secretRef {
|
|
t.Fatal("SecretRef changed across concurrent updates")
|
|
}
|
|
versions[current.reference.CredentialVersion] = struct{}{}
|
|
if current.reference.CredentialVersion == "v100" {
|
|
latest = current
|
|
}
|
|
}
|
|
if len(versions) != workers {
|
|
t.Fatalf("unique versions = %d, want %d", len(versions), workers)
|
|
}
|
|
if latest.reference == (Reference{}) {
|
|
t.Fatal("highest version v100 was not returned")
|
|
}
|
|
got, err := store.Resolve(context.Background(), latest.reference)
|
|
if err != nil {
|
|
t.Fatalf("Resolve(latest): %v", err)
|
|
}
|
|
if got != latest.value {
|
|
t.Fatal("Resolve(latest) returned a different concurrent write")
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreErrorsDoNotExposeInputs(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
value := Value{Username: "sensitive-user", Password: "sensitive-password"}
|
|
reference, err := store.Put(context.Background(), "sensitive-scope", value)
|
|
if err != nil {
|
|
t.Fatalf("Put(): %v", err)
|
|
}
|
|
_, capacityErr := store.Put(context.Background(), "other-sensitive-scope", value)
|
|
unknown := Reference{
|
|
SecretRef: "sensitive-secret-ref",
|
|
CredentialVersion: "v1",
|
|
}
|
|
_, missingErr := store.Resolve(context.Background(), unknown)
|
|
stale := reference
|
|
stale.CredentialVersion = "v2"
|
|
_, versionErr := store.Resolve(context.Background(), stale)
|
|
|
|
for _, operationErr := range []error{capacityErr, missingErr, versionErr} {
|
|
if operationErr == nil {
|
|
t.Fatal("operation unexpectedly succeeded")
|
|
}
|
|
for _, sensitive := range []string{
|
|
"sensitive-scope",
|
|
"other-sensitive-scope",
|
|
value.Username,
|
|
value.Password,
|
|
unknown.SecretRef,
|
|
reference.SecretRef,
|
|
} {
|
|
if strings.Contains(operationErr.Error(), sensitive) {
|
|
t.Fatalf("error exposes sensitive input: %v", operationErr)
|
|
}
|
|
}
|
|
}
|
|
}
|