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,141 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
|
||||
"git.morlana.online/f.weber/vault-tui/internal/vault"
|
||||
)
|
||||
|
||||
// ErrTokenInvalid means the token was resolved from storage but Vault
|
||||
// rejects it (expired, revoked, or simply wrong) — distinct from a
|
||||
// transport failure, so callers can route straight back to the auth
|
||||
// screen instead of showing a generic error.
|
||||
var ErrTokenInvalid = errors.New("token is invalid or expired")
|
||||
|
||||
// Info is the normalised result of auth/token/lookup-self.
|
||||
type Info struct {
|
||||
Accessor string
|
||||
DisplayName string
|
||||
EntityID string
|
||||
Policies []string
|
||||
IdentityPolicies []string
|
||||
Type string // "service" | "batch"
|
||||
Path string
|
||||
NamespacePath string
|
||||
Meta map[string]string
|
||||
|
||||
Renewable bool
|
||||
Orphan bool
|
||||
NumUses int
|
||||
TTL time.Duration
|
||||
CreationTTL time.Duration
|
||||
IssueTime time.Time
|
||||
ExpireTime *time.Time // nil => root token / never expires
|
||||
}
|
||||
|
||||
// String never includes the token value itself (Info never carries the raw
|
||||
// token in the first place), but is defined for consistent %v behaviour
|
||||
// alongside Resolved.
|
||||
func (i *Info) String() string {
|
||||
if i == nil {
|
||||
return "<no token info>"
|
||||
}
|
||||
return fmt.Sprintf("<token accessor=%s policies=%v>", i.Accessor, i.Policies)
|
||||
}
|
||||
|
||||
// Lookup calls auth/token/lookup-self and normalises the response. A 403
|
||||
// is reported as ErrTokenInvalid rather than propagated as a raw API error.
|
||||
func Lookup(ctx context.Context, c *api.Client) (*Info, error) {
|
||||
sec, err := c.Logical().ReadWithContext(ctx, "auth/token/lookup-self")
|
||||
if err != nil {
|
||||
kind, _ := vault.Classify(err)
|
||||
if kind == vault.ErrForbidden || kind == vault.ErrUnauthorized {
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if sec == nil || sec.Data == nil {
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
return infoFromSecret(sec), nil
|
||||
}
|
||||
|
||||
func infoFromSecret(sec *api.Secret) *Info {
|
||||
i := &Info{}
|
||||
i.Accessor, _ = sec.TokenAccessor()
|
||||
i.Policies, _ = sec.TokenPolicies()
|
||||
i.Renewable, _ = sec.TokenIsRenewable()
|
||||
i.TTL, _ = sec.TokenTTL()
|
||||
i.NumUses, _ = sec.TokenRemainingUses()
|
||||
i.Meta, _ = sec.TokenMetadata()
|
||||
|
||||
d := sec.Data
|
||||
i.DisplayName, _ = d["display_name"].(string)
|
||||
i.EntityID, _ = d["entity_id"].(string)
|
||||
i.Type, _ = d["type"].(string)
|
||||
i.Path, _ = d["path"].(string)
|
||||
i.NamespacePath, _ = d["namespace_path"].(string)
|
||||
i.Orphan, _ = d["orphan"].(bool)
|
||||
|
||||
if idp, ok := d["identity_policies"].([]interface{}); ok {
|
||||
for _, p := range idp {
|
||||
if s, ok := p.(string); ok {
|
||||
i.IdentityPolicies = append(i.IdentityPolicies, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if ct, ok := vault.AsInt(d["creation_ttl"]); ok {
|
||||
i.CreationTTL = time.Duration(ct) * time.Second
|
||||
}
|
||||
if it, ok := d["issue_time"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, it); err == nil {
|
||||
i.IssueTime = t
|
||||
}
|
||||
}
|
||||
// expire_time is absent/null for root tokens; handle that explicitly so
|
||||
// callers can render "never" instead of the zero time.
|
||||
if et, ok := d["expire_time"].(string); ok && et != "" {
|
||||
if t, err := time.Parse(time.RFC3339Nano, et); err == nil {
|
||||
i.ExpireTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// RenewSelf calls auth/token/renew-self. increment == 0 lets Vault choose
|
||||
// its own TTL extension.
|
||||
func RenewSelf(ctx context.Context, c *api.Client, increment time.Duration) (*Info, error) {
|
||||
body := map[string]interface{}{}
|
||||
if increment > 0 {
|
||||
body["increment"] = int(increment.Seconds())
|
||||
}
|
||||
sec, err := c.Logical().WriteWithContext(ctx, "auth/token/renew-self", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sec == nil {
|
||||
return nil, fmt.Errorf("renew-self: empty response")
|
||||
}
|
||||
// renew-self's response shape is an auth response (sec.Auth), not a
|
||||
// lookup-self data map; re-lookup so callers get one consistent Info shape.
|
||||
return Lookup(ctx, c)
|
||||
}
|
||||
|
||||
// RevokeSelf calls auth/token/revoke-self and then erases the given store
|
||||
// (best-effort: erase runs even if the API call itself already invalidated
|
||||
// the token from Vault's perspective).
|
||||
func RevokeSelf(ctx context.Context, c *api.Client, s Store) error {
|
||||
_, err := c.Logical().WriteWithContext(ctx, "auth/token/revoke-self", nil)
|
||||
if s != nil {
|
||||
if eraseErr := s.Erase(ctx); eraseErr != nil && err == nil {
|
||||
err = eraseErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user