Files
vault-tui/internal/token/lookup.go
T
f.weber ae30ba1240 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.
2026-08-14 11:09:03 +02:00

142 lines
4.2 KiB
Go

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
}