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:
Chris Lu
2026-03-10 21:19:40 -07:00
committed by GitHub
parent ac579c1746
commit f950a941e3
2 changed files with 256 additions and 1 deletions

View File

@@ -27,11 +27,18 @@ func (m *IAMManager) ValidateTrustPolicyForPrincipal(ctx context.Context, roleAr
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{
Principal: principalArn,
Action: "sts:AssumeRole",
Resource: roleArn,
RequestContext: map[string]interface{}{
"principal": principalArn,
"aws:PrincipalArn": principalArn,
},
}
// Evaluate the trust policy

View 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")
}
}