Browse Source

Replaced the coarse mapping with a comprehensive, context-aware action determination engine

pull/7160/head
chrislu 1 month ago
parent
commit
64e0c5ac7c
  1. 307
      weed/s3api/s3_granular_action_security_test.go
  2. 211
      weed/s3api/s3_iam_middleware.go
  3. 273
      weed/s3api/s3_iam_simple_test.go

307
weed/s3api/s3_granular_action_security_test.go

@ -0,0 +1,307 @@
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)
})
}
}

211
weed/s3api/s3_iam_middleware.go

@ -203,10 +203,13 @@ func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IA
// Extract request context for policy conditions
requestContext := extractRequestContext(r)
// Determine the specific S3 action based on the HTTP request details
specificAction := determineGranularS3Action(r, action, bucket, objectKey)
// Create action request
actionRequest := &integration.ActionRequest{
Principal: identity.Principal,
Action: mapS3ActionToIAMAction(action),
Action: specificAction,
Resource: resourceArn,
SessionToken: identity.SessionToken,
RequestContext: requestContext,
@ -270,26 +273,200 @@ func buildS3ResourceArn(bucket string, objectKey string) string {
return "arn:seaweed:s3:::" + bucket + "/" + objectKey
}
// mapS3ActionToIAMAction maps S3 API actions to IAM policy actions
func mapS3ActionToIAMAction(s3Action Action) string {
// Map S3 actions to standard IAM policy actions
actionMap := map[Action]string{
s3_constants.ACTION_READ: "s3:GetObject",
s3_constants.ACTION_WRITE: "s3:PutObject",
s3_constants.ACTION_LIST: "s3:ListBucket",
s3_constants.ACTION_TAGGING: "s3:GetObjectTagging",
s3_constants.ACTION_READ_ACP: "s3:GetObjectAcl",
s3_constants.ACTION_WRITE_ACP: "s3:PutObjectAcl",
s3_constants.ACTION_DELETE_BUCKET: "s3:DeleteBucket",
s3_constants.ACTION_ADMIN: "s3:*",
// determineGranularS3Action determines the specific S3 IAM action based on HTTP request details
// This provides granular, operation-specific actions for accurate IAM policy enforcement
func determineGranularS3Action(r *http.Request, fallbackAction Action, bucket string, objectKey string) string {
method := r.Method
query := r.URL.Query()
// Handle object-level operations
if objectKey != "" && objectKey != "/" {
switch method {
case "GET", "HEAD":
// Object read operations - check for specific query parameters
if _, hasAcl := query["acl"]; hasAcl {
return "s3:GetObjectAcl"
}
if _, hasTagging := query["tagging"]; hasTagging {
return "s3:GetObjectTagging"
}
if _, hasRetention := query["retention"]; hasRetention {
return "s3:GetObjectRetention"
}
if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
return "s3:GetObjectLegalHold"
}
if _, hasVersions := query["versions"]; hasVersions {
return "s3:GetObjectVersion"
}
// Default object read
return "s3:GetObject"
case "PUT", "POST":
// Object write operations - check for specific query parameters
if _, hasAcl := query["acl"]; hasAcl {
return "s3:PutObjectAcl"
}
if _, hasTagging := query["tagging"]; hasTagging {
return "s3:PutObjectTagging"
}
if _, hasRetention := query["retention"]; hasRetention {
return "s3:PutObjectRetention"
}
if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
return "s3:PutObjectLegalHold"
}
// Check for multipart upload operations
if _, hasUploads := query["uploads"]; hasUploads {
return "s3:CreateMultipartUpload"
}
if _, hasUploadId := query["uploadId"]; hasUploadId {
if _, hasPartNumber := query["partNumber"]; hasPartNumber {
return "s3:UploadPart"
}
return "s3:CompleteMultipartUpload" // Complete multipart upload
}
// Default object write
return "s3:PutObject"
case "DELETE":
// Object delete operations
if _, hasTagging := query["tagging"]; hasTagging {
return "s3:DeleteObjectTagging"
}
if _, hasUploadId := query["uploadId"]; hasUploadId {
return "s3:AbortMultipartUpload"
}
// Default object delete
return "s3:DeleteObject"
}
}
if iamAction, exists := actionMap[s3Action]; exists {
return iamAction
// Handle bucket-level operations
if bucket != "" {
switch method {
case "GET", "HEAD":
// Bucket read operations - check for specific query parameters
if _, hasAcl := query["acl"]; hasAcl {
return "s3:GetBucketAcl"
}
if _, hasPolicy := query["policy"]; hasPolicy {
return "s3:GetBucketPolicy"
}
if _, hasTagging := query["tagging"]; hasTagging {
return "s3:GetBucketTagging"
}
if _, hasCors := query["cors"]; hasCors {
return "s3:GetBucketCors"
}
if _, hasVersioning := query["versioning"]; hasVersioning {
return "s3:GetBucketVersioning"
}
if _, hasNotification := query["notification"]; hasNotification {
return "s3:GetBucketNotification"
}
if _, hasObjectLock := query["object-lock"]; hasObjectLock {
return "s3:GetBucketObjectLockConfiguration"
}
if _, hasUploads := query["uploads"]; hasUploads {
return "s3:ListMultipartUploads"
}
if _, hasVersions := query["versions"]; hasVersions {
return "s3:ListBucketVersions"
}
// Default bucket read/list
return "s3:ListBucket"
case "PUT":
// Bucket write operations - check for specific query parameters
if _, hasAcl := query["acl"]; hasAcl {
return "s3:PutBucketAcl"
}
if _, hasPolicy := query["policy"]; hasPolicy {
return "s3:PutBucketPolicy"
}
if _, hasTagging := query["tagging"]; hasTagging {
return "s3:PutBucketTagging"
}
if _, hasCors := query["cors"]; hasCors {
return "s3:PutBucketCors"
}
if _, hasVersioning := query["versioning"]; hasVersioning {
return "s3:PutBucketVersioning"
}
if _, hasNotification := query["notification"]; hasNotification {
return "s3:PutBucketNotification"
}
if _, hasObjectLock := query["object-lock"]; hasObjectLock {
return "s3:PutBucketObjectLockConfiguration"
}
// Default bucket creation
return "s3:CreateBucket"
case "DELETE":
// Bucket delete operations - check for specific query parameters
if _, hasPolicy := query["policy"]; hasPolicy {
return "s3:DeleteBucketPolicy"
}
if _, hasTagging := query["tagging"]; hasTagging {
return "s3:DeleteBucketTagging"
}
if _, hasCors := query["cors"]; hasCors {
return "s3:DeleteBucketCors"
}
// Default bucket delete
return "s3:DeleteBucket"
}
}
// Default to the string representation of the action
return string(s3Action)
// Fallback to legacy mapping for specific known actions
return mapLegacyActionToIAM(fallbackAction)
}
// mapLegacyActionToIAM provides fallback mapping for legacy actions
// This ensures backward compatibility while the system transitions to granular actions
func mapLegacyActionToIAM(legacyAction Action) string {
switch legacyAction {
case s3_constants.ACTION_READ:
return "s3:GetObject" // Fallback for unmapped read operations
case s3_constants.ACTION_WRITE:
return "s3:PutObject" // Fallback for unmapped write operations
case s3_constants.ACTION_LIST:
return "s3:ListBucket" // Fallback for unmapped list operations
case s3_constants.ACTION_TAGGING:
return "s3:GetObjectTagging" // Fallback for unmapped tagging operations
case s3_constants.ACTION_READ_ACP:
return "s3:GetObjectAcl" // Fallback for unmapped ACL read operations
case s3_constants.ACTION_WRITE_ACP:
return "s3:PutObjectAcl" // Fallback for unmapped ACL write operations
case s3_constants.ACTION_DELETE_BUCKET:
return "s3:DeleteBucket" // Fallback for unmapped bucket delete operations
case s3_constants.ACTION_ADMIN:
return "s3:*" // Fallback for unmapped admin operations
// Handle granular multipart actions (already correctly mapped)
case s3_constants.ACTION_CREATE_MULTIPART_UPLOAD:
return "s3:CreateMultipartUpload"
case s3_constants.ACTION_UPLOAD_PART:
return "s3:UploadPart"
case s3_constants.ACTION_COMPLETE_MULTIPART:
return "s3:CompleteMultipartUpload"
case s3_constants.ACTION_ABORT_MULTIPART:
return "s3:AbortMultipartUpload"
case s3_constants.ACTION_LIST_MULTIPART_UPLOADS:
return "s3:ListMultipartUploads"
case s3_constants.ACTION_LIST_PARTS:
return "s3:ListParts"
default:
// If it's already a properly formatted S3 action, return as-is
actionStr := string(legacyAction)
if strings.HasPrefix(actionStr, "s3:") {
return actionStr
}
// Fallback: convert to S3 action format
return "s3:" + actionStr
}
}
// extractRequestContext extracts request context for policy conditions

273
weed/s3api/s3_iam_simple_test.go

@ -4,6 +4,7 @@ import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
@ -11,6 +12,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
"github.com/seaweedfs/seaweedfs/weed/iam/utils"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -116,33 +118,274 @@ func TestBuildS3ResourceArn(t *testing.T) {
}
}
// TestMapS3ActionToIAMAction tests S3 to IAM action mapping
func TestMapS3ActionToIAMAction(t *testing.T) {
// TestDetermineGranularS3Action tests granular S3 action determination from HTTP requests
func TestDetermineGranularS3Action(t *testing.T) {
tests := []struct {
name string
s3Action Action
expected string
name string
method string
bucket string
objectKey string
queryParams map[string]string
fallbackAction Action
expected string
description string
}{
// Object-level operations
{
name: "get_object",
method: "GET",
bucket: "test-bucket",
objectKey: "test-object.txt",
queryParams: map[string]string{},
fallbackAction: s3_constants.ACTION_READ,
expected: "s3:GetObject",
description: "Basic object retrieval",
},
{
name: "get_object_acl",
method: "GET",
bucket: "test-bucket",
objectKey: "test-object.txt",
queryParams: map[string]string{"acl": ""},
fallbackAction: s3_constants.ACTION_READ_ACP,
expected: "s3:GetObjectAcl",
description: "Object ACL retrieval",
},
{
name: "get_object_tagging",
method: "GET",
bucket: "test-bucket",
objectKey: "test-object.txt",
queryParams: map[string]string{"tagging": ""},
fallbackAction: s3_constants.ACTION_TAGGING,
expected: "s3:GetObjectTagging",
description: "Object tagging retrieval",
},
{
name: "put_object",
method: "PUT",
bucket: "test-bucket",
objectKey: "test-object.txt",
queryParams: map[string]string{},
fallbackAction: s3_constants.ACTION_WRITE,
expected: "s3:PutObject",
description: "Basic object upload",
},
{
name: "put_object_acl",
method: "PUT",
bucket: "test-bucket",
objectKey: "test-object.txt",
queryParams: map[string]string{"acl": ""},
fallbackAction: s3_constants.ACTION_WRITE_ACP,
expected: "s3:PutObjectAcl",
description: "Object ACL modification",
},
{
name: "delete_object",
method: "DELETE",
bucket: "test-bucket",
objectKey: "test-object.txt",
queryParams: map[string]string{},
fallbackAction: s3_constants.ACTION_WRITE, // DELETE object uses WRITE fallback
expected: "s3:DeleteObject",
description: "Object deletion - correctly mapped to DeleteObject (not PutObject)",
},
{
name: "delete_object_tagging",
method: "DELETE",
bucket: "test-bucket",
objectKey: "test-object.txt",
queryParams: map[string]string{"tagging": ""},
fallbackAction: s3_constants.ACTION_TAGGING,
expected: "s3:DeleteObjectTagging",
description: "Object tag deletion",
},
// Multipart upload operations
{
name: "create_multipart_upload",
method: "POST",
bucket: "test-bucket",
objectKey: "large-file.txt",
queryParams: map[string]string{"uploads": ""},
fallbackAction: s3_constants.ACTION_WRITE,
expected: "s3:CreateMultipartUpload",
description: "Multipart upload initiation",
},
{
name: "upload_part",
method: "PUT",
bucket: "test-bucket",
objectKey: "large-file.txt",
queryParams: map[string]string{"uploadId": "12345", "partNumber": "1"},
fallbackAction: s3_constants.ACTION_WRITE,
expected: "s3:UploadPart",
description: "Multipart part upload",
},
{
name: "complete_multipart_upload",
method: "POST",
bucket: "test-bucket",
objectKey: "large-file.txt",
queryParams: map[string]string{"uploadId": "12345"},
fallbackAction: s3_constants.ACTION_WRITE,
expected: "s3:CompleteMultipartUpload",
description: "Multipart upload completion",
},
{
name: "abort_multipart_upload",
method: "DELETE",
bucket: "test-bucket",
objectKey: "large-file.txt",
queryParams: map[string]string{"uploadId": "12345"},
fallbackAction: s3_constants.ACTION_WRITE,
expected: "s3:AbortMultipartUpload",
description: "Multipart upload abort",
},
// Bucket-level operations
{
name: "list_bucket",
method: "GET",
bucket: "test-bucket",
objectKey: "",
queryParams: map[string]string{},
fallbackAction: s3_constants.ACTION_LIST,
expected: "s3:ListBucket",
description: "Bucket listing",
},
{
name: "get_bucket_acl",
method: "GET",
bucket: "test-bucket",
objectKey: "",
queryParams: map[string]string{"acl": ""},
fallbackAction: s3_constants.ACTION_READ_ACP,
expected: "s3:GetBucketAcl",
description: "Bucket ACL retrieval",
},
{
name: "put_bucket_policy",
method: "PUT",
bucket: "test-bucket",
objectKey: "",
queryParams: map[string]string{"policy": ""},
fallbackAction: s3_constants.ACTION_WRITE,
expected: "s3:PutBucketPolicy",
description: "Bucket policy modification",
},
{
name: "delete_bucket",
method: "DELETE",
bucket: "test-bucket",
objectKey: "",
queryParams: map[string]string{},
fallbackAction: s3_constants.ACTION_DELETE_BUCKET,
expected: "s3:DeleteBucket",
description: "Bucket deletion",
},
{
name: "list_multipart_uploads",
method: "GET",
bucket: "test-bucket",
objectKey: "",
queryParams: map[string]string{"uploads": ""},
fallbackAction: s3_constants.ACTION_LIST,
expected: "s3:ListMultipartUploads",
description: "List multipart uploads in bucket",
},
// Fallback scenarios
{
name: "legacy_read_fallback",
method: "GET",
bucket: "",
objectKey: "",
queryParams: map[string]string{},
fallbackAction: s3_constants.ACTION_READ,
expected: "s3:GetObject",
description: "Legacy read action fallback",
},
{
name: "already_granular_action",
method: "GET",
bucket: "",
objectKey: "",
queryParams: map[string]string{},
fallbackAction: "s3:GetBucketLocation", // Already granular
expected: "s3:GetBucketLocation",
description: "Already granular action passed through",
},
}
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 granular action determination
result := determineGranularS3Action(req, tt.fallbackAction, tt.bucket, tt.objectKey)
assert.Equal(t, tt.expected, result,
"Test %s failed: %s. Expected %s but got %s",
tt.name, tt.description, tt.expected, result)
})
}
}
// TestMapLegacyActionToIAM tests the legacy action fallback mapping
func TestMapLegacyActionToIAM(t *testing.T) {
tests := []struct {
name string
legacyAction Action
expected string
}{
{
name: "read action",
s3Action: "READ", // Assuming this is defined in s3_constants
expected: "READ", // Will fallback to string representation
name: "read_action_fallback",
legacyAction: s3_constants.ACTION_READ,
expected: "s3:GetObject",
},
{
name: "write_action_fallback",
legacyAction: s3_constants.ACTION_WRITE,
expected: "s3:PutObject",
},
{
name: "admin_action_fallback",
legacyAction: s3_constants.ACTION_ADMIN,
expected: "s3:*",
},
{
name: "granular_multipart_action",
legacyAction: s3_constants.ACTION_CREATE_MULTIPART_UPLOAD,
expected: "s3:CreateMultipartUpload",
},
{
name: "write action",
s3Action: "WRITE",
expected: "WRITE",
name: "unknown_action_with_s3_prefix",
legacyAction: "s3:CustomAction",
expected: "s3:CustomAction",
},
{
name: "list action",
s3Action: "LIST",
expected: "LIST",
name: "unknown_action_without_prefix",
legacyAction: "CustomAction",
expected: "s3:CustomAction",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := mapS3ActionToIAMAction(tt.s3Action)
result := mapLegacyActionToIAM(tt.legacyAction)
assert.Equal(t, tt.expected, result)
})
}

Loading…
Cancel
Save