mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-06 03:57:48 +01:00
iam: accept AWS action names for listing multipart uploads and parts (#11765)
* iam: accept AWS action names for listing multipart uploads and parts PutUserPolicy rejected s3:ListBucketMultipartUploads and s3:ListMultipartUploadParts as invalid actions, so no inline policy could grant these operations by their AWS names. Map both to Read, which is the action their routes are registered under. Fixes #11738 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1b55sEL1gA1c3FCMcQCbm * test: check each multipart listing action separately Granting both actions in one policy let a correct mapping for one hide a wrong mapping for the other. Test each action on its own and assert it grants only Read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1b55sEL1gA1c3FCMcQCbm * iam: scope multipart listing grants to their resolved action A policy naming s3:ListBucketMultipartUploads or s3:ListMultipartUploadParts projected to a Read:<bucket> grant, which the legacy route check honored for every object in the bucket. Map the AWS names to dedicated action constants and let VerifyActionPermission fall back to the resolved request action, so the grant covers only the listing operation it names. * iam: narrow legacy ListParts and ListMultipartUploads mappings Both policy names projected to Write:<bucket>, granting full object writes on any bucket where listing was intended. They now map to the same scoped listing actions as their AWS names. * iam: bind the multipart listing fallback to the listing routes The legacy-action fallback in VerifyActionPermission resolved the operation with ResolveS3Action, which reads the promoted authorization object. For GET /bucket?uploads&prefix=foo/ the prefix becomes the object, so the request resolved to GetObject and a ListBucketMultipartUploads grant was denied. Resolving from query priority also did not match how the router picks a handler. Only fall back for the two listing routes, identified from the route vars the same way the router dispatches them, and add the scoped actions to MapToIdentitiesAction so GetUserPolicy in the standalone IAM API rebuilds valid policy actions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1b55sEL1gA1c3FCMcQCbm --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
This commit is contained in:
5 files changed
+155
-3
No files matched your search
@@ -2160,6 +2160,114 @@ func TestEmbeddedIamGetActionsPrefixScopedResource(t *testing.T) {
|
||||
"PutObject outside the allowed prefix should be denied")
|
||||
}
|
||||
|
||||
// TestEmbeddedIamGetActionsMultipartListing covers the AWS names for
|
||||
// listing multipart uploads and parts: they grant only the named listing
|
||||
// operation, never a general Read on the bucket.
|
||||
func TestEmbeddedIamGetActionsMultipartListing(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{}, nil, "memory")
|
||||
|
||||
// Check each action on its own so a correct mapping for one cannot hide
|
||||
// a wrong mapping for the other.
|
||||
for _, tc := range []struct {
|
||||
policyAction string
|
||||
grant string
|
||||
url string
|
||||
object string
|
||||
}{
|
||||
{"s3:ListBucketMultipartUploads", "ListBucketMultipartUploads:demo", "/demo?uploads", ""},
|
||||
{"s3:ListMultipartUploadParts", "ListMultipartUploadParts:demo", "/demo/key?uploadId=u1", "key"},
|
||||
} {
|
||||
t.Run(tc.policyAction, func(t *testing.T) {
|
||||
policyDoc := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action": ["` + tc.policyAction + `"],
|
||||
"Resource": ["arn:aws:s3:::demo", "arn:aws:s3:::demo/*"]
|
||||
}]
|
||||
}`
|
||||
|
||||
policy, err := api.GetPolicyDocument(&policyDoc)
|
||||
require.NoError(t, err)
|
||||
|
||||
actions, err := api.getActions(&policy)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, actions)
|
||||
for _, a := range actions {
|
||||
assert.Equal(t, tc.grant, a,
|
||||
"%s should only grant its own listing operation", tc.policyAction)
|
||||
}
|
||||
|
||||
identity := &Identity{Name: "bob"}
|
||||
for _, a := range actions {
|
||||
identity.Actions = append(identity.Actions, Action(a))
|
||||
}
|
||||
|
||||
// The listing routes are gated on ACTION_READ, but the grant must
|
||||
// not authorize object reads or anything else.
|
||||
assert.False(t, identity.CanDo(ACTION_READ, "demo", "key"),
|
||||
"a listing grant must not allow object reads")
|
||||
assert.False(t, identity.CanDo(ACTION_WRITE, "demo", "key"),
|
||||
"a listing grant must not allow writes")
|
||||
assert.False(t, identity.CanDo(ACTION_LIST, "demo", ""),
|
||||
"a listing grant must not allow ListObjects")
|
||||
|
||||
routed := func(url, object string) *http.Request {
|
||||
return mux.SetURLVars(httptest.NewRequest(http.MethodGet, url, nil),
|
||||
map[string]string{"bucket": "demo", "object": object})
|
||||
}
|
||||
|
||||
req := routed(tc.url, tc.object)
|
||||
assert.Equal(t, s3err.ErrNone, iam.VerifyActionPermission(req, identity, ACTION_READ, "demo", tc.object),
|
||||
"the listing action should be authorized")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, iam.VerifyActionPermission(req, identity, ACTION_READ, "other", tc.object),
|
||||
"the grant must stay scoped to the policy's bucket")
|
||||
|
||||
getReq := routed("/demo/key", "key")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, iam.VerifyActionPermission(getReq, identity, ACTION_READ, "demo", "key"),
|
||||
"the grant must not authorize GetObject")
|
||||
|
||||
// Other ACTION_READ routes must not be authorized by a listing
|
||||
// grant, whatever query parameters ride along.
|
||||
policyReq := routed("/demo?policy", "")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, iam.VerifyActionPermission(policyReq, identity, ACTION_READ, "demo", ""),
|
||||
"the grant must not authorize GetBucketPolicy")
|
||||
})
|
||||
}
|
||||
|
||||
grant := func(a string) *Identity { return &Identity{Name: "bob", Actions: []Action{Action(a)}} }
|
||||
routed := func(url, object string) *http.Request {
|
||||
return mux.SetURLVars(httptest.NewRequest(http.MethodGet, url, nil),
|
||||
map[string]string{"bucket": "demo", "object": object})
|
||||
}
|
||||
|
||||
// authRequestWithAuthType promotes ?prefix into the object for upload
|
||||
// listings; the operation must still come from the route, not the prefix.
|
||||
t.Run("upload listing with prefix", func(t *testing.T) {
|
||||
req := routed("/demo?uploads&prefix=foo/", "")
|
||||
assert.Equal(t, s3err.ErrNone,
|
||||
iam.VerifyActionPermission(req, grant("ListBucketMultipartUploads:demo"), ACTION_READ, "demo", "foo/"))
|
||||
assert.Equal(t, s3err.ErrAccessDenied,
|
||||
iam.VerifyActionPermission(req, grant("ListMultipartUploadParts:demo"), ACTION_READ, "demo", "foo/"))
|
||||
})
|
||||
|
||||
// The ?uploads route is registered before ?policy, so this reaches the
|
||||
// upload listing handler and needs the upload listing grant.
|
||||
t.Run("upload listing with extra subresource", func(t *testing.T) {
|
||||
req := routed("/demo?policy&uploads", "")
|
||||
assert.Equal(t, s3err.ErrNone,
|
||||
iam.VerifyActionPermission(req, grant("ListBucketMultipartUploads:demo"), ACTION_READ, "demo", ""))
|
||||
assert.Equal(t, s3err.ErrAccessDenied,
|
||||
iam.VerifyActionPermission(req, grant("GetBucketPolicy:demo"), ACTION_READ, "demo", ""))
|
||||
})
|
||||
|
||||
// Policies rebuilt from these grants must use valid action names.
|
||||
for _, a := range []string{ACTION_LIST_MULTIPART_UPLOADS, ACTION_LIST_PARTS} {
|
||||
assert.Equal(t, a, iamMapToIdentitiesAction(a))
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbeddedIamPutUserPolicyAllResourceWildcard reproduces issue #9209:
|
||||
// an AWS-style policy using "Action":"s3:*" with a bare "Resource":"*" is
|
||||
// valid AWS IAM syntax (meaning "any resource") but was rejected with
|
||||
|
||||
Reference in new issue
Block a user