2 changed files with 7 additions and 1048 deletions
@ -1,792 +0,0 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"fmt" |
|||
"testing" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/iam/policy" |
|||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
// TestBucketPolicyValidationBasics tests the core validation logic
|
|||
func TestBucketPolicyValidationBasics(t *testing.T) { |
|||
s3Server := &S3ApiServer{} |
|||
|
|||
tests := []struct { |
|||
name string |
|||
policy *policy.PolicyDocument |
|||
bucket string |
|||
expectedValid bool |
|||
expectedError string |
|||
}{ |
|||
{ |
|||
name: "Valid bucket policy", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Sid: "TestStatement", |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{ |
|||
"arn:seaweed:s3:::test-bucket/*", |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "test-bucket", |
|||
expectedValid: true, |
|||
}, |
|||
{ |
|||
name: "Policy without Principal (invalid)", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:seaweed:s3:::test-bucket/*"}, |
|||
// Principal is missing
|
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "test-bucket", |
|||
expectedValid: false, |
|||
expectedError: "bucket policies must specify a Principal", |
|||
}, |
|||
{ |
|||
name: "Invalid version", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2008-10-17", // Wrong version
|
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:seaweed:s3:::test-bucket/*"}, |
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "test-bucket", |
|||
expectedValid: false, |
|||
expectedError: "unsupported policy version", |
|||
}, |
|||
{ |
|||
name: "Resource not matching bucket", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:seaweed:s3:::other-bucket/*"}, // Wrong bucket
|
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "test-bucket", |
|||
expectedValid: false, |
|||
expectedError: "does not match bucket", |
|||
}, |
|||
{ |
|||
name: "Non-S3 action", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"iam:GetUser"}, // Non-S3 action
|
|||
Resource: []string{"arn:seaweed:s3:::test-bucket/*"}, |
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "test-bucket", |
|||
expectedValid: false, |
|||
expectedError: "bucket policies only support S3 actions", |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
err := s3Server.validateBucketPolicy(tt.policy, tt.bucket) |
|||
|
|||
if tt.expectedValid { |
|||
assert.NoError(t, err, "Policy should be valid") |
|||
} else { |
|||
assert.Error(t, err, "Policy should be invalid") |
|||
if tt.expectedError != "" { |
|||
assert.Contains(t, err.Error(), tt.expectedError, "Error message should contain expected text") |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestBucketResourceValidation tests the resource ARN validation
|
|||
func TestBucketResourceValidation(t *testing.T) { |
|||
s3Server := &S3ApiServer{} |
|||
|
|||
tests := []struct { |
|||
name string |
|||
resource string |
|||
bucket string |
|||
valid bool |
|||
}{ |
|||
// SeaweedFS ARN format
|
|||
{ |
|||
name: "Exact bucket ARN (SeaweedFS)", |
|||
resource: "arn:seaweed:s3:::test-bucket", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
{ |
|||
name: "Bucket wildcard ARN (SeaweedFS)", |
|||
resource: "arn:seaweed:s3:::test-bucket/*", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
{ |
|||
name: "Specific object ARN (SeaweedFS)", |
|||
resource: "arn:seaweed:s3:::test-bucket/path/to/object.txt", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
// AWS ARN format (compatibility)
|
|||
{ |
|||
name: "Exact bucket ARN (AWS)", |
|||
resource: "arn:aws:s3:::test-bucket", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
{ |
|||
name: "Bucket wildcard ARN (AWS)", |
|||
resource: "arn:aws:s3:::test-bucket/*", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
{ |
|||
name: "Specific object ARN (AWS)", |
|||
resource: "arn:aws:s3:::test-bucket/path/to/object.txt", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
// Simplified format (without ARN prefix)
|
|||
{ |
|||
name: "Simplified bucket name", |
|||
resource: "test-bucket", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
{ |
|||
name: "Simplified bucket wildcard", |
|||
resource: "test-bucket/*", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
{ |
|||
name: "Simplified specific object", |
|||
resource: "test-bucket/path/to/object.txt", |
|||
bucket: "test-bucket", |
|||
valid: true, |
|||
}, |
|||
// Invalid cases
|
|||
{ |
|||
name: "Different bucket ARN (SeaweedFS)", |
|||
resource: "arn:seaweed:s3:::other-bucket/*", |
|||
bucket: "test-bucket", |
|||
valid: false, |
|||
}, |
|||
{ |
|||
name: "Different bucket ARN (AWS)", |
|||
resource: "arn:aws:s3:::other-bucket/*", |
|||
bucket: "test-bucket", |
|||
valid: false, |
|||
}, |
|||
{ |
|||
name: "Different bucket simplified", |
|||
resource: "other-bucket/*", |
|||
bucket: "test-bucket", |
|||
valid: false, |
|||
}, |
|||
{ |
|||
name: "Global S3 wildcard (SeaweedFS)", |
|||
resource: "arn:seaweed:s3:::*", |
|||
bucket: "test-bucket", |
|||
valid: false, |
|||
}, |
|||
{ |
|||
name: "Global S3 wildcard (AWS)", |
|||
resource: "arn:aws:s3:::*", |
|||
bucket: "test-bucket", |
|||
valid: false, |
|||
}, |
|||
{ |
|||
name: "Invalid ARN format", |
|||
resource: "invalid-arn", |
|||
bucket: "test-bucket", |
|||
valid: false, |
|||
}, |
|||
{ |
|||
name: "Bucket name prefix match but different bucket", |
|||
resource: "test-bucket-different/*", |
|||
bucket: "test-bucket", |
|||
valid: false, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := s3Server.validateResourceForBucket(tt.resource, tt.bucket) |
|||
assert.Equal(t, tt.valid, result, "Resource validation result should match expected") |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestBucketPolicyJSONSerialization tests policy JSON handling
|
|||
func TestBucketPolicyJSONSerialization(t *testing.T) { |
|||
policy := &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Sid: "PublicReadGetObject", |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{ |
|||
"arn:seaweed:s3:::public-bucket/*", |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Test that policy can be marshaled and unmarshaled correctly
|
|||
jsonData := marshalPolicy(t, policy) |
|||
assert.NotEmpty(t, jsonData, "JSON data should not be empty") |
|||
|
|||
// Verify the JSON contains expected elements
|
|||
jsonStr := string(jsonData) |
|||
assert.Contains(t, jsonStr, "2012-10-17", "JSON should contain version") |
|||
assert.Contains(t, jsonStr, "s3:GetObject", "JSON should contain action") |
|||
assert.Contains(t, jsonStr, "arn:seaweed:s3:::public-bucket/*", "JSON should contain resource") |
|||
assert.Contains(t, jsonStr, "PublicReadGetObject", "JSON should contain statement ID") |
|||
} |
|||
|
|||
// Helper function for marshaling policies
|
|||
func marshalPolicy(t *testing.T, policyDoc *policy.PolicyDocument) []byte { |
|||
data, err := json.Marshal(policyDoc) |
|||
require.NoError(t, err) |
|||
return data |
|||
} |
|||
|
|||
// TestBucketPolicyAnonymousAccessHelpers tests the helper functions for anonymous access evaluation (Issue #7469)
|
|||
func TestBucketPolicyAnonymousAccessHelpers(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
statement policy.Statement |
|||
s3Action string |
|||
resource string |
|||
expectedMatch bool |
|||
description string |
|||
}{ |
|||
{ |
|||
name: "Allow anonymous GetObject on specific path", |
|||
statement: policy.Statement{ |
|||
Sid: "PublicReadForTranscodedObjects", |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:aws:s3:::orbit/transcoded/*"}, |
|||
}, |
|||
s3Action: "s3:GetObject", |
|||
resource: "arn:aws:s3:::orbit/transcoded/video.mp4", |
|||
expectedMatch: true, |
|||
description: "Should match anonymous GetObject on transcoded/* path", |
|||
}, |
|||
{ |
|||
name: "Deny anonymous GetObject on different path", |
|||
statement: policy.Statement{ |
|||
Sid: "PublicReadForTranscodedObjects", |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:aws:s3:::orbit/transcoded/*"}, |
|||
}, |
|||
s3Action: "s3:GetObject", |
|||
resource: "arn:aws:s3:::orbit/private/document.pdf", |
|||
expectedMatch: false, |
|||
description: "Should not match paths outside of policy resource pattern", |
|||
}, |
|||
{ |
|||
name: "String principal wildcard", |
|||
statement: policy.Statement{ |
|||
Effect: "Allow", |
|||
Principal: "*", |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:aws:s3:::public-bucket/*"}, |
|||
}, |
|||
s3Action: "s3:GetObject", |
|||
resource: "arn:aws:s3:::public-bucket/any/file.txt", |
|||
expectedMatch: true, |
|||
description: "Should match with string principal wildcard", |
|||
}, |
|||
{ |
|||
name: "Wildcard action matching", |
|||
statement: policy.Statement{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:*"}, |
|||
Resource: []string{"arn:aws:s3:::bucket/*"}, |
|||
}, |
|||
s3Action: "s3:GetObject", |
|||
resource: "arn:aws:s3:::bucket/file.txt", |
|||
expectedMatch: true, |
|||
description: "Wildcard action should match any S3 action", |
|||
}, |
|||
{ |
|||
name: "ListBucket on bucket resource", |
|||
statement: policy.Statement{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:ListBucket"}, |
|||
Resource: []string{"arn:aws:s3:::bucket"}, |
|||
}, |
|||
s3Action: "s3:ListBucket", |
|||
resource: "arn:aws:s3:::bucket", |
|||
expectedMatch: true, |
|||
description: "Should match ListBucket on exact bucket resource", |
|||
}, |
|||
{ |
|||
name: "Non-anonymous principal", |
|||
statement: policy.Statement{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "arn:aws:iam::123456789012:user/alice", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:aws:s3:::bucket/*"}, |
|||
}, |
|||
s3Action: "s3:GetObject", |
|||
resource: "arn:aws:s3:::bucket/file.txt", |
|||
expectedMatch: false, |
|||
description: "Should not match when principal is not anonymous", |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
matched := statementMatchesAnonymousRequest(tt.statement, tt.s3Action, tt.resource) |
|||
assert.Equal(t, tt.expectedMatch, matched, "Statement match result: %s", tt.description) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestPrincipalMatchesAnonymous tests the principal matching logic
|
|||
func TestPrincipalMatchesAnonymous(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
principal interface{} |
|||
expected bool |
|||
}{ |
|||
{"String wildcard", "*", true}, |
|||
{"AWS map with wildcard", map[string]interface{}{"AWS": "*"}, true}, |
|||
{"AWS map with array containing wildcard", map[string]interface{}{"AWS": []interface{}{"*"}}, true}, |
|||
{"AWS map with string array containing wildcard", map[string]interface{}{"AWS": []string{"*"}}, true}, |
|||
{"Specific ARN", map[string]interface{}{"AWS": "arn:aws:iam::123:user/alice"}, false}, |
|||
{"Empty principal", nil, false}, |
|||
{"Empty map", map[string]interface{}{}, false}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := principalMatchesAnonymous(tt.principal) |
|||
assert.Equal(t, tt.expected, result) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestActionToS3Action tests the action conversion
|
|||
func TestActionToS3Action(t *testing.T) { |
|||
tests := []struct { |
|||
action Action |
|||
expected string |
|||
}{ |
|||
{s3_constants.ACTION_READ, "s3:GetObject"}, |
|||
{s3_constants.ACTION_WRITE, "s3:PutObject"}, |
|||
{s3_constants.ACTION_LIST, "s3:ListBucket"}, |
|||
{s3_constants.ACTION_TAGGING, "s3:PutObjectTagging"}, |
|||
{s3_constants.ACTION_ADMIN, "s3:*"}, |
|||
{Action("s3:DeleteObject"), "s3:DeleteObject"}, |
|||
{Action("CustomAction"), "s3:CustomAction"}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(string(tt.action), func(t *testing.T) { |
|||
result := actionToS3Action(tt.action) |
|||
assert.Equal(t, tt.expected, result) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestBuildResourceARN tests the resource ARN building
|
|||
func TestBuildResourceARN(t *testing.T) { |
|||
tests := []struct { |
|||
bucket string |
|||
object string |
|||
expected string |
|||
}{ |
|||
{"bucket", "", "arn:aws:s3:::bucket"}, |
|||
{"bucket", "/", "arn:aws:s3:::bucket"}, |
|||
{"bucket", "file.txt", "arn:aws:s3:::bucket/file.txt"}, |
|||
{"bucket", "/file.txt", "arn:aws:s3:::bucket/file.txt"}, |
|||
{"bucket", "path/to/file.txt", "arn:aws:s3:::bucket/path/to/file.txt"}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(fmt.Sprintf("%s/%s", tt.bucket, tt.object), func(t *testing.T) { |
|||
result := buildResourceARN(tt.bucket, tt.object) |
|||
assert.Equal(t, tt.expected, result) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestResourceMatching tests the resource pattern matching
|
|||
func TestResourceMatching(t *testing.T) { |
|||
tests := []struct { |
|||
pattern string |
|||
resource string |
|||
expected bool |
|||
}{ |
|||
{"*", "anything", true}, |
|||
{"arn:aws:s3:::bucket/*", "arn:aws:s3:::bucket/file.txt", true}, |
|||
{"arn:aws:s3:::bucket/*", "arn:aws:s3:::bucket/path/to/file.txt", true}, |
|||
{"arn:aws:s3:::bucket/*", "arn:aws:s3:::other-bucket/file.txt", false}, |
|||
{"bucket/*", "bucket/file.txt", true}, |
|||
{"bucket/prefix/*", "bucket/prefix/file.txt", true}, |
|||
{"bucket/prefix/*", "bucket/other/file.txt", false}, |
|||
{"arn:aws:s3:::bucket", "arn:aws:s3:::bucket", true}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(fmt.Sprintf("%s matches %s", tt.pattern, tt.resource), func(t *testing.T) { |
|||
result := matchesResourcePattern(tt.pattern, tt.resource) |
|||
assert.Equal(t, tt.expected, result) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestMatchesPatternRegexEscaping tests that regex metacharacters are properly escaped
|
|||
// This is a critical security test - ensures policies like "*.json" don't match "filexjson"
|
|||
func TestMatchesPatternRegexEscaping(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
pattern string |
|||
str string |
|||
expected bool |
|||
reason string |
|||
}{ |
|||
{ |
|||
name: "Wildcard .json extension - should match", |
|||
pattern: "*.json", |
|||
str: "file.json", |
|||
expected: true, |
|||
reason: "*.json should match file.json", |
|||
}, |
|||
{ |
|||
name: "Wildcard .json extension - should NOT match without dot", |
|||
pattern: "*.json", |
|||
str: "filexjson", |
|||
expected: false, |
|||
reason: "*.json should NOT match filexjson (critical security test)", |
|||
}, |
|||
{ |
|||
name: "Pattern with dots - exact match", |
|||
pattern: "file.test.json", |
|||
str: "file.test.json", |
|||
expected: true, |
|||
reason: "Exact match should work", |
|||
}, |
|||
{ |
|||
name: "Pattern with dots - should NOT match different chars", |
|||
pattern: "file.test.json", |
|||
str: "filextestxjson", |
|||
expected: false, |
|||
reason: "Dots should be literal, not regex wildcards", |
|||
}, |
|||
{ |
|||
name: "ARN with colons and slashes", |
|||
pattern: "arn:aws:s3:::bucket/*.json", |
|||
str: "arn:aws:s3:::bucket/file.json", |
|||
expected: true, |
|||
reason: "ARN pattern should match", |
|||
}, |
|||
{ |
|||
name: "ARN with colons - should NOT match without dots", |
|||
pattern: "arn:aws:s3:::bucket/*.json", |
|||
str: "arn:aws:s3:::bucket/filexjson", |
|||
expected: false, |
|||
reason: "ARN pattern should NOT match without literal dot (critical security test)", |
|||
}, |
|||
{ |
|||
name: "Question mark wildcard", |
|||
pattern: "file?.txt", |
|||
str: "file1.txt", |
|||
expected: true, |
|||
reason: "? should match single character", |
|||
}, |
|||
{ |
|||
name: "Question mark wildcard - should NOT match multiple chars", |
|||
pattern: "file?.txt", |
|||
str: "file123.txt", |
|||
expected: false, |
|||
reason: "? should only match single character", |
|||
}, |
|||
{ |
|||
name: "Parentheses in pattern", |
|||
pattern: "file(1).txt", |
|||
str: "file(1).txt", |
|||
expected: true, |
|||
reason: "Parentheses should be literal, not regex groups", |
|||
}, |
|||
{ |
|||
name: "Brackets in pattern", |
|||
pattern: "file[1].txt", |
|||
str: "file[1].txt", |
|||
expected: true, |
|||
reason: "Brackets should be literal, not regex character classes", |
|||
}, |
|||
{ |
|||
name: "Dollar sign in pattern", |
|||
pattern: "file$.txt", |
|||
str: "file$.txt", |
|||
expected: true, |
|||
reason: "Dollar sign should be literal, not regex anchor", |
|||
}, |
|||
{ |
|||
name: "Plus sign in pattern", |
|||
pattern: "file+.txt", |
|||
str: "file+.txt", |
|||
expected: true, |
|||
reason: "Plus should be literal, not regex quantifier", |
|||
}, |
|||
{ |
|||
name: "Complex pattern with multiple wildcards", |
|||
pattern: "prefix/*/file?.json", |
|||
str: "prefix/subdir/file1.json", |
|||
expected: true, |
|||
reason: "Multiple wildcards should work together", |
|||
}, |
|||
{ |
|||
name: "Complex pattern - should NOT match without dots", |
|||
pattern: "prefix/*/file?.json", |
|||
str: "prefix/subdir/file1xjson", |
|||
expected: false, |
|||
reason: "Dot should be literal even with multiple wildcards", |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := matchesPattern(tt.pattern, tt.str, true) // case-sensitive for resource tests
|
|||
assert.Equal(t, tt.expected, result, tt.reason) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestActionMatchingCaseInsensitive tests that S3 actions are case-insensitive
|
|||
func TestActionMatchingCaseInsensitive(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
pattern string |
|||
action string |
|||
expected bool |
|||
}{ |
|||
{"Exact match same case", "s3:GetObject", "s3:GetObject", true}, |
|||
{"Different case - lowercase", "s3:GetObject", "s3:getobject", true}, |
|||
{"Different case - uppercase", "s3:GetObject", "S3:GETOBJECT", true}, |
|||
{"Different case - mixed", "s3:GetObject", "S3:getObject", true}, |
|||
{"Wildcard with different case", "s3:Get*", "s3:getobject", true}, |
|||
{"Wildcard with uppercase", "s3:GET*", "s3:GetObject", true}, |
|||
{"No match different action", "s3:GetObject", "s3:PutObject", false}, |
|||
{"Question mark wildcard case-insensitive", "s3:?etObject", "s3:GetObject", true}, |
|||
{"Question mark wildcard different case", "s3:?etObject", "s3:GETOBJECT", true}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := matchesPattern(tt.pattern, tt.action, false) // case-insensitive for actions
|
|||
assert.Equal(t, tt.expected, result) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestResourceMatchingCaseSensitive tests that resources are case-sensitive
|
|||
func TestResourceMatchingCaseSensitive(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
pattern string |
|||
resource string |
|||
expected bool |
|||
}{ |
|||
{"Exact match same case", "bucket/file.txt", "bucket/file.txt", true}, |
|||
{"Different case should NOT match", "bucket/file.txt", "bucket/File.txt", false}, |
|||
{"Different case should NOT match - uppercase", "bucket/file.txt", "BUCKET/FILE.TXT", false}, |
|||
{"Wildcard pattern case-sensitive", "bucket/*.txt", "bucket/file.txt", true}, |
|||
{"Wildcard pattern different case should NOT match", "bucket/*.txt", "bucket/File.TXT", false}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := matchesPattern(tt.pattern, tt.resource, true) // case-sensitive for resources
|
|||
assert.Equal(t, tt.expected, result) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestActionMatchingWithRegexChars tests action matching with regex special characters
|
|||
func TestActionMatchingWithRegexChars(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
pattern string |
|||
action string |
|||
expected bool |
|||
}{ |
|||
{"Wildcard action", "s3:*", "s3:GetObject", true}, |
|||
{"Specific action with colon", "s3:GetObject", "s3:GetObject", true}, |
|||
{"Action should not match without colon", "s3GetObject", "s3:GetObject", false}, |
|||
{"Pattern with wildcard at end", "s3:Get*", "s3:GetObject", true}, |
|||
{"Pattern with wildcard at end", "s3:Get*", "s3:PutObject", false}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := actionMatches([]string{tt.pattern}, tt.action) |
|||
assert.Equal(t, tt.expected, result) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestIssue7252Examples tests the specific examples from GitHub issue #7252
|
|||
func TestIssue7252Examples(t *testing.T) { |
|||
s3Server := &S3ApiServer{} |
|||
|
|||
tests := []struct { |
|||
name string |
|||
policy *policy.PolicyDocument |
|||
bucket string |
|||
expectedValid bool |
|||
description string |
|||
}{ |
|||
{ |
|||
name: "Issue #7252 - Standard ARN with wildcard", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:aws:s3:::main-bucket/*"}, |
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "main-bucket", |
|||
expectedValid: true, |
|||
description: "AWS ARN format should be accepted", |
|||
}, |
|||
{ |
|||
name: "Issue #7252 - Simplified resource with wildcard", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"main-bucket/*"}, |
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "main-bucket", |
|||
expectedValid: true, |
|||
description: "Simplified format with wildcard should be accepted", |
|||
}, |
|||
{ |
|||
name: "Issue #7252 - Resource as exact bucket name", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"main-bucket"}, |
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "main-bucket", |
|||
expectedValid: true, |
|||
description: "Exact bucket name should be accepted", |
|||
}, |
|||
{ |
|||
name: "Public read policy with AWS ARN", |
|||
policy: &policy.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []policy.Statement{ |
|||
{ |
|||
Sid: "PublicReadGetObject", |
|||
Effect: "Allow", |
|||
Principal: map[string]interface{}{ |
|||
"AWS": "*", |
|||
}, |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:aws:s3:::my-public-bucket/*"}, |
|||
}, |
|||
}, |
|||
}, |
|||
bucket: "my-public-bucket", |
|||
expectedValid: true, |
|||
description: "Standard public read policy with AWS ARN should work", |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
err := s3Server.validateBucketPolicy(tt.policy, tt.bucket) |
|||
|
|||
if tt.expectedValid { |
|||
assert.NoError(t, err, "Policy should be valid: %s", tt.description) |
|||
} else { |
|||
assert.Error(t, err, "Policy should be invalid: %s", tt.description) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue