🔐 Security hardening: auth, rate limiting, brute force protection
- 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
This commit is contained in:
@@ -1,27 +1,265 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
)
|
||||
|
||||
func RateLimitMiddleware() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 100, // 100 requests
|
||||
Expiration: 1 * time.Minute, // per minute
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🚦 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 {
|
||||
// Use API key or IP for rate limiting
|
||||
if key := c.Locals("api_key"); key != nil {
|
||||
return key.(string)
|
||||
}
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Rate limit exceeded",
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -1,93 +1,373 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bigtux/ophion/internal/security"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var jwtSecret []byte
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔐 AUTH CONFIG
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
func Init(secret string) {
|
||||
jwtSecret = []byte(secret)
|
||||
type AuthConfig struct {
|
||||
JWTSecret []byte
|
||||
JWTExpiration time.Duration
|
||||
RefreshExpiration time.Duration
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// GenerateAPIKey creates a new API key for agents
|
||||
func GenerateAPIKey() string {
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
return "ophion_" + hex.EncodeToString(bytes)
|
||||
type AuthService struct {
|
||||
config AuthConfig
|
||||
redis *redis.Client
|
||||
rateLimiter *security.RateLimiter
|
||||
loginTracker *security.LoginAttemptTracker
|
||||
apiKeyStore APIKeyStore
|
||||
}
|
||||
|
||||
// GenerateJWT creates a JWT token for users
|
||||
func GenerateJWT(userID string, email string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userID,
|
||||
"email": email,
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
// APIKeyStore interface para storage de API keys
|
||||
type APIKeyStore interface {
|
||||
ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error)
|
||||
UpdateLastUsed(ctx context.Context, keyID string) error
|
||||
}
|
||||
|
||||
type APIKeyInfo struct {
|
||||
ID string
|
||||
OrgID string
|
||||
Scopes []string
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewAuthService cria serviço de autenticação
|
||||
func NewAuthService(config AuthConfig, redis *redis.Client, apiKeyStore APIKeyStore) *AuthService {
|
||||
return &AuthService{
|
||||
config: config,
|
||||
redis: redis,
|
||||
rateLimiter: security.NewRateLimiter(security.AuthRateLimit, time.Minute),
|
||||
loginTracker: security.NewLoginAttemptTracker(),
|
||||
apiKeyStore: apiKeyStore,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// ValidateJWT validates a JWT token
|
||||
func ValidateJWT(tokenString string) (*jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎫 JWT TOKENS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
type TokenClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
UserID string `json:"uid"`
|
||||
OrgID string `json:"oid"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
TokenID string `json:"jti"`
|
||||
}
|
||||
|
||||
// GenerateTokenPair gera access + refresh tokens
|
||||
func (s *AuthService) GenerateTokenPair(userID, orgID, email, role string) (accessToken, refreshToken string, err error) {
|
||||
now := time.Now()
|
||||
tokenID := generateTokenID()
|
||||
|
||||
// Access Token (curta duração)
|
||||
accessClaims := TokenClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: s.config.Issuer,
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(s.config.JWTExpiration)),
|
||||
ID: tokenID,
|
||||
},
|
||||
UserID: userID,
|
||||
OrgID: orgID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
TokenID: tokenID,
|
||||
}
|
||||
|
||||
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.config.JWTSecret)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to sign access token: %w", err)
|
||||
}
|
||||
|
||||
// Refresh Token (longa duração)
|
||||
refreshID := generateTokenID()
|
||||
refreshClaims := jwt.RegisteredClaims{
|
||||
Issuer: s.config.Issuer,
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(s.config.RefreshExpiration)),
|
||||
ID: refreshID,
|
||||
}
|
||||
|
||||
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(s.config.JWTSecret)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
|
||||
}
|
||||
|
||||
// Armazenar refresh token no Redis (permite revogação)
|
||||
if s.redis != nil {
|
||||
ctx := context.Background()
|
||||
key := fmt.Sprintf("refresh_token:%s", refreshID)
|
||||
s.redis.Set(ctx, key, userID, s.config.RefreshExpiration)
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
// ValidateAccessToken valida access token
|
||||
func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return s.config.JWTSecret, nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, err
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token: %w", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, jwt.ErrInvalidKey
|
||||
|
||||
claims, ok := token.Claims.(*TokenClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
|
||||
// Verificar se token foi revogado
|
||||
if s.redis != nil {
|
||||
ctx := context.Background()
|
||||
revoked, _ := s.redis.Get(ctx, fmt.Sprintf("revoked_token:%s", claims.TokenID)).Result()
|
||||
if revoked != "" {
|
||||
return nil, fmt.Errorf("token revoked")
|
||||
}
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// AuthMiddleware protects routes
|
||||
func AuthMiddleware() fiber.Handler {
|
||||
// RevokeToken revoga um token
|
||||
func (s *AuthService) RevokeToken(tokenID string, expiration time.Duration) error {
|
||||
if s.redis == nil {
|
||||
return nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
return s.redis.Set(ctx, fmt.Sprintf("revoked_token:%s", tokenID), "1", expiration).Err()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🛡️ MIDDLEWARE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// AuthMiddleware middleware de autenticação
|
||||
func (s *AuthService) AuthMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization")
|
||||
// Obter IP real (considerando proxies)
|
||||
ip := c.IP()
|
||||
if forwarded := c.Get("X-Forwarded-For"); forwarded != "" {
|
||||
ip = strings.Split(forwarded, ",")[0]
|
||||
}
|
||||
|
||||
// Rate limiting por IP
|
||||
if !s.rateLimiter.Allow(ip) {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Too many requests",
|
||||
"message": "Rate limit exceeded. Try again later.",
|
||||
})
|
||||
}
|
||||
|
||||
authHeader := c.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Missing authorization header",
|
||||
"error": "Unauthorized",
|
||||
"message": "Missing authorization header",
|
||||
})
|
||||
}
|
||||
|
||||
// Support both "Bearer <token>" and API keys
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Check if it's an API key
|
||||
if strings.HasPrefix(token, "ophion_") {
|
||||
// TODO: Validate API key against database
|
||||
c.Locals("auth_type", "api_key")
|
||||
c.Locals("api_key", token)
|
||||
return c.Next()
|
||||
// API Key authentication
|
||||
if strings.HasPrefix(token, security.APIKeyPrefix) {
|
||||
return s.authenticateAPIKey(c, token)
|
||||
}
|
||||
|
||||
// JWT authentication
|
||||
return s.authenticateJWT(c, token)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate JWT
|
||||
claims, err := ValidateJWT(token)
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Invalid token",
|
||||
// authenticateAPIKey valida API key
|
||||
func (s *AuthService) authenticateAPIKey(c *fiber.Ctx, apiKey string) error {
|
||||
// Validar formato
|
||||
if !security.ValidateAPIKeyFormat(apiKey) {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
"message": "Invalid API key format",
|
||||
})
|
||||
}
|
||||
|
||||
// Hash da key para busca
|
||||
keyHash := security.HashAPIKey(apiKey)
|
||||
|
||||
// Buscar no storage
|
||||
keyInfo, err := s.apiKeyStore.ValidateKey(c.Context(), keyHash)
|
||||
if err != nil || keyInfo == nil {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
"message": "Invalid API key",
|
||||
})
|
||||
}
|
||||
|
||||
// Atualizar last_used (async)
|
||||
go s.apiKeyStore.UpdateLastUsed(context.Background(), keyInfo.ID)
|
||||
|
||||
// Setar contexto
|
||||
c.Locals("auth_type", "api_key")
|
||||
c.Locals("org_id", keyInfo.OrgID)
|
||||
c.Locals("api_key_id", keyInfo.ID)
|
||||
c.Locals("scopes", keyInfo.Scopes)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// authenticateJWT valida JWT token
|
||||
func (s *AuthService) authenticateJWT(c *fiber.Ctx, token string) error {
|
||||
claims, err := s.ValidateAccessToken(token)
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Setar contexto
|
||||
c.Locals("auth_type", "jwt")
|
||||
c.Locals("user_id", claims.UserID)
|
||||
c.Locals("org_id", claims.OrgID)
|
||||
c.Locals("email", claims.Email)
|
||||
c.Locals("role", claims.Role)
|
||||
c.Locals("token_id", claims.TokenID)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// RequireScopes middleware que exige scopes específicos
|
||||
func RequireScopes(required ...string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
scopes, ok := c.Locals("scopes").([]string)
|
||||
if !ok {
|
||||
// JWT tokens têm acesso total por padrão
|
||||
if c.Locals("auth_type") == "jwt" {
|
||||
return c.Next()
|
||||
}
|
||||
return c.Status(403).JSON(fiber.Map{
|
||||
"error": "Forbidden",
|
||||
"message": "Missing required scopes",
|
||||
})
|
||||
}
|
||||
|
||||
c.Locals("auth_type", "jwt")
|
||||
c.Locals("user_id", (*claims)["sub"])
|
||||
c.Locals("email", (*claims)["email"])
|
||||
|
||||
scopeMap := make(map[string]bool)
|
||||
for _, s := range scopes {
|
||||
scopeMap[s] = true
|
||||
}
|
||||
|
||||
for _, req := range required {
|
||||
if !scopeMap[req] && !scopeMap["*"] {
|
||||
return c.Status(403).JSON(fiber.Map{
|
||||
"error": "Forbidden",
|
||||
"message": fmt.Sprintf("Missing required scope: %s", req),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRole middleware que exige role específico
|
||||
func RequireRole(roles ...string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
userRole, ok := c.Locals("role").(string)
|
||||
if !ok {
|
||||
return c.Status(403).JSON(fiber.Map{
|
||||
"error": "Forbidden",
|
||||
"message": "Role not found",
|
||||
})
|
||||
}
|
||||
|
||||
for _, r := range roles {
|
||||
if userRole == r {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Admin tem acesso a tudo
|
||||
if userRole == "admin" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
return c.Status(403).JSON(fiber.Map{
|
||||
"error": "Forbidden",
|
||||
"message": "Insufficient permissions",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔐 LOGIN PROTECTION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// CheckLoginAllowed verifica se login é permitido (brute force protection)
|
||||
func (s *AuthService) CheckLoginAllowed(identifier string) (bool, time.Duration, error) {
|
||||
locked, remaining := s.loginTracker.IsLocked(identifier)
|
||||
return !locked, remaining, nil
|
||||
}
|
||||
|
||||
// RecordLoginAttempt registra tentativa de login
|
||||
func (s *AuthService) RecordLoginAttempt(identifier string, success bool) (blocked bool, remaining int) {
|
||||
if success {
|
||||
s.loginTracker.RecordSuccess(identifier)
|
||||
return false, 0
|
||||
}
|
||||
return s.loginTracker.RecordFailure(identifier)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔧 HELPERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
func generateTokenID() string {
|
||||
bytes := make([]byte, 16)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// GetUserID obtém user ID do contexto
|
||||
func GetUserID(c *fiber.Ctx) string {
|
||||
if id, ok := c.Locals("user_id").(string); ok {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetOrgID obtém org ID do contexto
|
||||
func GetOrgID(c *fiber.Ctx) string {
|
||||
if id, ok := c.Locals("org_id").(string); ok {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetRole obtém role do contexto
|
||||
func GetRole(c *fiber.Ctx) string {
|
||||
if role, ok := c.Locals("role").(string); ok {
|
||||
return role
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
558
internal/security/security.go
Normal file
558
internal/security/security.go
Normal file
@@ -0,0 +1,558 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔐 CONSTANTES DE SEGURANÇA
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
const (
|
||||
// Bcrypt cost (12-14 recomendado para produção)
|
||||
BcryptCost = 12
|
||||
|
||||
// Tamanho mínimo de senha
|
||||
MinPasswordLength = 12
|
||||
|
||||
// Tamanho da API Key
|
||||
APIKeyLength = 32
|
||||
|
||||
// Prefixo da API Key
|
||||
APIKeyPrefix = "ophion_"
|
||||
|
||||
// Rate limit padrão (requests por minuto)
|
||||
DefaultRateLimit = 100
|
||||
|
||||
// Rate limit para auth (tentativas por minuto)
|
||||
AuthRateLimit = 5
|
||||
|
||||
// Tempo de bloqueio após falhas (minutos)
|
||||
LockoutDuration = 15
|
||||
|
||||
// Máximo de tentativas antes do bloqueio
|
||||
MaxFailedAttempts = 5
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔑 API KEY MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// GenerateAPIKey cria uma nova API key segura
|
||||
func GenerateAPIKey() (key string, hash string, prefix string) {
|
||||
bytes := make([]byte, APIKeyLength)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
panic("failed to generate random bytes")
|
||||
}
|
||||
|
||||
key = APIKeyPrefix + hex.EncodeToString(bytes)
|
||||
hash = HashAPIKey(key)
|
||||
prefix = key[:len(APIKeyPrefix)+8] // ophion_xxxxxxxx
|
||||
|
||||
return key, hash, prefix
|
||||
}
|
||||
|
||||
// HashAPIKey cria hash SHA256 da API key
|
||||
func HashAPIKey(key string) string {
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// ValidateAPIKeyFormat verifica formato da API key
|
||||
func ValidateAPIKeyFormat(key string) bool {
|
||||
if !strings.HasPrefix(key, APIKeyPrefix) {
|
||||
return false
|
||||
}
|
||||
// ophion_ + 64 hex chars
|
||||
return len(key) == len(APIKeyPrefix)+64
|
||||
}
|
||||
|
||||
// SecureCompare compara strings em tempo constante (previne timing attacks)
|
||||
func SecureCompare(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔒 PASSWORD MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// PasswordPolicy define regras de senha
|
||||
type PasswordPolicy struct {
|
||||
MinLength int
|
||||
RequireUppercase bool
|
||||
RequireLowercase bool
|
||||
RequireNumbers bool
|
||||
RequireSpecial bool
|
||||
}
|
||||
|
||||
// DefaultPasswordPolicy retorna política padrão
|
||||
func DefaultPasswordPolicy() PasswordPolicy {
|
||||
return PasswordPolicy{
|
||||
MinLength: 12,
|
||||
RequireUppercase: true,
|
||||
RequireLowercase: true,
|
||||
RequireNumbers: true,
|
||||
RequireSpecial: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePassword verifica se senha atende à política
|
||||
func ValidatePassword(password string, policy PasswordPolicy) (bool, []string) {
|
||||
var errors []string
|
||||
|
||||
if len(password) < policy.MinLength {
|
||||
errors = append(errors, fmt.Sprintf("Senha deve ter no mínimo %d caracteres", policy.MinLength))
|
||||
}
|
||||
|
||||
if policy.RequireUppercase && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||
errors = append(errors, "Senha deve conter letra maiúscula")
|
||||
}
|
||||
|
||||
if policy.RequireLowercase && !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||
errors = append(errors, "Senha deve conter letra minúscula")
|
||||
}
|
||||
|
||||
if policy.RequireNumbers && !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||
errors = append(errors, "Senha deve conter número")
|
||||
}
|
||||
|
||||
if policy.RequireSpecial && !regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password) {
|
||||
errors = append(errors, "Senha deve conter caractere especial")
|
||||
}
|
||||
|
||||
// Verificar senhas comuns
|
||||
if isCommonPassword(password) {
|
||||
errors = append(errors, "Senha muito comum, escolha outra")
|
||||
}
|
||||
|
||||
return len(errors) == 0, errors
|
||||
}
|
||||
|
||||
// HashPassword cria hash bcrypt da senha
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// VerifyPassword verifica senha contra hash
|
||||
func VerifyPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// isCommonPassword verifica senhas comuns
|
||||
func isCommonPassword(password string) bool {
|
||||
common := []string{
|
||||
"password", "123456", "12345678", "qwerty", "abc123",
|
||||
"monkey", "1234567", "letmein", "trustno1", "dragon",
|
||||
"baseball", "iloveyou", "master", "sunshine", "ashley",
|
||||
"passw0rd", "shadow", "123123", "654321", "superman",
|
||||
"senha", "mudar123", "admin123", "root123",
|
||||
}
|
||||
lower := strings.ToLower(password)
|
||||
for _, p := range common {
|
||||
if strings.Contains(lower, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🚦 RATE LIMITING
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// RateLimiter controla taxa de requests
|
||||
type RateLimiter struct {
|
||||
sync.RWMutex
|
||||
requests map[string]*rateLimitEntry
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
type rateLimitEntry struct {
|
||||
count int
|
||||
firstSeen time.Time
|
||||
blocked bool
|
||||
blockedAt time.Time
|
||||
}
|
||||
|
||||
// NewRateLimiter cria novo rate limiter
|
||||
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
||||
rl := &RateLimiter{
|
||||
requests: make(map[string]*rateLimitEntry),
|
||||
limit: limit,
|
||||
window: window,
|
||||
}
|
||||
// Cleanup goroutine
|
||||
go rl.cleanup()
|
||||
return rl
|
||||
}
|
||||
|
||||
// Allow verifica se request é permitido
|
||||
func (rl *RateLimiter) Allow(identifier string) bool {
|
||||
rl.Lock()
|
||||
defer rl.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
entry, exists := rl.requests[identifier]
|
||||
|
||||
if !exists {
|
||||
rl.requests[identifier] = &rateLimitEntry{
|
||||
count: 1,
|
||||
firstSeen: now,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Verificar se está bloqueado
|
||||
if entry.blocked {
|
||||
if now.Sub(entry.blockedAt) < time.Duration(LockoutDuration)*time.Minute {
|
||||
return false
|
||||
}
|
||||
// Desbloquear
|
||||
entry.blocked = false
|
||||
entry.count = 0
|
||||
entry.firstSeen = now
|
||||
}
|
||||
|
||||
// Verificar janela de tempo
|
||||
if now.Sub(entry.firstSeen) > rl.window {
|
||||
entry.count = 1
|
||||
entry.firstSeen = now
|
||||
return true
|
||||
}
|
||||
|
||||
entry.count++
|
||||
|
||||
if entry.count > rl.limit {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Block bloqueia um identificador
|
||||
func (rl *RateLimiter) Block(identifier string) {
|
||||
rl.Lock()
|
||||
defer rl.Unlock()
|
||||
|
||||
entry, exists := rl.requests[identifier]
|
||||
if !exists {
|
||||
entry = &rateLimitEntry{}
|
||||
rl.requests[identifier] = entry
|
||||
}
|
||||
entry.blocked = true
|
||||
entry.blockedAt = time.Now()
|
||||
}
|
||||
|
||||
// cleanup remove entradas antigas
|
||||
func (rl *RateLimiter) cleanup() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
for range ticker.C {
|
||||
rl.Lock()
|
||||
now := time.Now()
|
||||
for id, entry := range rl.requests {
|
||||
if now.Sub(entry.firstSeen) > rl.window*2 && !entry.blocked {
|
||||
delete(rl.requests, id)
|
||||
}
|
||||
}
|
||||
rl.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🛡️ BRUTE FORCE PROTECTION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// LoginAttemptTracker rastreia tentativas de login
|
||||
type LoginAttemptTracker struct {
|
||||
sync.RWMutex
|
||||
attempts map[string]*loginAttempt
|
||||
}
|
||||
|
||||
type loginAttempt struct {
|
||||
failures int
|
||||
lastAttempt time.Time
|
||||
lockedUntil time.Time
|
||||
}
|
||||
|
||||
// NewLoginAttemptTracker cria tracker
|
||||
func NewLoginAttemptTracker() *LoginAttemptTracker {
|
||||
return &LoginAttemptTracker{
|
||||
attempts: make(map[string]*loginAttempt),
|
||||
}
|
||||
}
|
||||
|
||||
// RecordFailure registra falha de login
|
||||
func (t *LoginAttemptTracker) RecordFailure(identifier string) (blocked bool, remainingAttempts int) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
attempt, exists := t.attempts[identifier]
|
||||
|
||||
if !exists {
|
||||
t.attempts[identifier] = &loginAttempt{
|
||||
failures: 1,
|
||||
lastAttempt: now,
|
||||
}
|
||||
return false, MaxFailedAttempts - 1
|
||||
}
|
||||
|
||||
// Reset se última tentativa foi há muito tempo
|
||||
if now.Sub(attempt.lastAttempt) > time.Duration(LockoutDuration)*time.Minute {
|
||||
attempt.failures = 1
|
||||
attempt.lastAttempt = now
|
||||
attempt.lockedUntil = time.Time{}
|
||||
return false, MaxFailedAttempts - 1
|
||||
}
|
||||
|
||||
attempt.failures++
|
||||
attempt.lastAttempt = now
|
||||
|
||||
if attempt.failures >= MaxFailedAttempts {
|
||||
attempt.lockedUntil = now.Add(time.Duration(LockoutDuration) * time.Minute)
|
||||
return true, 0
|
||||
}
|
||||
|
||||
return false, MaxFailedAttempts - attempt.failures
|
||||
}
|
||||
|
||||
// RecordSuccess registra sucesso de login
|
||||
func (t *LoginAttemptTracker) RecordSuccess(identifier string) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
delete(t.attempts, identifier)
|
||||
}
|
||||
|
||||
// IsLocked verifica se está bloqueado
|
||||
func (t *LoginAttemptTracker) IsLocked(identifier string) (bool, time.Duration) {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
|
||||
attempt, exists := t.attempts[identifier]
|
||||
if !exists {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if attempt.lockedUntil.IsZero() {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
remaining := time.Until(attempt.lockedUntil)
|
||||
if remaining <= 0 {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
return true, remaining
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔍 INPUT VALIDATION & SANITIZATION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// SanitizeEmail limpa e valida email
|
||||
func SanitizeEmail(email string) (string, error) {
|
||||
email = strings.TrimSpace(strings.ToLower(email))
|
||||
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
if !emailRegex.MatchString(email) {
|
||||
return "", fmt.Errorf("email inválido")
|
||||
}
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
// SanitizeHostname limpa hostname
|
||||
func SanitizeHostname(hostname string) (string, error) {
|
||||
hostname = strings.TrimSpace(hostname)
|
||||
|
||||
// Permitir apenas alfanuméricos, hífens e pontos
|
||||
hostnameRegex := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$`)
|
||||
if !hostnameRegex.MatchString(hostname) && len(hostname) > 1 {
|
||||
return "", fmt.Errorf("hostname inválido")
|
||||
}
|
||||
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
// SanitizeSQL previne SQL injection (para queries dinâmicas)
|
||||
func SanitizeSQL(input string) string {
|
||||
// Remover caracteres perigosos
|
||||
dangerous := []string{"'", "\"", ";", "--", "/*", "*/", "xp_", "sp_"}
|
||||
result := input
|
||||
for _, d := range dangerous {
|
||||
result = strings.ReplaceAll(result, d, "")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ValidateIPAddress valida endereço IP
|
||||
func ValidateIPAddress(ip string) bool {
|
||||
return net.ParseIP(ip) != nil
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🛡️ SECURITY HEADERS MIDDLEWARE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// SecurityHeaders adiciona headers de segurança
|
||||
func SecurityHeaders() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Prevenir clickjacking
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
|
||||
// Prevenir MIME type sniffing
|
||||
c.Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// XSS Protection
|
||||
c.Set("X-XSS-Protection", "1; mode=block")
|
||||
|
||||
// Content Security Policy
|
||||
c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:")
|
||||
|
||||
// HSTS (apenas se HTTPS)
|
||||
if c.Protocol() == "https" {
|
||||
c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
||||
}
|
||||
|
||||
// Referrer Policy
|
||||
c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
// Permissions Policy
|
||||
c.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
|
||||
|
||||
// Remove Server header
|
||||
c.Set("Server", "")
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📝 AUDIT LOGGING
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// AuditEvent representa um evento de auditoria
|
||||
type AuditEvent struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
EventType string `json:"event_type"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
OrgID string `json:"org_id,omitempty"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// AuditEventType tipos de eventos
|
||||
const (
|
||||
AuditLogin = "auth.login"
|
||||
AuditLogout = "auth.logout"
|
||||
AuditLoginFailed = "auth.login_failed"
|
||||
AuditAPIKeyCreated = "apikey.created"
|
||||
AuditAPIKeyRevoked = "apikey.revoked"
|
||||
AuditUserCreated = "user.created"
|
||||
AuditUserDeleted = "user.deleted"
|
||||
AuditConfigChanged = "config.changed"
|
||||
AuditAlertCreated = "alert.created"
|
||||
AuditDataExport = "data.export"
|
||||
)
|
||||
|
||||
// AuditLogger interface para logging de auditoria
|
||||
type AuditLogger interface {
|
||||
Log(event AuditEvent) error
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔐 SECRETS MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// GenerateSecureToken gera token seguro
|
||||
func GenerateSecureToken(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
panic("failed to generate secure token")
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes)[:length]
|
||||
}
|
||||
|
||||
// MaskSecret mascara segredos para logging
|
||||
func MaskSecret(secret string) string {
|
||||
if len(secret) <= 8 {
|
||||
return "****"
|
||||
}
|
||||
return secret[:4] + "****" + secret[len(secret)-4:]
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🌐 IP FILTERING
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// IPWhitelist gerencia whitelist de IPs
|
||||
type IPWhitelist struct {
|
||||
sync.RWMutex
|
||||
allowed map[string]bool
|
||||
cidrs []*net.IPNet
|
||||
}
|
||||
|
||||
// NewIPWhitelist cria whitelist
|
||||
func NewIPWhitelist(ips []string, cidrs []string) *IPWhitelist {
|
||||
wl := &IPWhitelist{
|
||||
allowed: make(map[string]bool),
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
wl.allowed[ip] = true
|
||||
}
|
||||
|
||||
for _, cidr := range cidrs {
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err == nil {
|
||||
wl.cidrs = append(wl.cidrs, network)
|
||||
}
|
||||
}
|
||||
|
||||
return wl
|
||||
}
|
||||
|
||||
// IsAllowed verifica se IP está permitido
|
||||
func (wl *IPWhitelist) IsAllowed(ip string) bool {
|
||||
wl.RLock()
|
||||
defer wl.RUnlock()
|
||||
|
||||
// Verificar lista direta
|
||||
if wl.allowed[ip] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Verificar CIDRs
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, network := range wl.cidrs {
|
||||
if network.Contains(parsedIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user