Files
Authorization/models/authorize.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

124 lines
2.9 KiB
Go

package models
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/golang-jwt/jwt/v5"
)
type AuthorizationRequest struct {
UsersID string `json:"users_id"`
Resource string `json:"resource"`
Action string `json:"action"`
}
type AuthorizationResponse struct {
Allowed bool `json:"allowed"`
Reason string `json:"reason,omitempty"`
}
type ProjectClaim struct {
ProjectID int `json:"project_id,omitempty"`
Alias string `json:"alias,omitempty"`
RoleID RoleIDs `json:"role_id,omitempty"`
OfficeID int `json:"office_id,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 {
UsersID string `json:"users_id,omitempty"`
EmailAddress string `json:"email_address,omitempty"`
RoleID RoleIDs `json:"role_id"`
Projects []ProjectClaim `json:"projects,omitempty"`
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 {
UserID string `json:"user_id"`
Email string `json:"email"`
*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 == "" && aux.UserID != "" {
c.UsersID = aux.UserID
}
// If EmailAddress is empty but Email is set, copy Email to EmailAddress
if c.EmailAddress == "" && aux.Email != "" {
c.EmailAddress = aux.Email
}
return nil
}
// ContextKey is a custom type for context keys to avoid collisions
type ContextKey string
// CacheEntry represents a token cache entry
type CacheEntry struct {
Claims *Claims
ExpiresAt time.Time
}