diff --git a/weed/iam/policy/aws_iam_compliance_test.go b/weed/iam/policy/aws_iam_compliance_test.go index 0979589a5..cff61c9a4 100644 --- a/weed/iam/policy/aws_iam_compliance_test.go +++ b/weed/iam/policy/aws_iam_compliance_test.go @@ -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{}{ diff --git a/weed/iam/policy/policy_engine.go b/weed/iam/policy/policy_engine.go index 7feca5c92..2daa0a3e1 100644 --- a/weed/iam/policy/policy_engine.go +++ b/weed/iam/policy/policy_engine.go @@ -600,12 +600,31 @@ func (e *PolicyEngine) statementMatches(statement *Statement, evalCtx *Evaluatio return true } -// matchesActions checks if any action in the list matches the requested action +// multipartActionSet contains lowercased S3 multipart upload actions that are +// implicitly granted when s3:PutObject is allowed, since multipart upload is an +// implementation detail of putting objects. Keys are lowercased for +// case-insensitive lookup (AWS IAM actions are case-insensitive). +var multipartActionSet = map[string]bool{ + "s3:createmultipartupload": true, + "s3:uploadpart": true, + "s3:completemultipartupload": true, + "s3:abortmultipartupload": true, + "s3:listmultipartuploadparts": true, + "s3:listbucketmultipartuploads": true, +} + +// matchesActions checks if any action in the list matches the requested action. +// It also implicitly grants multipart upload actions when s3:PutObject is allowed, +// mirroring the behavior in the S3 API policy engine (see PR #8445). func (e *PolicyEngine) matchesActions(actions []string, requestedAction string, evalCtx *EvaluationContext) bool { + isMultipart := multipartActionSet[strings.ToLower(requestedAction)] for _, action := range actions { if awsIAMMatch(action, requestedAction, evalCtx) { return true } + if isMultipart && awsIAMMatch(action, "s3:PutObject", evalCtx) { + return true + } } return false }