Fix IAM OIDC role mapping and OIDC claims in trust policy (#8104)
* Fix IAM OIDC role mapping and OIDC claims in trust policy * Address PR review: Add config safety checks and refactor tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user