package admin import ( "context" "encoding/json" "log" ) // SecurityLog records admin-side security events (login failures, allowlist // rejections, lockouts) to both the structured logger and the audit_log table. // // Per doc/02 §1 the admin backend records ONLY security events — never routine // access logs. type SecurityLog struct { store Store logger *log.Logger } // NewSecurityLog creates a SecurityLog. A nil logger falls back to the // standard logger. func NewSecurityLog(store Store, logger *log.Logger) *SecurityLog { if logger == nil { logger = log.Default() } return &SecurityLog{store: store, logger: logger} } func (s *SecurityLog) write(ctx context.Context, actor, action, target string, meta map[string]any) { if s == nil { return } metaJSON := "" if meta != nil { if b, err := json.Marshal(meta); err == nil { metaJSON = string(b) } } s.logger.Printf("admin security event action=%s actor=%s target=%s", action, actor, target) if s.store != nil { if err := s.store.WriteAudit(ctx, actor, action, target, metaJSON); err != nil { s.logger.Printf("admin security event audit write failed: %v", err) } } } // LoginFail records a failed login attempt. func (s *SecurityLog) LoginFail(ctx context.Context, username, ip, reason string) { s.write(ctx, safeActor(username), "admin_login_fail", "ip:"+ip, map[string]any{"reason": reason}) } // LoginLocked records a login attempt against a locked-out account. func (s *SecurityLog) LoginLocked(ctx context.Context, username, ip string) { s.write(ctx, safeActor(username), "admin_login_locked", "ip:"+ip, nil) } // LoginOK records a successful login. func (s *SecurityLog) LoginOK(ctx context.Context, username, ip string) { s.write(ctx, safeActor(username), "admin_login_ok", "ip:"+ip, nil) } // IPBlocked records an allowlist rejection. func (s *SecurityLog) IPBlocked(ctx context.Context, ip, path string) { s.write(ctx, "-", "admin_ip_blocked", "ip:"+ip, map[string]any{"path": path}) } func safeActor(username string) string { if username == "" { return "-" } if len(username) > 64 { return username[:64] } return username }