Files
auth-proxy/auth.go
db123-test 5ffafbd38f feat: add filtering and pagination to audit logs endpoint
The logsHandler now accepts query parameters to filter logs by action
and IP address, with support for pagination via limit and offset
parameters. Query parameters are safely bound to prepared statements
to prevent SQL injection, and pagination values are validated to
ensure they fall within acceptable ranges.
2026-05-04 13:15:55 +03:30

284 lines
7.5 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"net"
"net/http"
"strconv"
"strings"
"time"
)
func CheckAuth(db *sql.DB, r *http.Request) bool {
ip := r.Header.Get("X-Real-IP")
if ip == "" {
return false
}
ok, err := IsPermanentWhitelisted(db, ip)
if err == nil && ok {
return true
}
ok, err = IsTempWhitelisted(db, ip)
if err == nil && ok {
return true
}
return false
}
func authHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !CheckAuth(db, r) {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func whitelistTempHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !verifyAPIKey(r, cfg.APIToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
type request struct {
IP string `json:"ip"`
TTLSeconds int `json:"ttl_seconds"`
Reason string `json:"reason,omitempty"`
}
var req request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if req.IP == "" || req.TTLSeconds <= 0 {
http.Error(w, "ip and ttl_seconds required", http.StatusBadRequest)
return
}
clientIP := getClientIP(r)
if err := AddTempEntry(db, req.IP, req.TTLSeconds, req.Reason, clientIP); err != nil {
slog.Error("failed to add temp entry", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
slog.Info("temp whitelist added", "ip", req.IP, "ttl", req.TTLSeconds)
w.WriteHeader(http.StatusNoContent)
}
}
func whitelistListHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !verifyAPIKey(r, cfg.APIToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
entries, err := ListTempEntries(db)
if err != nil {
http.Error(w, "database error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}
}
func permWhitelistListHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !verifyAPIKey(r, cfg.APIToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
entries, err := ListPermEntries(db)
if err != nil {
http.Error(w, "database error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}
}
func whitelistDeleteHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !verifyAPIKey(r, cfg.APIToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ip := r.PathValue("ip")
if ip == "" {
http.Error(w, "ip required", http.StatusBadRequest)
return
}
clientIP := getClientIP(r)
if err := DeleteTempEntry(db, ip, clientIP); err != nil {
slog.Error("failed to delete temp entry", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
slog.Info("temp whitelist deleted", "ip", ip)
w.WriteHeader(http.StatusNoContent)
}
}
func logsHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !verifyAPIKey(r, cfg.APIToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
query := `SELECT timestamp, action, ip, cidr, ttl_seconds, reason, api_client_ip FROM audit_log`
var args []any
argIndex := 1
filterAction := r.URL.Query().Get("action")
filterIP := r.URL.Query().Get("ip")
limitStr := r.URL.Query().Get("limit")
offsetStr := r.URL.Query().Get("offset")
var conditions []string
if filterAction != "" {
conditions = append(conditions, fmt.Sprintf(`action = $%d`, argIndex))
args = append(args, filterAction)
argIndex++
}
if filterIP != "" {
conditions = append(conditions, fmt.Sprintf(`ip = $%d`, argIndex))
args = append(args, filterIP)
argIndex++
}
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
query += ` ORDER BY timestamp DESC`
limit := 200
offset := 0
if limitStr != "" {
if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= 1000 {
limit = parsed
}
}
if offsetStr != "" {
if parsed, err := strconv.Atoi(offsetStr); err == nil && parsed >= 0 {
offset = parsed
}
}
query += fmt.Sprintf(` LIMIT %d OFFSET %d`, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
slog.Error("failed to query logs", "error", err)
http.Error(w, "database error", http.StatusInternalServerError)
return
}
defer rows.Close()
logs := make([]map[string]interface{}, 0)
for rows.Next() {
var ts time.Time
var action, apiClientIP, reason string
var ip, cidr sql.NullString
var ttlSeconds sql.NullInt64
if err := rows.Scan(&ts, &action, &ip, &cidr, &ttlSeconds, &reason, &apiClientIP); err != nil {
continue
}
entry := map[string]interface{}{
"timestamp": ts,
"action": action,
"api_client_ip": apiClientIP,
"reason": reason,
}
if ip.Valid {
entry["ip"] = ip.String
}
if cidr.Valid {
entry["cidr"] = cidr.String
}
if ttlSeconds.Valid {
entry["ttl_seconds"] = ttlSeconds.Int64
}
logs = append(logs, entry)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(logs)
}
}
func permWhitelistAddHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !verifyAPIKey(r, cfg.APIToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
type request struct {
Entry string `json:"entry"`
}
var req request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if req.Entry == "" {
http.Error(w, "entry required", http.StatusBadRequest)
return
}
ok, err := AddPermanentEntry(db, req.Entry, getClientIP(r))
if err != nil {
slog.Error("failed to add perm entry", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if !ok {
http.Error(w, "entry already exists", http.StatusConflict)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func permWhitelistDeleteHandler(cfg Config, db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !verifyAPIKey(r, cfg.APIToken) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
entry := r.PathValue("entry")
if entry == "" {
http.Error(w, "entry required", http.StatusBadRequest)
return
}
if err := DeletePermanentEntry(db, entry, getClientIP(r)); err != nil {
slog.Error("failed to delete perm entry", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func getClientIP(r *http.Request) string {
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return ip
}
func verifyAPIKey(r *http.Request, expectedToken string) bool {
return r.Header.Get("Authorization") == "Bearer "+expectedToken
}
func statusHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok\n"))
}