Files
Authorization/models/rbac.go
T
admin 6262c875b7 feat(authz): support multi-role claim evaluation and role-aware permission checks
Parse and normalize user and project role claims (role_id + projects[].role_id)
Intersect requested roles with JWT-available roles before authorization
Evaluate permissions across candidate roles in both cached and non-cached flows
Fix claim field fallbacks (user_id/email) and role ID log formatting
Update tests and SQL mock expectations for new role-resolution behavior
2026-02-27 08:39:33 +08:00

144 lines
5.1 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
RoleIDs []int `json:"-"`
CandidateRoles []int `json:"-"`
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
ac.RoleIDs = []int{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]
ac.RoleIDs = roleArray
}
} 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)
}
ac.RoleIDs = []int{ac.RoleID}
}
} 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"`
}