Browse Source

Fix: Populate Claims from STS session RequestContext for policy variable substitution (#8082)

* Fix: Populate Claims from STS session RequestContext for policy variable substitution

When using STS temporary credentials (from AssumeRoleWithWebIdentity) with
AWS Signature V4 authentication, JWT claims like preferred_username were
not available for bucket policy variable substitution (e.g., ${jwt:preferred_username}).

Root Cause:
- STS session tokens store user claims in the req_ctx field (added in PR #8079)
- validateSTSSessionToken() created Identity but didn't populate Claims field
- authorizeWithIAM() created IAMIdentity but didn't copy Claims
- Policy engine couldn't resolve ${jwt:*} variables without claims

Changes:
1. auth_signature_v4.go: Extract claims from sessionInfo.RequestContext
   and populate Identity.Claims in validateSTSSessionToken()
2. auth_credentials.go: Copy Claims when creating IAMIdentity in
   authorizeWithIAM()
3. auth_sts_identity_test.go: Add TestSTSIdentityClaimsPopulation to
   verify claims are properly populated from RequestContext

This enables bucket policies with JWT claim variables to work correctly
with STS temporary credentials obtained via AssumeRoleWithWebIdentity.

Fixes #8037

* Refactor: Idiomatic map population for STS claims
pull/8083/head
Chris Lu 1 day ago
committed by GitHub
parent
commit
5472061231
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      weed/s3api/auth_credentials.go
  2. 9
      weed/s3api/auth_signature_v4.go
  3. 65
      weed/s3api/auth_sts_identity_test.go

1
weed/s3api/auth_credentials.go

@ -1408,6 +1408,7 @@ func (iam *IdentityAccessManagement) authorizeWithIAM(r *http.Request, identity
Name: identity.Name,
Account: identity.Account,
PolicyNames: identity.PolicyNames,
Claims: identity.Claims, // Copy claims for policy variable substitution
}
// Determine authorization path and configure identity

9
weed/s3api/auth_signature_v4.go

@ -382,6 +382,14 @@ func (iam *IdentityAccessManagement) validateSTSSessionToken(r *http.Request, se
Expiration: sessionInfo.ExpiresAt.Unix(),
}
// Create claims map from request context
// The request context contains user information from the original OIDC token
// that was used in AssumeRoleWithWebIdentity (e.g., preferred_username, email, etc.)
claims := make(map[string]interface{}, len(sessionInfo.RequestContext))
for k, v := range sessionInfo.RequestContext {
claims[k] = v
}
// Create an identity for the STS session
// The identity represents the assumed role user
identity := &Identity{
@ -390,6 +398,7 @@ func (iam *IdentityAccessManagement) validateSTSSessionToken(r *http.Request, se
Credentials: []*Credential{cred},
PrincipalArn: sessionInfo.Principal,
PolicyNames: sessionInfo.Policies, // Populate PolicyNames for IAM authorization
Claims: claims, // Populate Claims for policy variable substitution
}
glog.V(2).Infof("Successfully validated STS session token for principal: %s, assumed role user: %s",

65
weed/s3api/auth_sts_identity_test.go

@ -304,6 +304,71 @@ func TestValidateSTSSessionTokenIntegration(t *testing.T) {
t.Log("✓ Integration test passed: STS identity properly configured for IAM authorization")
}
// TestSTSIdentityClaimsPopulation tests that Claims are properly populated from RequestContext
// This is critical for policy variable substitution like ${jwt:preferred_username}
func TestSTSIdentityClaimsPopulation(t *testing.T) {
// Setup STS service
stsService, config := setupTestSTSService(t)
// Create IAM with STS integration
iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{}, "memory")
s3iam := &S3IAMIntegration{
stsService: stsService,
}
iam.SetIAMIntegration(s3iam)
// Create a mock HTTP request
req, err := http.NewRequest("PUT", "/test-bucket/test-object.txt", nil)
require.NoError(t, err)
// Generate session token with RequestContext containing user claims
sessionId := "claims-test-session"
expiresAt := time.Now().Add(time.Hour)
sessionClaims := sts.NewSTSSessionClaims(sessionId, config.Issuer, expiresAt).
WithSessionName("claims-test").
WithRoleInfo("arn:aws:iam::role/S3UserRole", "arn:aws:sts::assumed-role/S3UserRole/claims-test", "arn:aws:sts::assumed-role/S3UserRole/claims-test")
// Add RequestContext with user claims (simulating AssumeRoleWithWebIdentity)
sessionClaims.RequestContext = map[string]interface{}{
"preferred_username": "f2wbnp",
"email": "user@example.com",
"name": "Test User",
"groups": []string{"developers", "users"},
}
sessionClaims.Policies = []string{"S3UserPolicy"}
tokenGen := sts.NewTokenGenerator(config.SigningKey, config.Issuer)
sessionToken, err := tokenGen.GenerateJWTWithClaims(sessionClaims)
require.NoError(t, err)
// Validate session token
sessionInfo, err := stsService.ValidateSessionToken(context.Background(), sessionToken)
require.NoError(t, err)
require.NotNil(t, sessionInfo)
require.NotNil(t, sessionInfo.RequestContext, "RequestContext should be populated")
// Verify RequestContext has the claims
assert.Equal(t, "f2wbnp", sessionInfo.RequestContext["preferred_username"])
assert.Equal(t, "user@example.com", sessionInfo.RequestContext["email"])
// Validate session token and check identity creation
identity, _, errCode := iam.validateSTSSessionToken(req, sessionToken, sessionInfo.Credentials.AccessKeyId)
require.Equal(t, s3err.ErrNone, errCode)
require.NotNil(t, identity)
// Verify Claims are populated from RequestContext
assert.NotNil(t, identity.Claims, "Claims should be populated")
assert.Equal(t, "f2wbnp", identity.Claims["preferred_username"], "preferred_username should be in Claims")
assert.Equal(t, "user@example.com", identity.Claims["email"], "email should be in Claims")
assert.Equal(t, "Test User", identity.Claims["name"], "name should be in Claims")
// Verify PolicyNames are also populated
assert.Equal(t, []string{"S3UserPolicy"}, identity.PolicyNames)
t.Log("✓ Claims properly populated from RequestContext for policy variable substitution")
}
// Helper functions for tests
func setupTestSTSService(t *testing.T) (*sts.STSService, *sts.STSConfig) {

Loading…
Cancel
Save