package s3api import ( "encoding/json" "net/http/httptest" "testing" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/stretchr/testify/require" ) // ListObjects requests carry their key scope in the ?prefix= query parameter, // which authRequestWithAuthType promotes into object so the legacy CanDo path // can honor prefix-scoped Action strings. That promoted object must not reach // the policy resource ARN: s3:ListBucket is a bucket-level action, so the // resource stays arn:aws:s3:::bucket and any prefix scoping is expressed via // the s3:prefix Condition. func TestEvaluateIAMPolicies_ListBucketWithPrefix(t *testing.T) { const bucket = "test-bucket" iam := &IdentityAccessManagement{} require.NoError(t, iam.PutPolicy("list-bucket", mustPolicy(t, map[string]any{ "Version": "2012-10-17", "Statement": []map[string]any{{ "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::" + bucket, }}, }))) identity := &Identity{ Name: "alice", Account: &AccountAdmin, PolicyNames: []string{"list-bucket"}, Credentials: []*Credential{{AccessKey: "AKIAEXAMPLE", SecretKey: "secret"}}, } // authRequestWithAuthType promotes prefix into object before reaching the // IAM evaluator; pass the post-promotion value to mirror that flow. withPrefix := httptest.NewRequest("GET", "/"+bucket+"?list-type=2&prefix=foo/", nil) require.True(t, iam.evaluateIAMPolicies(withPrefix, identity, s3_constants.ACTION_LIST, bucket, "foo/"), "s3:ListBucket on the bucket ARN must allow listing with a prefix") noPrefix := httptest.NewRequest("GET", "/"+bucket+"?list-type=2", nil) require.True(t, iam.evaluateIAMPolicies(noPrefix, identity, s3_constants.ACTION_LIST, bucket, ""), "s3:ListBucket on the bucket ARN must allow listing without a prefix") } // Prefix scoping still works once it moves to the Condition: a policy that // grants s3:ListBucket on the bucket only under an s3:prefix StringLike must // allow a matching prefix and deny a non-matching one. func TestEvaluateIAMPolicies_ListBucketPrefixCondition(t *testing.T) { const bucket = "test-bucket" iam := &IdentityAccessManagement{} require.NoError(t, iam.PutPolicy("list-scoped", mustPolicy(t, map[string]any{ "Version": "2012-10-17", "Statement": []map[string]any{{ "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::" + bucket, "Condition": map[string]any{"StringLike": map[string]any{"s3:prefix": "warehouse/*"}}, }}, }))) identity := &Identity{ Name: "bob", Account: &AccountAdmin, PolicyNames: []string{"list-scoped"}, Credentials: []*Credential{{AccessKey: "AKIAEXAMPLE", SecretKey: "secret"}}, } matching := httptest.NewRequest("GET", "/"+bucket+"?list-type=2&prefix=warehouse/data", nil) require.True(t, iam.evaluateIAMPolicies(matching, identity, s3_constants.ACTION_LIST, bucket, "warehouse/data"), "prefix matching the s3:prefix condition must be allowed") nonMatching := httptest.NewRequest("GET", "/"+bucket+"?list-type=2&prefix=secrets/", nil) require.False(t, iam.evaluateIAMPolicies(nonMatching, identity, s3_constants.ACTION_LIST, bucket, "secrets/"), "prefix outside the s3:prefix condition must be denied") } // Listing variants keep their specific action even when a prefix is promoted // into object: ?versions resolves to s3:ListBucketVersions, not s3:ListBucket. func TestEvaluateIAMPolicies_ListBucketVersionsWithPrefix(t *testing.T) { const bucket = "test-bucket" iam := &IdentityAccessManagement{} require.NoError(t, iam.PutPolicy("list-versions", mustPolicy(t, map[string]any{ "Version": "2012-10-17", "Statement": []map[string]any{{ "Effect": "Allow", "Action": "s3:ListBucketVersions", "Resource": "arn:aws:s3:::" + bucket, }}, }))) identity := &Identity{ Name: "carol", Account: &AccountAdmin, PolicyNames: []string{"list-versions"}, Credentials: []*Credential{{AccessKey: "AKIAEXAMPLE", SecretKey: "secret"}}, } r := httptest.NewRequest("GET", "/"+bucket+"?versions&prefix=foo/", nil) require.True(t, iam.evaluateIAMPolicies(r, identity, s3_constants.ACTION_LIST, bucket, "foo/"), "s3:ListBucketVersions must still resolve when listing with a prefix") } func mustPolicy(t *testing.T, doc map[string]any) string { t.Helper() b, err := json.Marshal(doc) require.NoError(t, err) return string(b) }