- Added internal/vault/client.go for creating a Vault client with configuration settings. - Introduced internal/vault/errors.go to classify Vault API errors for better UI handling. - Created internal/vault/kv.go to manage KV secrets, including listing, reading, writing, and deleting operations. - Implemented internal/vault/mounts.go to list and describe secret engine mounts. - Developed internal/vault/service.go to provide a unified entry point for Vault operations. - Added internal/vault/kv_test.go for comprehensive testing of KV operations. - Introduced internal/ui/toast.go for transient notifications in the UI. - Added renovate.json for dependency management and updates.
146 lines
3.9 KiB
Go
146 lines
3.9 KiB
Go
package vault
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
)
|
|
|
|
// EngineKind is what the UI needs to know to talk to a mount's data plane.
|
|
type EngineKind uint8
|
|
|
|
const (
|
|
EngineKVv1 EngineKind = iota
|
|
EngineKVv2
|
|
EngineOther
|
|
)
|
|
|
|
// Mount describes one secret engine mount that the current token can see.
|
|
type Mount struct {
|
|
Path string // e.g. "secret/" — always slash-terminated, as Vault returns it
|
|
Type string // "kv", "cubbyhole", "ssh", ...
|
|
Kind EngineKind
|
|
Description string
|
|
Accessor string
|
|
Local bool
|
|
Options map[string]string
|
|
}
|
|
|
|
// Supported reports whether internal/vault.KV (see kv.go) knows how to
|
|
// browse/read/write this mount. Cubbyhole is included: it behaves like a
|
|
// single-version KV v1 mount (no /data or /metadata split, no versioning).
|
|
func (m Mount) Supported() bool {
|
|
switch m.Type {
|
|
case "kv", "cubbyhole":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ListMounts returns every secret engine mount the current token is
|
|
// permitted to see.
|
|
//
|
|
// sys/mounts requires broad "sys" access and commonly returns 403 for
|
|
// ordinary tokens (verified against a real Vault instance in this
|
|
// project — see the plan's "Erkenntnisse aus der Zielumgebung"). The Vault
|
|
// web UI itself falls back to sys/internal/ui/mounts, which is scoped to
|
|
// exactly what the caller's token may use and is unauthenticated-safe to
|
|
// call broadly. We do the same: try sys/mounts first (it has richer
|
|
// `local`/accessor detail when it works), and fall back on any error.
|
|
func ListMounts(ctx context.Context, c *api.Client) ([]Mount, error) {
|
|
mounts, err := listViaSysMounts(ctx, c)
|
|
if err == nil {
|
|
return mounts, nil
|
|
}
|
|
mounts, ferr := listViaUIMounts(ctx, c)
|
|
if ferr != nil {
|
|
// Report the original sys/mounts error: it is usually the more
|
|
// informative one (e.g. "permission denied" vs. a generic parse
|
|
// failure), and callers use Classify() on it.
|
|
return nil, err
|
|
}
|
|
return mounts, nil
|
|
}
|
|
|
|
func listViaSysMounts(ctx context.Context, c *api.Client) ([]Mount, error) {
|
|
sec, err := c.Logical().ReadWithContext(ctx, "sys/mounts")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sec == nil {
|
|
return nil, fmt.Errorf("sys/mounts: empty response")
|
|
}
|
|
out := make([]Mount, 0, len(sec.Data))
|
|
for path, raw := range sec.Data {
|
|
entry, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = append(out, mountFromMap(path, entry))
|
|
}
|
|
sortMounts(out)
|
|
return out, nil
|
|
}
|
|
|
|
func listViaUIMounts(ctx context.Context, c *api.Client) ([]Mount, error) {
|
|
sec, err := c.Logical().ReadWithContext(ctx, "sys/internal/ui/mounts")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sec == nil {
|
|
return nil, fmt.Errorf("sys/internal/ui/mounts: empty response")
|
|
}
|
|
secretRaw, _ := sec.Data["secret"].(map[string]interface{})
|
|
out := make([]Mount, 0, len(secretRaw))
|
|
for path, raw := range secretRaw {
|
|
entry, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = append(out, mountFromMap(path, entry))
|
|
}
|
|
sortMounts(out)
|
|
return out, nil
|
|
}
|
|
|
|
func mountFromMap(path string, entry map[string]interface{}) Mount {
|
|
m := Mount{Path: path}
|
|
m.Type, _ = entry["type"].(string)
|
|
m.Description, _ = entry["description"].(string)
|
|
m.Accessor, _ = entry["accessor"].(string)
|
|
m.Local, _ = entry["local"].(bool)
|
|
if opts, ok := entry["options"].(map[string]interface{}); ok {
|
|
m.Options = make(map[string]string, len(opts))
|
|
for k, v := range opts {
|
|
if s, ok := v.(string); ok {
|
|
m.Options[k] = s
|
|
}
|
|
}
|
|
}
|
|
m.Kind = classifyEngine(m)
|
|
return m
|
|
}
|
|
|
|
func classifyEngine(m Mount) EngineKind {
|
|
switch m.Type {
|
|
case "kv":
|
|
if m.Options != nil && m.Options["version"] == "2" {
|
|
return EngineKVv2
|
|
}
|
|
return EngineKVv1
|
|
case "cubbyhole":
|
|
// Cubbyhole has no /data or /metadata split and no versioning — it
|
|
// behaves like a single-version KV v1 mount for our purposes.
|
|
return EngineKVv1
|
|
default:
|
|
return EngineOther
|
|
}
|
|
}
|
|
|
|
func sortMounts(m []Mount) {
|
|
sort.Slice(m, func(i, j int) bool { return m[i].Path < m[j].Path })
|
|
}
|