293 lines
6.9 KiB
Go
293 lines
6.9 KiB
Go
package credentials
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidCapacity = errors.New("invalid credential store capacity")
|
|
ErrInvalidStore = errors.New("invalid credential store")
|
|
ErrInvalidScope = errors.New("invalid credential scope")
|
|
ErrInvalidReference = errors.New("invalid credential reference")
|
|
ErrCapacityExceeded = errors.New("credential store capacity exceeded")
|
|
ErrCredentialMissing = errors.New("credential not found")
|
|
ErrCredentialVersionMismatch = errors.New("credential version mismatch")
|
|
ErrReferenceCreation = errors.New("credential reference creation failed")
|
|
)
|
|
|
|
// Value is resolved credential material. Callers must not log this value.
|
|
type Value struct {
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
func (Value) Format(state fmt.State, _ rune) {
|
|
_, _ = state.Write([]byte("credentials.Value{Username:<redacted>, Password:<redacted>}"))
|
|
}
|
|
|
|
// Reference identifies one exact version of stored credential material.
|
|
type Reference struct {
|
|
SecretRef string
|
|
CredentialVersion string
|
|
}
|
|
|
|
func (reference Reference) Format(state fmt.State, _ rune) {
|
|
formatted := "credentials.Reference{SecretRef:<redacted>, CredentialVersion:" +
|
|
strconv.Quote(reference.CredentialVersion) + "}"
|
|
_, _ = state.Write([]byte(formatted))
|
|
}
|
|
|
|
type Store interface {
|
|
Put(context.Context, string, Value) (Reference, error)
|
|
Resolve(context.Context, Reference) (Value, error)
|
|
}
|
|
|
|
// Releaser removes transient credential material after the consumer has copied
|
|
// it into its authoritative storage. Release is idempotent and version fenced.
|
|
type Releaser interface {
|
|
Release(context.Context, Reference) error
|
|
}
|
|
|
|
type CapacityEnsurer interface {
|
|
EnsureCapacity(context.Context, int) error
|
|
}
|
|
|
|
type entry struct {
|
|
scope *scopeState
|
|
value Value
|
|
reference Reference
|
|
}
|
|
|
|
type scopeState struct {
|
|
name string
|
|
value Value
|
|
version uint64
|
|
leases int
|
|
}
|
|
|
|
// MemoryStore keeps credentials in process memory and serializes access with a
|
|
// context-aware lock.
|
|
type MemoryStore struct {
|
|
lock chan struct{}
|
|
capacity int
|
|
byScope map[string]*scopeState
|
|
byRef map[string]*entry
|
|
}
|
|
|
|
func (s *MemoryStore) Format(state fmt.State, _ rune) {
|
|
if s == nil {
|
|
_, _ = state.Write([]byte("credentials.MemoryStore<nil>"))
|
|
return
|
|
}
|
|
_, _ = state.Write([]byte("credentials.MemoryStore{capacity:" + strconv.Itoa(s.capacity) + "}"))
|
|
}
|
|
|
|
var _ Store = (*MemoryStore)(nil)
|
|
|
|
func NewMemoryStore(capacity int) (*MemoryStore, error) {
|
|
if capacity <= 0 {
|
|
return nil, ErrInvalidCapacity
|
|
}
|
|
lock := make(chan struct{}, 1)
|
|
lock <- struct{}{}
|
|
return &MemoryStore{
|
|
lock: lock,
|
|
capacity: capacity,
|
|
byScope: make(map[string]*scopeState),
|
|
byRef: make(map[string]*entry),
|
|
}, nil
|
|
}
|
|
|
|
func (s *MemoryStore) Put(ctx context.Context, scope string, value Value) (Reference, error) {
|
|
if err := contextError(ctx); err != nil {
|
|
return Reference{}, err
|
|
}
|
|
if !s.valid() {
|
|
return Reference{}, ErrInvalidStore
|
|
}
|
|
if strings.TrimSpace(scope) == "" {
|
|
return Reference{}, ErrInvalidScope
|
|
}
|
|
if err := s.acquire(ctx); err != nil {
|
|
return Reference{}, err
|
|
}
|
|
defer s.unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return Reference{}, err
|
|
}
|
|
if len(s.byRef) >= s.capacity {
|
|
return Reference{}, ErrCapacityExceeded
|
|
}
|
|
|
|
current, exists := s.byScope[scope]
|
|
if !exists && len(s.byScope) >= s.capacity {
|
|
return Reference{}, ErrCapacityExceeded
|
|
}
|
|
secretRef, err := s.newUniqueSecretRef()
|
|
if err != nil {
|
|
return Reference{}, err
|
|
}
|
|
if !exists {
|
|
current = &scopeState{name: scope, value: value, version: 1}
|
|
s.byScope[scope] = current
|
|
} else if current.value != value {
|
|
current.value = value
|
|
current.version++
|
|
}
|
|
created := &entry{
|
|
scope: current,
|
|
value: value,
|
|
reference: Reference{
|
|
SecretRef: secretRef,
|
|
CredentialVersion: versionString(current.version),
|
|
},
|
|
}
|
|
current.leases++
|
|
s.byRef[secretRef] = created
|
|
return created.reference, nil
|
|
}
|
|
|
|
func (s *MemoryStore) Resolve(ctx context.Context, reference Reference) (Value, error) {
|
|
if err := contextError(ctx); err != nil {
|
|
return Value{}, err
|
|
}
|
|
if !s.valid() {
|
|
return Value{}, ErrInvalidStore
|
|
}
|
|
if reference.SecretRef == "" || !validVersion(reference.CredentialVersion) {
|
|
return Value{}, ErrInvalidReference
|
|
}
|
|
if err := s.acquire(ctx); err != nil {
|
|
return Value{}, err
|
|
}
|
|
defer s.unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return Value{}, err
|
|
}
|
|
|
|
current, ok := s.byRef[reference.SecretRef]
|
|
if !ok {
|
|
return Value{}, ErrCredentialMissing
|
|
}
|
|
if current.reference.CredentialVersion != reference.CredentialVersion {
|
|
return Value{}, ErrCredentialVersionMismatch
|
|
}
|
|
return current.value, nil
|
|
}
|
|
|
|
func (s *MemoryStore) Release(ctx context.Context, reference Reference) error {
|
|
if err := contextError(ctx); err != nil {
|
|
return err
|
|
}
|
|
if !s.valid() {
|
|
return ErrInvalidStore
|
|
}
|
|
if reference.SecretRef == "" || !validVersion(reference.CredentialVersion) {
|
|
return ErrInvalidReference
|
|
}
|
|
if err := s.acquire(ctx); err != nil {
|
|
return err
|
|
}
|
|
defer s.unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
current, ok := s.byRef[reference.SecretRef]
|
|
if !ok || current.reference != reference {
|
|
return nil
|
|
}
|
|
delete(s.byRef, reference.SecretRef)
|
|
current.scope.leases--
|
|
if current.scope.leases == 0 {
|
|
delete(s.byScope, current.scope.name)
|
|
current.scope.value = Value{}
|
|
}
|
|
current.value = Value{}
|
|
return nil
|
|
}
|
|
|
|
func (s *MemoryStore) EnsureCapacity(ctx context.Context, minimum int) error {
|
|
if err := contextError(ctx); err != nil {
|
|
return err
|
|
}
|
|
if !s.valid() {
|
|
return ErrInvalidStore
|
|
}
|
|
if minimum <= 0 {
|
|
return ErrInvalidCapacity
|
|
}
|
|
if err := s.acquire(ctx); err != nil {
|
|
return err
|
|
}
|
|
defer s.unlock()
|
|
if minimum > s.capacity {
|
|
s.capacity = minimum
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *MemoryStore) acquire(ctx context.Context) error {
|
|
if err := contextError(ctx); err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-s.lock:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *MemoryStore) unlock() {
|
|
s.lock <- struct{}{}
|
|
}
|
|
|
|
func (s *MemoryStore) valid() bool {
|
|
return s != nil && s.lock != nil && s.capacity > 0 && s.byScope != nil && s.byRef != nil
|
|
}
|
|
|
|
func (s *MemoryStore) newUniqueSecretRef() (string, error) {
|
|
for {
|
|
secretRef, err := newSecretRef()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, exists := s.byRef[secretRef]; !exists {
|
|
return secretRef, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func newSecretRef() (string, error) {
|
|
var random [24]byte
|
|
if _, err := rand.Read(random[:]); err != nil {
|
|
return "", ErrReferenceCreation
|
|
}
|
|
return "cred_" + hex.EncodeToString(random[:]), nil
|
|
}
|
|
|
|
func versionString(version uint64) string {
|
|
return "v" + strconv.FormatUint(version, 10)
|
|
}
|
|
|
|
func validVersion(version string) bool {
|
|
if len(version) < 2 || version[0] != 'v' || version[1] == '0' {
|
|
return false
|
|
}
|
|
_, err := strconv.ParseUint(version[1:], 10, 64)
|
|
return err == nil
|
|
}
|
|
|
|
func contextError(ctx context.Context) error {
|
|
if ctx == nil {
|
|
return context.Canceled
|
|
}
|
|
return ctx.Err()
|
|
}
|