1
0
forked from baron/baron-sso

fix(audit): stop default read logging and dedupe dashboard timeline

- skip read audit logging unless a path is explicitly allowlisted
- exclude audit-facing endpoints from backend audit collection
- remove duplicate auth timeline fetch logic from dashboard screen
- add regression tests for default GET skip and dashboard timeline dedup

Co-Authored-By: First Fluke <our.first.fluke@gmail.com>
This commit is contained in:
Lectom C Han
2026-04-17 18:04:09 +09:00
parent b72d04f184
commit 114f203ecd
5 changed files with 94 additions and 107 deletions

View File

@@ -17,6 +17,7 @@ import (
type AuditConfig struct {
Repo domain.AuditRepository
ExcludePaths map[string]struct{}
ReadPaths map[string]struct{}
BodyDump bool
WorkerCount int
QueueSize int
@@ -30,9 +31,8 @@ func isNil(i any) bool {
return v.Kind() == reflect.Ptr && v.IsNil()
}
// AuditMiddleware provides comprehensive audit logging for all requests.
// It enforces strict logging for state-changing commands (POST, PUT, DELETE, PATCH)
// and best-effort logging for queries (GET, HEAD, OPTIONS).
// AuditMiddleware provides comprehensive audit logging for write requests by default.
// Read requests are skipped unless they are explicitly allowlisted in ReadPaths.
func AuditMiddleware(config AuditConfig) fiber.Handler {
// 0. Initialize Worker Pool for Async Logging
if config.WorkerCount <= 0 {
@@ -77,6 +77,9 @@ func AuditMiddleware(config AuditConfig) fiber.Handler {
if config.ExcludePaths == nil {
config.ExcludePaths = map[string]struct{}{}
}
if config.ReadPaths == nil {
config.ReadPaths = map[string]struct{}{}
}
return func(c *fiber.Ctx) error {
// 1. Check exclusions
@@ -186,6 +189,7 @@ func AuditMiddleware(config AuditConfig) fiber.Handler {
// 9. Store Log (Policy Enforcement)
_, isWrite := writeMethods[c.Method()]
_, allowRead := config.ReadPaths[c.Path()]
if isNil(config.Repo) {
if isWrite {
@@ -200,7 +204,7 @@ func AuditMiddleware(config AuditConfig) fiber.Handler {
slog.Error("Failed to write audit log (sync)", "error", createErr, "req_id", reqID)
return fiber.NewError(fiber.StatusServiceUnavailable, "Audit logging failed")
}
} else {
} else if allowRead {
// Best Effort: Load Shedding via Buffered Channel
select {
case auditQueue <- auditLog:

View File

@@ -7,6 +7,7 @@ import (
"errors"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
@@ -48,6 +49,44 @@ func (m *MockAuditRepository) Ping(ctx context.Context) error {
return args.Error(0)
}
type recordingAuditRepository struct {
mu sync.Mutex
count int
}
func (r *recordingAuditRepository) Create(log *domain.AuditLog) error {
r.mu.Lock()
defer r.mu.Unlock()
r.count++
return nil
}
func (r *recordingAuditRepository) FindPage(ctx context.Context, limit int, cursor *domain.AuditCursor, tenantID string) ([]domain.AuditLog, error) {
return nil, nil
}
func (r *recordingAuditRepository) FindByUserAndEvents(ctx context.Context, userID string, eventTypes []string, limit int) ([]domain.AuditLog, error) {
return nil, nil
}
func (r *recordingAuditRepository) CountFailuresSince(ctx context.Context, since time.Time, tenantID string) (int64, error) {
return 0, nil
}
func (r *recordingAuditRepository) CountActiveSessionsSince(ctx context.Context, since time.Time, tenantID string) (int64, error) {
return 0, nil
}
func (r *recordingAuditRepository) Ping(ctx context.Context) error {
return nil
}
func (r *recordingAuditRepository) Calls() int {
r.mu.Lock()
defer r.mu.Unlock()
return r.count
}
func TestAuditMiddleware(t *testing.T) {
t.Run("POST request - Sync Success", func(t *testing.T) {
app := fiber.New()
@@ -191,4 +230,24 @@ func TestAuditMiddleware(t *testing.T) {
resp2, _ := app.Test(req2)
assert.Equal(t, fiber.StatusOK, resp2.StatusCode)
})
t.Run("GET request - Read audit is skipped by default", func(t *testing.T) {
app := fiber.New()
repo := &recordingAuditRepository{}
app.Use(AuditMiddleware(AuditConfig{
Repo: repo,
}))
app.Get("/test", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
req := httptest.NewRequest("GET", "/test", nil)
resp, _ := app.Test(req)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
time.Sleep(50 * time.Millisecond)
assert.Equal(t, 0, repo.Calls())
})
}