Some checks failed
CI / build-and-test (push) Failing after -31m49s
* Add tool tracking to AccessTracker and metrics * Update tests to validate tool tracking functionality * Modify middleware to record tool usage * Enhance observability with tool context * Update UI to display unique tools in metrics
174 lines
3.9 KiB
Go
174 lines
3.9 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"`
|
|
}
|
|
|
|
type AccessTracker struct {
|
|
mu sync.RWMutex
|
|
entries map[string]AccessSnapshot
|
|
ipCounts map[string]int
|
|
agentCounts map[string]int
|
|
toolCounts map[string]int
|
|
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),
|
|
}
|
|
}
|
|
|
|
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]++
|
|
}
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
func (t *AccessTracker) Metrics(topN int) AccessMetrics {
|
|
if t == nil {
|
|
return AccessMetrics{}
|
|
}
|
|
if topN <= 0 {
|
|
topN = 10
|
|
}
|
|
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
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),
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|