ae1831e61f
- Rename user_id → users_id across all models, handlers, services, and tests
- Add custom RoleIDs type supporting string/int/array unmarshaling (e.g., "1", 1, [1])
- Implement flexible JSON unmarshaling for JWT Claims to handle field name variants
- Support both user_id/users_id and email/email_address field names
- Enable role_id as string ("1"), int (1), or array ([1,2])
- Update AuthorizationContext to handle role_id type flexibility
- Add comprehensive logging to repository, service, and handler layers
- Entry/exit logs with full context
- Success (✓) and failure (✗) indicators
- Step-by-step authorization flow tracking
- Add containsRole helper for multi-role membership checks
- Fix database queries: user_id → users_id, id → permissions_id
- Update all tests to use models.RoleIDs{} syntax
- Change GetRole middleware return type: string → []int
- Maintain backward compatibility with legacy JWT tokens
This change improves integration with external services (MIS) that may send
role_id in different formats and standardizes field naming conventions
throughout the authorization microservice.
100 lines
2.5 KiB
Go
100 lines
2.5 KiB
Go
package middleware
|
|
|
|
import (
|
|
"authorization/helper"
|
|
"authorization/models"
|
|
"authorization/redisclient"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// DefaultRateLimitConfig returns default rate limiting settings
|
|
func DefaultRateLimitConfig() models.RateLimitConfig {
|
|
return models.RateLimitConfig{
|
|
RequestsPerMinute: 100,
|
|
BurstSize: 20,
|
|
}
|
|
}
|
|
|
|
// RateLimiterMiddleware implements distributed rate limiting using Redis
|
|
func RateLimiterMiddleware(config models.RateLimitConfig) func(http.HandlerFunc) http.HandlerFunc {
|
|
return func(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Fail-open: Skip rate limiting if Redis is not available (prevents full outage)
|
|
if redisclient.RDB == nil {
|
|
helper.LogError(nil, "Rate limiter: Redis not available, allowing request (fail-open)")
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Extract user identifier (prefer users_id from JWT, fallback to IP)
|
|
var identifier string
|
|
if userID, ok := GetUserID(r); ok {
|
|
identifier = "user:" + userID
|
|
} else {
|
|
identifier = "ip:" + getClientIP(r)
|
|
}
|
|
|
|
// Check rate limit
|
|
allowed, err := checkRateLimit(identifier, config)
|
|
if err != nil {
|
|
// On error, fail open (allow request) but log the error
|
|
helper.LogError(err, "rate limiter error")
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
if !allowed {
|
|
helper.RespondWithError(w, http.StatusTooManyRequests, "Rate limit exceeded")
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkRateLimit uses Redis INCR with sliding window
|
|
func checkRateLimit(identifier string, config models.RateLimitConfig) (bool, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
key := fmt.Sprintf("ratelimit:%s", identifier)
|
|
|
|
// Use Redis pipeline for atomic operations
|
|
pipe := redisclient.RDB.Pipeline()
|
|
|
|
incrCmd := pipe.Incr(ctx, key)
|
|
pipe.Expire(ctx, key, time.Minute)
|
|
|
|
_, err := pipe.Exec(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
count := incrCmd.Val()
|
|
|
|
// Allow burst + requests per minute
|
|
return count <= int64(config.RequestsPerMinute+config.BurstSize), nil
|
|
}
|
|
|
|
// getClientIP extracts the client IP from the request
|
|
func getClientIP(r *http.Request) string {
|
|
// Check X-Forwarded-For header first (for proxies/load balancers)
|
|
forwarded := r.Header.Get("X-Forwarded-For")
|
|
if forwarded != "" {
|
|
return forwarded
|
|
}
|
|
|
|
// Check X-Real-IP header
|
|
realIP := r.Header.Get("X-Real-IP")
|
|
if realIP != "" {
|
|
return realIP
|
|
}
|
|
|
|
// Fallback to RemoteAddr
|
|
return r.RemoteAddr
|
|
}
|