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
This commit is contained in:
+17
-7
@@ -21,6 +21,13 @@ type AuthorizationResponse struct {
|
||||
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,...]).
|
||||
@@ -75,9 +82,10 @@ func (r *RoleIDs) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UsersID string `json:"users_id,omitempty"`
|
||||
EmailAddress string `json:"email_address,omitempty"`
|
||||
RoleID RoleIDs `json:"role_id"`
|
||||
UsersID string `json:"users_id,omitempty"`
|
||||
EmailAddress string `json:"email_address,omitempty"`
|
||||
RoleID RoleIDs `json:"role_id"`
|
||||
Projects []ProjectClaim `json:"projects,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -85,6 +93,8 @@ type Claims struct {
|
||||
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),
|
||||
@@ -93,12 +103,12 @@ func (c *Claims) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
// If UsersID is empty but UserID is set, copy UserID to UsersID
|
||||
if c.UsersID == "" && c.UsersID != "" {
|
||||
c.UsersID = c.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 == "" && c.EmailAddress != "" {
|
||||
c.EmailAddress = c.EmailAddress
|
||||
if c.EmailAddress == "" && aux.Email != "" {
|
||||
c.EmailAddress = aux.Email
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ type AuthorizationContext struct {
|
||||
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.
|
||||
@@ -91,12 +93,14 @@ func (ac *AuthorizationContext) UnmarshalJSON(data []byte) error {
|
||||
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
|
||||
@@ -108,6 +112,7 @@ func (ac *AuthorizationContext) UnmarshalJSON(data []byte) error {
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user