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) | |
| 		}) | |
| 	} | |
| }
 |