diff --git a/weed/iam/integration/iam_integration_test.go b/weed/iam/integration/iam_integration_test.go index 4740152a8..dcdda7bed 100644 --- a/weed/iam/integration/iam_integration_test.go +++ b/weed/iam/integration/iam_integration_test.go @@ -519,6 +519,75 @@ func TestTrustPolicyWildcardPrincipal(t *testing.T) { } } +// TestOIDCClaimsTrustPolicy tests that OIDC claims are correctly mapped to trust policy context +func TestOIDCClaimsTrustPolicy(t *testing.T) { + iamManager := setupIntegratedIAMSystem(t) + ctx := context.Background() + + // Create a role that requires a specific OIDC role claim + err := iamManager.CreateRole(ctx, "", "OIDCRoleClaimRole", &RoleDefinition{ + RoleName: "OIDCRoleClaimRole", + TrustPolicy: &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Effect: "Allow", + Principal: map[string]interface{}{ + "Federated": "test-oidc", + }, + Action: []string{"sts:AssumeRoleWithWebIdentity"}, + Condition: map[string]map[string]interface{}{ + "StringLike": { + "oidc:roles": "Dev.SeaweedFS.*", + }, + }, + }, + }, + }, + AttachedPolicies: []string{"S3ReadOnlyPolicy"}, + }) + require.NoError(t, err) + + // Helper: Create a JWT with the specified roles claim + createTokenWithRoles := func(roles []string) string { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iss": "https://test-issuer.com", + "sub": "test-user-123", + "aud": "test-client-id", + "exp": time.Now().Add(time.Hour).Unix(), + "iat": time.Now().Unix(), + "roles": roles, + }) + signedToken, err := token.SignedString([]byte("test-signing-key-32-characters-long")) + require.NoError(t, err) + return signedToken + } + + // Create JWT tokens using the helper + validToken := createTokenWithRoles([]string{"Dev.SeaweedFS.Admin", "Dev.SeaweedFS.Audit"}) + invalidToken := createTokenWithRoles([]string{"Other.Role"}) + + // Test case 1: Valid roles -> Should succeed + assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{ + RoleArn: "arn:aws:iam::role/OIDCRoleClaimRole", + WebIdentityToken: validToken, + RoleSessionName: "oidc-claims-test", + } + response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest) + require.NoError(t, err, "Should allow role assumption when oidc:roles claim matches") + require.NotNil(t, response) + + // Test case 2: Invalid roles -> Should fail + badRequest := &sts.AssumeRoleWithWebIdentityRequest{ + RoleArn: "arn:aws:iam::role/OIDCRoleClaimRole", + WebIdentityToken: invalidToken, + RoleSessionName: "oidc-claims-fail-test", + } + response, err = iamManager.AssumeRoleWithWebIdentity(ctx, badRequest) + assert.Error(t, err, "Should deny role assumption when oidc:roles claim does not match") + assert.Nil(t, response) +} + // Helper functions and test setup // createTestJWT creates a test JWT token with the specified issuer, subject and signing key diff --git a/weed/iam/integration/iam_manager.go b/weed/iam/integration/iam_manager.go index 739181b59..ccbb5ef00 100644 --- a/weed/iam/integration/iam_manager.go +++ b/weed/iam/integration/iam_manager.go @@ -508,6 +508,18 @@ func (m *IAMManager) validateTrustPolicyForWebIdentity(ctx context.Context, role } // Custom claims can be prefixed if needed, but for "be 100% compatible with AWS", // we should rely on standard OIDC claims. + + // Add all other claims with oidc: prefix to support custom claims in trust policies + // This enables checking claims like "oidc:roles", "oidc:groups", "oidc:email", etc. + for k, v := range tokenClaims { + // Skip claims we've already handled explicitly or shouldn't expose + if k == "iss" || k == "sub" || k == "aud" { + continue + } + + // Add with oidc: prefix + requestContext["oidc:"+k] = v + } } // Add DurationSeconds to context if provided diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index e219265bb..7670773e6 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -780,11 +780,35 @@ func loadIAMManagerFromConfig(configPath string, filerAddressProvider func() str // Load identity providers providerFactory := sts.NewProviderFactory() for _, providerConfig := range configRoot.Providers { + // Check for required fields with explicit type assertion + name, ok := providerConfig["name"].(string) + if !ok || name == "" { + glog.Warningf("Skipping provider with invalid or missing name: %+v", providerConfig) + continue + } + providerType, ok := providerConfig["type"].(string) + if !ok || providerType == "" { + glog.Warningf("Skipping provider %s with invalid or missing type", name) + continue + } + + // Fix: providerConfig["roleMapping"] might be missing from "config" map if configured externally + // We inject it into the config map so the factory can find it + configMap, ok := providerConfig["config"].(map[string]interface{}) + if !ok { + glog.Warningf("Validation failed for provider %s: config must be a map", name) + continue + } + + if roleMapping, ok := providerConfig["roleMapping"]; ok { + configMap["roleMapping"] = roleMapping + } + provider, err := providerFactory.CreateProvider(&sts.ProviderConfig{ - Name: providerConfig["name"].(string), - Type: providerConfig["type"].(string), + Name: name, + Type: providerType, Enabled: true, - Config: providerConfig["config"].(map[string]interface{}), + Config: configMap, }) if err != nil { glog.Warningf("Failed to create provider %s: %v", providerConfig["name"], err)