~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
317 lines · 7.4 KB Raw History
package vaultmemory

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"sync"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/component"
	contractvault "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/marketidentity/atproto"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/vault/openbao"
)

var ErrNotFound = fmt.Errorf("memory vault: secret not found: %w", openbao.ErrNotFound)

var ErrInvalidRef = errors.New("memory vault: a KV call needs a mount and a path")

type kvKey struct {
	mount string
	path  string
}

type entry struct {
	data    map[string]any
	version int
}

type Store struct {
	mu       sync.RWMutex
	entries  map[kvKey]entry
	onChange func()
}

func (s *Store) SetOnChange(fn func()) {
	s.mu.Lock()
	s.onChange = fn
	s.mu.Unlock()
}

var _ contractvault.KV = (*Store)(nil)

func NewStore() *Store { return &Store{entries: map[kvKey]entry{}} }

func (s *Store) Read(ctx context.Context, mount, path string, version int) (map[string]any, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	k, err := kvKeyOf(mount, path)
	if err != nil {
		return nil, err
	}
	s.mu.RLock()
	e, ok := s.entries[k]
	s.mu.RUnlock()
	if !ok || (version > 0 && version != e.version) {
		return nil, fmt.Errorf("memory vault: %s/%s: %w", k.mount, k.path, ErrNotFound)
	}
	return cloneData(e.data), nil
}

func (s *Store) Write(ctx context.Context, mount, path string, data map[string]any) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	k, err := kvKeyOf(mount, path)
	if err != nil {
		return err
	}
	s.mu.Lock()
	if s.entries == nil {
		s.entries = map[kvKey]entry{}
	}
	s.entries[k] = entry{data: cloneData(data), version: s.entries[k].version + 1}
	fn := s.onChange
	s.mu.Unlock()
	if fn != nil {
		fn()
	}
	return nil
}

func (s *Store) Delete(ctx context.Context, mount, path string) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	k, err := kvKeyOf(mount, path)
	if err != nil {
		return err
	}
	s.mu.Lock()
	delete(s.entries, k)
	fn := s.onChange
	s.mu.Unlock()
	if fn != nil {
		fn()
	}
	return nil
}

func (s *Store) List(ctx context.Context, mount, path string) ([]string, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	mount = strings.Trim(strings.TrimSpace(mount), "/")
	path = strings.Trim(strings.TrimSpace(path), "/")
	if mount == "" {
		return nil, ErrInvalidRef
	}
	prefix := path
	if prefix != "" {
		prefix += "/"
	}
	seen := map[string]struct{}{}
	s.mu.RLock()
	for k := range s.entries {
		if k.mount != mount || !strings.HasPrefix(k.path, prefix) {
			continue
		}
		rest := strings.TrimPrefix(k.path, prefix)
		if rest == "" {
			continue
		}
		if i := strings.IndexByte(rest, '/'); i >= 0 {
			rest = rest[:i+1]
		}
		seen[rest] = struct{}{}
	}
	s.mu.RUnlock()
	out := make([]string, 0, len(seen))
	for name := range seen {
		out = append(out, name)
	}
	sort.Strings(out)
	return out, nil
}

type StoreEntry struct {
	Mount   string         `json:"mount"`
	Path    string         `json:"path"`
	Version int            `json:"version"`
	Data    map[string]any `json:"data"`
}

type StoreSnapshot struct {
	Entries []StoreEntry `json:"entries"`
}

func (s *Store) Snapshot() StoreSnapshot {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := StoreSnapshot{Entries: make([]StoreEntry, 0, len(s.entries))}
	for k, e := range s.entries {
		out.Entries = append(out.Entries, StoreEntry{
			Mount: k.mount, Path: k.path, Version: e.version, Data: cloneData(e.data),
		})
	}
	sort.Slice(out.Entries, func(i, j int) bool {
		if out.Entries[i].Mount != out.Entries[j].Mount {
			return out.Entries[i].Mount < out.Entries[j].Mount
		}
		return out.Entries[i].Path < out.Entries[j].Path
	})
	return out
}

func (s *Store) Restore(snapshot StoreSnapshot) {
	entries := make(map[kvKey]entry, len(snapshot.Entries))
	for _, e := range snapshot.Entries {
		k, err := kvKeyOf(e.Mount, e.Path)
		if err != nil {
			continue
		}
		version := e.Version
		if version < 1 {
			version = 1
		}
		entries[k] = entry{data: cloneData(e.Data), version: version}
	}
	s.mu.Lock()
	s.entries = entries
	s.mu.Unlock()
}

type Secrets struct {
	store *Store
}

var _ atproto.SecretStore = (*Secrets)(nil)

func NewSecrets(store *Store) *Secrets {
	if store == nil {
		store = NewStore()
	}
	return &Secrets{store: store}
}

func (s *Secrets) Get(ctx context.Context, ref commonv1.SecretRef) (map[string]any, error) {
	data, err := s.store.Read(ctx, ref.Mount, ref.Path, 0)
	if errors.Is(err, ErrNotFound) {
		return nil, atproto.ErrSecretNotFound
	}
	if err != nil {
		return nil, err
	}
	if len(data) == 0 {
		return nil, atproto.ErrSecretNotFound
	}
	return data, nil
}

func (s *Secrets) Put(ctx context.Context, ref commonv1.SecretRef, data map[string]any) error {
	return s.store.Write(ctx, ref.Mount, ref.Path, data)
}

func (s *Secrets) Delete(ctx context.Context, ref commonv1.SecretRef) error {
	return s.store.Delete(ctx, ref.Mount, ref.Path)
}

type Credentials struct {
	store *Store
}

var (
	_ component.CredentialResolver = (*Credentials)(nil)
	_ component.CredentialWriter   = (*Credentials)(nil)
	_ component.CredentialDeleter  = (*Credentials)(nil)
)

func NewCredentials(store *Store) *Credentials {
	if store == nil {
		store = NewStore()
	}
	return &Credentials{store: store}
}

func (c *Credentials) Resolve(ctx context.Context, ref component.CredentialRef) (component.Credential, error) {
	if err := ref.Check(); err != nil {
		return component.Credential{}, err
	}
	data, err := c.store.Read(ctx, ref.Mount, ref.Path, ref.Version)
	if err != nil {
		return component.Credential{}, fmt.Errorf("memory vault: reading %s/%s: %w", ref.Mount, ref.Path, err)
	}
	if len(data) == 0 {
		return component.Credential{}, fmt.Errorf("memory vault: %s/%s: %w", ref.Mount, ref.Path, ErrNotFound)
	}
	return component.Credential{Ref: ref, Data: filterKeys(data, ref.Keys)}, nil
}

func (c *Credentials) WriteCredential(ctx context.Context, ref component.CredentialRef, data map[string]any) error {
	if err := ref.Check(); err != nil {
		return err
	}
	if data == nil {
		return errors.New("memory vault: a credential write needs data")
	}
	return c.store.Write(ctx, ref.Mount, ref.Path, data)
}

func (c *Credentials) DeleteCredential(ctx context.Context, ref component.CredentialRef) error {
	if err := ref.Check(); err != nil {
		return err
	}
	return c.store.Delete(ctx, ref.Mount, ref.Path)
}

func kvKeyOf(mount, path string) (kvKey, error) {
	mount = strings.Trim(strings.TrimSpace(mount), "/")
	path = strings.Trim(strings.TrimSpace(path), "/")
	path = strings.TrimPrefix(strings.TrimPrefix(path, mount+"/data/"), mount+"/metadata/")
	if mount == "" || path == "" {
		return kvKey{}, ErrInvalidRef
	}
	return kvKey{mount: mount, path: path}, nil
}

func filterKeys(data map[string]any, keys []string) map[string]any {
	if len(keys) == 0 {
		return data
	}
	out := make(map[string]any, len(keys))
	for _, key := range keys {
		if value, ok := data[key]; ok {
			out[key] = value
		}
	}
	return out
}

func cloneData(in map[string]any) map[string]any {
	out := make(map[string]any, len(in))
	for k, v := range in {
		out[k] = cloneValue(v)
	}
	return out
}

func cloneValue(v any) any {
	switch x := v.(type) {
	case []byte:
		return append([]byte(nil), x...)
	case map[string]any:
		return cloneData(x)
	case []any:
		out := make([]any, len(x))
		for i, item := range x {
			out[i] = cloneValue(item)
		}
		return out
	default:
		return v
	}
}