Files
Authorization/handlers/authorize.go
T
admin 6262c875b7 feat(authz): support multi-role claim evaluation and role-aware permission checks
Parse and normalize user and project role claims (role_id + projects[].role_id)
Intersect requested roles with JWT-available roles before authorization
Evaluate permissions across candidate roles in both cached and non-cached flows
Fix claim field fallbacks (user_id/email) and role ID log formatting
Update tests and SQL mock expectations for new role-resolution behavior
2026-02-27 08:39:33 +08:00

187 lines
5.8 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)
}
claimRoles := collectClaimRoles(claims)
requestedRoles := collectRequestedRoles(&ctx)
if len(requestedRoles) == 0 {
requestedRoles = claimRoles
}
validRoles := intersectRoles(requestedRoles, claimRoles)
if len(validRoles) == 0 {
helper.RespondWithError(w, http.StatusForbidden, "Role ID mismatch")
return
}
ctx.CandidateRoles = validRoles
ctx.RoleID = validRoles[0]
ctx.RoleIDs = validRoles
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 collectClaimRoles(claims *models.Claims) []int {
unique := make(map[int]struct{})
roles := make([]int, 0, len(claims.RoleID))
for _, role := range claims.RoleID {
if _, exists := unique[role]; !exists {
unique[role] = struct{}{}
roles = append(roles, role)
}
}
for _, project := range claims.Projects {
for _, role := range project.RoleID {
if _, exists := unique[role]; !exists {
unique[role] = struct{}{}
roles = append(roles, role)
}
}
}
return roles
}
func collectRequestedRoles(ctx *models.AuthorizationContext) []int {
if len(ctx.RoleIDs) > 0 {
return append([]int(nil), ctx.RoleIDs...)
}
if ctx.RoleID != 0 {
return []int{ctx.RoleID}
}
return nil
}
func intersectRoles(requested, available []int) []int {
availableSet := make(map[int]struct{}, len(available))
for _, role := range available {
availableSet[role] = struct{}{}
}
unique := make(map[int]struct{})
result := make([]int, 0, len(requested))
for _, role := range requested {
if _, ok := availableSet[role]; !ok {
continue
}
if _, seen := unique[role]; seen {
continue
}
unique[role] = struct{}{}
result = append(result, role)
}
return result
}