Add token management helpers for API credentials

This commit is contained in:
Joe Julian
2026-03-29 23:17:34 -07:00
parent 74dfe3f3d0
commit e9eca73336
4 changed files with 270 additions and 0 deletions
+121
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"slices"
"strings"
"time"
"git.julianfamily.org/keepassgo/apiapproval"
"git.julianfamily.org/keepassgo/apitokens"
@@ -115,6 +116,126 @@ func (s *State) ResolveApproval(id string, outcome apiapproval.Outcome) error {
return err
}
func (s *State) APITokens() ([]apitokens.Token, error) {
model, err := s.currentModel()
if err != nil {
return nil, err
}
return apitokens.Entries(model)
}
func (s *State) IssueAPIToken(name, clientName string, expiresAt *time.Time, now time.Time) (apitokens.Token, string, error) {
session, ok := s.Session.(MutableSession)
if !ok {
return apitokens.Token{}, "", fmt.Errorf("session is not mutable")
}
model, err := session.Current()
if err != nil {
return apitokens.Token{}, "", err
}
token, secret, err := apitokens.Issue(name, clientName, expiresAt, now)
if err != nil {
return apitokens.Token{}, "", err
}
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
return token, secret, nil
}
func (s *State) RotateAPIToken(id string, now time.Time) (apitokens.Token, string, error) {
session, ok := s.Session.(MutableSession)
if !ok {
return apitokens.Token{}, "", fmt.Errorf("session is not mutable")
}
model, err := session.Current()
if err != nil {
return apitokens.Token{}, "", err
}
token, err := apitokens.Find(model, id)
if err != nil {
return apitokens.Token{}, "", err
}
token, secret, err := apitokens.Rotate(token, now)
if err != nil {
return apitokens.Token{}, "", err
}
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
return token, secret, nil
}
func (s *State) UpsertAPIToken(token apitokens.Token) error {
session, ok := s.Session.(MutableSession)
if !ok {
return fmt.Errorf("session is not mutable")
}
model, err := session.Current()
if err != nil {
return err
}
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
return nil
}
func (s *State) DisableAPIToken(id string) error {
session, ok := s.Session.(MutableSession)
if !ok {
return fmt.Errorf("session is not mutable")
}
model, err := session.Current()
if err != nil {
return err
}
token, err := apitokens.Find(model, id)
if err != nil {
return err
}
apitokens.Upsert(&model, apitokens.Disable(token))
session.Replace(model)
s.Dirty = true
return nil
}
func (s *State) RevokeAPIToken(id string, when time.Time) error {
session, ok := s.Session.(MutableSession)
if !ok {
return fmt.Errorf("session is not mutable")
}
model, err := session.Current()
if err != nil {
return err
}
token, err := apitokens.Find(model, id)
if err != nil {
return err
}
apitokens.Upsert(&model, apitokens.Revoke(token, when))
session.Replace(model)
s.Dirty = true
return nil
}
func (s *State) DeleteAPIToken(id string) error {
session, ok := s.Session.(MutableSession)
if !ok {
return fmt.Errorf("session is not mutable")
}
model, err := session.Current()
if err != nil {
return err
}
if err := apitokens.Delete(&model, id); err != nil {
return err
}
session.Replace(model)
s.Dirty = true
return nil
}
func (s *State) ShowSection(section Section) {
s.Section = section
s.CurrentPath = nil
+60
View File
@@ -4,6 +4,7 @@ import (
"errors"
"slices"
"testing"
"time"
"git.julianfamily.org/keepassgo/apiapproval"
"git.julianfamily.org/keepassgo/apitokens"
@@ -73,6 +74,65 @@ func TestResolveApprovalDelegatesToManager(t *testing.T) {
}
}
func TestIssueRotateDisableRevokeAndDeleteAPIToken(t *testing.T) {
t.Parallel()
session := &mutableStubSession{model: vault.Model{}}
state := State{Session: session}
now := time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
expiresAt := now.Add(24 * time.Hour)
issued, secret, err := state.IssueAPIToken("CLI", "grpc-cli", &expiresAt, now)
if err != nil {
t.Fatalf("IssueAPIToken() error = %v", err)
}
if issued.ID == "" || secret == "" {
t.Fatalf("IssueAPIToken() = %#v, %q, want non-empty id and secret", issued, secret)
}
tokens, err := state.APITokens()
if err != nil {
t.Fatalf("APITokens() error = %v", err)
}
if len(tokens) != 1 || tokens[0].ID != issued.ID {
t.Fatalf("APITokens() = %#v, want issued token", tokens)
}
rotated, rotatedSecret, err := state.RotateAPIToken(issued.ID, now.Add(time.Hour))
if err != nil {
t.Fatalf("RotateAPIToken() error = %v", err)
}
if rotated.ID != issued.ID || rotatedSecret == "" || rotatedSecret == secret {
t.Fatalf("RotateAPIToken() = %#v, %q, want same id and new secret", rotated, rotatedSecret)
}
if err := state.DisableAPIToken(issued.ID); err != nil {
t.Fatalf("DisableAPIToken() error = %v", err)
}
if err := state.RevokeAPIToken(issued.ID, now.Add(2*time.Hour)); err != nil {
t.Fatalf("RevokeAPIToken() error = %v", err)
}
tokens, err = state.APITokens()
if err != nil {
t.Fatalf("APITokens() after revoke error = %v", err)
}
if len(tokens) != 1 || !tokens[0].Disabled || tokens[0].RevokedAt == nil {
t.Fatalf("APITokens() after revoke = %#v, want disabled revoked token", tokens)
}
if err := state.DeleteAPIToken(issued.ID); err != nil {
t.Fatalf("DeleteAPIToken() error = %v", err)
}
tokens, err = state.APITokens()
if err != nil {
t.Fatalf("APITokens() after delete error = %v", err)
}
if len(tokens) != 0 {
t.Fatalf("APITokens() after delete = %#v, want empty", tokens)
}
}
func TestVisibleEntriesUsesGlobalSearchWhenQueryPresent(t *testing.T) {
t.Parallel()