200 lines
6.3 KiB
Go
200 lines
6.3 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 {
|
|
log.Printf("ERROR: Missing JWT claims in request context (method=%s, path=%s)", r.Method, r.URL.Path)
|
|
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 {
|
|
log.Printf("ERROR: Failed to read authorization request body: %v", err)
|
|
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)
|
|
if len(claimRoles) == 0 {
|
|
log.Printf("ERROR: No roles found in JWT claims for user=%s", claims.UsersID)
|
|
}
|
|
requestedRoles := collectRequestedRoles(&ctx)
|
|
if len(requestedRoles) == 0 {
|
|
requestedRoles = claimRoles
|
|
}
|
|
|
|
validRoles := intersectRoles(requestedRoles, claimRoles)
|
|
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")
|
|
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 _, 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
|
|
}
|