package handlers import ( "authorization/middleware" "authorization/models" "authorization/services" "encoding/json" "io" "log" "net/http" sabat "github.com/cespares/response" ) 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 { log.Printf("ERROR: Missing JWT claims in request context (method=%s, path=%s)", r.Method, r.URL.Path) sabat.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 { log.Printf("ERROR: Failed to read authorization request body: %v", err) sabat.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) sabat.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) sabat.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) sabat.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) projectRoleCount := 0 for _, project := range claims.Projects { projectRoleCount += len(project.RoleID) log.Printf("[Handler] Project role claim - project_id=%d, alias=%s, roles=%v", project.ProjectID, project.Alias, project.RoleID) } log.Printf("[Handler] Claim roles parsed - base=%v, additional=%v, projects=%d, projectRoleEntries=%d, combined=%v", claims.RoleID, claims.AdditionalRoleID, len(claims.Projects), projectRoleCount, claimRoles, ) if len(claimRoles) == 0 { log.Printf("ERROR: No roles found in JWT claims for user=%s", claims.UsersID) } requestedRoles := collectRequestedRoles(&ctx) validRoles := buildRoleCandidates(requestedRoles, claimRoles) 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) sabat.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 { sabat.LogError(err, "Authorization service error") log.Printf("✗ Authorization service error for user=%s: %v", ctx.UsersID, err) 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 sabat matching Authorizationsabat model for client compatibility response := map[string]interface{}{ "allowed": result.Allowed, "reason": result.Message, } sabat.RespondWithJSON(w, http.StatusOK, response) } else { log.Printf("✗ [Handler] Authorization DENIED - Returning 403 Forbidden to client (reason: %s)", result.Message) // Return sabat matching Authorizationsabat model for client compatibility response := map[string]interface{}{ "allowed": result.Allowed, "reason": result.Message, } sabat.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 _, role := range claims.AdditionalRoleID { 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 } func buildRoleCandidates(requested, claimRoles []int) []int { if len(claimRoles) == 0 { return nil } if len(requested) == 0 { return append([]int(nil), claimRoles...) } primary := intersectRoles(requested, claimRoles) if len(primary) == 0 { return nil } seen := make(map[int]struct{}, len(primary)) for _, role := range primary { seen[role] = struct{}{} } result := append([]int(nil), primary...) for _, role := range claimRoles { if _, exists := seen[role]; exists { continue } seen[role] = struct{}{} result = append(result, role) } return result }