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:
+83
-4
@@ -1,13 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type AuthorizationRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
UsersID string `json:"users_id"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
@@ -17,13 +21,88 @@ type AuthorizationResponse struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// RoleIDs represents one or more role IDs.
|
||||
// It is defined as a custom type so we can implement flexible JSON unmarshalling
|
||||
// that accepts a single string ("1"), a single number (1), or an array ([1,2,...]).
|
||||
type RoleIDs []int
|
||||
|
||||
// UnmarshalJSON allows RoleIDs to be populated from different JSON shapes:
|
||||
// string: "1" -> [1]
|
||||
// number: 1 -> [1]
|
||||
// array: [1] or [1,2,...] -> [1] or [1,2,...]
|
||||
func (r *RoleIDs) UnmarshalJSON(data []byte) error {
|
||||
// Handle null or empty
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
||||
*r = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
switch trimmed[0] {
|
||||
case '"':
|
||||
// String value, e.g. "1"
|
||||
var s string
|
||||
if err := json.Unmarshal(trimmed, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
*r = nil
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*r = RoleIDs{v}
|
||||
return nil
|
||||
case '[':
|
||||
// Standard JSON array of ints
|
||||
var arr []int
|
||||
if err := json.Unmarshal(trimmed, &arr); err != nil {
|
||||
return err
|
||||
}
|
||||
*r = RoleIDs(arr)
|
||||
return nil
|
||||
default:
|
||||
// Try to decode as a single number
|
||||
var v int
|
||||
if err := json.Unmarshal(trimmed, &v); err == nil {
|
||||
*r = RoleIDs{v}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported JSON for role_id: %s", string(trimmed))
|
||||
}
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
EmailAddress string `json:"email_address"`
|
||||
RoleID string `json:"role_id"`
|
||||
UsersID string `json:"users_id,omitempty"`
|
||||
EmailAddress string `json:"email_address,omitempty"`
|
||||
RoleID RoleIDs `json:"role_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles both "user_id" and "users_id" field names in JWT claims
|
||||
func (c *Claims) UnmarshalJSON(data []byte) error {
|
||||
type Alias Claims
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(c),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
// If UsersID is empty but UserID is set, copy UserID to UsersID
|
||||
if c.UsersID == "" && c.UsersID != "" {
|
||||
c.UsersID = c.UsersID
|
||||
}
|
||||
// If EmailAddress is empty but Email is set, copy Email to EmailAddress
|
||||
if c.EmailAddress == "" && c.EmailAddress != "" {
|
||||
c.EmailAddress = c.EmailAddress
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextKey is a custom type for context keys to avoid collisions
|
||||
type ContextKey string
|
||||
|
||||
|
||||
+57
-4
@@ -1,6 +1,11 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Permission represents a system permission
|
||||
type Permission struct {
|
||||
@@ -38,7 +43,7 @@ type UserAttribute struct {
|
||||
|
||||
// User represents a system user
|
||||
type User struct {
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
UsersID string `json:"users_id" db:"user_id"`
|
||||
FirstName string `json:"first_name" db:"first_name"`
|
||||
MiddleInitial string `json:"middle_initial" db:"middle_initial"`
|
||||
LastName string `json:"last_name" db:"last_name"`
|
||||
@@ -57,15 +62,63 @@ type User struct {
|
||||
|
||||
// AuthorizationContext holds all context needed for authorization decisions
|
||||
type AuthorizationContext struct {
|
||||
UserID string `json:"user_id"`
|
||||
UsersID string `json:"users_id"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
RoleID string `json:"role_id"` // User's role ID
|
||||
RoleID int `json:"role_id"` // User's role ID
|
||||
UserAttributes map[string]string `json:"user_attributes"`
|
||||
ResourceData map[string]string `json:"resource_data"` // Additional resource context
|
||||
Environment map[string]string `json:"environment"` // Time, location, etc.
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles role_id as either string or int
|
||||
func (ac *AuthorizationContext) UnmarshalJSON(data []byte) error {
|
||||
type Alias AuthorizationContext
|
||||
aux := &struct {
|
||||
RoleIDRaw json.RawMessage `json:"role_id"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(ac),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle role_id as string, int, or array
|
||||
if len(aux.RoleIDRaw) > 0 {
|
||||
// Try unmarshaling as int first
|
||||
var roleInt int
|
||||
if err := json.Unmarshal(aux.RoleIDRaw, &roleInt); err == nil {
|
||||
ac.RoleID = roleInt
|
||||
} else {
|
||||
// Try as array of ints (take first element)
|
||||
var roleArray []int
|
||||
if err := json.Unmarshal(aux.RoleIDRaw, &roleArray); err == nil {
|
||||
if len(roleArray) > 0 {
|
||||
ac.RoleID = roleArray[0]
|
||||
}
|
||||
} else {
|
||||
// Try as string
|
||||
var roleStr string
|
||||
if err := json.Unmarshal(aux.RoleIDRaw, &roleStr); err == nil {
|
||||
if roleStr != "" {
|
||||
var convErr error
|
||||
ac.RoleID, convErr = strconv.Atoi(roleStr)
|
||||
if convErr != nil {
|
||||
return fmt.Errorf("invalid role_id: %s", roleStr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("role_id must be a number, numeric string, or array of numbers")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizationResult contains the result of an authorization check
|
||||
type AuthorizationResult struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
|
||||
Reference in New Issue
Block a user