diff --git a/weed/iam/oidc/oidc_provider.go b/weed/iam/oidc/oidc_provider.go index b5c3c1969..fddd3ddd3 100644 --- a/weed/iam/oidc/oidc_provider.go +++ b/weed/iam/oidc/oidc_provider.go @@ -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, diff --git a/weed/iam/providers/provider.go b/weed/iam/providers/provider.go index 3c4fe5088..854590724 100644 --- a/weed/iam/providers/provider.go +++ b/weed/iam/providers/provider.go @@ -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"` diff --git a/weed/iam/sts/sts_service.go b/weed/iam/sts/sts_service.go index 1d520af2f..e7ca17262 100644 --- a/weed/iam/sts/sts_service.go +++ b/weed/iam/sts/sts_service.go @@ -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).