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