fix(s3): apply PutObject multipart expansion to STS session policies (#8932)

* fix(s3): apply PutObject multipart expansion to STS session policy evaluation (#8929)

PR #8445 added logic to implicitly grant multipart upload actions when
s3:PutObject is authorized, but only in the S3 API policy engine's
CompiledStatement.MatchesAction(). STS session policies are evaluated
through the IAM policy engine's matchesActions() -> awsIAMMatch() path,
which did plain pattern matching without the multipart expansion.

Add the same multipart expansion logic to the IAM policy engine's
matchesActions() so that session policies containing s3:PutObject
correctly allow multipart upload operations.

* fix: make multipart action set lookup case-insensitive and optimize

Address PR review feedback:
- Lowercase multipartActionSet keys and use strings.ToLower for lookup,
  since AWS IAM actions are case-insensitive
- Only check for s3:PutObject permission when the requested action is
  actually a multipart action, avoiding unnecessary awsIAMMatch calls
- Add test case for case-insensitive multipart action matching
This commit is contained in:
Chris Lu
2026-04-05 14:06:50 -07:00
committed by GitHub
parent 69cd5fa37b
commit b8fc99a9cd
2 changed files with 106 additions and 1 deletions

View File

@@ -110,6 +110,92 @@ func TestAWSIAMMatch(t *testing.T) {
}
}
func TestMatchesActionsMultipartExpansion(t *testing.T) {
engine := &PolicyEngine{initialized: true}
evalCtx := &EvaluationContext{}
tests := []struct {
name string
actions []string
requestedAction string
expected bool
}{
{
name: "PutObject directly matches PutObject",
actions: []string{"s3:PutObject"},
requestedAction: "s3:PutObject",
expected: true,
},
{
name: "PutObject implicitly allows CreateMultipartUpload",
actions: []string{"s3:PutObject"},
requestedAction: "s3:CreateMultipartUpload",
expected: true,
},
{
name: "PutObject implicitly allows UploadPart",
actions: []string{"s3:PutObject"},
requestedAction: "s3:UploadPart",
expected: true,
},
{
name: "PutObject implicitly allows CompleteMultipartUpload",
actions: []string{"s3:PutObject"},
requestedAction: "s3:CompleteMultipartUpload",
expected: true,
},
{
name: "PutObject implicitly allows AbortMultipartUpload",
actions: []string{"s3:PutObject"},
requestedAction: "s3:AbortMultipartUpload",
expected: true,
},
{
name: "PutObject implicitly allows ListMultipartUploadParts",
actions: []string{"s3:PutObject"},
requestedAction: "s3:ListMultipartUploadParts",
expected: true,
},
{
name: "PutObject implicitly allows ListBucketMultipartUploads",
actions: []string{"s3:PutObject"},
requestedAction: "s3:ListBucketMultipartUploads",
expected: true,
},
{
name: "PutObject does not allow GetObject",
actions: []string{"s3:PutObject"},
requestedAction: "s3:GetObject",
expected: false,
},
{
name: "GetObject does not allow CreateMultipartUpload",
actions: []string{"s3:GetObject"},
requestedAction: "s3:CreateMultipartUpload",
expected: false,
},
{
name: "wildcard s3:Put* implicitly allows multipart via PutObject match",
actions: []string{"s3:Put*"},
requestedAction: "s3:CreateMultipartUpload",
expected: true,
},
{
name: "case-insensitive multipart action lookup",
actions: []string{"s3:PutObject"},
requestedAction: "S3:CREATEMULTIPARTUPLOAD",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := engine.matchesActions(tt.actions, tt.requestedAction, evalCtx)
assert.Equal(t, tt.expected, result)
})
}
}
func TestExpandPolicyVariables(t *testing.T) {
evalCtx := &EvaluationContext{
RequestContext: map[string]interface{}{