- Add comprehensive security package with: - API Key generation and validation (SHA256 hash) - Password policy enforcement (min 12 chars, complexity) - Rate limiting with presets (auth, api, ingest, export) - Brute force protection (5 attempts, 15min lockout) - Security headers middleware - IP whitelisting - Audit logging structure - Secure token generation - Enhanced auth middleware: - JWT + API Key dual authentication - Token revocation via Redis - Scope-based authorization - Role-based access control - Updated installer with: - Interactive setup for client customization - Auto-generated secure credentials - Docker all-in-one image - Agent installer script - Added documentation: - SECURITY.md - Complete security guide - INSTALL.md - Installation guide - .env.example - Configuration reference
266 lines
7.0 KiB
Go
266 lines
7.0 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 🚦 RATE LIMITING MIDDLEWARE
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
// RateLimitConfig configuração do rate limiter
|
|
type RateLimitConfig struct {
|
|
// Requests máximos por janela
|
|
Max int
|
|
|
|
// Janela de tempo
|
|
Window time.Duration
|
|
|
|
// Função para extrair identificador (default: IP)
|
|
KeyGenerator func(*fiber.Ctx) string
|
|
|
|
// Função para resposta quando limitado
|
|
LimitReached func(*fiber.Ctx) error
|
|
|
|
// Pular rate limit para certos paths
|
|
SkipPaths []string
|
|
|
|
// Headers customizados
|
|
Headers RateLimitHeaders
|
|
}
|
|
|
|
// RateLimitHeaders headers do rate limit
|
|
type RateLimitHeaders struct {
|
|
Limit string // X-RateLimit-Limit
|
|
Remaining string // X-RateLimit-Remaining
|
|
Reset string // X-RateLimit-Reset
|
|
RetryAfter string // Retry-After
|
|
}
|
|
|
|
// DefaultRateLimitConfig configuração padrão
|
|
func DefaultRateLimitConfig() RateLimitConfig {
|
|
return RateLimitConfig{
|
|
Max: 100,
|
|
Window: time.Minute,
|
|
KeyGenerator: func(c *fiber.Ctx) string {
|
|
return c.IP()
|
|
},
|
|
LimitReached: func(c *fiber.Ctx) error {
|
|
return c.Status(429).JSON(fiber.Map{
|
|
"error": "Too Many Requests",
|
|
"message": "Rate limit exceeded. Please slow down.",
|
|
"retry_after": 60,
|
|
})
|
|
},
|
|
Headers: RateLimitHeaders{
|
|
Limit: "X-RateLimit-Limit",
|
|
Remaining: "X-RateLimit-Remaining",
|
|
Reset: "X-RateLimit-Reset",
|
|
RetryAfter: "Retry-After",
|
|
},
|
|
}
|
|
}
|
|
|
|
// rateLimitStore armazena contagem de requests
|
|
type rateLimitStore struct {
|
|
sync.RWMutex
|
|
entries map[string]*rateLimitEntry
|
|
}
|
|
|
|
type rateLimitEntry struct {
|
|
count int
|
|
expiresAt time.Time
|
|
}
|
|
|
|
var store = &rateLimitStore{
|
|
entries: make(map[string]*rateLimitEntry),
|
|
}
|
|
|
|
// Limpar entries expirados periodicamente
|
|
func init() {
|
|
go func() {
|
|
ticker := time.NewTicker(time.Minute)
|
|
for range ticker.C {
|
|
store.cleanup()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *rateLimitStore) cleanup() {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
|
|
now := time.Now()
|
|
for key, entry := range s.entries {
|
|
if now.After(entry.expiresAt) {
|
|
delete(s.entries, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RateLimit middleware de rate limiting
|
|
func RateLimit(config ...RateLimitConfig) fiber.Handler {
|
|
cfg := DefaultRateLimitConfig()
|
|
if len(config) > 0 {
|
|
cfg = config[0]
|
|
}
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
// Verificar skip paths
|
|
path := c.Path()
|
|
for _, skip := range cfg.SkipPaths {
|
|
if path == skip {
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// Gerar key
|
|
key := cfg.KeyGenerator(c)
|
|
|
|
// Verificar/atualizar contagem
|
|
store.Lock()
|
|
|
|
now := time.Now()
|
|
entry, exists := store.entries[key]
|
|
|
|
if !exists || now.After(entry.expiresAt) {
|
|
// Nova janela
|
|
store.entries[key] = &rateLimitEntry{
|
|
count: 1,
|
|
expiresAt: now.Add(cfg.Window),
|
|
}
|
|
store.Unlock()
|
|
|
|
// Headers
|
|
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
|
|
c.Set(cfg.Headers.Remaining, strconv.Itoa(cfg.Max-1))
|
|
c.Set(cfg.Headers.Reset, strconv.FormatInt(now.Add(cfg.Window).Unix(), 10))
|
|
|
|
return c.Next()
|
|
}
|
|
|
|
entry.count++
|
|
remaining := cfg.Max - entry.count
|
|
resetTime := entry.expiresAt.Unix()
|
|
|
|
store.Unlock()
|
|
|
|
// Headers
|
|
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
|
|
c.Set(cfg.Headers.Remaining, strconv.Itoa(max(0, remaining)))
|
|
c.Set(cfg.Headers.Reset, strconv.FormatInt(resetTime, 10))
|
|
|
|
// Verificar se excedeu
|
|
if remaining < 0 {
|
|
retryAfter := int(time.Until(entry.expiresAt).Seconds())
|
|
c.Set(cfg.Headers.RetryAfter, strconv.Itoa(max(1, retryAfter)))
|
|
return cfg.LimitReached(c)
|
|
}
|
|
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 🎯 RATE LIMIT PRESETS
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
// RateLimitAuth rate limit para endpoints de autenticação (mais restritivo)
|
|
func RateLimitAuth() fiber.Handler {
|
|
return RateLimit(RateLimitConfig{
|
|
Max: 5,
|
|
Window: time.Minute,
|
|
KeyGenerator: func(c *fiber.Ctx) string {
|
|
// Combinar IP + email para prevenir ataques distribuídos
|
|
email := c.FormValue("email")
|
|
if email == "" {
|
|
email = c.Query("email")
|
|
}
|
|
return fmt.Sprintf("auth:%s:%s", c.IP(), email)
|
|
},
|
|
LimitReached: func(c *fiber.Ctx) error {
|
|
return c.Status(429).JSON(fiber.Map{
|
|
"error": "Too Many Login Attempts",
|
|
"message": "You have exceeded the maximum number of login attempts. Please wait before trying again.",
|
|
"retry_after": 60,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
// RateLimitAPI rate limit para API geral
|
|
func RateLimitAPI() fiber.Handler {
|
|
return RateLimit(RateLimitConfig{
|
|
Max: 100,
|
|
Window: time.Minute,
|
|
KeyGenerator: func(c *fiber.Ctx) string {
|
|
// Usar API key ou IP
|
|
if apiKey := c.Locals("api_key_id"); apiKey != nil {
|
|
return fmt.Sprintf("api:%v", apiKey)
|
|
}
|
|
if userID := c.Locals("user_id"); userID != nil {
|
|
return fmt.Sprintf("user:%v", userID)
|
|
}
|
|
return fmt.Sprintf("ip:%s", c.IP())
|
|
},
|
|
})
|
|
}
|
|
|
|
// RateLimitIngest rate limit para ingestão de dados (mais permissivo)
|
|
func RateLimitIngest() fiber.Handler {
|
|
return RateLimit(RateLimitConfig{
|
|
Max: 1000,
|
|
Window: time.Minute,
|
|
KeyGenerator: func(c *fiber.Ctx) string {
|
|
if orgID := c.Locals("org_id"); orgID != nil {
|
|
return fmt.Sprintf("ingest:%v", orgID)
|
|
}
|
|
return fmt.Sprintf("ingest:ip:%s", c.IP())
|
|
},
|
|
LimitReached: func(c *fiber.Ctx) error {
|
|
return c.Status(429).JSON(fiber.Map{
|
|
"error": "Ingest Rate Limit Exceeded",
|
|
"message": "Your organization has exceeded the data ingestion rate limit.",
|
|
"retry_after": 10,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
// RateLimitExport rate limit para exportação de dados
|
|
func RateLimitExport() fiber.Handler {
|
|
return RateLimit(RateLimitConfig{
|
|
Max: 10,
|
|
Window: time.Hour,
|
|
KeyGenerator: func(c *fiber.Ctx) string {
|
|
if userID := c.Locals("user_id"); userID != nil {
|
|
return fmt.Sprintf("export:%v", userID)
|
|
}
|
|
return fmt.Sprintf("export:ip:%s", c.IP())
|
|
},
|
|
LimitReached: func(c *fiber.Ctx) error {
|
|
return c.Status(429).JSON(fiber.Map{
|
|
"error": "Export Rate Limit Exceeded",
|
|
"message": "You have exceeded the maximum number of exports per hour.",
|
|
"retry_after": 3600,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 🔧 HELPERS
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
func max(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|