- 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.
142 lines
4.2 KiB
Go
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
|
|
}
|