Files
Authorization/handlers/authorize.go
T
admin e6bbaeffa6 feat(authz): redirect deleted accounts to /login
detect soft-deleted users during authorization lookup
return a dedicated deleted-user result from auth services
redirect deleted accounts to /login in the handler
update repository, service, and handler tests for the new flow
2026-05-08 09:04:28 +08:00

246 lines
7.5 KiB
Go

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
}
writeAuthorizationResponse(w, r, result)
}
func writeAuthorizationResponse(w http.ResponseWriter, r *http.Request, result *models.AuthorizationResult) {
if result.RedirectRoute != "" {
log.Printf("✗ [Handler] Authorization redirect to %s (reason: %s)", result.RedirectRoute, result.Message)
http.Redirect(w, r, result.RedirectRoute, http.StatusFound)
return
}
response := map[string]interface{}{
"allowed": result.Allowed,
"reason": result.Message,
}
if result.Allowed {
log.Printf("✓ [Handler] Authorization ALLOWED - Returning 200 OK to client")
sabat.RespondWithJSON(w, http.StatusOK, response)
return
}
log.Printf("✗ [Handler] Authorization DENIED - Returning 403 Forbidden to client (reason: %s)", 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
}