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 "" } return fmt.Sprintf("", 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 }