~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
209 lines · 5.6 KB Raw History
package baosync

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/vault/openbao"
)

func mountType(m openbao.Mount) string {
	if m.Type == MountKV && fmt.Sprint(m.Options["version"]) == "2" {
		return MountKV2
	}
	return m.Type
}

func objectMounted(p string, a Actual) bool {
	segs := strings.Split(strings.Trim(p, "/"), "/")
	if segs[0] == "auth" {
		if len(segs) < 2 {
			return false
		}
		_, ok := a.AuthMounts[segs[1]]
		return ok
	}
	_, ok := a.SecretMounts[segs[0]]
	return ok
}

func Read(ctx context.Context, c *openbao.Client, d Desired) (Actual, error) {
	a := Actual{Plugins: map[string]PluginEntry{}, AuthSHA256: map[string]string{}, SecretMounts: map[string]string{}, AuthMounts: map[string]string{}, Policies: map[string]string{},
		Collections: map[string][]string{}, Objects: map[string]map[string]any{}, KV: map[string][]string{},
		KVUpdated: map[string]time.Time{}, Now: time.Now()}
	secrets, err := c.SecretMounts(ctx)
	if err != nil {
		return a, fmt.Errorf("baosync: listing the secret mounts: %w; an unread list is not an empty one", err)
	}
	for p, m := range secrets {
		a.SecretMounts[p] = mountType(m)
	}
	auths, err := c.AuthMounts(ctx)
	if err != nil {
		return a, fmt.Errorf("baosync: listing the auth mounts: %w; an unread list is not an empty one", err)
	}
	for p, m := range auths {
		a.AuthMounts[p] = m.Type
		a.AuthSHA256[p] = m.RunningSHA256
	}
	if err := readPlugins(ctx, c, d, &a); err != nil {
		return a, err
	}
	names, err := c.List(ctx, "sys/policies/acl")
	if err != nil {
		return a, fmt.Errorf("baosync: listing the policies: %w; an unread list is not an empty one", err)
	}
	if len(names) == 0 {
		return a, errors.New("baosync: the policy list came back empty; a server always holds the default policy, so the read is wrong")
	}
	want := map[string]bool{}
	for _, p := range d.Policies {
		want[p.Name] = true
	}
	for _, n := range names {
		a.Policies[n] = ""
		if !want[n] {
			continue
		}
		hcl, err := c.GetPolicy(ctx, n)
		if err != nil {
			return a, fmt.Errorf("baosync: reading policy %s: %w", n, err)
		}
		a.Policies[n] = hcl
	}
	for _, coll := range d.Collections() {
		if !objectMounted(coll, a) || !collectionTypeMatches(coll, d, a) {
			continue
		}
		keys, err := c.List(ctx, coll)
		if err != nil {
			return a, fmt.Errorf("baosync: listing %s: %w; an unread list is not an empty one", coll, err)
		}
		a.Collections[coll] = keys
	}
	for _, o := range d.Objects {
		p := strings.Trim(o.Path, "/")
		if !objectMounted(p, a) {
			continue
		}
		data, err := c.Read(ctx, p)
		if err != nil {
			continue
		}
		a.Objects[p] = data
	}
	for _, s := range d.KV {
		if _, ok := a.SecretMounts[strings.Trim(s.Mount, "/")]; !ok {
			continue
		}
		key := KVScopeKey(s)
		keys, err := c.List(ctx, key)
		if err != nil {
			return a, fmt.Errorf("baosync: listing %s: %w; an unread list is not an empty one", key, err)
		}
		keep := KeptKV(s)
		var entries []string
		for _, name := range keys {
			if keep[KVTop(name)] {
				entries = append(entries, name)
				continue
			}
			leaves, err := kvLeaves(ctx, c, key, name)
			if err != nil {
				return a, err
			}
			for _, leaf := range leaves {
				entries = append(entries, leaf)
				if s.Grace <= 0 {
					continue
				}
				target := strings.TrimSuffix(key, "/") + "/" + leaf
				meta, err := c.Read(ctx, target)
				if errors.Is(err, openbao.ErrNotFound) {
					continue
				}
				at, perr := time.Parse(time.RFC3339Nano, fmt.Sprint(meta["updated_time"]))
				if err != nil || perr != nil {
					at = a.Now
				}
				a.KVUpdated[target] = at
			}
		}
		a.KV[key] = entries
	}
	return a, nil
}

func kvLeaves(ctx context.Context, c *openbao.Client, key, name string) ([]string, error) {
	if !strings.HasSuffix(name, "/") {
		return []string{name}, nil
	}
	path := strings.TrimSuffix(key, "/") + "/" + strings.TrimSuffix(name, "/")
	under, err := c.List(ctx, path)
	if err != nil {
		return nil, fmt.Errorf("baosync: listing %s: %w; an unread list is not an empty one", path, err)
	}
	var out []string
	for _, u := range under {
		leaves, err := kvLeaves(ctx, c, path, u)
		if err != nil {
			return nil, err
		}
		for _, l := range leaves {
			out = append(out, name+l)
		}
	}
	return out, nil
}

func collectionTypeMatches(coll string, d Desired, a Actual) bool {
	if coll == IdentityRoleCollection {
		return d.IdentityOIDC
	}
	for _, m := range d.AuthMounts {
		if d.AuthRoleCollection(m) == coll {
			return a.AuthMounts[strings.Trim(m.Path, "/")] == m.Type
		}
	}
	for _, m := range d.SecretMounts {
		if m.Type == MountPKI && PKIRoleCollection(m.Path) == coll {
			return a.SecretMounts[strings.Trim(m.Path, "/")] == MountPKI
		}
	}
	return false
}

func readPlugins(ctx context.Context, c *openbao.Client, d Desired, a *Actual) error {
	if len(d.Plugins) == 0 {
		return nil
	}
	cat, err := c.Read(ctx, "sys/plugins/catalog")
	if err != nil {
		return fmt.Errorf("baosync: reading the plugin catalog: %w; an unread catalog is not an empty one", err)
	}
	detailed, _ := cat["detailed"].([]any)
	for _, x := range detailed {
		e, ok := x.(map[string]any)
		if !ok || e["builtin"] == true {
			continue
		}
		a.External = append(a.External, fmt.Sprint(e["type"])+"/"+fmt.Sprint(e["name"]))
	}
	for _, p := range d.Plugins {
		got, err := c.Read(ctx, "sys/plugins/catalog/"+PluginKey(p))
		if errors.Is(err, openbao.ErrNotFound) {
			continue
		}
		if err != nil {
			return fmt.Errorf("baosync: reading plugin %s: %w", PluginKey(p), err)
		}
		if got["builtin"] == true {
			continue
		}
		a.Plugins[PluginKey(p)] = PluginEntry{Command: fmt.Sprint(got["command"]), SHA256: fmt.Sprint(got["sha256"])}
	}
	return nil
}