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 }) }