Audit API token lifecycle actions

This commit is contained in:
Joe Julian
2026-04-10 23:40:34 -07:00
parent 8dfba6e94f
commit 533fb2d550
4 changed files with 62 additions and 3 deletions
+6
View File
@@ -16,6 +16,12 @@ const (
EventApprovalDenied EventType = "approval_denied"
EventApprovalCanceled EventType = "approval_canceled"
EventApprovalTimedOut EventType = "approval_timed_out"
EventTokenIssued EventType = "token_issued"
EventTokenUpdated EventType = "token_updated"
EventTokenRotated EventType = "token_rotated"
EventTokenDisabled EventType = "token_disabled"
EventTokenRevoked EventType = "token_revoked"
EventTokenDeleted EventType = "token_deleted"
EventAutofillFound EventType = "autofill_found"
EventAutofillAmbiguous EventType = "autofill_ambiguous"
EventAutofillBlocked EventType = "autofill_blocked"
+34 -2
View File
@@ -8,6 +8,7 @@ import (
"time"
"git.julianfamily.org/keepassgo/internal/apiapproval"
"git.julianfamily.org/keepassgo/internal/apiaudit"
"git.julianfamily.org/keepassgo/internal/apitokens"
"git.julianfamily.org/keepassgo/internal/vault"
"git.julianfamily.org/keepassgo/internal/webdav"
@@ -101,6 +102,7 @@ type ApprovalManager interface {
type State struct {
Session CurrentSession
Approvals ApprovalManager
AuditLog *apiaudit.Log
Section Section
CurrentPath []string
SearchQuery string
@@ -195,6 +197,7 @@ func (s *State) IssueAPIToken(name, clientName string, expiresAt *time.Time, now
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
s.recordTokenAudit(apiaudit.EventTokenIssued, token, "issued API token")
return token, secret, nil
}
@@ -218,6 +221,7 @@ func (s *State) RotateAPIToken(id string, now time.Time) (apitokens.Token, strin
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
s.recordTokenAudit(apiaudit.EventTokenRotated, token, "rotated API token")
return token, secret, nil
}
@@ -233,6 +237,7 @@ func (s *State) UpsertAPIToken(token apitokens.Token) error {
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
s.recordTokenAudit(apiaudit.EventTokenUpdated, token, "updated API token")
return nil
}
@@ -249,9 +254,11 @@ func (s *State) DisableAPIToken(id string) error {
if err != nil {
return err
}
apitokens.Upsert(&model, apitokens.Disable(token))
token = apitokens.Disable(token)
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
s.recordTokenAudit(apiaudit.EventTokenDisabled, token, "disabled API token")
return nil
}
@@ -268,9 +275,11 @@ func (s *State) RevokeAPIToken(id string, when time.Time) error {
if err != nil {
return err
}
apitokens.Upsert(&model, apitokens.Revoke(token, when))
token = apitokens.Revoke(token, when)
apitokens.Upsert(&model, token)
session.Replace(model)
s.Dirty = true
s.recordTokenAudit(apiaudit.EventTokenRevoked, token, "revoked API token")
return nil
}
@@ -283,14 +292,37 @@ func (s *State) DeleteAPIToken(id string) error {
if err != nil {
return err
}
token, err := apitokens.Find(model, id)
if err != nil {
return err
}
if err := apitokens.Delete(&model, id); err != nil {
return err
}
session.Replace(model)
s.Dirty = true
s.recordTokenAudit(apiaudit.EventTokenDeleted, token, "deleted API token")
return nil
}
func (s *State) recordTokenAudit(eventType apiaudit.EventType, token apitokens.Token, message string) {
if s.AuditLog == nil {
return
}
s.AuditLog.Record(apiaudit.Event{
Type: eventType,
TokenID: token.ID,
TokenName: token.Name,
ClientName: token.ClientName,
Resource: apitokens.Resource{
Kind: apitokens.ResourceEntry,
Path: apitokens.EntryPath,
EntryID: token.ID,
},
Message: message,
})
}
func (s *State) SecuritySettings() (vault.SecuritySettings, error) {
security, ok := s.Session.(SecurityConfigurableSession)
if !ok {
+21 -1
View File
@@ -7,6 +7,7 @@ import (
"time"
"git.julianfamily.org/keepassgo/internal/apiapproval"
"git.julianfamily.org/keepassgo/internal/apiaudit"
"git.julianfamily.org/keepassgo/internal/apitokens"
"git.julianfamily.org/keepassgo/internal/session"
"git.julianfamily.org/keepassgo/internal/vault"
@@ -109,7 +110,8 @@ func TestIssueRotateDisableRevokeAndDeleteAPIToken(t *testing.T) {
t.Parallel()
session := &mutableStubSession{model: vault.Model{}}
state := State{Session: session}
auditLog := apiaudit.New(10)
state := State{Session: session, AuditLog: auditLog}
now := time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
expiresAt := now.Add(24 * time.Hour)
@@ -162,6 +164,24 @@ func TestIssueRotateDisableRevokeAndDeleteAPIToken(t *testing.T) {
if len(tokens) != 0 {
t.Fatalf("APITokens() after delete = %#v, want empty", tokens)
}
events := auditLog.Events()
if len(events) != 5 {
t.Fatalf("len(AuditLog.Events()) = %d, want 5", len(events))
}
if events[0].Type != apiaudit.EventTokenDeleted ||
events[1].Type != apiaudit.EventTokenRevoked ||
events[2].Type != apiaudit.EventTokenDisabled ||
events[3].Type != apiaudit.EventTokenRotated ||
events[4].Type != apiaudit.EventTokenIssued {
t.Fatalf("AuditLog.Events() types = %#v, want deleted/revoked/disabled/rotated/issued", events)
}
if events[0].TokenID != issued.ID || events[0].Resource.EntryID != issued.ID {
t.Fatalf("delete audit event = %#v, want token/resource id %q", events[0], issued.ID)
}
if events[4].TokenName != "CLI" || events[4].ClientName != "grpc-cli" {
t.Fatalf("issued audit event = %#v, want CLI/grpc-cli metadata", events[4])
}
}
func TestRemoteProfilesReturnsVaultProfiles(t *testing.T) {
+1
View File
@@ -75,6 +75,7 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
} else if host != nil {
ui.apiHost = host
ui.auditLog = host.Server().AuditLog()
ui.state.AuditLog = ui.auditLog
ui.grpcAddress = host.Address()
ui.state.Approvals = &uiApprovalManager{server: host.Server()}
defer func() { _ = host.Stop() }()