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:
+62
-7
@@ -84,12 +84,21 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx.Environment = make(map[string]string)
|
||||
}
|
||||
|
||||
// containsRole checks if a role exists in a slice of roles
|
||||
claimRoles := collectClaimRoles(claims)
|
||||
requestedRoles := collectRequestedRoles(&ctx)
|
||||
if len(requestedRoles) == 0 {
|
||||
requestedRoles = claimRoles
|
||||
}
|
||||
|
||||
if !containsRole([]int(claims.RoleID), ctx.RoleID) {
|
||||
validRoles := intersectRoles(requestedRoles, claimRoles)
|
||||
if len(validRoles) == 0 {
|
||||
helper.RespondWithError(w, http.StatusForbidden, "Role ID mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.CandidateRoles = validRoles
|
||||
ctx.RoleID = validRoles[0]
|
||||
ctx.RoleIDs = validRoles
|
||||
log.Print("User role verified: ", ctx.RoleID)
|
||||
// Perform authorization
|
||||
log.Printf("[Handler] Performing authorization check for user=%s, resource=%s, action=%s", ctx.UsersID, ctx.Resource, ctx.Action)
|
||||
@@ -121,11 +130,57 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func containsRole(roles []int, role int) bool {
|
||||
for _, r := range roles {
|
||||
if r == role {
|
||||
return true
|
||||
func collectClaimRoles(claims *models.Claims) []int {
|
||||
unique := make(map[int]struct{})
|
||||
roles := make([]int, 0, len(claims.RoleID))
|
||||
|
||||
for _, role := range claims.RoleID {
|
||||
if _, exists := unique[role]; !exists {
|
||||
unique[role] = struct{}{}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
for _, project := range claims.Projects {
|
||||
for _, role := range project.RoleID {
|
||||
if _, exists := unique[role]; !exists {
|
||||
unique[role] = struct{}{}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
func collectRequestedRoles(ctx *models.AuthorizationContext) []int {
|
||||
if len(ctx.RoleIDs) > 0 {
|
||||
return append([]int(nil), ctx.RoleIDs...)
|
||||
}
|
||||
if ctx.RoleID != 0 {
|
||||
return []int{ctx.RoleID}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func intersectRoles(requested, available []int) []int {
|
||||
availableSet := make(map[int]struct{}, len(available))
|
||||
for _, role := range available {
|
||||
availableSet[role] = struct{}{}
|
||||
}
|
||||
|
||||
unique := make(map[int]struct{})
|
||||
result := make([]int, 0, len(requested))
|
||||
for _, role := range requested {
|
||||
if _, ok := availableSet[role]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, seen := unique[role]; seen {
|
||||
continue
|
||||
}
|
||||
unique[role] = struct{}{}
|
||||
result = append(result, role)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -375,3 +375,52 @@ func TestAuthorizeHandlerWithResourceData(t *testing.T) {
|
||||
t.Errorf("Handler returned bad request with valid ResourceData")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectClaimRolesIncludesProjectRoles(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
RoleID: models.RoleIDs{2},
|
||||
Projects: []models.ProjectClaim{
|
||||
{ProjectID: 7, RoleID: models.RoleIDs{2, 4}},
|
||||
{ProjectID: 8, RoleID: models.RoleIDs{5}},
|
||||
},
|
||||
}
|
||||
|
||||
roles := collectClaimRoles(claims)
|
||||
if len(roles) != 3 {
|
||||
t.Fatalf("expected 3 unique roles, got %d (%v)", len(roles), roles)
|
||||
}
|
||||
|
||||
if roles[0] != 2 || roles[1] != 4 || roles[2] != 5 {
|
||||
t.Fatalf("unexpected role order/content: %v", roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntersectRolesReturnsOverlap(t *testing.T) {
|
||||
requested := []int{4, 9, 4, 2}
|
||||
available := []int{2, 4, 5}
|
||||
|
||||
result := intersectRoles(requested, available)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 matching roles, got %d (%v)", len(result), result)
|
||||
}
|
||||
|
||||
if result[0] != 4 || result[1] != 2 {
|
||||
t.Fatalf("unexpected intersection result: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectRequestedRolesFromArray(t *testing.T) {
|
||||
ctx := &models.AuthorizationContext{
|
||||
RoleIDs: []int{3, 7},
|
||||
RoleID: 3,
|
||||
}
|
||||
|
||||
result := collectRequestedRoles(ctx)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 requested roles, got %d (%v)", len(result), result)
|
||||
}
|
||||
|
||||
if result[0] != 3 || result[1] != 7 {
|
||||
t.Fatalf("unexpected requested roles: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user