Files
Authorization/handlers/authorize.go
T
admin ae1831e61f feat: standardize field names and add flexible role_id handling for JWT compatibility
- 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.
2026-02-03 16:35:16 +08:00

132 lines
4.5 KiB
Go

package handlers
import (
"authorization/helper"
"authorization/middleware"
"authorization/models"
"authorization/services"
"encoding/json"
"io"
"log"
"net/http"
)
var authService *models.CachedAuthorizationService
// InitAuthService initializes the authorization service with caching
func InitAuthService() {
authService = services.NewCachedAuthorizationService()
}
// AuthorizeHandler godoc
// @Summary Check user authorization (RBAC + ABAC)
// @Description Validates if a user has permission to perform an action on a resource using Role-Based and Attribute-Based Access Control
// @Tags authorization
// @Accept json
// @Produce json
// @Param request body models.AuthorizationContext true "Authorization context with resource data"
// @Success 200 {object} models.AuthorizationResult
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} models.AuthorizationResult
// @Security BearerToken
// @Router /v1/auth/check [post]
func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
// Get claims from JWT middleware
claims, ok := middleware.GetClaims(r)
if !ok {
helper.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("JWT Claims: UsersID='%s', EmailAddress='%s', RoleID=%v", claims.UsersID, claims.EmailAddress, claims.RoleID)
var ctx models.AuthorizationContext
// Read and log raw request body
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
helper.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
log.Printf("Raw authorization request body: %s", string(bodyBytes))
// 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")
return
}
// Validate request
log.Printf("Decoded authorization context: %+v", ctx)
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")
return
}
log.Print("Authorization request for user=", ctx.UsersID, ", resource=", ctx.Resource, ", action=", ctx.Action)
log.Print("JWT claims user=", claims.UsersID, ", role=", claims.RoleID)
// 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")
return
}
// Initialize maps if nil
if ctx.ResourceData == nil {
ctx.ResourceData = make(map[string]string)
}
if ctx.Environment == nil {
ctx.Environment = make(map[string]string)
}
// containsRole checks if a role exists in a slice of roles
if !containsRole([]int(claims.RoleID), ctx.RoleID) {
helper.RespondWithError(w, http.StatusForbidden, "Role ID mismatch")
return
}
log.Print("User role verified: ", ctx.RoleID)
// Perform authorization
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")
log.Printf("✗ Authorization service error for user=%s: %v", ctx.UsersID, err)
helper.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
response := map[string]interface{}{
"allowed": result.Allowed,
"reason": result.Message,
}
helper.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
response := map[string]interface{}{
"allowed": result.Allowed,
"reason": result.Message,
}
helper.RespondWithJSON(w, http.StatusForbidden, response)
}
}
func containsRole(roles []int, role int) bool {
for _, r := range roles {
if r == role {
return true
}
}
return false
}