Files
amcs/internal/auth/access_tracker.go
T
warkanum 0227912325
CI / build-and-test (push) Failing after 0s
feat(auth): add recent activity logging to access tracker
* Introduced AccessLogEntry type for logging access details
* Updated AccessTracker to maintain a recent activity log
* Modified Metrics to include recent activity log in response
* Added RecentActivityLog component to display logged activities
2026-05-24 16:36:59 +02:00

205 lines
4.8 KiB
Go

package auth
import (
"net"
"sort"
"strings"
"sync"
"time"
)
type AccessSnapshot struct {
KeyID string `json:"key_id"`
LastPath string `json:"last_path"`
RemoteAddr string `json:"remote_addr"`
UserAgent string `json:"user_agent"`
RequestCount int `json:"request_count"`
LastAccessedAt time.Time `json:"last_accessed_at"`
}
const maxRecentLog = 100
type AccessLogEntry struct {
Timestamp time.Time `json:"timestamp"`
KeyID string `json:"key_id"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Tool string `json:"tool"`
Path string `json:"path"`
}
type AccessTracker struct {
mu sync.RWMutex
entries map[string]AccessSnapshot
ipCounts map[string]int
agentCounts map[string]int
toolCounts map[string]int
recentLog []AccessLogEntry
totalRequests int
}
func NewAccessTracker() *AccessTracker {
return &AccessTracker{
entries: make(map[string]AccessSnapshot),
ipCounts: make(map[string]int),
agentCounts: make(map[string]int),
toolCounts: make(map[string]int),
recentLog: make([]AccessLogEntry, 0, maxRecentLog),
}
}
func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent, toolName string, now time.Time) {
if t == nil || keyID == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
normalizedRemoteAddr := normalizeRemoteAddr(remoteAddr)
entry := t.entries[keyID]
entry.KeyID = keyID
entry.LastPath = path
entry.RemoteAddr = normalizedRemoteAddr
entry.UserAgent = userAgent
entry.LastAccessedAt = now.UTC()
entry.RequestCount++
t.entries[keyID] = entry
t.totalRequests++
if normalizedRemoteAddr != "" {
t.ipCounts[normalizedRemoteAddr]++
}
if userAgent != "" {
t.agentCounts[userAgent]++
}
if tool := strings.TrimSpace(toolName); tool != "" {
t.toolCounts[tool]++
}
logEntry := AccessLogEntry{
Timestamp: now.UTC(),
KeyID: keyID,
IP: normalizedRemoteAddr,
UserAgent: userAgent,
Tool: strings.TrimSpace(toolName),
Path: path,
}
t.recentLog = append([]AccessLogEntry{logEntry}, t.recentLog...)
if len(t.recentLog) > maxRecentLog {
t.recentLog = t.recentLog[:maxRecentLog]
}
}
func normalizeRemoteAddr(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
host, _, err := net.SplitHostPort(trimmed)
if err == nil {
return host
}
return trimmed
}
func (t *AccessTracker) Snapshot() []AccessSnapshot {
if t == nil {
return nil
}
t.mu.RLock()
defer t.mu.RUnlock()
items := make([]AccessSnapshot, 0, len(t.entries))
for _, entry := range t.entries {
items = append(items, entry)
}
sort.Slice(items, func(i, j int) bool {
return items[i].LastAccessedAt.After(items[j].LastAccessedAt)
})
return items
}
func (t *AccessTracker) ConnectedCount(now time.Time, window time.Duration) int {
if t == nil {
return 0
}
cutoff := now.UTC().Add(-window)
t.mu.RLock()
defer t.mu.RUnlock()
count := 0
for _, entry := range t.entries {
if !entry.LastAccessedAt.Before(cutoff) {
count++
}
}
return count
}
type RequestAggregate struct {
Key string `json:"key"`
RequestCount int `json:"request_count"`
}
type AccessMetrics struct {
TotalRequests int `json:"total_requests"`
UniquePrincipals int `json:"unique_principals"`
UniqueIPs int `json:"unique_ips"`
UniqueAgents int `json:"unique_agents"`
UniqueTools int `json:"unique_tools"`
TopIPs []RequestAggregate `json:"top_ips"`
TopAgents []RequestAggregate `json:"top_agents"`
TopTools []RequestAggregate `json:"top_tools"`
RecentLog []AccessLogEntry `json:"recent_log"`
}
func (t *AccessTracker) Metrics(topN int) AccessMetrics {
if t == nil {
return AccessMetrics{}
}
if topN <= 0 {
topN = 10
}
t.mu.RLock()
defer t.mu.RUnlock()
log := make([]AccessLogEntry, len(t.recentLog))
copy(log, t.recentLog)
return AccessMetrics{
TotalRequests: t.totalRequests,
UniquePrincipals: len(t.entries),
UniqueIPs: len(t.ipCounts),
UniqueAgents: len(t.agentCounts),
UniqueTools: len(t.toolCounts),
TopIPs: topAggregates(t.ipCounts, topN),
TopAgents: topAggregates(t.agentCounts, topN),
TopTools: topAggregates(t.toolCounts, topN),
RecentLog: log,
}
}
func topAggregates(items map[string]int, topN int) []RequestAggregate {
out := make([]RequestAggregate, 0, len(items))
for key, count := range items {
out = append(out, RequestAggregate{Key: key, RequestCount: count})
}
sort.Slice(out, func(i, j int) bool {
if out[i].RequestCount == out[j].RequestCount {
return out[i].Key < out[j].Key
}
return out[i].RequestCount > out[j].RequestCount
})
if len(out) > topN {
out = out[:topN]
}
return out
}