~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
275 lines · 7.7 KB Raw History
package vault

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"k8s.io/apimachinery/pkg/runtime/schema"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/bus"
	contractvault "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/lease"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/vault/openbao"
)

var ErrUnbound = errors.New("vault: the Vault is not bound to a ready backend")

var VaultGroupKind = schema.GroupKind{Group: v1alpha1.GroupName, Kind: "Vault"}

type Binding struct {
	Vault commonv1.ObjectRef

	Backend commonv1.ObjectRef

	Server *v1alpha1.OpenBaoServerStatus

	Remote *v1alpha1.RemoteOpenBao
}

type bindingContract struct {
	Vault commonv1.ObjectRef

	Backend commonv1.ObjectRef

	Address string

	CABundle string

	Refs *v1alpha1.OpenBaoServerRefs

	Clients []v1alpha1.OpenBaoServerClientRefs

	Remote *v1alpha1.RemoteOpenBaoSpec

	Ready bool
}

func (b Binding) fingerprint() string {
	c := bindingContract{Vault: b.Vault, Backend: b.Backend}
	if b.Server != nil {
		c.Address, c.CABundle, c.Refs, c.Clients = b.Server.Address, b.Server.CABundle, b.Server.Refs, b.Server.Clients
		c.Ready = b.Server.Phase == v1alpha1.OpenBaoServerPhaseReady
	}
	if b.Remote != nil {
		spec := b.Remote.Spec
		c.Remote = &spec
	}
	raw, _ := json.Marshal(c)
	sum := sha256.Sum256(raw)
	return hex.EncodeToString(sum[:])
}

type vaultKey struct {
	workspace string

	name string
}

func keyOf(ref commonv1.ObjectRef) vaultKey {
	return vaultKey{workspace: strings.TrimSpace(ref.Workspace), name: strings.TrimSpace(ref.Name)}
}

type remoteLogin struct {
	fingerprint string

	client *lease.Source[*openbao.Client]
}

type Provider struct {
	remote *openbao.Remote

	logins *openbao.Logins

	mu sync.RWMutex

	bindings map[vaultKey]Binding

	remotes map[vaultKey]remoteLogin

	bus *bus.Bus
}

func NewProvider(remote *openbao.Remote) *Provider {
	p := &Provider{remote: remote, bindings: map[vaultKey]Binding{}, remotes: map[vaultKey]remoteLogin{}}
	p.logins = openbao.NewLogins(openbao.SourceFunc(func(context.Context, string, string) (openbao.Server, error) {
		return openbao.Server{}, errors.New("vault: the provider opens servers from its bindings")
	}))
	return p
}

func (p *Provider) Bind(b Binding) bool {
	key := keyOf(b.Vault)
	p.mu.Lock()
	old, ok := p.bindings[key]
	if ok && old.fingerprint() == b.fingerprint() {
		p.mu.Unlock()
		return false
	}
	p.bindings[key] = b
	delete(p.remotes, key)
	if ok && old.Backend.Name != "" {
		p.logins.Forget(old.Backend.Workspace, old.Backend.Name)
	}
	notify := p.bus
	p.mu.Unlock()
	notify.NotifyBound(bus.Key{Workspace: b.Vault.Workspace, Kind: b.Vault.Kind, Name: b.Vault.Name})
	return true
}

func (p *Provider) Unbind(ref commonv1.ObjectRef) {
	key := keyOf(ref)
	p.mu.Lock()
	defer p.mu.Unlock()
	if old, ok := p.bindings[key]; ok && old.Backend.Name != "" {
		p.logins.Forget(old.Backend.Workspace, old.Backend.Name)
	}
	delete(p.bindings, key)
	delete(p.remotes, key)
}

func (p *Provider) binding(ref commonv1.ObjectRef) (Binding, bool) {
	p.mu.RLock()
	defer p.mu.RUnlock()
	b, ok := p.bindings[keyOf(ref)]
	return b, ok
}

func (p *Provider) clientFor(to commonv1.ObjectRef) openbao.ClientFor {
	return p.clientAs(to, clientName)
}

func (p *Provider) clientAs(to commonv1.ObjectRef, pick func(clients []v1alpha1.OpenBaoServerClientRefs, mount string) string) openbao.ClientFor {
	return func(ctx context.Context, mount string) (*openbao.Client, error) {
		b, ok := p.binding(to)
		if !ok {
			return nil, fmt.Errorf("%w: %s/%s", ErrUnbound, to.Workspace, to.Name)
		}
		switch {
		case b.Server != nil:
			srv, err := openbao.ServerOf(b.Backend.Workspace, b.Backend.Name, *b.Server, pick(b.Server.Clients, mount))
			if err != nil {
				return nil, err
			}
			client, _, err := p.logins.Open(ctx, srv)
			return client, err
		case b.Remote != nil:
			return p.remoteClient(ctx, keyOf(to), b)
		default:
			return nil, fmt.Errorf("%w: %s/%s", ErrUnbound, to.Workspace, to.Name)
		}
	}
}

func clientName(clients []v1alpha1.OpenBaoServerClientRefs, mount string) string {
	if name := openbao.ClientForMount(clients, mount); name != "" || strings.Trim(mount, "/") != "" {
		return name
	}
	name := ""
	for _, c := range clients {
		if c.ClientCert == "" || strings.TrimSpace(c.KVMount) != "" {
			continue
		}
		if name != "" {
			return ""
		}
		name = c.Name
	}
	return name
}

func sessionClient([]v1alpha1.OpenBaoServerClientRefs, string) string {
	return contractvault.AtprotoSessionClient
}

func (p *Provider) remoteClient(ctx context.Context, key vaultKey, b Binding) (*openbao.Client, error) {
	fp := b.fingerprint()
	p.mu.Lock()
	entry, ok := p.remotes[key]
	if !ok || entry.fingerprint != fp {
		remote := b.Remote.DeepCopy()
		entry = remoteLogin{fingerprint: fp, client: lease.New(func(ctx context.Context) (*openbao.Client, time.Time, error) {
			c, err := p.remote.Open(ctx, b.Backend.Workspace, remote)
			return c, time.Time{}, err
		})}
		p.remotes[key] = entry
	}
	p.mu.Unlock()
	return entry.client.Get(ctx)
}

func (p *Provider) defaultMount(to commonv1.ObjectRef) string {
	b, ok := p.binding(to)
	if !ok {
		return ""
	}
	if b.Server != nil && b.Server.Refs != nil {
		return b.Server.Refs.KVMount
	}
	if b.Remote != nil {
		return b.Remote.Spec.Mounts.KV
	}
	return ""
}

func (p *Provider) open(_ context.Context, to commonv1.ObjectRef, scope []string) (*openbao.Handle, error) {
	if _, ok := p.binding(to); !ok {
		return nil, fmt.Errorf("%w: %s/%s", ErrUnbound, to.Workspace, to.Name)
	}
	return openbao.NewHandle(p.clientFor(to), p.defaultMount(to), scope)
}

func (p *Provider) Register(b *bus.Bus) {
	p.mu.Lock()
	p.bus = b
	p.mu.Unlock()
	bus.Provide(b, VaultGroupKind, contractvault.CapabilityRead, func(ctx context.Context, to commonv1.ObjectRef, scope []string) (contractvault.KV, error) {
		h, err := p.open(ctx, to, scope)
		if err != nil {
			return nil, err
		}
		return readOnly{h}, nil
	})
	bus.Provide(b, VaultGroupKind, contractvault.CapabilityWrite, func(ctx context.Context, to commonv1.ObjectRef, scope []string) (contractvault.KV, error) {
		return p.open(ctx, to, scope)
	})
	bus.Provide(b, VaultGroupKind, contractvault.CapabilityLease, func(ctx context.Context, to commonv1.ObjectRef, scope []string) (contractvault.Leases, error) {
		return p.open(ctx, to, scope)
	})
	bus.Provide(b, VaultGroupKind, contractvault.CapabilityWrite, func(ctx context.Context, to commonv1.ObjectRef, scope []string) (contractvault.Keys, error) {
		return p.open(ctx, to, scope)
	})
	bus.Provide(b, VaultGroupKind, contractvault.CapabilityAtprotoSession, func(ctx context.Context, to commonv1.ObjectRef, scope []string) (contractvault.AtprotoSessions, error) {
		if _, ok := p.binding(to); !ok {
			return nil, fmt.Errorf("%w: %s/%s", ErrUnbound, to.Workspace, to.Name)
		}
		return openbao.NewHandle(p.clientAs(to, sessionClient), "", scope)
	})
	bus.Provide(b, VaultGroupKind, contractvault.CapabilityPKIIssue, func(ctx context.Context, to commonv1.ObjectRef, scope []string) (contractvault.PKI, error) {
		return p.open(ctx, to, scope)
	})
}

var ErrReadOnly = errors.New("vault: the handle was granted vault.read only")

var _ contractvault.KV = readOnly{}

type readOnly struct {
	h *openbao.Handle
}

func (r readOnly) Read(ctx context.Context, mount, path string, version int) (map[string]any, error) {
	return r.h.Read(ctx, mount, path, version)
}

func (readOnly) Write(context.Context, string, string, map[string]any) error { return ErrReadOnly }

func (readOnly) Delete(context.Context, string, string) error { return ErrReadOnly }