fixed region fetching in user_attributes

This commit is contained in:
2026-04-16 13:42:50 +08:00
parent f0bc603a5f
commit 29cf10c379
7 changed files with 43 additions and 54 deletions
+4 -3
View File
@@ -3,7 +3,10 @@ module authorization
go 1.25.1
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/alicebob/miniredis/v2 v2.35.0
github.com/getsentry/sentry-go v0.39.0
github.com/go-redis/redismock/v9 v9.2.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/gorilla/mux v1.8.1
@@ -17,17 +20,15 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/alicebob/miniredis/v2 v2.35.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cespares/response v0.0.0-20260416052801-b22a14921d12 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-redis/redismock/v9 v9.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+12
View File
@@ -14,12 +14,16 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespares/response v0.0.0-20260416052801-b22a14921d12 h1:VtwZEFXJADUIpECuGittyHNLRAkEOr2HdCcticfz6Ow=
github.com/cespares/response v0.0.0-20260416052801-b22a14921d12/go.mod h1:+9XxGj6565PWnCxvMvC0QCrg90beAzkK5jOHNNPNG3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/sentry-go v0.39.0 h1:uhnexj8PNCyCve37GSqxXOeXHh4cJNLNNB4w70Jtgo0=
github.com/getsentry/sentry-go v0.39.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@@ -67,6 +71,12 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -129,6 +139,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+14 -13
View File
@@ -1,7 +1,6 @@
package handlers
import (
"authorization/helper"
"authorization/middleware"
"authorization/models"
"authorization/services"
@@ -9,6 +8,8 @@ import (
"io"
"log"
"net/http"
sabat "github.com/cespares/response"
)
var authService *models.CachedAuthorizationService
@@ -36,7 +37,7 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := middleware.GetClaims(r)
if !ok {
log.Printf("ERROR: Missing JWT claims in request context (method=%s, path=%s)", r.Method, r.URL.Path)
helper.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
sabat.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
@@ -48,7 +49,7 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("ERROR: Failed to read authorization request body: %v", err)
helper.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
sabat.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
log.Printf("Raw authorization request body: %s", string(bodyBytes))
@@ -56,7 +57,7 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
// Decode JSON into AuthorizationContext
if err := json.Unmarshal(bodyBytes, &ctx); err != nil {
log.Printf("ERROR: Failed to unmarshal request body: %v", err)
helper.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
sabat.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
@@ -65,7 +66,7 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("User ID ctx=%s, resource=%s, action=%s, roleID=%d", ctx.UsersID, ctx.Resource, ctx.Action, ctx.RoleID)
if ctx.UsersID == "" || ctx.Resource == "" || ctx.Action == "" {
log.Printf("ERROR: Missing required fields - UsersID=%s, Resource=%s, Action=%s", ctx.UsersID, ctx.Resource, ctx.Action)
helper.RespondWithError(w, http.StatusBadRequest, "Missing required fields: users_id, resource, action")
sabat.RespondWithError(w, http.StatusBadRequest, "Missing required fields: users_id, resource, action")
return
}
@@ -74,7 +75,7 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
// Verify JWT user matches request user (security check)
if ctx.UsersID != claims.UsersID {
log.Printf("ERROR: User ID mismatch - ctx.UsersID='%s' vs claims.UsersID='%s'", ctx.UsersID, claims.UsersID)
helper.RespondWithError(w, http.StatusForbidden, "User ID mismatch")
sabat.RespondWithError(w, http.StatusForbidden, "User ID mismatch")
return
}
@@ -107,7 +108,7 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[Handler] Role candidate resolution - requested=%v, finalCandidates=%v", requestedRoles, validRoles)
if len(validRoles) == 0 {
log.Printf("ERROR: Role mismatch for user=%s - requestedRoles=%v, claimRoles=%v", ctx.UsersID, requestedRoles, claimRoles)
helper.RespondWithError(w, http.StatusForbidden, "Role ID mismatch")
sabat.RespondWithError(w, http.StatusForbidden, "Role ID mismatch")
return
}
@@ -119,29 +120,29 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[Handler] Performing authorization check for user=%s, resource=%s, action=%s", ctx.UsersID, ctx.Resource, ctx.Action)
result, err := services.AuthorizeWithCache(authService, &ctx)
if err != nil {
helper.LogError(err, "Authorization service error")
sabat.LogError(err, "Authorization service error")
log.Printf("✗ Authorization service error for user=%s: %v", ctx.UsersID, err)
helper.RespondWithError(w, http.StatusInternalServerError, "Authorization check failed")
sabat.RespondWithError(w, http.StatusInternalServerError, "Authorization check failed")
return
}
// Return result
if result.Allowed {
log.Printf("✓ [Handler] Authorization ALLOWED - Returning 200 OK to client")
// Return response matching AuthorizationResponse model for client compatibility
// Return sabat matching Authorizationsabat model for client compatibility
response := map[string]interface{}{
"allowed": result.Allowed,
"reason": result.Message,
}
helper.RespondWithJSON(w, http.StatusOK, response)
sabat.RespondWithJSON(w, http.StatusOK, response)
} else {
log.Printf("✗ [Handler] Authorization DENIED - Returning 403 Forbidden to client (reason: %s)", result.Message)
// Return response matching AuthorizationResponse model for client compatibility
// Return sabat matching Authorizationsabat model for client compatibility
response := map[string]interface{}{
"allowed": result.Allowed,
"reason": result.Message,
}
helper.RespondWithJSON(w, http.StatusForbidden, response)
sabat.RespondWithJSON(w, http.StatusForbidden, response)
}
}
+4 -3
View File
@@ -2,13 +2,14 @@ package handlers
import (
"authorization/db"
"authorization/helper"
"authorization/models"
"authorization/redisclient"
"context"
"encoding/json"
"net/http"
"time"
sabat "github.com/cespares/response"
)
// HealthHandler provides a basic liveness check
@@ -22,7 +23,7 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) {
response := models.HealthResponse{
Status: "ok",
}
helper.RespondWithJSON(w, http.StatusOK, response)
sabat.RespondWithJSON(w, http.StatusOK, response)
}
// ReadyHandler checks if the service is ready to handle requests
@@ -81,6 +82,6 @@ func ReadyHandler(w http.ResponseWriter, r *http.Request) {
Status: status,
Services: services,
}); err != nil {
helper.LogError(err, "Error encoding health response")
sabat.LogError(err, "Error encoding health response")
}
}
-28
View File
@@ -1,28 +0,0 @@
package helper
import (
"encoding/json"
"net/http"
)
func RespondWithError(w http.ResponseWriter, statusCode int, message string) {
w.Header().Set(ContentTypeHeader, ApplicationJSON)
w.WriteHeader(statusCode)
if encodeErr := json.NewEncoder(w).Encode(map[string]string{ErrorLabel: message}); encodeErr != nil {
LogError(encodeErr, ErrorEncodingResponse)
}
}
func RespondWithMessage(w http.ResponseWriter, message string) {
if encodeErr := json.NewEncoder(w).Encode(map[string]string{MessageLabel: message}); encodeErr != nil {
LogError(encodeErr, ErrorEncodingResponse)
}
}
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set(ContentTypeHeader, ApplicationJSON)
w.WriteHeader(statusCode)
if encodeErr := json.NewEncoder(w).Encode(data); encodeErr != nil {
LogError(encodeErr, ErrorEncodingResponse)
}
}
+4 -3
View File
@@ -1,7 +1,6 @@
package middleware
import (
"authorization/helper"
"authorization/models"
"authorization/redisclient"
"context"
@@ -16,6 +15,8 @@ import (
"sync"
"time"
sabat "github.com/cespares/response"
"github.com/golang-jwt/jwt/v5"
)
@@ -208,7 +209,7 @@ func JWTAuth(next http.HandlerFunc) http.HandlerFunc {
// Extract token from header
tokenString, ok := extractBearerToken(r.Header.Get("Authorization"))
if !ok {
helper.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
sabat.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
@@ -223,7 +224,7 @@ func JWTAuth(next http.HandlerFunc) http.HandlerFunc {
// Parse and validate token
claims, err := parseAndValidateToken(tokenString)
if err != nil {
helper.RespondWithError(w, http.StatusUnauthorized, errExpiredToken)
sabat.RespondWithError(w, http.StatusUnauthorized, errExpiredToken)
return
}
+5 -4
View File
@@ -1,13 +1,14 @@
package middleware
import (
"authorization/helper"
"authorization/models"
"authorization/redisclient"
"context"
"fmt"
"net/http"
"time"
sabat "github.com/cespares/response"
)
// DefaultRateLimitConfig returns default rate limiting settings
@@ -24,7 +25,7 @@ func RateLimiterMiddleware(config models.RateLimitConfig) func(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)")
sabat.LogError(nil, "Rate limiter: Redis not available, allowing request (fail-open)")
next.ServeHTTP(w, r)
return
}
@@ -41,13 +42,13 @@ func RateLimiterMiddleware(config models.RateLimitConfig) func(http.HandlerFunc)
allowed, err := checkRateLimit(identifier, config)
if err != nil {
// On error, fail open (allow request) but log the error
helper.LogError(err, "rate limiter error")
sabat.LogError(err, "rate limiter error")
next.ServeHTTP(w, r)
return
}
if !allowed {
helper.RespondWithError(w, http.StatusTooManyRequests, "Rate limit exceeded")
sabat.RespondWithError(w, http.StatusTooManyRequests, "Rate limit exceeded")
return
}