Audit API token lifecycle actions
This commit is contained in:
@@ -16,6 +16,12 @@ const (
|
|||||||
EventApprovalDenied EventType = "approval_denied"
|
EventApprovalDenied EventType = "approval_denied"
|
||||||
EventApprovalCanceled EventType = "approval_canceled"
|
EventApprovalCanceled EventType = "approval_canceled"
|
||||||
EventApprovalTimedOut EventType = "approval_timed_out"
|
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"
|
EventAutofillFound EventType = "autofill_found"
|
||||||
EventAutofillAmbiguous EventType = "autofill_ambiguous"
|
EventAutofillAmbiguous EventType = "autofill_ambiguous"
|
||||||
EventAutofillBlocked EventType = "autofill_blocked"
|
EventAutofillBlocked EventType = "autofill_blocked"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.julianfamily.org/keepassgo/internal/apiapproval"
|
"git.julianfamily.org/keepassgo/internal/apiapproval"
|
||||||
|
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||||
"git.julianfamily.org/keepassgo/internal/vault"
|
"git.julianfamily.org/keepassgo/internal/vault"
|
||||||
"git.julianfamily.org/keepassgo/internal/webdav"
|
"git.julianfamily.org/keepassgo/internal/webdav"
|
||||||
@@ -101,6 +102,7 @@ type ApprovalManager interface {
|
|||||||
type State struct {
|
type State struct {
|
||||||
Session CurrentSession
|
Session CurrentSession
|
||||||
Approvals ApprovalManager
|
Approvals ApprovalManager
|
||||||
|
AuditLog *apiaudit.Log
|
||||||
Section Section
|
Section Section
|
||||||
CurrentPath []string
|
CurrentPath []string
|
||||||
SearchQuery string
|
SearchQuery string
|
||||||
@@ -195,6 +197,7 @@ func (s *State) IssueAPIToken(name, clientName string, expiresAt *time.Time, now
|
|||||||
apitokens.Upsert(&model, token)
|
apitokens.Upsert(&model, token)
|
||||||
session.Replace(model)
|
session.Replace(model)
|
||||||
s.Dirty = true
|
s.Dirty = true
|
||||||
|
s.recordTokenAudit(apiaudit.EventTokenIssued, token, "issued API token")
|
||||||
return token, secret, nil
|
return token, secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +221,7 @@ func (s *State) RotateAPIToken(id string, now time.Time) (apitokens.Token, strin
|
|||||||
apitokens.Upsert(&model, token)
|
apitokens.Upsert(&model, token)
|
||||||
session.Replace(model)
|
session.Replace(model)
|
||||||
s.Dirty = true
|
s.Dirty = true
|
||||||
|
s.recordTokenAudit(apiaudit.EventTokenRotated, token, "rotated API token")
|
||||||
return token, secret, nil
|
return token, secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +237,7 @@ func (s *State) UpsertAPIToken(token apitokens.Token) error {
|
|||||||
apitokens.Upsert(&model, token)
|
apitokens.Upsert(&model, token)
|
||||||
session.Replace(model)
|
session.Replace(model)
|
||||||
s.Dirty = true
|
s.Dirty = true
|
||||||
|
s.recordTokenAudit(apiaudit.EventTokenUpdated, token, "updated API token")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,9 +254,11 @@ func (s *State) DisableAPIToken(id string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
apitokens.Upsert(&model, apitokens.Disable(token))
|
token = apitokens.Disable(token)
|
||||||
|
apitokens.Upsert(&model, token)
|
||||||
session.Replace(model)
|
session.Replace(model)
|
||||||
s.Dirty = true
|
s.Dirty = true
|
||||||
|
s.recordTokenAudit(apiaudit.EventTokenDisabled, token, "disabled API token")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +275,11 @@ func (s *State) RevokeAPIToken(id string, when time.Time) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
apitokens.Upsert(&model, apitokens.Revoke(token, when))
|
token = apitokens.Revoke(token, when)
|
||||||
|
apitokens.Upsert(&model, token)
|
||||||
session.Replace(model)
|
session.Replace(model)
|
||||||
s.Dirty = true
|
s.Dirty = true
|
||||||
|
s.recordTokenAudit(apiaudit.EventTokenRevoked, token, "revoked API token")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,14 +292,37 @@ func (s *State) DeleteAPIToken(id string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
token, err := apitokens.Find(model, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := apitokens.Delete(&model, id); err != nil {
|
if err := apitokens.Delete(&model, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
session.Replace(model)
|
session.Replace(model)
|
||||||
s.Dirty = true
|
s.Dirty = true
|
||||||
|
s.recordTokenAudit(apiaudit.EventTokenDeleted, token, "deleted API token")
|
||||||
return nil
|
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) {
|
func (s *State) SecuritySettings() (vault.SecuritySettings, error) {
|
||||||
security, ok := s.Session.(SecurityConfigurableSession)
|
security, ok := s.Session.(SecurityConfigurableSession)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.julianfamily.org/keepassgo/internal/apiapproval"
|
"git.julianfamily.org/keepassgo/internal/apiapproval"
|
||||||
|
"git.julianfamily.org/keepassgo/internal/apiaudit"
|
||||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||||
"git.julianfamily.org/keepassgo/internal/session"
|
"git.julianfamily.org/keepassgo/internal/session"
|
||||||
"git.julianfamily.org/keepassgo/internal/vault"
|
"git.julianfamily.org/keepassgo/internal/vault"
|
||||||
@@ -109,7 +110,8 @@ func TestIssueRotateDisableRevokeAndDeleteAPIToken(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
session := &mutableStubSession{model: vault.Model{}}
|
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)
|
now := time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
|
||||||
expiresAt := now.Add(24 * time.Hour)
|
expiresAt := now.Add(24 * time.Hour)
|
||||||
|
|
||||||
@@ -162,6 +164,24 @@ func TestIssueRotateDisableRevokeAndDeleteAPIToken(t *testing.T) {
|
|||||||
if len(tokens) != 0 {
|
if len(tokens) != 0 {
|
||||||
t.Fatalf("APITokens() after delete = %#v, want empty", tokens)
|
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) {
|
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 {
|
} else if host != nil {
|
||||||
ui.apiHost = host
|
ui.apiHost = host
|
||||||
ui.auditLog = host.Server().AuditLog()
|
ui.auditLog = host.Server().AuditLog()
|
||||||
|
ui.state.AuditLog = ui.auditLog
|
||||||
ui.grpcAddress = host.Address()
|
ui.grpcAddress = host.Address()
|
||||||
ui.state.Approvals = &uiApprovalManager{server: host.Server()}
|
ui.state.Approvals = &uiApprovalManager{server: host.Server()}
|
||||||
defer func() { _ = host.Stop() }()
|
defer func() { _ = host.Stop() }()
|
||||||
|
|||||||
Reference in New Issue
Block a user