Audit API token lifecycle actions
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() }()
|
||||
|
||||
Reference in New Issue
Block a user