feat(vault-tui): implement KV client and service for managing secrets
- 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.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
// Package auth defines the auth-method abstraction vault-tui uses for every
|
||||
// login flow. A Method declares only which fields it needs (Fields); the
|
||||
// TUI and headless CLI both render that declaration generically — a
|
||||
// textinput form in the TUI, a TTY prompt or env/flag lookup on the CLI —
|
||||
// so adding a new method never requires UI code (see prefill.go and
|
||||
// registry.go).
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
// FieldKind tells a renderer how to present a field.
|
||||
type FieldKind uint8
|
||||
|
||||
const (
|
||||
FieldText FieldKind = iota
|
||||
FieldSecret
|
||||
FieldBool
|
||||
FieldSelect
|
||||
FieldPath
|
||||
FieldInt
|
||||
)
|
||||
|
||||
// Field declares one credential input a Method needs.
|
||||
type Field struct {
|
||||
Name string
|
||||
Label string
|
||||
Help string
|
||||
Kind FieldKind
|
||||
Required bool
|
||||
Default string
|
||||
Options []string
|
||||
|
||||
// EnvFallback lists env vars consulted during Prefill, in order, before
|
||||
// falling back to config params. Mirrors Vault CLI behaviour (e.g.
|
||||
// VAULT_AUTH_GITHUB_TOKEN-style env overrides).
|
||||
EnvFallback []string
|
||||
|
||||
// ConfigKey is the key looked up in profile.auth.params. Empty => Name.
|
||||
ConfigKey string
|
||||
|
||||
// Validate runs before Login and must not have side effects.
|
||||
Validate func(value string) error
|
||||
}
|
||||
|
||||
// Credentials is a filled-in form: field name -> value.
|
||||
type Credentials map[string]string
|
||||
|
||||
func (c Credentials) Get(name string) string { return c[name] }
|
||||
func (c Credentials) Has(name string) bool {
|
||||
v, ok := c[name]
|
||||
return ok && v != ""
|
||||
}
|
||||
|
||||
// Redacted returns a copy safe for logs: every FieldSecret value becomes
|
||||
// "***". This is the only sanctioned way to log a Credentials map.
|
||||
func (c Credentials) Redacted(fields []Field) map[string]string {
|
||||
secret := map[string]bool{}
|
||||
for _, f := range fields {
|
||||
if f.Kind == FieldSecret {
|
||||
secret[f.Name] = true
|
||||
}
|
||||
}
|
||||
out := make(map[string]string, len(c))
|
||||
for k, v := range c {
|
||||
if secret[k] {
|
||||
out[k] = "***"
|
||||
} else {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// EventKind classifies out-of-band progress from a long-running login (OIDC
|
||||
// browser wait, Okta push polling).
|
||||
type EventKind uint8
|
||||
|
||||
const (
|
||||
EventStatus EventKind = iota
|
||||
EventOpenURL
|
||||
EventWarning
|
||||
)
|
||||
|
||||
// Event is one piece of progress emitted on Request.Events.
|
||||
type Event struct {
|
||||
Kind EventKind
|
||||
Message string
|
||||
URL string
|
||||
}
|
||||
|
||||
// Request is everything a Method needs to perform one login.
|
||||
type Request struct {
|
||||
Mount string
|
||||
Namespace string
|
||||
Creds Credentials
|
||||
// Events, if non-nil, receives progress notifications. Login must send
|
||||
// non-blockingly (see emit) so a stalled consumer can never deadlock it.
|
||||
Events chan<- Event
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// Emit sends a progress event to Request.Events, non-blocking. Safe to
|
||||
// call even when Events is nil (a headless caller that doesn't want
|
||||
// progress) or when nobody is currently draining the channel.
|
||||
func (r Request) Emit(e Event) {
|
||||
if r.Events == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case r.Events <- e:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Result is the normalised outcome of a login.
|
||||
type Result struct {
|
||||
Secret *api.Secret
|
||||
Token string
|
||||
Accessor string
|
||||
Renewable bool
|
||||
TTL time.Duration
|
||||
Policies []string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func (r *Result) String() string {
|
||||
if r == nil {
|
||||
return "<nil result>"
|
||||
}
|
||||
return "<login result accessor=" + r.Accessor + ">"
|
||||
}
|
||||
func (r *Result) GoString() string { return r.String() }
|
||||
|
||||
// NewResult normalises an *api.Secret returned by a successful Login.
|
||||
func NewResult(sec *api.Secret, namespace string) (*Result, error) {
|
||||
if sec == nil || sec.Auth == nil {
|
||||
return nil, errNoAuth
|
||||
}
|
||||
ttl, _ := sec.TokenTTL()
|
||||
policies, _ := sec.TokenPolicies()
|
||||
accessor, _ := sec.TokenAccessor()
|
||||
return &Result{
|
||||
Secret: sec,
|
||||
Token: sec.Auth.ClientToken,
|
||||
Accessor: accessor,
|
||||
Renewable: sec.Auth.Renewable,
|
||||
TTL: ttl,
|
||||
Policies: policies,
|
||||
Namespace: namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Method is the single abstraction every auth method implements.
|
||||
//
|
||||
// Contract: Fields() is pure and cheap. Login must respect ctx cancellation
|
||||
// and must never read os.Stdin — interactive prompting is the caller's job
|
||||
// (TUI form or CLI prompt), driven by Fields()/Missing(). Login must not
|
||||
// mutate client's token; token persistence is internal/token's job.
|
||||
type Method interface {
|
||||
Name() string
|
||||
DisplayName() string
|
||||
DefaultMount() string
|
||||
Fields() []Field
|
||||
Login(ctx context.Context, client *api.Client, req Request) (*api.Secret, error)
|
||||
}
|
||||
|
||||
// Describable is optional; implemented by methods with extra help text.
|
||||
type Describable interface{ Description() string }
|
||||
Reference in New Issue
Block a user