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

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/baosync"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/sha256sum"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/versions"
)

const (
	PluginDirName      = "plugins"
	PluginCommandBytes = 16
)

var (
	PluginAssets = []string{"openbao-plugin-auth-atproto", "openbao-plugin-auth-sshsig"}

	pluginNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
)

var PluginCorePolicyRules = []v1alpha1.OpenBaoPolicyRule{
	{Path: "sys/plugins/catalog", Capabilities: []string{"read", "list", "sudo"}},
	{Path: "sys/plugins/catalog/*", Capabilities: []string{"create", "read", "update", "delete", "sudo"}},
	{Path: "sys/plugins/reload/backend", Capabilities: []string{"update", "sudo"}},
}

func PluginDir(dir string) string { return filepath.Join(dir, PluginDirName) }

type placedPlugin struct {
	spec   v1alpha1.OpenBaoPlugin
	plugin baosync.Plugin
}

func checkPlugins(ps []v1alpha1.OpenBaoPlugin) error {
	names, mounts := map[string]bool{}, map[string]bool{}
	for _, p := range ps {
		if len(p.Name) > 63 || !pluginNamePattern.MatchString(p.Name) {
			return fmt.Errorf("plugin name %q: lower case letters, digits and dashes", p.Name)
		}
		if p.Type != baosync.PluginAuth {
			return fmt.Errorf("plugin %s: type %q; auth plugins are supported", p.Name, p.Type)
		}
		if !slices.Contains(PluginAssets, p.Asset.Name) {
			return fmt.Errorf("plugin %s: asset %q is none of the OpenBao plugin assets (%s)", p.Name, p.Asset.Name, strings.Join(PluginAssets, ", "))
		}
		mount := strings.Trim(p.MountPath, "/")
		if mount == "" || strings.Contains(mount, "/") || reservedMountNames[mount] || mount == AppRoleMount {
			return fmt.Errorf("plugin %s: mountPath %q is not a mount of its own", p.Name, p.MountPath)
		}
		if names[p.Name] || mounts[mount] {
			return fmt.Errorf("plugin %s: names and mount paths are each used once", p.Name)
		}
		if p.Atproto != nil {
			if !atprotoPlugin(p) {
				return fmt.Errorf("plugin %s: atproto settings need the %s asset", p.Name, AtprotoPluginAsset)
			}
			if c := p.Atproto.Config; (c.ClientID == "") != (len(c.RedirectURIs) == 0) {
				return fmt.Errorf("plugin %s: atproto config names a clientID and redirectURIs together or neither", p.Name)
			}
			for _, r := range p.Atproto.Roles {
				if len(r.Name) > 63 || !pluginNamePattern.MatchString(r.Name) || (len(r.BoundDIDs) == 0 && len(r.BoundHandles) == 0) {
					return fmt.Errorf("plugin %s: atproto role %q needs a lower case name and boundDIDs or boundHandles", p.Name, r.Name)
				}
			}
		}
		names[p.Name], mounts[mount] = true, true
	}
	return nil
}

func (s *Servers) placePlugins(o v1alpha1.OpenBaoServer, dir string) ([]placedPlugin, error) {
	if len(o.Spec.Plugins) == 0 {
		return nil, nil
	}
	if err := checkPlugins(o.Spec.Plugins); err != nil {
		return nil, err
	}
	if s.c.Resolve == nil {
		return nil, fmt.Errorf("this core resolves no assets, so it cannot place plugin %s", o.Spec.Plugins[0].Name)
	}
	pdir := PluginDir(dir)
	if err := os.MkdirAll(pdir, 0o700); err != nil {
		return nil, err
	}
	platform := runtime.GOOS + "/" + runtime.GOARCH
	var out []placedPlugin
	for _, p := range o.Spec.Plugins {
		src, err := s.c.Resolve(p.Asset)
		if err != nil {
			return nil, fmt.Errorf("plugin %s: asset %s: %w", p.Name, p.Asset.Name, err)
		}
		sum, err := sha256sum.File(src)
		if err != nil {
			return nil, fmt.Errorf("plugin %s: %w", p.Name, err)
		}
		pin, err := versions.FetchSHA256(p.Asset.Name, platform)
		if err != nil {
			return nil, fmt.Errorf("plugin %s: %w", p.Name, err)
		}
		if sum != pin || (p.Asset.Sha256 != "" && sum != p.Asset.Sha256) {
			return nil, fmt.Errorf("plugin %s: %s has sha256 %s; internal/versions/versions.json pins %s for %s", p.Name, src, sum, pin, platform)
		}
		command := p.Asset.Name + "-" + sum[:PluginCommandBytes]
		dst := filepath.Join(pdir, command)
		if have, err := sha256sum.File(dst); err != nil || have != sum {
			body, err := os.ReadFile(src)
			if err != nil {
				return nil, fmt.Errorf("plugin %s: %w", p.Name, err)
			}
			if err := writeIfChanged(dst, body, 0o700); err != nil {
				return nil, fmt.Errorf("plugin %s: %w", p.Name, err)
			}
			s.log.Info("placed an OpenBao plugin in the server's plugin directory", "openbaoserver", o.Name, "plugin", p.Name,
				"path", dst, "sha256", sum)
		}
		out = append(out, placedPlugin{spec: p, plugin: baosync.Plugin{Type: p.Type, Name: p.Name, Command: command, SHA256: sum}})
	}
	return out, nil
}

func prunePlugins(dir string, keep []placedPlugin) []string {
	entries, err := os.ReadDir(PluginDir(dir))
	if err != nil {
		return nil
	}
	want := map[string]bool{}
	for _, p := range keep {
		want[p.plugin.Command] = true
	}
	var removed []string
	for _, e := range entries {
		if want[e.Name()] {
			continue
		}
		if os.Remove(filepath.Join(PluginDir(dir), e.Name())) == nil {
			removed = append(removed, e.Name())
		}
	}
	return removed
}

func pluginStatus(placed []placedPlugin, running map[string]string, err error) []v1alpha1.OpenBaoServerPluginStatus {
	var out []v1alpha1.OpenBaoServerPluginStatus
	for _, p := range placed {
		mount := strings.Trim(p.spec.MountPath, "/")
		st := v1alpha1.OpenBaoServerPluginStatus{Name: p.spec.Name, Type: p.spec.Type, Sha256: p.plugin.SHA256, Command: p.plugin.Command,
			MountPath: mount, RunningSha256: running[mount]}
		switch {
		case err != nil:
			st.Message = err.Error()
		case st.RunningSha256 != st.Sha256:
			st.Message = "the mount does not run the declared sha256 yet"
		}
		out = append(out, st)
	}
	return out
}