195 lines
4.7 KiB
Go
195 lines
4.7 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"`
|
|
}
|
|
|
|
func (p *ProjectClaim) UnmarshalJSON(data []byte) error {
|
|
type Alias ProjectClaim
|
|
aux := &struct {
|
|
RoleIDs RoleIDs `json:"role_ids"`
|
|
*Alias
|
|
}{
|
|
Alias: (*Alias)(p),
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(p.RoleID) == 0 && len(aux.RoleIDs) > 0 {
|
|
p.RoleID = aux.RoleIDs
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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"`
|
|
AdditionalRoleID RoleIDs `json:"additional_role_id,omitempty"`
|
|
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 {
|
|
aux := struct {
|
|
UsersID string `json:"users_id"`
|
|
UserID string `json:"user_id"`
|
|
EmailAddress string `json:"email_address"`
|
|
Email string `json:"email"`
|
|
RoleID RoleIDs `json:"role_id"`
|
|
AdditionalRoleID RoleIDs `json:"additional_role_id"`
|
|
Projects json.RawMessage `json:"projects"`
|
|
ProjectMetadata json.RawMessage `json:"project_metadata"`
|
|
ProjectsMetadata json.RawMessage `json:"projects_metadata"`
|
|
jwt.RegisteredClaims
|
|
}{}
|
|
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.UsersID = aux.UsersID
|
|
c.EmailAddress = aux.EmailAddress
|
|
c.RoleID = aux.RoleID
|
|
c.AdditionalRoleID = aux.AdditionalRoleID
|
|
c.RegisteredClaims = aux.RegisteredClaims
|
|
|
|
if c.UsersID == "" && aux.UserID != "" {
|
|
c.UsersID = aux.UserID
|
|
}
|
|
|
|
if c.EmailAddress == "" && aux.Email != "" {
|
|
c.EmailAddress = aux.Email
|
|
}
|
|
|
|
projects := make([]ProjectClaim, 0)
|
|
rawProjectFields := []json.RawMessage{aux.Projects, aux.ProjectMetadata, aux.ProjectsMetadata}
|
|
for _, raw := range rawProjectFields {
|
|
parsedProjects, err := parseProjectClaims(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(parsedProjects) > 0 {
|
|
projects = append(projects, parsedProjects...)
|
|
}
|
|
}
|
|
|
|
c.Projects = projects
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseProjectClaims(raw json.RawMessage) ([]ProjectClaim, error) {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
|
return nil, nil
|
|
}
|
|
|
|
switch trimmed[0] {
|
|
case '[':
|
|
var projects []ProjectClaim
|
|
if err := json.Unmarshal(trimmed, &projects); err != nil {
|
|
return nil, err
|
|
}
|
|
return projects, nil
|
|
case '{':
|
|
var project ProjectClaim
|
|
if err := json.Unmarshal(trimmed, &project); err != nil {
|
|
return nil, err
|
|
}
|
|
return []ProjectClaim{project}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported JSON for projects claim: %s", string(trimmed))
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|