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.
This commit is contained in:
+37
-10
@@ -6,6 +6,7 @@ import (
|
||||
"authorization/models"
|
||||
"authorization/services"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
@@ -38,24 +39,39 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("JWT Claims: UsersID='%s', EmailAddress='%s', RoleID=%v", claims.UsersID, claims.EmailAddress, claims.RoleID)
|
||||
|
||||
var ctx models.AuthorizationContext
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&ctx)
|
||||
// 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
|
||||
if ctx.UserID == "" || ctx.Resource == "" || ctx.Action == "" {
|
||||
helper.RespondWithError(w, http.StatusBadRequest, "Missing required fields: user_id, resource, action")
|
||||
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.UserID, ", resource=", ctx.Resource, ", action=", ctx.Action)
|
||||
log.Print("JWT claims user=", claims.UserID, ", role=", claims.RoleID)
|
||||
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.UserID != claims.UserID {
|
||||
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
|
||||
}
|
||||
@@ -68,17 +84,19 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx.Environment = make(map[string]string)
|
||||
}
|
||||
|
||||
if ctx.RoleID != claims.RoleID {
|
||||
// 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.UserID, ctx.Resource, ctx.Action)
|
||||
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.UserID, err)
|
||||
log.Printf("✗ Authorization service error for user=%s: %v", ctx.UsersID, err)
|
||||
helper.RespondWithError(w, http.StatusInternalServerError, "Authorization check failed")
|
||||
return
|
||||
}
|
||||
@@ -102,3 +120,12 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
helper.RespondWithJSON(w, http.StatusForbidden, response)
|
||||
}
|
||||
}
|
||||
|
||||
func containsRole(roles []int, role int) bool {
|
||||
for _, r := range roles {
|
||||
if r == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+29
-29
@@ -44,8 +44,8 @@ func TestAuthorizeHandlerNoJWTClaims(t *testing.T) {
|
||||
func TestAuthorizeHandlerInvalidJSON(t *testing.T) {
|
||||
// Setup - no need to init service, we're testing JSON parsing before auth
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBufferString("invalid json"))
|
||||
@@ -73,19 +73,19 @@ func TestAuthorizeHandlerMissingRequiredFields(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Missing Resource",
|
||||
payload: models.AuthorizationContext{UserID: "user123", Action: "read"},
|
||||
payload: models.AuthorizationContext{UsersID: "user123", Action: "read"},
|
||||
},
|
||||
{
|
||||
name: "Missing Action",
|
||||
payload: models.AuthorizationContext{UserID: "user123", Resource: "document"},
|
||||
payload: models.AuthorizationContext{UsersID: "user123", Resource: "document"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(tc.payload)
|
||||
@@ -106,12 +106,12 @@ func TestAuthorizeHandlerMissingRequiredFields(t *testing.T) {
|
||||
func TestAuthorizeHandlerUserIDMismatch(t *testing.T) {
|
||||
// Setup
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "differentUser",
|
||||
UsersID: "differentUser",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
}
|
||||
@@ -134,12 +134,12 @@ func TestAuthorizeHandlerUserIDMismatch(t *testing.T) {
|
||||
func TestAuthorizeHandlerNilMaps(t *testing.T) {
|
||||
// Test that nil maps don't cause additional panics beyond missing authService
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: nil, // nil map
|
||||
@@ -171,12 +171,12 @@ func TestAuthorizeHandlerNilMaps(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerEmptyUserID(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "",
|
||||
UsersID: "",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
}
|
||||
@@ -196,12 +196,12 @@ func TestAuthorizeHandlerEmptyUserID(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerEmptyResource(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "",
|
||||
Action: "read",
|
||||
}
|
||||
@@ -221,12 +221,12 @@ func TestAuthorizeHandlerEmptyResource(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerEmptyAction(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "",
|
||||
}
|
||||
@@ -261,8 +261,8 @@ func TestAuthorizeHandlerInvalidClaimsType(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerMalformedJSON(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -307,7 +307,7 @@ func TestAuthorizeHandlerSpecialCharactersInFields(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: tc.userID,
|
||||
UsersID: tc.userID,
|
||||
Resource: tc.resource,
|
||||
Action: tc.action,
|
||||
}
|
||||
@@ -317,8 +317,8 @@ func TestAuthorizeHandlerSpecialCharactersInFields(t *testing.T) {
|
||||
|
||||
// Update claims to match userID
|
||||
testClaims := &models.Claims{
|
||||
UserID: tc.userID,
|
||||
RoleID: "admin",
|
||||
UsersID: tc.userID,
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), testClaims)
|
||||
req = req.WithContext(ctx)
|
||||
@@ -343,12 +343,12 @@ func TestAuthorizeHandlerSpecialCharactersInFields(t *testing.T) {
|
||||
func TestAuthorizeHandlerWithResourceData(t *testing.T) {
|
||||
// Test that ResourceData is properly passed through to authorization
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "personnel",
|
||||
Action: "assign_role",
|
||||
ResourceData: map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user