Fix trust policy validation for specific AWS user principals (#8597)
* Add tests for AWS user principal in AssumeRole trust policies Add test cases that verify trust policy validation when using specific AWS user principals (e.g., "arn:aws:iam::000000000000:user/backend") in the Principal field of trust policies for AssumeRole. Covers single user, multiple users (array), wildcard, and plain string principal formats. These tests demonstrate the bug reported in #8588 where specific user principals always fail validation. * Populate RequestContext in ValidateTrustPolicyForPrincipal ValidateTrustPolicyForPrincipal was creating an EvaluationContext with a nil RequestContext. The policy engine's principal matching logic looks up "aws:PrincipalArn" in RequestContext for non-wildcard principals, so specific user ARNs like "arn:aws:iam::000000000000:user/backend" always failed to match, while wildcard "*" worked because it short-circuits before the lookup. Populate RequestContext with both "principal" and "aws:PrincipalArn" keys, consistent with how IsActionAllowed already does it. Fixes #8588 * Remove GitHub discussion URL from source code comments * Add specific error message assertions in trust policy tests
This commit is contained in:
@@ -27,11 +27,18 @@ func (m *IAMManager) ValidateTrustPolicyForPrincipal(ctx context.Context, roleAr
|
|||||||
return fmt.Errorf("role has no trust policy")
|
return fmt.Errorf("role has no trust policy")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create evaluation context
|
// Create evaluation context with RequestContext populated so that
|
||||||
|
// principal matching works for specific (non-wildcard) principals.
|
||||||
|
// Without this, evaluatePrincipalValue cannot look up "aws:PrincipalArn"
|
||||||
|
// and always returns false for non-wildcard trust policy principals.
|
||||||
evalCtx := &policy.EvaluationContext{
|
evalCtx := &policy.EvaluationContext{
|
||||||
Principal: principalArn,
|
Principal: principalArn,
|
||||||
Action: "sts:AssumeRole",
|
Action: "sts:AssumeRole",
|
||||||
Resource: roleArn,
|
Resource: roleArn,
|
||||||
|
RequestContext: map[string]interface{}{
|
||||||
|
"principal": principalArn,
|
||||||
|
"aws:PrincipalArn": principalArn,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate the trust policy
|
// Evaluate the trust policy
|
||||||
|
|||||||
248
weed/iam/integration/trust_policy_principal_test.go
Normal file
248
weed/iam/integration/trust_policy_principal_test.go
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestTrustPolicyAWSUserPrincipal tests that trust policies with specific AWS user
|
||||||
|
// principals work correctly with ValidateTrustPolicyForPrincipal.
|
||||||
|
// This is a regression test for the case where setting a specific user principal like:
|
||||||
|
//
|
||||||
|
// "Principal": {"AWS": "arn:aws:iam::000000000000:user/backend"}
|
||||||
|
//
|
||||||
|
// would fail with "trust policy denies access to principal" even though the
|
||||||
|
// caller's principal ARN matched.
|
||||||
|
func TestTrustPolicyAWSUserPrincipal(t *testing.T) {
|
||||||
|
iamManager := setupIntegratedIAMSystem(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
const (
|
||||||
|
accountID = "000000000000"
|
||||||
|
backendUser = "backend"
|
||||||
|
backendArn = "arn:aws:iam::" + accountID + ":user/" + backendUser
|
||||||
|
otherUser = "other"
|
||||||
|
otherArn = "arn:aws:iam::" + accountID + ":user/" + otherUser
|
||||||
|
clientRoleN = "ClientRole"
|
||||||
|
clientRoleA = "arn:aws:iam::role/" + clientRoleN
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create role with trust policy restricted to a specific AWS user principal
|
||||||
|
// (the exact scenario from discussion #8588)
|
||||||
|
err := iamManager.CreateRole(ctx, "", clientRoleN, &RoleDefinition{
|
||||||
|
RoleName: clientRoleN,
|
||||||
|
TrustPolicy: &policy.PolicyDocument{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []policy.Statement{
|
||||||
|
{
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: map[string]interface{}{
|
||||||
|
"AWS": backendArn,
|
||||||
|
},
|
||||||
|
Action: []string{"sts:AssumeRole"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
principalArn string
|
||||||
|
expectErr bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matching user principal should be allowed",
|
||||||
|
principalArn: backendArn,
|
||||||
|
expectErr: false,
|
||||||
|
description: "Backend user ARN matches the trust policy principal exactly",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching user principal should be denied",
|
||||||
|
principalArn: otherArn,
|
||||||
|
expectErr: true,
|
||||||
|
description: "Other user ARN does not match the trust policy principal",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := iamManager.ValidateTrustPolicyForPrincipal(ctx, clientRoleA, tt.principalArn)
|
||||||
|
if tt.expectErr {
|
||||||
|
assert.Error(t, err, tt.description)
|
||||||
|
assert.Contains(t, err.Error(), "trust policy denies access")
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err, tt.description)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTrustPolicyAWSWildcardPrincipal tests that wildcard AWS principals
|
||||||
|
// continue to work correctly (this already works, serves as a regression guard).
|
||||||
|
func TestTrustPolicyAWSWildcardPrincipal(t *testing.T) {
|
||||||
|
iamManager := setupIntegratedIAMSystem(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
const roleName = "WildcardAWSRole"
|
||||||
|
const roleArn = "arn:aws:iam::role/" + roleName
|
||||||
|
|
||||||
|
err := iamManager.CreateRole(ctx, "", roleName, &RoleDefinition{
|
||||||
|
RoleName: roleName,
|
||||||
|
TrustPolicy: &policy.PolicyDocument{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []policy.Statement{
|
||||||
|
{
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: map[string]interface{}{
|
||||||
|
"AWS": "*",
|
||||||
|
},
|
||||||
|
Action: []string{"sts:AssumeRole"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Any user should be able to assume a role with wildcard AWS principal
|
||||||
|
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn, "arn:aws:iam::000000000000:user/anyuser")
|
||||||
|
assert.NoError(t, err, "Wildcard AWS principal should allow any user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTrustPolicyAWSUserArrayPrincipal tests trust policies with an array of
|
||||||
|
// AWS user principals.
|
||||||
|
func TestTrustPolicyAWSUserArrayPrincipal(t *testing.T) {
|
||||||
|
iamManager := setupIntegratedIAMSystem(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
const (
|
||||||
|
accountID = "000000000000"
|
||||||
|
roleName = "MultiUserRole"
|
||||||
|
roleArn = "arn:aws:iam::role/" + roleName
|
||||||
|
user1Arn = "arn:aws:iam::" + accountID + ":user/user1"
|
||||||
|
user2Arn = "arn:aws:iam::" + accountID + ":user/user2"
|
||||||
|
user3Arn = "arn:aws:iam::" + accountID + ":user/user3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create role with multiple allowed user principals
|
||||||
|
err := iamManager.CreateRole(ctx, "", roleName, &RoleDefinition{
|
||||||
|
RoleName: roleName,
|
||||||
|
TrustPolicy: &policy.PolicyDocument{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []policy.Statement{
|
||||||
|
{
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: map[string]interface{}{
|
||||||
|
"AWS": []interface{}{user1Arn, user2Arn},
|
||||||
|
},
|
||||||
|
Action: []string{"sts:AssumeRole"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
principalArn string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "first listed user is allowed",
|
||||||
|
principalArn: user1Arn,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "second listed user is allowed",
|
||||||
|
principalArn: user2Arn,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unlisted user is denied",
|
||||||
|
principalArn: user3Arn,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn, tt.principalArn)
|
||||||
|
if tt.expectErr {
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "trust policy denies access")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTrustPolicyPlainStringPrincipal tests trust policies with a plain string
|
||||||
|
// principal (not wrapped in {"AWS": ...}).
|
||||||
|
func TestTrustPolicyPlainStringPrincipal(t *testing.T) {
|
||||||
|
iamManager := setupIntegratedIAMSystem(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
const (
|
||||||
|
roleName = "PlainStringPrincipalRole"
|
||||||
|
roleArn = "arn:aws:iam::role/" + roleName
|
||||||
|
userArn = "arn:aws:iam::000000000000:user/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plain string wildcard - should work
|
||||||
|
err := iamManager.CreateRole(ctx, "", roleName, &RoleDefinition{
|
||||||
|
RoleName: roleName,
|
||||||
|
TrustPolicy: &policy.PolicyDocument{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []policy.Statement{
|
||||||
|
{
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: "*",
|
||||||
|
Action: []string{"sts:AssumeRole"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn, userArn)
|
||||||
|
assert.NoError(t, err, "Plain wildcard principal should allow any user")
|
||||||
|
|
||||||
|
// Plain string with specific ARN
|
||||||
|
const roleName2 = "PlainStringSpecificRole"
|
||||||
|
const roleArn2 = "arn:aws:iam::role/" + roleName2
|
||||||
|
|
||||||
|
err = iamManager.CreateRole(ctx, "", roleName2, &RoleDefinition{
|
||||||
|
RoleName: roleName2,
|
||||||
|
TrustPolicy: &policy.PolicyDocument{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []policy.Statement{
|
||||||
|
{
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: userArn,
|
||||||
|
Action: []string{"sts:AssumeRole"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn2, userArn)
|
||||||
|
assert.NoError(t, err, "Matching plain string principal should allow user")
|
||||||
|
|
||||||
|
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn2, "arn:aws:iam::000000000000:user/other")
|
||||||
|
if assert.Error(t, err, "Non-matching plain string principal should deny user") {
|
||||||
|
assert.Contains(t, err.Error(), "trust policy denies access")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user