diff --git a/weed/s3api/s3_action_resolver.go b/weed/s3api/s3_action_resolver.go index d96d150de..49de7fd17 100644 --- a/weed/s3api/s3_action_resolver.go +++ b/weed/s3api/s3_action_resolver.go @@ -53,6 +53,37 @@ func ResolveS3Action(r *http.Request, baseAction string, bucket string, object s return mapBaseActionToS3Format(baseAction) } +// bucketQueryActions maps bucket-level query parameters to their corresponding S3 actions by HTTP method +var bucketQueryActions = map[string]map[string]string{ + "policy": { + http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_POLICY, + http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_POLICY, + http.MethodDelete: s3_constants.S3_ACTION_DELETE_BUCKET_POLICY, + }, + "cors": { + http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_CORS, + http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_CORS, + http.MethodDelete: s3_constants.S3_ACTION_DELETE_BUCKET_CORS, + }, + "lifecycle": { + http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_LIFECYCLE, + http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_LIFECYCLE, + http.MethodDelete: s3_constants.S3_ACTION_DELETE_BUCKET_LIFECYCLE, + }, + "versioning": { + http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_VERSIONING, + http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_VERSIONING, + }, + "notification": { + http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_NOTIFICATION, + http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_NOTIFICATION, + }, + "object-lock": { + http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_OBJECT_LOCK, + http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_OBJECT_LOCK, + }, +} + // resolveFromQueryParameters checks query parameters to determine specific S3 actions func resolveFromQueryParameters(query url.Values, method string, hasObject bool) string { // Multipart upload operations @@ -118,15 +149,6 @@ func resolveFromQueryParameters(query url.Values, method string, hasObject bool) } } } - - // Versioning operations - if query.Has("versioning") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_BUCKET_VERSIONING - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_BUCKET_VERSIONING - } - } if query.Has("versions") { if method == http.MethodGet { @@ -144,75 +166,36 @@ func resolveFromQueryParameters(query url.Values, method string, hasObject bool) } } - // Policy operations - if query.Has("policy") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_BUCKET_POLICY - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_BUCKET_POLICY - } else if method == http.MethodDelete { - return s3_constants.S3_ACTION_DELETE_BUCKET_POLICY - } - } - - // CORS operations - if query.Has("cors") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_BUCKET_CORS - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_BUCKET_CORS - } else if method == http.MethodDelete { - return s3_constants.S3_ACTION_DELETE_BUCKET_CORS - } - } - - // Lifecycle operations - if query.Has("lifecycle") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_BUCKET_LIFECYCLE - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_BUCKET_LIFECYCLE - } else if method == http.MethodDelete { - return s3_constants.S3_ACTION_DELETE_BUCKET_LIFECYCLE + // Check bucket-level query parameters using data-driven approach + for param, actions := range bucketQueryActions { + if query.Has(param) { + if action, ok := actions[method]; ok { + return action + } } } - // Location + // Location (GET only) if query.Has("location") { return s3_constants.S3_ACTION_GET_BUCKET_LOCATION } - - // Notification - if query.Has("notification") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_BUCKET_NOTIFICATION - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_BUCKET_NOTIFICATION - } - } - - // Object Lock operations - if query.Has("object-lock") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_BUCKET_OBJECT_LOCK - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_BUCKET_OBJECT_LOCK - } - } - if query.Has("retention") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_OBJECT_RETENTION - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_OBJECT_RETENTION + // Object retention and legal hold operations (object-level only) + if hasObject { + if query.Has("retention") { + if method == http.MethodGet { + return s3_constants.S3_ACTION_GET_OBJECT_RETENTION + } else if method == http.MethodPut { + return s3_constants.S3_ACTION_PUT_OBJECT_RETENTION + } } - } - - if query.Has("legal-hold") { - if method == http.MethodGet { - return s3_constants.S3_ACTION_GET_OBJECT_LEGAL_HOLD - } else if method == http.MethodPut { - return s3_constants.S3_ACTION_PUT_OBJECT_LEGAL_HOLD + + if query.Has("legal-hold") { + if method == http.MethodGet { + return s3_constants.S3_ACTION_GET_OBJECT_LEGAL_HOLD + } else if method == http.MethodPut { + return s3_constants.S3_ACTION_PUT_OBJECT_LEGAL_HOLD + } } } @@ -236,7 +219,7 @@ func resolveObjectLevelAction(method string, baseAction string, r *http.Request) if baseAction == s3_constants.ACTION_WRITE { // Check for copy operation if r.Header.Get("X-Amz-Copy-Source") != "" { - return s3_constants.S3_ACTION_PUT_OBJECT // CopyObject also requires PutObject permission + return s3_constants.S3_ACTION_COPY_OBJECT } return s3_constants.S3_ACTION_PUT_OBJECT } diff --git a/weed/s3api/s3_constants/s3_action_strings.go b/weed/s3api/s3_constants/s3_action_strings.go index 96d10d03f..19585a93f 100644 --- a/weed/s3api/s3_constants/s3_action_strings.go +++ b/weed/s3api/s3_constants/s3_action_strings.go @@ -6,6 +6,7 @@ const ( // Object operations S3_ACTION_GET_OBJECT = "s3:GetObject" S3_ACTION_PUT_OBJECT = "s3:PutObject" + S3_ACTION_COPY_OBJECT = "s3:CopyObject" S3_ACTION_DELETE_OBJECT = "s3:DeleteObject" S3_ACTION_DELETE_OBJECT_VERSION = "s3:DeleteObjectVersion" S3_ACTION_GET_OBJECT_VERSION = "s3:GetObjectVersion" diff --git a/weed/s3api/s3_granular_action_security_test.go b/weed/s3api/s3_granular_action_security_test.go index 4f07cc6b3..3477e18f3 100644 --- a/weed/s3api/s3_granular_action_security_test.go +++ b/weed/s3api/s3_granular_action_security_test.go @@ -3,6 +3,7 @@ package s3api import ( "net/http" "net/url" + "strings" "testing" "github.com/gorilla/mux" @@ -311,40 +312,40 @@ func TestPolicyEnforcementScenarios(t *testing.T) { // Previously, DeleteObject operations were mapped to s3:PutObject, preventing fine-grained policies from working func TestDeleteObjectPolicyEnforcement(t *testing.T) { tests := []struct { - name string - method string - bucket string - objectKey string - baseAction Action + name string + method string + bucket string + objectKey string + baseAction Action expectedS3Action string - policyScenario string + policyScenario string }{ { - name: "delete_object_maps_to_correct_action", - method: http.MethodDelete, - bucket: "test-bucket", - objectKey: "test-object.txt", - baseAction: s3_constants.ACTION_WRITE, + name: "delete_object_maps_to_correct_action", + method: http.MethodDelete, + bucket: "test-bucket", + objectKey: "test-object.txt", + baseAction: s3_constants.ACTION_WRITE, expectedS3Action: "s3:DeleteObject", - policyScenario: "Policy that denies s3:DeleteObject but allows s3:PutObject should now work correctly", + policyScenario: "Policy that denies s3:DeleteObject but allows s3:PutObject should now work correctly", }, { - name: "put_object_maps_to_correct_action", - method: http.MethodPut, - bucket: "test-bucket", - objectKey: "test-object.txt", - baseAction: s3_constants.ACTION_WRITE, + name: "put_object_maps_to_correct_action", + method: http.MethodPut, + bucket: "test-bucket", + objectKey: "test-object.txt", + baseAction: s3_constants.ACTION_WRITE, expectedS3Action: "s3:PutObject", - policyScenario: "Policy that allows s3:PutObject but denies s3:DeleteObject should allow uploads", + policyScenario: "Policy that allows s3:PutObject but denies s3:DeleteObject should allow uploads", }, { - name: "batch_delete_maps_to_delete_action", - method: http.MethodPost, - bucket: "test-bucket", - objectKey: "", - baseAction: s3_constants.ACTION_WRITE, + name: "batch_delete_maps_to_delete_action", + method: http.MethodPost, + bucket: "test-bucket", + objectKey: "", + baseAction: s3_constants.ACTION_WRITE, expectedS3Action: "s3:DeleteObject", - policyScenario: "Batch delete operations should also map to s3:DeleteObject", + policyScenario: "Batch delete operations should also map to s3:DeleteObject", }, } @@ -496,13 +497,13 @@ func TestFineGrainedPolicyExample(t *testing.T) { // was always mapped to s3:PutObject, preventing fine-grained policies from working. func TestCoarseActionResolution(t *testing.T) { testCases := []struct { - name string - method string - path string - queryParams map[string]string - coarseAction Action - expectedS3Action string - policyScenario string + name string + method string + path string + queryParams map[string]string + coarseAction Action + expectedS3Action string + policyScenario string }{ { name: "PUT_with_ACTION_WRITE_resolves_to_PutObject", @@ -541,7 +542,7 @@ func TestCoarseActionResolution(t *testing.T) { policyScenario: "Policy allowing s3:PutObject but denying s3:CreateMultipartUpload can now work", }, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Build URL with query parameters @@ -552,70 +553,43 @@ func TestCoarseActionResolution(t *testing.T) { q.Add(k, v) } u.RawQuery = q.Encode() - + // Create HTTP request req, err := http.NewRequest(tc.method, u.String(), nil) assert.NoError(t, err) - - // Extract bucket and object from path for mux.Vars simulation - // Path format: /bucket/object or /bucket - pathParts := []string{} - for _, part := range []byte(u.Path) { - if part == '/' { - continue - } - pathParts = append(pathParts, string(part)) - } - + // Parse path to extract bucket and object + parts := strings.Split(strings.TrimPrefix(u.Path, "/"), "/") bucket := "" object := "" - if u.Path != "" { - parts := []string{} - current := "" - for _, ch := range u.Path { - if ch == '/' { - if current != "" { - parts = append(parts, current) - current = "" - } - } else { - current += string(ch) - } - } - if current != "" { - parts = append(parts, current) - } - - if len(parts) > 0 { - bucket = parts[0] - if len(parts) > 1 { - object = "/" + parts[1] - } - } + if len(parts) > 0 { + bucket = parts[0] + } + if len(parts) > 1 { + object = "/" + strings.Join(parts[1:], "/") } - + // Simulate mux.Vars for GetBucketAndObject req = mux.SetURLVars(req, map[string]string{ "bucket": bucket, "object": object, }) - + // Call ResolveS3Action with coarse action constant resolvedAction := ResolveS3Action(req, string(tc.coarseAction), bucket, object) - + // Verify correct S3 action is resolved assert.Equal(t, tc.expectedS3Action, resolvedAction, "Coarse action %s with method %s should resolve to %s", tc.coarseAction, tc.method, tc.expectedS3Action) - + t.Logf("SUCCESS: %s", tc.name) t.Logf(" Input: %s %s + ACTION_WRITE", tc.method, tc.path) t.Logf(" Output: %s", resolvedAction) t.Logf(" Policy impact: %s", tc.policyScenario) }) } - + t.Log("\n=== ARCHITECTURAL LIMITATION RESOLVED ===") t.Log("Handlers can use coarse ACTION_WRITE constant, and the context-aware") t.Log("resolver will map it to the correct specific S3 action (PutObject,") diff --git a/weed/s3api/s3_iam_middleware.go b/weed/s3api/s3_iam_middleware.go index ebe179af1..001e139c6 100644 --- a/weed/s3api/s3_iam_middleware.go +++ b/weed/s3api/s3_iam_middleware.go @@ -246,9 +246,7 @@ func buildS3ResourceArn(bucket string, objectKey string) string { } // Remove leading slash from object key if present - if strings.HasPrefix(objectKey, "/") { - objectKey = objectKey[1:] - } + objectKey = strings.TrimPrefix(objectKey, "/") return "arn:aws:s3:::" + bucket + "/" + objectKey } @@ -271,165 +269,13 @@ func determineGranularS3Action(r *http.Request, fallbackAction Action, bucket st return mapLegacyActionToIAM(fallbackAction) } - // Use the shared action resolver for consistent resolution - // This now handles most of the action resolution logic + // Use the shared action resolver for consistent resolution across all S3 operations + // ResolveS3Action handles all query parameters, HTTP methods, and object/bucket distinctions if resolvedAction := ResolveS3Action(r, string(fallbackAction), bucket, objectKey); resolvedAction != "" { return resolvedAction } - // Legacy IAM-specific object-level handling (for backward compatibility) - if objectKey != "" && objectKey != "/" { - switch r.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" - } - if _, hasUploadId := query["uploadId"]; hasUploadId { - return "s3:ListParts" - } - // 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" - } - } - - // Handle bucket-level operations - if bucket != "" { - switch r.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 "POST": - // Bucket POST operations - check for specific query parameters - if _, hasDelete := query["delete"]; hasDelete { - // Batch delete operation - return "s3:DeleteObject" - } - // Default bucket POST (e.g., policy form upload) - return "s3:PutObject" - - 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" - } - } - - // Fallback to legacy mapping for specific known actions + // Final fallback to legacy mapping if no specific resolution found return mapLegacyActionToIAM(fallbackAction) } diff --git a/weed/s3api/s3api_bucket_handlers.go b/weed/s3api/s3api_bucket_handlers.go index 0e16bbcde..5ebb06b21 100644 --- a/weed/s3api/s3api_bucket_handlers.go +++ b/weed/s3api/s3api_bucket_handlers.go @@ -597,44 +597,44 @@ func (s3a *S3ApiServer) AuthWithPublicRead(handler http.HandlerFunc, action Acti glog.V(4).Infof("AuthWithPublicRead: bucket=%s, object=%s, authType=%v, isAnonymous=%v", bucket, object, authType, isAnonymous) - // For anonymous requests, check if bucket allows public read via ACLs or bucket policies - if isAnonymous { - // First check ACL-based public access - isPublic := s3a.isBucketPublicRead(bucket) - glog.V(4).Infof("AuthWithPublicRead: bucket=%s, isPublicACL=%v", bucket, isPublic) - if isPublic { - glog.V(3).Infof("AuthWithPublicRead: allowing anonymous access to public-read bucket %s (ACL)", bucket) - handler(w, r) - return - } - - // Check bucket policy for anonymous access using the policy engine - principal := "*" // Anonymous principal - // Use context-aware policy evaluation to get the correct S3 action - allowed, evaluated, err := s3a.policyEngine.EvaluatePolicyWithContext(bucket, object, string(action), principal, r) - if err != nil { - // SECURITY: Fail-close on policy evaluation errors - // If we can't evaluate the policy, deny access rather than falling through to IAM - glog.Errorf("AuthWithPublicRead: error evaluating bucket policy for %s/%s: %v - denying access", bucket, object, err) - s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) - return - } else if evaluated { - // A bucket policy exists and was evaluated with a matching statement - if allowed { - // Policy explicitly allows anonymous access - glog.V(3).Infof("AuthWithPublicRead: allowing anonymous access to bucket %s (bucket policy)", bucket) + // For anonymous requests, check if bucket allows public read via ACLs or bucket policies + if isAnonymous { + // First check ACL-based public access + isPublic := s3a.isBucketPublicRead(bucket) + glog.V(4).Infof("AuthWithPublicRead: bucket=%s, isPublicACL=%v", bucket, isPublic) + if isPublic { + glog.V(3).Infof("AuthWithPublicRead: allowing anonymous access to public-read bucket %s (ACL)", bucket) handler(w, r) return - } else { - // Policy explicitly denies anonymous access - glog.V(3).Infof("AuthWithPublicRead: bucket policy explicitly denies anonymous access to %s/%s", bucket, object) + } + + // Check bucket policy for anonymous access using the policy engine + principal := "*" // Anonymous principal + // Use context-aware policy evaluation to get the correct S3 action + allowed, evaluated, err := s3a.policyEngine.EvaluatePolicyWithContext(bucket, object, string(action), principal, r) + if err != nil { + // SECURITY: Fail-close on policy evaluation errors + // If we can't evaluate the policy, deny access rather than falling through to IAM + glog.Errorf("AuthWithPublicRead: error evaluating bucket policy for %s/%s: %v - denying access", bucket, object, err) s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) return + } else if evaluated { + // A bucket policy exists and was evaluated with a matching statement + if allowed { + // Policy explicitly allows anonymous access + glog.V(3).Infof("AuthWithPublicRead: allowing anonymous access to bucket %s (bucket policy)", bucket) + handler(w, r) + return + } else { + // Policy explicitly denies anonymous access + glog.V(3).Infof("AuthWithPublicRead: bucket policy explicitly denies anonymous access to %s/%s", bucket, object) + s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) + return + } } + // No matching policy statement - fall through to check ACLs and then IAM auth + glog.V(3).Infof("AuthWithPublicRead: no bucket policy match for %s, checking ACLs", bucket) } - // No matching policy statement - fall through to check ACLs and then IAM auth - glog.V(3).Infof("AuthWithPublicRead: no bucket policy match for %s, checking ACLs", bucket) - } // For all authenticated requests and anonymous requests to non-public buckets, // use normal IAM auth to enforce policies diff --git a/weed/s3api/s3api_bucket_policy_engine.go b/weed/s3api/s3api_bucket_policy_engine.go index 51ebd644f..92d648d24 100644 --- a/weed/s3api/s3api_bucket_policy_engine.go +++ b/weed/s3api/s3api_bucket_policy_engine.go @@ -9,7 +9,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/iam/policy" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" - "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" ) // BucketPolicyEngine wraps the policy_engine to provide bucket policy evaluation @@ -102,8 +101,8 @@ func (bpe *BucketPolicyEngine) EvaluatePolicy(bucket, object, action, principal return false, false, fmt.Errorf("action cannot be empty") } - // Convert action to S3 action format - s3Action := convertActionToS3Format(action, nil) + // Convert action to S3 action format using base mapping (no HTTP context available) + s3Action := mapBaseActionToS3Format(action) // Build resource ARN resource := buildResourceARN(bucket, object) @@ -147,7 +146,17 @@ func (bpe *BucketPolicyEngine) EvaluatePolicyWithContext(bucket, object, action, } // Convert action to S3 action format using request context - s3Action := convertActionToS3Format(action, r) + // Use ResolveS3Action for context-aware resolution, fall back to base mapping + var s3Action string + if r != nil { + if resolved := ResolveS3Action(r, action, bucket, object); resolved != "" { + s3Action = resolved + } else { + s3Action = mapBaseActionToS3Format(action) + } + } else { + s3Action = mapBaseActionToS3Format(action) + } // Build resource ARN resource := buildResourceARN(bucket, object) @@ -180,28 +189,6 @@ func (bpe *BucketPolicyEngine) EvaluatePolicyWithContext(bucket, object, action, } } -// convertActionToS3Format converts internal action strings to S3 action format -// with optional HTTP request context for fine-grained action resolution. -// -// This function now uses the shared ResolveS3Action utility for consistent -// action resolution across the bucket policy engine and IAM integration. -// -// Parameters: -// - action: The internal action constant (e.g., ACTION_WRITE, ACTION_READ) -// - r: Optional HTTP request for context-aware resolution. If nil, uses legacy mapping. -func convertActionToS3Format(action string, r *http.Request) string { - // If request context is provided, use the shared action resolver - if r != nil { - bucket, object := s3_constants.GetBucketAndObject(r) - if resolvedAction := ResolveS3Action(r, action, bucket, object); resolvedAction != "" { - return resolvedAction - } - } - - // Fallback to base action mapping - return mapBaseActionToS3Format(action) -} - -// NOTE: resolveS3ActionFromRequest has been replaced by the shared ResolveS3Action -// function in s3_action_resolver.go. This consolidates action resolution logic -// used by both the bucket policy engine and IAM integration. +// NOTE: The convertActionToS3Format wrapper has been removed for simplicity. +// EvaluatePolicy and EvaluatePolicyWithContext now call ResolveS3Action or +// mapBaseActionToS3Format directly, making the control flow more explicit.