mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-19 05:36:58 +00:00
Replaced the coarse mapping with a comprehensive, context-aware action determination engine
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+194
-17
@@ -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
|
||||
|
||||
@@ -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: "read action",
|
||||
s3Action: "READ", // Assuming this is defined in s3_constants
|
||||
expected: "READ", // Will fallback to string representation
|
||||
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: "write action",
|
||||
s3Action: "WRITE",
|
||||
expected: "WRITE",
|
||||
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: "list action",
|
||||
s3Action: "LIST",
|
||||
expected: "LIST",
|
||||
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) {
|
||||
result := mapS3ActionToIAMAction(tt.s3Action)
|
||||
// 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_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: "unknown_action_with_s3_prefix",
|
||||
legacyAction: "s3:CustomAction",
|
||||
expected: "s3:CustomAction",
|
||||
},
|
||||
{
|
||||
name: "unknown_action_without_prefix",
|
||||
legacyAction: "CustomAction",
|
||||
expected: "s3:CustomAction",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mapLegacyActionToIAM(tt.legacyAction)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user