435 lines
14 KiB
Go
435 lines
14 KiB
Go
package credentials
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMemoryStorePutCreatesIndependentLeasesForUnchangedScope(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 || first.SecretRef == second.SecretRef {
|
|
t.Fatalf("references = %#v and %#v, want independent leases", first, second)
|
|
}
|
|
if first.SecretRef == "" || first.CredentialVersion != "v1" || second.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)
|
|
}
|
|
if err := store.Release(context.Background(), first); err != nil {
|
|
t.Fatalf("Release(first lease): %v", err)
|
|
}
|
|
if got, err := store.Resolve(context.Background(), second); err != nil || got != value {
|
|
t.Fatalf("Resolve(second lease) = %#v, %v", got, err)
|
|
}
|
|
}
|
|
|
|
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 TestMemoryStorePutIncrementsVersionWithoutRevokingActiveLease(t *testing.T) {
|
|
store, err := NewMemoryStore(2)
|
|
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 reused an active lease")
|
|
}
|
|
if newReference.CredentialVersion != "v2" {
|
|
t.Fatalf("new CredentialVersion = %q, want v2", newReference.CredentialVersion)
|
|
}
|
|
old, err := store.Resolve(context.Background(), oldReference)
|
|
if err != nil || old.Password != "old-password" {
|
|
t.Fatalf("Resolve(active old lease) = %#v, %v", old, 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); !errors.Is(err, ErrCapacityExceeded) {
|
|
t.Fatalf("Put(second lease at capacity) error = %v, want ErrCapacityExceeded", err)
|
|
}
|
|
if err := store.Release(context.Background(), reference); err != nil {
|
|
t.Fatalf("Release(first lease): %v", err)
|
|
}
|
|
if _, err := store.Put(context.Background(), "provider-a", want); err != nil {
|
|
t.Fatalf("Put(after lease release): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreReleaseMakesCapacityReusable(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
first, err := store.Put(context.Background(), "provider-a", Value{Password: "first-password"})
|
|
if err != nil {
|
|
t.Fatalf("Put(first): %v", err)
|
|
}
|
|
if err := store.Release(context.Background(), first); err != nil {
|
|
t.Fatalf("Release(first): %v", err)
|
|
}
|
|
if _, err := store.Resolve(context.Background(), first); !errors.Is(err, ErrCredentialMissing) {
|
|
t.Fatalf("Resolve(released) error = %v, want ErrCredentialMissing", err)
|
|
}
|
|
if _, err := store.Put(context.Background(), "provider-b", Value{Password: "second-password"}); err != nil {
|
|
t.Fatalf("Put(after release): %v", err)
|
|
}
|
|
if err := store.Release(context.Background(), first); err != nil {
|
|
t.Fatalf("Release(idempotent): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreReleaseOfStaleReferencePreservesCurrentVersion(t *testing.T) {
|
|
store, err := NewMemoryStore(2)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
stale, err := store.Put(context.Background(), "provider-a", Value{Password: "old-password"})
|
|
if err != nil {
|
|
t.Fatalf("Put(old): %v", err)
|
|
}
|
|
current, err := store.Put(context.Background(), "provider-a", Value{Password: "new-password"})
|
|
if err != nil {
|
|
t.Fatalf("Put(new): %v", err)
|
|
}
|
|
if err := store.Release(context.Background(), stale); err != nil {
|
|
t.Fatalf("Release(stale): %v", err)
|
|
}
|
|
if _, err := store.Resolve(context.Background(), current); err != nil {
|
|
t.Fatalf("Resolve(current): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreEnsureCapacityOnlyGrowsLimit(t *testing.T) {
|
|
store, err := NewMemoryStore(1)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
if err := store.EnsureCapacity(context.Background(), 2); err != nil {
|
|
t.Fatalf("EnsureCapacity(2): %v", err)
|
|
}
|
|
if _, err := store.Put(context.Background(), "provider-a", Value{}); err != nil {
|
|
t.Fatalf("Put(provider-a): %v", err)
|
|
}
|
|
if _, err := store.Put(context.Background(), "provider-b", Value{}); err != nil {
|
|
t.Fatalf("Put(provider-b): %v", err)
|
|
}
|
|
if err := store.EnsureCapacity(context.Background(), 1); err != nil {
|
|
t.Fatalf("EnsureCapacity(shrink request): %v", err)
|
|
}
|
|
if _, err := store.Put(context.Background(), "provider-c", Value{}); !errors.Is(err, ErrCapacityExceeded) {
|
|
t.Fatalf("Put(provider-c) error = %v, want retained capacity 2", 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 TestMemoryStoreCreatesIndependentConcurrentLeases(t *testing.T) {
|
|
const workers = 100
|
|
store, err := NewMemoryStore(workers)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
want := Value{Username: "alice", Password: "shared-password"}
|
|
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)
|
|
}
|
|
secretRefs := make(map[string]struct{}, workers)
|
|
for reference := range references {
|
|
if reference.CredentialVersion != "v1" {
|
|
t.Errorf("concurrent version = %q, want v1", reference.CredentialVersion)
|
|
}
|
|
secretRefs[reference.SecretRef] = struct{}{}
|
|
}
|
|
if len(secretRefs) != workers {
|
|
t.Fatalf("unique concurrent leases = %d, want %d", len(secretRefs), workers)
|
|
}
|
|
for secretRef := range secretRefs {
|
|
got, err := store.Resolve(context.Background(), Reference{
|
|
SecretRef: secretRef, CredentialVersion: "v1",
|
|
})
|
|
if err != nil || got != want {
|
|
t.Fatalf("Resolve(concurrent lease) = %#v, %v", got, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreSerializesConcurrentCredentialChanges(t *testing.T) {
|
|
const workers = 100
|
|
store, err := NewMemoryStore(workers)
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryStore(): %v", err)
|
|
}
|
|
type result struct {
|
|
value Value
|
|
reference Reference
|
|
err error
|
|
}
|
|
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)
|
|
secretRefs := make(map[string]struct{}, workers)
|
|
var latest result
|
|
for current := range results {
|
|
if current.err != nil {
|
|
t.Fatalf("concurrent Put(): %v", current.err)
|
|
}
|
|
secretRefs[current.reference.SecretRef] = struct{}{}
|
|
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 len(secretRefs) != workers {
|
|
t.Fatalf("unique leases = %d, want %d", len(secretRefs), 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)
|
|
}
|
|
}
|
|
}
|
|
}
|