~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
214 lines · 6.9 KB Raw History
package compose

import (
	"context"
	"crypto/rand"
	"crypto/tls"
	"encoding/hex"
	"errors"
	"fmt"
	"math/big"
	"net/http"
	"os"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	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/controllers/runtime/kube"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/openbaoctl"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/vault/openbao"
)

const (
	RepoKeyField  = "repoKeyHex"
	passwordBytes = 24
)

func (c *Composer) ApplyVault(ctx context.Context, r Root, name string) error {
	obj := &v1alpha1.Vault{ObjectMeta: metav1.ObjectMeta{Name: name},
		Spec: v1alpha1.VaultSpec{BackendRef: commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: KindOpenBaoServer, Name: name}}}
	return c.ApplyTyped(ctx, r, r.Workspace, VaultGVR, v1alpha1.GroupVersion.String(), KindVault, obj)
}

func RootVaultLink(r Root, vaultName string, prefixes ...string) Link {
	return VaultLink(r.Ref(), vaultName, true, prefixes...)
}

func (c *Composer) KV(ctx context.Context, r Root, vaultName string, scope ...string) (contractvault.KV, error) {
	return bus.Resolve[contractvault.KV](ctx, c.cfg.Bus, r.Ref(), VaultRefIn(vaultName, r.Workspace),
		bus.Capability{Name: contractvault.CapabilityWrite, Scope: cleanPrefixes(scope)})
}

func readKV(ctx context.Context, kv contractvault.KV, mount, path string) (map[string]any, error) {
	data, err := kv.Read(ctx, mount, path, 0)
	var re *openbao.ResponseError
	if err != nil && !(errors.As(err, &re) && re.Status == http.StatusNotFound) {
		return nil, err
	}
	if data == nil {
		data = map[string]any{}
	}
	return data, nil
}

func ReadKV(ctx context.Context, kv contractvault.KV, mount, path string) (map[string]any, error) {
	return readKV(ctx, kv, mount, path)
}

func NewPassword() (string, error) {
	buf := make([]byte, passwordBytes)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return hex.EncodeToString(buf), nil
}

var secp256k1N, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)

func NewRepoKeyHex() (string, error) {
	for {
		buf := make([]byte, 32)
		if _, err := rand.Read(buf); err != nil {
			return "", err
		}
		k := new(big.Int).SetBytes(buf)
		if k.Sign() > 0 && k.Cmp(secp256k1N) < 0 {
			return hex.EncodeToString(buf), nil
		}
	}
}

func (c *Composer) EnsureRepoKey(ctx context.Context, r Root, vaultName, mount, keysPath string) error {
	kv, err := c.KV(ctx, r, vaultName, mount+"/identity/*")
	if err != nil {
		return err
	}
	if err := c.ApplyVaultCredential(ctx, r, vaultName, mount, keysPath); err != nil {
		return err
	}
	data, err := readKV(ctx, kv, mount, keysPath)
	if err != nil {
		return err
	}
	if k, _ := data[RepoKeyField].(string); k != "" {
		return nil
	}
	key, err := NewRepoKeyHex()
	if err != nil {
		return err
	}
	data[RepoKeyField] = key
	if err := kv.Write(ctx, mount, keysPath, data); err != nil {
		return err
	}
	c.log.Info("the application's repo key is in its own vault", "vault", vaultName, "path", keysPath)
	return nil
}

func (c *Composer) Password(ctx context.Context, r Root, vaultName, mount, path string) (string, error) {
	kv, err := c.KV(ctx, r, vaultName, mount+"/identity/*")
	if err != nil {
		return "", err
	}
	data, err := readKV(ctx, kv, mount, path)
	if err != nil {
		return "", err
	}
	if password, _ := data["password"].(string); password != "" {
		return password, nil
	}
	password, err := NewPassword()
	if err != nil {
		return "", err
	}
	data["password"] = password
	if err := kv.Write(ctx, mount, path, data); err != nil {
		return "", err
	}
	return password, nil
}

func (c *Composer) VaultRefs(ctx context.Context, ws, server, clientName string) (v1alpha1.OpenBaoServerStatus, v1alpha1.OpenBaoServerClientRefs, error) {
	o, err := Get[v1alpha1.OpenBaoServer](ctx, c, ws, OpenBaoServerGVR, server)
	if err != nil {
		return v1alpha1.OpenBaoServerStatus{}, v1alpha1.OpenBaoServerClientRefs{}, err
	}
	st := o.Status
	if st.Address == "" || st.Refs == nil {
		return st, v1alpha1.OpenBaoServerClientRefs{}, fmt.Errorf("the OpenBaoServer %s reports no address yet (phase %s)", server, st.Phase)
	}
	for _, x := range st.Clients {
		if x.Name == clientName && x.ClientCert != "" {
			return st, x, nil
		}
	}
	return st, v1alpha1.OpenBaoServerClientRefs{}, fmt.Errorf("the OpenBaoServer %s reports no client %s yet", server, clientName)
}

func (c *Composer) KVMount(ctx context.Context, ws, server, clientName, fallback string) string {
	_, refs, err := c.VaultRefs(ctx, ws, server, clientName)
	if err != nil || strings.TrimSpace(refs.KVMount) == "" {
		return fallback
	}
	return strings.Trim(refs.KVMount, "/")
}

func ServerClient(ctx context.Context, r kube.Resource, server, clientName string) (*openbao.Client, v1alpha1.OpenBaoServerStatus, v1alpha1.OpenBaoServerClientRefs, error) {
	o, err := kube.Get[v1alpha1.OpenBaoServer](ctx, r, server)
	if err != nil {
		return nil, v1alpha1.OpenBaoServerStatus{}, v1alpha1.OpenBaoServerClientRefs{}, err
	}
	st := o.Status
	if st.Address == "" || st.Refs == nil {
		return nil, st, v1alpha1.OpenBaoServerClientRefs{}, fmt.Errorf("the OpenBaoServer %s reports no address yet (phase %s)", server, st.Phase)
	}
	var cl v1alpha1.OpenBaoServerClientRefs
	for _, x := range st.Clients {
		if x.Name == clientName {
			cl = x
		}
	}
	if cl.ClientCert == "" {
		return nil, st, cl, fmt.Errorf("the OpenBaoServer %s reports no client %s yet", server, clientName)
	}
	caPEM, err := os.ReadFile(openbaoctl.FileRef(st.Refs.CA))
	if err != nil {
		return nil, st, cl, err
	}
	pool, err := openbao.CertPoolFromPEM(caPEM)
	if err != nil {
		return nil, st, cl, err
	}
	cert, err := tls.LoadX509KeyPair(openbaoctl.FileRef(cl.ClientCert), openbaoctl.FileRef(cl.ClientKey))
	if err != nil {
		return nil, st, cl, err
	}
	client := openbao.New(st.Address, "", openbao.WithCACertPool(pool), openbao.WithClientCertificate(cert))
	logged, err := client.LoginCert(ctx, cl.CertAuthMount, cl.CertAuthRole)
	if err != nil {
		return nil, st, cl, err
	}
	return logged, st, cl, nil
}

func (c *Composer) ServerClient(ctx context.Context, ws, server, clientName string) (*openbao.Client, v1alpha1.OpenBaoServerClientRefs, error) {
	ri, err := c.Client(ws, OpenBaoServerGVR)
	if err != nil {
		return nil, v1alpha1.OpenBaoServerClientRefs{}, err
	}
	client, _, refs, err := ServerClient(ctx, ri, server, clientName)
	return client, refs, err
}

func (c *Composer) DeleteVaults(ctx context.Context, r Root) (int, error) {
	vaults, err := c.DeleteOwned(ctx, r.Workspace, VaultGVR, r.Owner())
	if err != nil {
		return vaults, err
	}
	servers, err := c.DeleteOwned(ctx, r.Workspace, OpenBaoServerGVR, r.Owner())
	return vaults + servers, err
}