proxy-pool/internal/platform/credentials/store.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()
}