iam: surface OIDC groups and roles into the STS session request context for resource-policy ABAC (#10263)

* iam: surface OIDC groups into STS session request context for resource-policy ABAC

The groups claim was added to ExternalIdentity.Groups but excluded from Attributes
(processedClaims), so it never reached the session RequestContext. As a result
group membership was usable only in role trust policies (assume-time), not in
resource/permission policies (request-time) - so a single role could not scope
access by the caller's groups (aggregate ABAC). Surface Groups as a []string in
the request context; the string-condition evaluator already handles multi-valued
context keys, and the S3 middleware exposes it as jwt:groups.

* iam: also surface OIDC roles into the STS request context (companion to groups)

The 'roles' claim is excluded from the OIDC attributes by the same processedClaims
set that excluded 'groups', so it never reached the session request context and was
usable only via provider-configured role mapping - not a raw token roles claim. Add
ExternalIdentity.Roles, populate it from the token's roles claim, and surface it as a
[]string in the request context so resource policies can gate on jwt:roles, exactly
as the previous commit did for jwt:groups.
This commit is contained in:
Chris Wydra
2026-07-08 20:52:39 +12:00
committed by GitHub
parent e873e671b6
commit 63db56ff7d
3 changed files with 32 additions and 0 deletions
+5
View File
@@ -481,6 +481,10 @@ func (p *OIDCProvider) Authenticate(ctx context.Context, token string) (*provide
email, _ := claims.GetClaimString("email")
displayName, _ := claims.GetClaimString("name")
groups, _ := claims.GetClaimStringSlice("groups")
// The raw `roles` claim (e.g. from a Keycloak realm-role mapper). Surfaced into
// the STS request context below (see ExternalIdentity.Roles) so resource policies
// can gate on jwt:roles. Distinct from the provider-configured role mapping below.
rawRoles, _ := claims.GetClaimStringSlice("roles")
// Debug: Log available claims
glog.V(3).Infof("Available claims: %+v", claims.Claims)
@@ -535,6 +539,7 @@ func (p *OIDCProvider) Authenticate(ctx context.Context, token string) (*provide
Email: email,
DisplayName: displayName,
Groups: groups,
Roles: rawRoles,
Attributes: attributes,
Provider: p.name,
Issuer: claims.Issuer,
+7
View File
@@ -43,6 +43,13 @@ type ExternalIdentity struct {
// Groups are the groups the user belongs to
Groups []string `json:"groups,omitempty"`
// Roles are the roles the user holds (from the token's `roles` claim).
// Surfaced into the STS request context so resource policies can gate on
// jwt:roles - same rationale as Groups (the claim is otherwise excluded from
// attributes by the provider's processedClaims set, so it never reached the
// request context and was usable only via provider-configured role mapping).
Roles []string `json:"roles,omitempty"`
// Attributes are additional user attributes
Attributes map[string]string `json:"attributes,omitempty"`
+20
View File
@@ -640,6 +640,26 @@ func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *Ass
// Add sub as well since it's commonly used
requestContext["sub"] = externalIdentity.UserID
// Surface federated group memberships so resource (permission) policies can do
// group-based ABAC - not just role trust policies. Stored as a []string; the
// string-condition evaluator already handles multi-valued context keys, and the
// S3 middleware additionally exposes it as jwt:groups. Without this, "groups"
// was excluded from the OIDC attributes (see oidc_provider processedClaims) and
// so only usable at assume-time (trust policy), never at request-time - meaning
// a single role's permission policy could not scope access by the caller's
// groups (aggregate/ABAC).
if len(externalIdentity.Groups) > 0 {
requestContext["groups"] = externalIdentity.Groups
}
// Same for the caller's roles (the `roles` claim). Surfaced as a []string so a
// resource policy can gate on jwt:roles - the S3 middleware exposes it that way.
// Without this, "roles" was excluded from the OIDC attributes (processedClaims)
// and so unusable in a request-time permission policy.
if len(externalIdentity.Roles) > 0 {
requestContext["roles"] = externalIdentity.Roles
}
// Compute a stable parent-user hash from (sub, iss). Only this tuple is
// guaranteed stable across token refresh per OIDC Core 1.0, so this is the
// right key for any per-identity state (audit trail, future quotas).