You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of stacklok/stacklok-enterprise-platform#stacklok/stacklok-enterprise-platform#376
Description
Extend the existing group extraction logic (added in upstream commit 5c258a1) to support dual-claim extraction (group_claim_name + role_claim_name), dot-notation traversal for nested claims (e.g., realm_access.roles), and deduplication across both claim sources. Fix the merge-order hazard by wiring group parent UIDs onto the Client entity via the variadic parents parameter (from #4765) instead of creating dynamic THVGroup entities.
This is the convergence point that makes group-based authorization functional end-to-end with the enterprise controller's compiled Cedar policies.
Context
Upstream commit 5c258a1 ("Enforce Cedar policies on upstream IDP token claims") implemented basic group extraction:
extractGroupsFromClaims — single-claim extraction from flat JWT claims
THVGroup entity creation with the signature CreatePrincipalEntity(..., groups []string)
Groups set as Client parents via dynamically-created THVGroup entities
The RFC (Section 6) marks this as Partial (Changes #3 and #4) and identifies two problems with the current implementation:
Merge-order hazard: Dynamic THVGroup entities with empty parents overwrite static ones from entities_json (which carry THVRole parents), severing the role hierarchy
Missing functionality: No role_claim_name support, no dot-notation for nested claims (Keycloak realm_access.roles), no dedup across both claim sources
This task addresses both: it extends extraction and fixes the entity wiring.
Dependencies: #4763 (RoleClaimName field), #4764 (serverName on Authorizer), #4765 (variadic parents replacing groups parameter) Blocks: #4769 (MCP parent on resource entities), #4771 (end-to-end integration test)
Acceptance Criteria
resolveNestedClaim(claims jwt.MapClaims, path string) interface{} is implemented: tries exact top-level match first (handles Auth0 URL claims with dots), then falls back to dot-notation traversal (handles Keycloak nesting)
extractGroups(claims jwt.MapClaims, groupClaim string) []string extends the existing extractGroupsFromClaims: resolves via resolveNestedClaim, coerces to []string, returns nil when claim is empty or missing
dedup(groups []string) []string removes duplicate group names while preserving order
The existing extractGroupsFromClaims from 5c258a1 handles flat claims only. Replace it with three pure functions:
resolveNestedClaim — exact-match-first, then dot-notation traversal
extractGroups — wraps resolveNestedClaim with type coercion to []string
dedup — order-preserving deduplication
Phase 2 — Dual-claim extraction in AuthorizeWithJWTClaims:
After extracting claims and clientID but before preprocessClaims, extract groups from a.groupClaim and roles from a.roleClaim, merge, and dedup. This replaces the single-claim extraction from 5c258a1.
Phase 3 — Thread groups through authorize* methods:
Each of the four methods gains a groups []string parameter. They pass it through to CreateEntitiesForRequest.
Phase 4 — Wire group parents in CreateEntitiesForRequest:
Build THVGroup parent UIDs from the groups slice. Pass to CreatePrincipalEntity via the variadic parents parameter (from #4765). Do NOT create intermediate THVGroup entities — the static entity store from entities_json already contains them with the correct THVRole parents.
Patterns & Frameworks
Exact-match-first resolution: resolveNestedClaim tries a direct top-level key lookup before splitting on . for nested traversal. This handles Auth0 URL-based claim names (e.g., https://myapp.example.com/roles) that contain dots but are top-level keys.
Dual-claim model: Both GroupClaimName and RoleClaimName extract from different JWT claims but produce the same Cedar entity type (THVGroup). Values are unioned and deduplicated before being set as Client parents.
Backward compatibility: When both claim names are empty, extractGroups returns nil, dedup returns nil, and no parents are added — identical to pre-5c258a11 behavior.
Code Pointers
pkg/authz/authorizers/cedar/core.go — AuthorizeWithJWTClaims: replace single-claim extraction from 5c258a1 with dual-claim logic; extract BEFORE preprocessClaims
pkg/authz/authorizers/cedar/core.go — extractGroupsFromClaims (from 5c258a1): replace with resolveNestedClaim + extractGroups
pkg/authz/authorizers/cedar/core.go — authorizeToolCall, authorizePromptGet, authorizeResourceRead, authorizeFeatureList: add groups []string parameter
pkg/authz/authorizers/cedar/entity.go — CreateEntitiesForRequest: add groups and serverName parameters; build parent UIDs; pass to CreatePrincipalEntity via variadic; remove dynamic group entity creation from 5c258a1
pkg/authz/authorizers/cedar/core_test.go — existing test call sites need updating for new parameter
Component Interfaces
// resolveNestedClaim resolves a dot-separated claim path from JWT claims.// Tries exact top-level match first (handles Auth0 URL claim names),// then falls back to dot-notation traversal (handles Keycloak nesting).funcresolveNestedClaim(claims jwt.MapClaims, pathstring) interface{}
// extractGroups extracts group/role names from JWT claims.// Replaces the simpler extractGroupsFromClaims from 5c258a11 with// dot-notation support and better type handling.funcextractGroups(claims jwt.MapClaims, groupClaimstring) []string// dedup removes duplicate strings preserving order.funcdedup(groups []string) []string
Updated CreateEntitiesForRequest:
// Build group parent UIDs for the principal (NO dynamic Group entities)vargroupParents []cedar.EntityUIDfor_, g:=rangegroups {
groupParents=append(groupParents,
cedar.NewEntityUID("THVGroup", cedar.String(g)))
}
// Create principal entity with group parentsprincipalUID, principalEntity:=f.CreatePrincipalEntity(
principalType, principalID, claimsMap, groupParents...)
entities[principalUID] =principalEntity
Testing Strategy
Unit Tests
TestResolveNestedClaim_ExactMatch: top-level key "groups" returns the value directly
TestExtractGroups_NonArrayClaim: string claim value returns nil
TestDedup_WithDuplicates: removes duplicates, preserving first-occurrence order
TestDedup_NilInput: nil returns nil
TestAuthorizeWithJWTClaims_DualClaim: groups from both groupClaim and roleClaim are merged and deduplicated
TestCreateEntitiesForRequest_GroupParents: call with groups; verify principal has THVGroup parent UIDs and NO separate THVGroup entities in the entity map (merge-order hazard regression test)
TestCreateEntitiesForRequest_NoGroups: call with nil groups; verify empty parent set (backward compat)
Edge Cases
Auth0 URL claim name with dots does NOT trigger nested traversal when exact match exists
Part of stacklok/stacklok-enterprise-platform#stacklok/stacklok-enterprise-platform#376
Description
Extend the existing group extraction logic (added in upstream commit 5c258a1) to support dual-claim extraction (
group_claim_name+role_claim_name), dot-notation traversal for nested claims (e.g.,realm_access.roles), and deduplication across both claim sources. Fix the merge-order hazard by wiring group parent UIDs onto theCliententity via the variadicparentsparameter (from #4765) instead of creating dynamicTHVGroupentities.This is the convergence point that makes group-based authorization functional end-to-end with the enterprise controller's compiled Cedar policies.
Context
Upstream commit 5c258a1 ("Enforce Cedar policies on upstream IDP token claims") implemented basic group extraction:
extractGroupsFromClaims— single-claim extraction from flat JWT claimsTHVGroupentity creation with the signatureCreatePrincipalEntity(..., groups []string)Clientparents via dynamically-createdTHVGroupentitiesThe RFC (Section 6) marks this as Partial (Changes #3 and #4) and identifies two problems with the current implementation:
THVGroupentities with empty parents overwrite static ones fromentities_json(which carryTHVRoleparents), severing the role hierarchyrole_claim_namesupport, no dot-notation for nested claims (Keycloakrealm_access.roles), no dedup across both claim sourcesThis task addresses both: it extends extraction and fixes the entity wiring.
Dependencies: #4763 (RoleClaimName field), #4764 (serverName on Authorizer), #4765 (variadic parents replacing groups parameter)
Blocks: #4769 (MCP parent on resource entities), #4771 (end-to-end integration test)
Acceptance Criteria
resolveNestedClaim(claims jwt.MapClaims, path string) interface{}is implemented: tries exact top-level match first (handles Auth0 URL claims with dots), then falls back to dot-notation traversal (handles Keycloak nesting)extractGroups(claims jwt.MapClaims, groupClaim string) []stringextends the existingextractGroupsFromClaims: resolves viaresolveNestedClaim, coerces to[]string, returns nil when claim is empty or missingdedup(groups []string) []stringremoves duplicate group names while preserving orderAuthorizeWithJWTClaims: groups extracted from botha.groupClaimANDa.roleClaim(from Add RoleClaimName to ConfigOptions (complement existing GroupClaimName) #4763), merged and deduplicated BEFOREpreprocessClaimsrunsauthorize*methods accept agroups []stringparameterCreateEntitiesForRequestacceptsgroups []stringandserverName stringparametersCreateEntitiesForRequestbuildsTHVGroupparent UIDs from thegroupsslice and passes them toCreatePrincipalEntityvia the variadicparentsparameterCreateEntitiesForRequestdoes NOT create dynamicTHVGroupentities (merge-order hazard fix — RFC Section 6, Change Bump golangci/golangci-lint-action from 2f856675483cb8b9378ee77ee0beb67955aca9d7 to 4696ba8babb6127d732c3c6dde519db15edab9ea #3)groupClaimandroleClaimare both empty, no groups are extracted and behavior is identical to pre-5c258a11 behavior (backward compatible)Technical Approach
Recommended Implementation
Phase 1 — Replace
extractGroupsFromClaimswithresolveNestedClaim+extractGroups+dedup:The existing
extractGroupsFromClaimsfrom 5c258a1 handles flat claims only. Replace it with three pure functions:resolveNestedClaim— exact-match-first, then dot-notation traversalextractGroups— wrapsresolveNestedClaimwith type coercion to[]stringdedup— order-preserving deduplicationPhase 2 — Dual-claim extraction in
AuthorizeWithJWTClaims:After extracting
claimsandclientIDbut beforepreprocessClaims, extract groups froma.groupClaimand roles froma.roleClaim, merge, and dedup. This replaces the single-claim extraction from 5c258a1.Phase 3 — Thread groups through
authorize*methods:Each of the four methods gains a
groups []stringparameter. They pass it through toCreateEntitiesForRequest.Phase 4 — Wire group parents in
CreateEntitiesForRequest:Build
THVGroupparent UIDs from the groups slice. Pass toCreatePrincipalEntityvia the variadicparentsparameter (from #4765). Do NOT create intermediateTHVGroupentities — the static entity store fromentities_jsonalready contains them with the correctTHVRoleparents.Patterns & Frameworks
resolveNestedClaimtries a direct top-level key lookup before splitting on.for nested traversal. This handles Auth0 URL-based claim names (e.g.,https://myapp.example.com/roles) that contain dots but are top-level keys.GroupClaimNameandRoleClaimNameextract from different JWT claims but produce the same Cedar entity type (THVGroup). Values are unioned and deduplicated before being set asClientparents.Cliententity's parent set is modified. TheTHVGroupentities with theirTHVRoleparents come from the staticentities_json. This is the core merge-order hazard fix described in RFC Section 6, Change Bump golangci/golangci-lint-action from 2f856675483cb8b9378ee77ee0beb67955aca9d7 to 4696ba8babb6127d732c3c6dde519db15edab9ea #3.extractGroupsreturns nil,dedupreturns nil, and no parents are added — identical to pre-5c258a11 behavior.Code Pointers
pkg/authz/authorizers/cedar/core.go—AuthorizeWithJWTClaims: replace single-claim extraction from 5c258a1 with dual-claim logic; extract BEFOREpreprocessClaimspkg/authz/authorizers/cedar/core.go—extractGroupsFromClaims(from 5c258a1): replace withresolveNestedClaim+extractGroupspkg/authz/authorizers/cedar/core.go—authorizeToolCall,authorizePromptGet,authorizeResourceRead,authorizeFeatureList: addgroups []stringparameterpkg/authz/authorizers/cedar/entity.go—CreateEntitiesForRequest: addgroupsandserverNameparameters; build parent UIDs; pass toCreatePrincipalEntityvia variadic; remove dynamic group entity creation from 5c258a1pkg/authz/authorizers/cedar/core_test.go— existing test call sites need updating for new parameterComponent Interfaces
Updated
CreateEntitiesForRequest:Testing Strategy
Unit Tests
TestResolveNestedClaim_ExactMatch: top-level key"groups"returns the value directlyTestResolveNestedClaim_DotNotation:"realm_access.roles"traverses nested mapTestResolveNestedClaim_Auth0URL:"https://myapp.example.com/roles"matches exact top-level key despite dotsTestResolveNestedClaim_MissingClaim: non-existent key returns nilTestResolveNestedClaim_NestedTraversalHitsNonObject:"foo.bar"wherefoois a string returns nilTestExtractGroups_FlatClaim: replaces existing single-claim test;groupClaim="groups"with flat arrayTestExtractGroups_NestedClaim:groupClaim="realm_access.roles"with Keycloak-style nestingTestExtractGroups_EmptyClaimName: empty claim returns nilTestExtractGroups_MissingClaim: absent claim returns nilTestExtractGroups_NonArrayClaim: string claim value returns nilTestDedup_WithDuplicates: removes duplicates, preserving first-occurrence orderTestDedup_NilInput: nil returns nilTestAuthorizeWithJWTClaims_DualClaim: groups from bothgroupClaimandroleClaimare merged and deduplicatedTestCreateEntitiesForRequest_GroupParents: call with groups; verify principal has THVGroup parent UIDs and NO separate THVGroup entities in the entity map (merge-order hazard regression test)TestCreateEntitiesForRequest_NoGroups: call with nil groups; verify empty parent set (backward compat)Edge Cases
resource_access.my-app.roles) resolves correctly[]produces no parent UIDsOut of Scope
THVGroupentities inCreateEntitiesForRequest(explicitly prohibited — merge-order hazard)FeatureTypewithMCPfor list operations (Replace FeatureType with MCP for list operations #4770)IsAuthorizedor the entity merge logicgroup_claimsplural) — deferred future extensionReferences