S3: Enforce bucket policy (#7471)

* evaluate policies during authorization

* cache bucket policy

* refactor

* matching with regex special characters

* Case Sensitivity, pattern cache, Dead Code Removal

* Fixed Typo, Restored []string Case, Added Cache Size Limit

* hook up with policy engine

* remove old implementation

* action mapping

* validate

* if not specified, fall through to IAM checks

* fmt

* Fail-close on policy evaluation errors

* Explicit `Allow` bypasses IAM checks

* fix error message

* arn:seaweed => arn:aws

* remove legacy support

* fix tests

* Clean up bucket policy after this test

* fix for tests

* address comments

* security fixes

* fix tests

* temp comment out
This commit is contained in:
Chris Lu
2025-11-12 22:14:50 -08:00
committed by GitHub
parent 50f067bcfd
commit 508d06d9a5
47 changed files with 1104 additions and 749 deletions

View File

@@ -95,7 +95,7 @@ type EvaluationContext struct {
// Action being requested (e.g., "s3:GetObject")
Action string `json:"action"`
// Resource being accessed (e.g., "arn:seaweed:s3:::bucket/key")
// Resource being accessed (e.g., "arn:aws:s3:::bucket/key")
Resource string `json:"resource"`
// RequestContext contains additional request information

View File

@@ -47,13 +47,13 @@ func TestDistributedPolicyEngine(t *testing.T) {
Sid: "AllowS3Read",
Effect: "Allow",
Action: []string{"s3:GetObject", "s3:ListBucket"},
Resource: []string{"arn:seaweed:s3:::test-bucket/*", "arn:seaweed:s3:::test-bucket"},
Resource: []string{"arn:aws:s3:::test-bucket/*", "arn:aws:s3:::test-bucket"},
},
{
Sid: "DenyS3Write",
Effect: "Deny",
Action: []string{"s3:PutObject", "s3:DeleteObject"},
Resource: []string{"arn:seaweed:s3:::test-bucket/*"},
Resource: []string{"arn:aws:s3:::test-bucket/*"},
},
},
}
@@ -83,9 +83,9 @@ func TestDistributedPolicyEngine(t *testing.T) {
t.Run("evaluation_consistency", func(t *testing.T) {
// Create evaluation context
evalCtx := &EvaluationContext{
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
Principal: "arn:aws:sts::assumed-role/TestRole/session",
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::test-bucket/file.txt",
Resource: "arn:aws:s3:::test-bucket/file.txt",
RequestContext: map[string]interface{}{
"sourceIp": "192.168.1.100",
},
@@ -118,9 +118,9 @@ func TestDistributedPolicyEngine(t *testing.T) {
// Test explicit deny precedence
t.Run("deny_precedence_consistency", func(t *testing.T) {
evalCtx := &EvaluationContext{
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
Principal: "arn:aws:sts::assumed-role/TestRole/session",
Action: "s3:PutObject",
Resource: "arn:seaweed:s3:::test-bucket/newfile.txt",
Resource: "arn:aws:s3:::test-bucket/newfile.txt",
}
// All instances should consistently apply deny precedence
@@ -146,9 +146,9 @@ func TestDistributedPolicyEngine(t *testing.T) {
// Test default effect consistency
t.Run("default_effect_consistency", func(t *testing.T) {
evalCtx := &EvaluationContext{
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
Principal: "arn:aws:sts::assumed-role/TestRole/session",
Action: "filer:CreateEntry", // Action not covered by any policy
Resource: "arn:seaweed:filer::path/test",
Resource: "arn:aws:filer::path/test",
}
result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
@@ -196,9 +196,9 @@ func TestPolicyEngineConfigurationConsistency(t *testing.T) {
// Test with an action not covered by any policy
evalCtx := &EvaluationContext{
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
Principal: "arn:aws:sts::assumed-role/TestRole/session",
Action: "uncovered:action",
Resource: "arn:seaweed:test:::resource",
Resource: "arn:aws:test:::resource",
}
result1, _ := instance1.Evaluate(context.Background(), "", evalCtx, []string{})
@@ -277,9 +277,9 @@ func TestPolicyStoreDistributed(t *testing.T) {
require.NoError(t, err)
evalCtx := &EvaluationContext{
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
Principal: "arn:aws:sts::assumed-role/TestRole/session",
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::bucket/key",
Resource: "arn:aws:s3:::bucket/key",
}
// Evaluate with non-existent policies
@@ -350,7 +350,7 @@ func TestPolicyEvaluationPerformance(t *testing.T) {
Sid: fmt.Sprintf("Statement%d", i),
Effect: "Allow",
Action: []string{"s3:GetObject", "s3:ListBucket"},
Resource: []string{fmt.Sprintf("arn:seaweed:s3:::bucket%d/*", i)},
Resource: []string{fmt.Sprintf("arn:aws:s3:::bucket%d/*", i)},
},
},
}
@@ -361,9 +361,9 @@ func TestPolicyEvaluationPerformance(t *testing.T) {
// Test evaluation performance
evalCtx := &EvaluationContext{
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
Principal: "arn:aws:sts::assumed-role/TestRole/session",
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::bucket5/file.txt",
Resource: "arn:aws:s3:::bucket5/file.txt",
}
policyNames := make([]string, 10)

View File

@@ -71,7 +71,7 @@ func TestPolicyDocumentValidation(t *testing.T) {
Sid: "AllowS3Read",
Effect: "Allow",
Action: []string{"s3:GetObject", "s3:ListBucket"},
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
Resource: []string{"arn:aws:s3:::mybucket/*"},
},
},
},
@@ -84,7 +84,7 @@ func TestPolicyDocumentValidation(t *testing.T) {
{
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
Resource: []string{"arn:aws:s3:::mybucket/*"},
},
},
},
@@ -108,7 +108,7 @@ func TestPolicyDocumentValidation(t *testing.T) {
{
Effect: "Maybe",
Action: []string{"s3:GetObject"},
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
Resource: []string{"arn:aws:s3:::mybucket/*"},
},
},
},
@@ -146,8 +146,8 @@ func TestPolicyEvaluation(t *testing.T) {
Effect: "Allow",
Action: []string{"s3:GetObject", "s3:ListBucket"},
Resource: []string{
"arn:seaweed:s3:::public-bucket/*", // For object operations
"arn:seaweed:s3:::public-bucket", // For bucket operations
"arn:aws:s3:::public-bucket/*", // For object operations
"arn:aws:s3:::public-bucket", // For bucket operations
},
},
},
@@ -163,7 +163,7 @@ func TestPolicyEvaluation(t *testing.T) {
Sid: "DenyS3Delete",
Effect: "Deny",
Action: []string{"s3:DeleteObject"},
Resource: []string{"arn:seaweed:s3:::*"},
Resource: []string{"arn:aws:s3:::*"},
},
},
}
@@ -182,7 +182,7 @@ func TestPolicyEvaluation(t *testing.T) {
context: &EvaluationContext{
Principal: "user:alice",
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
Resource: "arn:aws:s3:::public-bucket/file.txt",
RequestContext: map[string]interface{}{
"sourceIP": "192.168.1.100",
},
@@ -195,7 +195,7 @@ func TestPolicyEvaluation(t *testing.T) {
context: &EvaluationContext{
Principal: "user:alice",
Action: "s3:DeleteObject",
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
Resource: "arn:aws:s3:::public-bucket/file.txt",
},
policies: []string{"read-policy", "deny-policy"},
want: EffectDeny,
@@ -205,7 +205,7 @@ func TestPolicyEvaluation(t *testing.T) {
context: &EvaluationContext{
Principal: "user:alice",
Action: "s3:PutObject",
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
Resource: "arn:aws:s3:::public-bucket/file.txt",
},
policies: []string{"read-policy"},
want: EffectDeny,
@@ -215,7 +215,7 @@ func TestPolicyEvaluation(t *testing.T) {
context: &EvaluationContext{
Principal: "user:admin",
Action: "s3:ListBucket",
Resource: "arn:seaweed:s3:::public-bucket",
Resource: "arn:aws:s3:::public-bucket",
},
policies: []string{"read-policy"},
want: EffectAllow,
@@ -249,7 +249,7 @@ func TestConditionEvaluation(t *testing.T) {
Sid: "AllowFromOfficeIP",
Effect: "Allow",
Action: []string{"s3:*"},
Resource: []string{"arn:seaweed:s3:::*"},
Resource: []string{"arn:aws:s3:::*"},
Condition: map[string]map[string]interface{}{
"IpAddress": {
"seaweed:SourceIP": []string{"192.168.1.0/24", "10.0.0.0/8"},
@@ -272,7 +272,7 @@ func TestConditionEvaluation(t *testing.T) {
context: &EvaluationContext{
Principal: "user:alice",
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::mybucket/file.txt",
Resource: "arn:aws:s3:::mybucket/file.txt",
RequestContext: map[string]interface{}{
"sourceIP": "192.168.1.100",
},
@@ -284,7 +284,7 @@ func TestConditionEvaluation(t *testing.T) {
context: &EvaluationContext{
Principal: "user:alice",
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::mybucket/file.txt",
Resource: "arn:aws:s3:::mybucket/file.txt",
RequestContext: map[string]interface{}{
"sourceIP": "8.8.8.8",
},
@@ -296,7 +296,7 @@ func TestConditionEvaluation(t *testing.T) {
context: &EvaluationContext{
Principal: "user:alice",
Action: "s3:PutObject",
Resource: "arn:seaweed:s3:::mybucket/newfile.txt",
Resource: "arn:aws:s3:::mybucket/newfile.txt",
RequestContext: map[string]interface{}{
"sourceIP": "10.1.2.3",
},
@@ -325,32 +325,32 @@ func TestResourceMatching(t *testing.T) {
}{
{
name: "exact match",
policyResource: "arn:seaweed:s3:::mybucket/file.txt",
requestResource: "arn:seaweed:s3:::mybucket/file.txt",
policyResource: "arn:aws:s3:::mybucket/file.txt",
requestResource: "arn:aws:s3:::mybucket/file.txt",
want: true,
},
{
name: "wildcard match",
policyResource: "arn:seaweed:s3:::mybucket/*",
requestResource: "arn:seaweed:s3:::mybucket/folder/file.txt",
policyResource: "arn:aws:s3:::mybucket/*",
requestResource: "arn:aws:s3:::mybucket/folder/file.txt",
want: true,
},
{
name: "bucket wildcard",
policyResource: "arn:seaweed:s3:::*",
requestResource: "arn:seaweed:s3:::anybucket/file.txt",
policyResource: "arn:aws:s3:::*",
requestResource: "arn:aws:s3:::anybucket/file.txt",
want: true,
},
{
name: "no match different bucket",
policyResource: "arn:seaweed:s3:::mybucket/*",
requestResource: "arn:seaweed:s3:::otherbucket/file.txt",
policyResource: "arn:aws:s3:::mybucket/*",
requestResource: "arn:aws:s3:::otherbucket/file.txt",
want: false,
},
{
name: "prefix match",
policyResource: "arn:seaweed:s3:::mybucket/documents/*",
requestResource: "arn:seaweed:s3:::mybucket/documents/secret.txt",
policyResource: "arn:aws:s3:::mybucket/documents/*",
requestResource: "arn:aws:s3:::mybucket/documents/secret.txt",
want: true,
},
}