Record audit events for API token authorization

This commit is contained in:
Joe Julian
2026-03-29 23:19:54 -07:00
parent e6b33134c9
commit 980d30f6c2
4 changed files with 213 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
package apiaudit
import (
"slices"
"sync"
"time"
"git.julianfamily.org/keepassgo/apitokens"
)
type EventType string
const (
EventApprovalRequested EventType = "approval_requested"
EventApprovalAllowed EventType = "approval_allowed"
EventApprovalDenied EventType = "approval_denied"
EventApprovalCanceled EventType = "approval_canceled"
EventApprovalTimedOut EventType = "approval_timed_out"
EventAuthRejected EventType = "auth_rejected"
)
type Event struct {
Type EventType
At time.Time
TokenID string
TokenName string
ClientName string
Operation apitokens.Operation
Resource apitokens.Resource
Message string
}
type Log struct {
mu sync.Mutex
max int
now func() time.Time
events []Event
}
func New(max int) *Log {
if max < 1 {
max = 1
}
return &Log{
max: max,
now: func() time.Time {
return time.Now().UTC()
},
}
}
func (l *Log) Record(event Event) {
if l == nil {
return
}
l.mu.Lock()
defer l.mu.Unlock()
if event.At.IsZero() {
event.At = l.now()
}
l.events = append([]Event{event}, l.events...)
if len(l.events) > l.max {
l.events = l.events[:l.max]
}
}
func (l *Log) Events() []Event {
if l == nil {
return nil
}
l.mu.Lock()
defer l.mu.Unlock()
return slices.Clone(l.events)
}
+49
View File
@@ -0,0 +1,49 @@
package apiaudit
import (
"testing"
"time"
"git.julianfamily.org/keepassgo/apitokens"
)
func TestLogKeepsNewestEventsWithinBound(t *testing.T) {
t.Parallel()
log := New(2)
log.now = func() time.Time { return time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC) }
log.Record(Event{Type: EventApprovalRequested, TokenID: "token-1"})
log.Record(Event{Type: EventApprovalAllowed, TokenID: "token-2"})
log.Record(Event{Type: EventApprovalDenied, TokenID: "token-3"})
events := log.Events()
if len(events) != 2 {
t.Fatalf("len(Events()) = %d, want 2", len(events))
}
if events[0].TokenID != "token-3" || events[1].TokenID != "token-2" {
t.Fatalf("Events() = %#v, want newest-first bounded list", events)
}
}
func TestLogPreservesRecordedMetadata(t *testing.T) {
t.Parallel()
log := New(5)
log.Record(Event{
Type: EventApprovalRequested,
TokenID: "token-1",
TokenName: "CLI",
ClientName: "grpc-cli",
Operation: apitokens.OperationListEntries,
Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root", "Internet"}},
Message: "prompted for access",
})
events := log.Events()
if len(events) != 1 {
t.Fatalf("len(Events()) = %d, want 1", len(events))
}
if events[0].Operation != apitokens.OperationListEntries || events[0].Message != "prompted for access" {
t.Fatalf("Events()[0] = %#v, want preserved metadata", events[0])
}
}