Files
Authorization/models/rbac.go
T
admin ae1831e61f 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.
2026-02-03 16:35:16 +08:00

139 lines
4.9 KiB
Go

package models
import (
"encoding/json"
"fmt"
"strconv"
"time"
)
// Permission represents a system permission
type Permission struct {
ID int `json:"id" db:"id"`
PermissionName string `json:"permission_name" db:"permission_name"`
Description string `json:"description" db:"description"`
Resource string `json:"resource" db:"resource"`
Action string `json:"action" db:"action"`
}
// RolePermission represents the junction table linking roles to permissions
type RolePermission struct {
ID int `json:"id" db:"id"`
RoleID int `json:"role_id" db:"role_id"`
PermissionID int `json:"permission_id" db:"permission_id"`
}
// PolicyAttribute represents an ABAC policy attribute/constraint
type PolicyAttribute struct {
ID int `json:"id" db:"id"`
AttributeName string `json:"attribute_name" db:"attribute_name"`
AttributeType string `json:"attribute_type" db:"attribute_type"` // user, resource, environment
Comparison string `json:"comparison" db:"comparison"` // =, !=, >, <, >=, <=, IN, CONTAINS
AttributeValue string `json:"attribute_value" db:"attribute_value"`
PermissionID int `json:"permission_id" db:"permission_id"`
}
// UserAttribute represents user-specific attributes for ABAC
type UserAttribute struct {
ID int `json:"id" db:"id"`
UserID string `json:"user_id" db:"user_id"`
AttributeName string `json:"attribute_name" db:"attribute_name"`
AttributeValue string `json:"attribute_value" db:"attribute_value"`
}
// User represents a system user
type User struct {
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"`
Suffix string `json:"suffix" db:"suffix"`
EmailAddress string `json:"email_address" db:"email_address"`
HomeAddress string `json:"home_address" db:"home_address"`
ContactNumber string `json:"contact_number" db:"contact_number"`
RoleID int `json:"role_id" db:"role_id"`
IsDeleted string `json:"is_deleted" db:"is_deleted"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// AuthorizationContext holds all context needed for authorization decisions
type AuthorizationContext struct {
UsersID string `json:"users_id"`
Resource string `json:"resource"`
Action string `json:"action"`
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"`
RedirectRoute string `json:"redirect_route,omitempty"` // Optional redirect route
Message string `json:"message,omitempty"` // Optional message
}
// CachedAuthorizationService adds caching layer to authorization
type CachedAuthorizationService struct {
PermissionCache map[string]*Permission `json:"-"` // key: "resource:action"
PolicyCache map[int][]PolicyAttribute `json:"-"`
UserAttrCache map[string]map[string]string `json:"-"` // key: userID
CacheMutex interface{} `json:"-"` // sync.RWMutex
UserAttrMutex interface{} `json:"-"` // sync.RWMutex
CacheExpiry time.Duration `json:"cache_expiry"`
LastCacheRefresh time.Time `json:"last_cache_refresh"`
}