1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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
}