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

import (
	"fmt"
	"slices"
	"sort"
	"strings"
	"time"
)

const (
	MountKV2               = "kv-v2"
	MountKV                = "kv"
	MountPKI               = "pki"
	AuthCert               = "cert"
	AuthJWT                = "jwt"
	AuthAppRole            = "approle"
	AuthToken              = "token"
	policyRoot             = "root"
	policyDflt             = "default"
	mountSys               = "sys"
	mountIdent             = "identity"
	mountCubby             = "cubbyhole"
	collCerts              = "certs"
	collRole               = "role"
	collPKIRoles           = "roles"
	IdentityRoleCollection = "identity/oidc/role"
	PluginAuth             = "auth"
)

type SecretMount struct {
	Path        string
	Type        string
	MaxLeaseTTL string
}

type AuthMount struct {
	Path string
	Type string
}

type Policy struct {
	Name string
	HCL  string
}

type Object struct {
	Path      string
	Data      map[string]any
	WriteOnly []string
	Derived   map[string]any
}

type KVScope struct {
	Mount  string
	Prefix string
	Keys   []string
	Grace  time.Duration
}

type Plugin struct {
	Type         string
	Name         string
	Command      string
	SHA256       string
	ManagedRoles bool
}

type PluginEntry struct {
	Command string
	SHA256  string
}

type Desired struct {
	Plugins      []Plugin
	SecretMounts []SecretMount
	AuthMounts   []AuthMount
	Policies     []Policy
	Objects      []Object
	KV           []KVScope
	IdentityOIDC bool
}

type Actual struct {
	Plugins      map[string]PluginEntry
	External     []string
	AuthMounts   map[string]string
	AuthSHA256   map[string]string
	SecretMounts 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.Time
}

type OpKind string

const (
	OpEnableSecret  OpKind = "enable-secret-mount"
	OpEnableAuth    OpKind = "enable-auth-mount"
	OpDisableAuth   OpKind = "disable-auth-mount"
	OpPutPolicy     OpKind = "put-policy"
	OpWriteObject   OpKind = "write"
	OpDeleteObject  OpKind = "delete"
	OpDeletePolicy  OpKind = "delete-policy"
	OpDisableSecret OpKind = "disable-secret-mount"
	OpDeleteKV      OpKind = "delete-kv"
	OpRegister      OpKind = "register-plugin"
	OpReload        OpKind = "reload-plugin"
	OpDeregister    OpKind = "deregister-plugin"
)

type Op struct {
	Kind   OpKind
	Target string
	Type   string
	Body   map[string]any
	HCL    string
	Why    string
}

func (o Op) String() string { return string(o.Kind) + " " + o.Target }

type Plan struct {
	Ops       []Op
	Conflicts []string
}

func BuiltinSecretMount(p string) bool {
	switch strings.Trim(p, "/") {
	case mountSys, mountIdent, mountCubby:
		return true
	}
	return false
}

func BuiltinAuthMount(p string) bool { return strings.Trim(p, "/") == AuthToken }

func BuiltinPolicy(name string) bool { return name == policyRoot || name == policyDflt }

func PluginKey(p Plugin) string { return p.Type + "/" + p.Name }

func (d Desired) authPlugins() map[string]bool {
	out := map[string]bool{}
	for _, p := range d.Plugins {
		if p.Type == PluginAuth {
			out[p.Name] = true
		}
	}
	return out
}

func RoleCollection(mountPath, mountType string) string {
	m := strings.Trim(mountPath, "/")
	switch mountType {
	case AuthCert:
		return "auth/" + m + "/" + collCerts
	case AuthJWT, AuthAppRole:
		return "auth/" + m + "/" + collRole
	}
	return ""
}

func PKIRoleCollection(mountPath string) string {
	return strings.Trim(mountPath, "/") + "/" + collPKIRoles
}

func KVScopeKey(s KVScope) string {
	return strings.Trim(s.Mount, "/") + "/metadata/" + strings.Trim(s.Prefix, "/")
}

func (d Desired) managedRoles(mountType string) bool {
	for _, p := range d.Plugins {
		if p.Type == PluginAuth && p.Name == mountType && p.ManagedRoles {
			return true
		}
	}
	return false
}

func (d Desired) AuthRoleCollection(m AuthMount) string {
	if c := RoleCollection(m.Path, m.Type); c != "" {
		return c
	}
	if d.managedRoles(m.Type) {
		return "auth/" + strings.Trim(m.Path, "/") + "/" + collRole
	}
	return ""
}

func (d Desired) Collections() []string {
	var out []string
	for _, m := range d.AuthMounts {
		if c := d.AuthRoleCollection(m); c != "" {
			out = append(out, c)
		}
	}
	for _, m := range d.SecretMounts {
		if m.Type == MountPKI {
			out = append(out, PKIRoleCollection(m.Path))
		}
	}
	if d.IdentityOIDC {
		out = append(out, IdentityRoleCollection)
	}
	sort.Strings(out)
	return slices.Compact(out)
}

func (d Desired) Validate() error {
	secrets, auths, policies, objects := map[string]bool{}, map[string]bool{}, map[string]bool{}, map[string]bool{}
	plugins := map[string]bool{}
	for _, p := range d.Plugins {
		if p.Type != PluginAuth || p.Name == "" || strings.ContainsAny(p.Name, "/ ") || plugins[PluginKey(p)] {
			return fmt.Errorf("baosync: plugin %s/%s: type auth, a name without a slash or a space, declared once", p.Type, p.Name)
		}
		switch p.Name {
		case AuthCert, AuthJWT, AuthAppRole, AuthToken:
			return fmt.Errorf("baosync: plugin %s shadows a built-in auth method", p.Name)
		}
		if len(p.SHA256) != 64 || p.Command == "" || strings.ContainsAny(p.Command, "/ ") {
			return fmt.Errorf("baosync: plugin %s needs a sha256 and a command name in the plugin directory", p.Name)
		}
		plugins[PluginKey(p)] = true
	}
	authPlugins := d.authPlugins()
	for _, m := range d.SecretMounts {
		p := strings.Trim(m.Path, "/")
		if p == "" || BuiltinSecretMount(p) || secrets[p] {
			return fmt.Errorf("baosync: secret mount %q is empty, a built-in or declared twice", m.Path)
		}
		switch m.Type {
		case MountKV2, MountKV, MountPKI:
		default:
			return fmt.Errorf("baosync: secret mount %s has type %q; kv-v2, kv and pki are declared", p, m.Type)
		}
		secrets[p] = true
	}
	for _, m := range d.AuthMounts {
		p := strings.Trim(m.Path, "/")
		if p == "" || BuiltinAuthMount(p) || auths[p] {
			return fmt.Errorf("baosync: auth mount %q is empty, a built-in or declared twice", m.Path)
		}
		switch {
		case m.Type == AuthCert, m.Type == AuthJWT, m.Type == AuthAppRole, authPlugins[m.Type]:
		default:
			return fmt.Errorf("baosync: auth mount %s has type %q; cert, jwt, approle and the declared auth plugins are declared", p, m.Type)
		}
		auths[p] = true
	}
	for _, p := range d.Policies {
		if p.Name == "" || BuiltinPolicy(p.Name) || policies[p.Name] || strings.ContainsAny(p.Name, "/ ") {
			return fmt.Errorf("baosync: policy %q is empty, a built-in, holds a slash or a space, or is declared twice", p.Name)
		}
		policies[p.Name] = true
	}
	for _, o := range d.Objects {
		p := strings.Trim(o.Path, "/")
		if p == "" || objects[p] {
			return fmt.Errorf("baosync: object %q is empty or declared twice", o.Path)
		}
		objects[p] = true
	}
	for _, s := range d.KV {
		if !secrets[strings.Trim(s.Mount, "/")] {
			return fmt.Errorf("baosync: kv scope %s names no declared secret mount", KVScopeKey(s))
		}
	}
	return nil
}

func Diff(d Desired, a Actual) Plan {
	var plan Plan
	add := func(o Op) { plan.Ops = append(plan.Ops, o) }

	wantPlugin := map[string]bool{}
	for _, p := range d.Plugins {
		key := PluginKey(p)
		wantPlugin[key] = true
		got, ok := a.Plugins[key]
		body := map[string]any{"sha256": p.SHA256, "command": p.Command}
		switch {
		case !ok:
			add(Op{Kind: OpRegister, Target: key, Body: body, Why: "declared and not in the catalog"})
		case got.SHA256 != p.SHA256 || got.Command != p.Command:
			add(Op{Kind: OpRegister, Target: key, Body: body, Why: "the catalog holds sha256 " + got.SHA256 + ", " + p.SHA256 + " is declared"})
		}
		for _, m := range d.AuthMounts {
			mp := strings.Trim(m.Path, "/")
			running, mounted := a.AuthSHA256[mp]
			if p.Type == PluginAuth && m.Type == p.Name && mounted && a.AuthMounts[mp] == p.Name && running != p.SHA256 {
				add(Op{Kind: OpReload, Target: p.Name, Body: map[string]any{"plugin": p.Name},
					Why: "mount " + mp + " runs sha256 " + running + ", " + p.SHA256 + " is declared"})
				break
			}
		}
	}

	wantSecret := map[string]SecretMount{}
	for _, m := range d.SecretMounts {
		p := strings.Trim(m.Path, "/")
		wantSecret[p] = m
		got, ok := a.SecretMounts[p]
		switch {
		case !ok:
			add(Op{Kind: OpEnableSecret, Target: p, Type: m.Type, Body: maxLease(m.MaxLeaseTTL), Why: "declared and missing"})
		case got != m.Type:
			plan.Conflicts = append(plan.Conflicts, fmt.Sprintf("secret mount %s is %s and is declared %s; a secret mount is never "+
				"replaced, because disabling it destroys what it holds", p, got, m.Type))
		}
	}
	wantAuth := map[string]AuthMount{}
	for _, m := range d.AuthMounts {
		p := strings.Trim(m.Path, "/")
		wantAuth[p] = m
		got, ok := a.AuthMounts[p]
		switch {
		case !ok:
			add(Op{Kind: OpEnableAuth, Target: p, Type: m.Type, Why: "declared and missing"})
		case got != m.Type:
			add(Op{Kind: OpDisableAuth, Target: p, Why: "type " + got + " where " + m.Type + " is declared"})
			add(Op{Kind: OpEnableAuth, Target: p, Type: m.Type, Why: "declared as " + m.Type})
		}
	}
	wantPolicy := map[string]bool{}
	for _, p := range d.Policies {
		wantPolicy[p.Name] = true
		got, ok := a.Policies[p.Name]
		switch {
		case !ok:
			add(Op{Kind: OpPutPolicy, Target: p.Name, HCL: p.HCL, Why: "declared and missing"})
		case strings.TrimSpace(got) != strings.TrimSpace(p.HCL):
			add(Op{Kind: OpPutPolicy, Target: p.Name, HCL: p.HCL, Why: "its rules drifted from the declaration"})
		}
	}
	wantObject := map[string]bool{}
	for _, o := range d.Objects {
		p := strings.Trim(o.Path, "/")
		wantObject[p] = true
		got, ok := a.Objects[p]
		switch {
		case !ok:
			add(Op{Kind: OpWriteObject, Target: p, Body: o.Data, Why: "declared and missing"})
		default:
			if k := Drifted(o.Data, got, o.WriteOnly...); k != "" {
				add(Op{Kind: OpWriteObject, Target: p, Body: o.Data, Why: "field " + k + " drifted from the declaration"})
			} else if k := Drifted(o.Derived, got); k != "" {
				add(Op{Kind: OpWriteObject, Target: p, Body: o.Data, Why: "field " + k + " drifted from the declaration"})
			}
		}
	}
	for _, c := range d.Collections() {
		for _, name := range a.Collections[c] {
			if strings.HasSuffix(name, "/") {
				continue
			}
			p := c + "/" + name
			if !wantObject[p] {
				add(Op{Kind: OpDeleteObject, Target: p, Why: "no declaration names it"})
			}
		}
	}
	for _, name := range sortedKeys(a.Policies) {
		if !wantPolicy[name] && !BuiltinPolicy(name) {
			add(Op{Kind: OpDeletePolicy, Target: name, Why: "no declaration names it"})
		}
	}
	for _, p := range sortedKeys(a.AuthMounts) {
		if _, ok := wantAuth[p]; !ok && !BuiltinAuthMount(p) {
			add(Op{Kind: OpDisableAuth, Target: p, Why: "no declaration names it"})
		}
	}
	for _, p := range sortedKeys(a.SecretMounts) {
		if _, ok := wantSecret[p]; !ok && !BuiltinSecretMount(p) {
			add(Op{Kind: OpDisableSecret, Target: p, Why: "no declaration names it"})
		}
	}
	for _, key := range a.External {
		if !wantPlugin[key] {
			add(Op{Kind: OpDeregister, Target: key, Why: "an external plugin no declaration names"})
		}
	}
	for _, s := range d.KV {
		key := KVScopeKey(s)
		keep := KeptKV(s)
		for _, name := range a.KV[key] {
			if name == "" || strings.HasSuffix(name, "/") || keep[KVTop(name)] {
				continue
			}
			target := strings.TrimSuffix(key, "/") + "/" + name
			if at, ok := a.KVUpdated[target]; ok && s.Grace > 0 && a.Now.Sub(at) < s.Grace {
				continue
			}
			add(Op{Kind: OpDeleteKV, Target: target, Why: "no declaration names this entry"})
		}
	}
	return plan
}

func KeptKV(s KVScope) map[string]bool {
	keep := map[string]bool{}
	for _, k := range s.Keys {
		keep[strings.Trim(k, "/")] = true
	}
	return keep
}

func KVTop(name string) string {
	top, _, _ := strings.Cut(strings.Trim(name, "/"), "/")
	return top
}

func maxLease(ttl string) map[string]any {
	if ttl == "" {
		return nil
	}
	return map[string]any{"max_lease_ttl": ttl}
}

func sortedKeys[V any](m map[string]V) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

func (d Desired) String() string {
	return fmt.Sprintf("%d secret mounts, %d auth mounts, %d policies, %d roles and configs, %d kv scopes",
		len(d.SecretMounts), len(d.AuthMounts), len(d.Policies), len(d.Objects), len(d.KV))
}