Files
seaweedfs/weed/s3api/auth_credentials_native_floor_test.go
T
Chris LuGitHubDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f9289f0570 s3: do not promote ?prefix into the object for non-List actions (#11494)
* s3: do not promote ?prefix into the object for non-List actions

authRequestWithAuthType mapped an empty object to the prefix parameter for
every action, so PUT /bucket?versioning&prefix=x authorized as Write:bucket/x.
An object-scoped grant (Write:bucket/*) could then change bucket versioning,
lifecycle, cors, and object-lock configuration, and the promoted object also
made ResolveS3Action report s3:PutObject to attached IAM policies.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: treat GET ?uploads as a bucket listing for authorization

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: resolve the listing action through the bucket-level object

resolveS3AuthTarget fed the promoted prefix to ResolveS3Action, so a
bucket-level ?uploads request resolved as s3:GetObject on the prefix ARN
in the admin explicit-deny check. Resolve both action and resource
against the object the bucket listing actually scopes.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: resolve the listing action through the bucket-level object in AuthorizeAction

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: drop the unreachable object-level uploads case from the resolver test

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 07:17:58 +08:00

178 lines
6.8 KiB
Go

package s3api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newNativeFloorTestIAM(t *testing.T) (*IdentityAccessManagement, *Identity) {
t.Helper()
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*"},
},
})
require.NoError(t, iam.PutPolicy("ListBucketsOnly", string(doc)))
identity := &Identity{
Name: "admin",
Account: &Account{DisplayName: "admin", Id: "admin"},
Actions: []Action{s3_constants.ACTION_ADMIN},
PolicyNames: []string{"ListBucketsOnly"},
PrincipalArn: "arn:aws:iam::111122223333:user/admin",
}
return iam, identity
}
func putObjectRequest() *http.Request {
return httptest.NewRequest(http.MethodPut, "/mybucket/file.txt", nil)
}
// TestNativeAdminSurvivesAttachedPolicy reproduces issue #11226: attaching an
// IAM policy to a user with native Admin must not override the native grant.
// The effective permissions are the union of native permissions and attached
// policy grants, so a Write the attached policy never mentions still succeeds.
func TestNativeAdminSurvivesAttachedPolicy(t *testing.T) {
iam, identity := newNativeFloorTestIAM(t)
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrNone, errCode,
"native Admin must remain effective after attaching a policy")
}
// TestNativeAdminSurvivesDeletedPolicy reproduces the second half of #11226:
// deleting the attached policy without detaching it first must not leave the
// user locked out of their native permissions.
func TestNativeAdminSurvivesDeletedPolicy(t *testing.T) {
iam, identity := newNativeFloorTestIAM(t)
require.NoError(t, iam.DeletePolicy("ListBucketsOnly"))
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrNone, errCode,
"native Admin must remain effective after the attached policy is deleted")
}
// TestNativeAdminSurvivesAttachedPolicyWithoutPrincipalArn covers the IAM
// integration path when the Admin identity has no PrincipalArn (and no session
// token), so the auth-path switch would otherwise deny before the native floor.
func TestNativeAdminSurvivesAttachedPolicyWithoutPrincipalArn(t *testing.T) {
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*"},
},
})
require.NoError(t, iam.PutPolicy("ListBucketsOnly", string(doc)))
identity := &Identity{
Name: "admin",
Account: &Account{DisplayName: "admin", Id: "admin"},
Actions: []Action{s3_constants.ACTION_ADMIN},
PolicyNames: []string{"ListBucketsOnly"},
}
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrNone, errCode,
"native Admin must remain effective without a PrincipalArn")
}
// TestAttachedPolicyExplicitDenyOverridesNativeAdmin ensures deny-always-wins:
// an explicit Deny in an attached policy still constrains a native admin.
func TestAttachedPolicyExplicitDenyOverridesNativeAdmin(t *testing.T) {
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Deny", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::mybucket/*"},
},
})
require.NoError(t, iam.PutPolicy("DenyPutMyBucket", string(doc)))
identity := &Identity{
Name: "admin",
Account: &Account{DisplayName: "admin", Id: "admin"},
Actions: []Action{s3_constants.ACTION_ADMIN},
PolicyNames: []string{"DenyPutMyBucket"},
PrincipalArn: "arn:aws:iam::111122223333:user/admin",
}
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrAccessDenied, errCode,
"explicit Deny in an attached policy must override native Admin")
}
// GET ?uploads lists multipart uploads at bucket level but routes under Read,
// so the prefix promoted into object must not change the evaluated action or
// resource: an explicit Deny on s3:ListBucketMultipartUploads must still
// constrain a native admin, and an Allow on the bucket ARN must satisfy a
// plain attached-policy identity.
func TestMultipartListingResolvesBucketAction(t *testing.T) {
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
denyDoc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Deny", "Action": "s3:ListBucketMultipartUploads", "Resource": "arn:aws:s3:::mybucket"},
},
})
require.NoError(t, iam.PutPolicy("DenyUploadsListing", string(denyDoc)))
allowDoc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Allow", "Action": "s3:ListBucketMultipartUploads", "Resource": "arn:aws:s3:::mybucket"},
},
})
require.NoError(t, iam.PutPolicy("AllowUploadsListing", string(allowDoc)))
uploadsReq := func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/mybucket?uploads&prefix=x/", nil)
}
admin := &Identity{
Name: "admin",
Account: &Account{DisplayName: "admin", Id: "admin"},
Actions: []Action{s3_constants.ACTION_ADMIN},
PolicyNames: []string{"DenyUploadsListing"},
PrincipalArn: "arn:aws:iam::111122223333:user/admin",
}
reader := &Identity{
Name: "reader",
Account: &Account{DisplayName: "reader", Id: "reader"},
PolicyNames: []string{"AllowUploadsListing"},
PrincipalArn: "arn:aws:iam::111122223333:user/reader",
}
// object carries the promoted prefix, matching authRequestWithAuthType
assert.Equal(t, s3err.ErrAccessDenied,
iam.VerifyActionPermission(uploadsReq(), admin, s3_constants.ACTION_READ, "mybucket", "x/"),
"explicit Deny on the uploads listing must override native Admin")
assert.Equal(t, s3err.ErrNone,
iam.VerifyActionPermission(uploadsReq(), reader, s3_constants.ACTION_READ, "mybucket", "x/"),
"s3:ListBucketMultipartUploads on the bucket must allow the uploads listing")
}