You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							307 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							307 lines
						
					
					
						
							11 KiB
						
					
					
				
								package s3api
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"net/http"
							 | 
						|
									"net/url"
							 | 
						|
									"testing"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
							 | 
						|
									"github.com/stretchr/testify/assert"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// TestGranularActionMappingSecurity demonstrates how the new granular action mapping
							 | 
						|
								// fixes critical security issues that existed with the previous coarse mapping
							 | 
						|
								func TestGranularActionMappingSecurity(t *testing.T) {
							 | 
						|
									tests := []struct {
							 | 
						|
										name                  string
							 | 
						|
										method                string
							 | 
						|
										bucket                string
							 | 
						|
										objectKey             string
							 | 
						|
										queryParams           map[string]string
							 | 
						|
										description           string
							 | 
						|
										problemWithOldMapping string
							 | 
						|
										granularActionResult  string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name:        "delete_object_security_fix",
							 | 
						|
											method:      "DELETE",
							 | 
						|
											bucket:      "sensitive-bucket",
							 | 
						|
											objectKey:   "confidential-file.txt",
							 | 
						|
											queryParams: map[string]string{},
							 | 
						|
											description: "DELETE object operations should map to s3:DeleteObject, not s3:PutObject",
							 | 
						|
											problemWithOldMapping: "Old mapping incorrectly mapped DELETE object to s3:PutObject, " +
							 | 
						|
												"allowing users with only PUT permissions to delete objects - a critical security flaw",
							 | 
						|
											granularActionResult: "s3:DeleteObject",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:        "get_object_acl_precision",
							 | 
						|
											method:      "GET",
							 | 
						|
											bucket:      "secure-bucket",
							 | 
						|
											objectKey:   "private-file.pdf",
							 | 
						|
											queryParams: map[string]string{"acl": ""},
							 | 
						|
											description: "GET object ACL should map to s3:GetObjectAcl, not generic s3:GetObject",
							 | 
						|
											problemWithOldMapping: "Old mapping would allow users with s3:GetObject permission to " +
							 | 
						|
												"read ACLs, potentially exposing sensitive permission information",
							 | 
						|
											granularActionResult: "s3:GetObjectAcl",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:        "put_object_tagging_precision",
							 | 
						|
											method:      "PUT",
							 | 
						|
											bucket:      "data-bucket",
							 | 
						|
											objectKey:   "business-document.xlsx",
							 | 
						|
											queryParams: map[string]string{"tagging": ""},
							 | 
						|
											description: "PUT object tagging should map to s3:PutObjectTagging, not generic s3:PutObject",
							 | 
						|
											problemWithOldMapping: "Old mapping couldn't distinguish between actual object uploads and " +
							 | 
						|
												"metadata operations like tagging, making fine-grained permissions impossible",
							 | 
						|
											granularActionResult: "s3:PutObjectTagging",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:        "multipart_upload_precision",
							 | 
						|
											method:      "POST",
							 | 
						|
											bucket:      "large-files",
							 | 
						|
											objectKey:   "video.mp4",
							 | 
						|
											queryParams: map[string]string{"uploads": ""},
							 | 
						|
											description: "Multipart upload initiation should map to s3:CreateMultipartUpload",
							 | 
						|
											problemWithOldMapping: "Old mapping would treat multipart operations as generic s3:PutObject, " +
							 | 
						|
												"preventing policies that allow regular uploads but restrict large multipart operations",
							 | 
						|
											granularActionResult: "s3:CreateMultipartUpload",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:        "bucket_policy_vs_bucket_creation",
							 | 
						|
											method:      "PUT",
							 | 
						|
											bucket:      "corporate-bucket",
							 | 
						|
											objectKey:   "",
							 | 
						|
											queryParams: map[string]string{"policy": ""},
							 | 
						|
											description: "Bucket policy modifications should map to s3:PutBucketPolicy, not s3:CreateBucket",
							 | 
						|
											problemWithOldMapping: "Old mapping couldn't distinguish between creating buckets and " +
							 | 
						|
												"modifying bucket policies, potentially allowing unauthorized policy changes",
							 | 
						|
											granularActionResult: "s3:PutBucketPolicy",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:        "list_vs_read_distinction",
							 | 
						|
											method:      "GET",
							 | 
						|
											bucket:      "inventory-bucket",
							 | 
						|
											objectKey:   "",
							 | 
						|
											queryParams: map[string]string{"uploads": ""},
							 | 
						|
											description: "Listing multipart uploads should map to s3:ListMultipartUploads",
							 | 
						|
											problemWithOldMapping: "Old mapping would use generic s3:ListBucket for all bucket operations, " +
							 | 
						|
												"preventing fine-grained control over who can see ongoing multipart operations",
							 | 
						|
											granularActionResult: "s3:ListMultipartUploads",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:        "delete_object_tagging_precision",
							 | 
						|
											method:      "DELETE",
							 | 
						|
											bucket:      "metadata-bucket",
							 | 
						|
											objectKey:   "tagged-file.json",
							 | 
						|
											queryParams: map[string]string{"tagging": ""},
							 | 
						|
											description: "Delete object tagging should map to s3:DeleteObjectTagging, not s3:DeleteObject",
							 | 
						|
											problemWithOldMapping: "Old mapping couldn't distinguish between deleting objects and " +
							 | 
						|
												"deleting tags, preventing policies that allow tag management but not object deletion",
							 | 
						|
											granularActionResult: "s3:DeleteObjectTagging",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tt := range tests {
							 | 
						|
										t.Run(tt.name, func(t *testing.T) {
							 | 
						|
											// Create HTTP request with query parameters
							 | 
						|
											req := &http.Request{
							 | 
						|
												Method: tt.method,
							 | 
						|
												URL:    &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Add query parameters
							 | 
						|
											query := req.URL.Query()
							 | 
						|
											for key, value := range tt.queryParams {
							 | 
						|
												query.Set(key, value)
							 | 
						|
											}
							 | 
						|
											req.URL.RawQuery = query.Encode()
							 | 
						|
								
							 | 
						|
											// Test the new granular action determination
							 | 
						|
											result := determineGranularS3Action(req, s3_constants.ACTION_WRITE, tt.bucket, tt.objectKey)
							 | 
						|
								
							 | 
						|
											assert.Equal(t, tt.granularActionResult, result,
							 | 
						|
												"Security Fix Test: %s\n"+
							 | 
						|
													"Description: %s\n"+
							 | 
						|
													"Problem with old mapping: %s\n"+
							 | 
						|
													"Expected: %s, Got: %s",
							 | 
						|
												tt.name, tt.description, tt.problemWithOldMapping, tt.granularActionResult, result)
							 | 
						|
								
							 | 
						|
											// Log the security improvement
							 | 
						|
											t.Logf("SECURITY IMPROVEMENT: %s", tt.description)
							 | 
						|
											t.Logf("   Problem Fixed: %s", tt.problemWithOldMapping)
							 | 
						|
											t.Logf("   Granular Action: %s", result)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestBackwardCompatibilityFallback tests that the new system maintains backward compatibility
							 | 
						|
								// with existing generic actions while providing enhanced granularity
							 | 
						|
								func TestBackwardCompatibilityFallback(t *testing.T) {
							 | 
						|
									tests := []struct {
							 | 
						|
										name           string
							 | 
						|
										method         string
							 | 
						|
										bucket         string
							 | 
						|
										objectKey      string
							 | 
						|
										fallbackAction Action
							 | 
						|
										expectedResult string
							 | 
						|
										description    string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name:           "generic_read_fallback",
							 | 
						|
											method:         "GET", // Generic method without specific query params
							 | 
						|
											bucket:         "",    // Edge case: no bucket specified
							 | 
						|
											objectKey:      "",    // Edge case: no object specified
							 | 
						|
											fallbackAction: s3_constants.ACTION_READ,
							 | 
						|
											expectedResult: "s3:GetObject",
							 | 
						|
											description:    "Generic read operations should fall back to s3:GetObject for compatibility",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "generic_write_fallback",
							 | 
						|
											method:         "PUT", // Generic method without specific query params
							 | 
						|
											bucket:         "",    // Edge case: no bucket specified
							 | 
						|
											objectKey:      "",    // Edge case: no object specified
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expectedResult: "s3:PutObject",
							 | 
						|
											description:    "Generic write operations should fall back to s3:PutObject for compatibility",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "already_granular_passthrough",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "",
							 | 
						|
											objectKey:      "",
							 | 
						|
											fallbackAction: "s3:GetBucketLocation", // Already specific
							 | 
						|
											expectedResult: "s3:GetBucketLocation",
							 | 
						|
											description:    "Already granular actions should pass through unchanged",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "unknown_action_conversion",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "",
							 | 
						|
											objectKey:      "",
							 | 
						|
											fallbackAction: "CustomAction", // Not S3-prefixed
							 | 
						|
											expectedResult: "s3:CustomAction",
							 | 
						|
											description:    "Unknown actions should be converted to S3 format for consistency",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tt := range tests {
							 | 
						|
										t.Run(tt.name, func(t *testing.T) {
							 | 
						|
											req := &http.Request{
							 | 
						|
												Method: tt.method,
							 | 
						|
												URL:    &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											result := determineGranularS3Action(req, tt.fallbackAction, tt.bucket, tt.objectKey)
							 | 
						|
								
							 | 
						|
											assert.Equal(t, tt.expectedResult, result,
							 | 
						|
												"Backward Compatibility Test: %s\nDescription: %s\nExpected: %s, Got: %s",
							 | 
						|
												tt.name, tt.description, tt.expectedResult, result)
							 | 
						|
								
							 | 
						|
											t.Logf("COMPATIBILITY: %s - %s", tt.description, result)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestPolicyEnforcementScenarios demonstrates how granular actions enable
							 | 
						|
								// more precise and secure IAM policy enforcement
							 | 
						|
								func TestPolicyEnforcementScenarios(t *testing.T) {
							 | 
						|
									scenarios := []struct {
							 | 
						|
										name            string
							 | 
						|
										policyExample   string
							 | 
						|
										method          string
							 | 
						|
										bucket          string
							 | 
						|
										objectKey       string
							 | 
						|
										queryParams     map[string]string
							 | 
						|
										expectedAction  string
							 | 
						|
										securityBenefit string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name: "allow_read_deny_acl_access",
							 | 
						|
											policyExample: `{
							 | 
						|
												"Version": "2012-10-17",
							 | 
						|
												"Statement": [
							 | 
						|
													{
							 | 
						|
														"Effect": "Allow",
							 | 
						|
														"Action": "s3:GetObject",
							 | 
						|
														"Resource": "arn:aws:s3:::sensitive-bucket/*"
							 | 
						|
													}
							 | 
						|
												]
							 | 
						|
											}`,
							 | 
						|
											method:          "GET",
							 | 
						|
											bucket:          "sensitive-bucket",
							 | 
						|
											objectKey:       "document.pdf",
							 | 
						|
											queryParams:     map[string]string{"acl": ""},
							 | 
						|
											expectedAction:  "s3:GetObjectAcl",
							 | 
						|
											securityBenefit: "Policy allows reading objects but denies ACL access - granular actions enable this distinction",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name: "allow_tagging_deny_object_modification",
							 | 
						|
											policyExample: `{
							 | 
						|
												"Version": "2012-10-17",
							 | 
						|
												"Statement": [
							 | 
						|
													{
							 | 
						|
														"Effect": "Allow", 
							 | 
						|
														"Action": ["s3:PutObjectTagging", "s3:DeleteObjectTagging"],
							 | 
						|
														"Resource": "arn:aws:s3:::data-bucket/*"
							 | 
						|
													}
							 | 
						|
												]
							 | 
						|
											}`,
							 | 
						|
											method:          "PUT",
							 | 
						|
											bucket:          "data-bucket",
							 | 
						|
											objectKey:       "metadata-file.json",
							 | 
						|
											queryParams:     map[string]string{"tagging": ""},
							 | 
						|
											expectedAction:  "s3:PutObjectTagging",
							 | 
						|
											securityBenefit: "Policy allows tag management but prevents actual object uploads - critical for metadata-only roles",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name: "restrict_multipart_uploads",
							 | 
						|
											policyExample: `{
							 | 
						|
												"Version": "2012-10-17",
							 | 
						|
												"Statement": [
							 | 
						|
													{
							 | 
						|
														"Effect": "Allow",
							 | 
						|
														"Action": "s3:PutObject",
							 | 
						|
														"Resource": "arn:aws:s3:::uploads/*"
							 | 
						|
													},
							 | 
						|
													{
							 | 
						|
														"Effect": "Deny",
							 | 
						|
														"Action": ["s3:CreateMultipartUpload", "s3:UploadPart"],
							 | 
						|
														"Resource": "arn:aws:s3:::uploads/*"
							 | 
						|
													}
							 | 
						|
												]
							 | 
						|
											}`,
							 | 
						|
											method:          "POST",
							 | 
						|
											bucket:          "uploads",
							 | 
						|
											objectKey:       "large-file.zip",
							 | 
						|
											queryParams:     map[string]string{"uploads": ""},
							 | 
						|
											expectedAction:  "s3:CreateMultipartUpload",
							 | 
						|
											securityBenefit: "Policy allows regular uploads but blocks large multipart uploads - prevents resource abuse",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, scenario := range scenarios {
							 | 
						|
										t.Run(scenario.name, func(t *testing.T) {
							 | 
						|
											req := &http.Request{
							 | 
						|
												Method: scenario.method,
							 | 
						|
												URL:    &url.URL{Path: "/" + scenario.bucket + "/" + scenario.objectKey},
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											query := req.URL.Query()
							 | 
						|
											for key, value := range scenario.queryParams {
							 | 
						|
												query.Set(key, value)
							 | 
						|
											}
							 | 
						|
											req.URL.RawQuery = query.Encode()
							 | 
						|
								
							 | 
						|
											result := determineGranularS3Action(req, s3_constants.ACTION_WRITE, scenario.bucket, scenario.objectKey)
							 | 
						|
								
							 | 
						|
											assert.Equal(t, scenario.expectedAction, result,
							 | 
						|
												"Policy Enforcement Scenario: %s\nExpected Action: %s, Got: %s",
							 | 
						|
												scenario.name, scenario.expectedAction, result)
							 | 
						|
								
							 | 
						|
											t.Logf("🔒 SECURITY SCENARIO: %s", scenario.name)
							 | 
						|
											t.Logf("   Expected Action: %s", result)
							 | 
						|
											t.Logf("   Security Benefit: %s", scenario.securityBenefit)
							 | 
						|
											t.Logf("   Policy Example:\n%s", scenario.policyExample)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 |