5 changed files with 582 additions and 278 deletions
-
337weed/s3api/s3_action_resolver.go
-
85weed/s3api/s3_constants/s3_action_strings.go
-
136weed/s3api/s3_granular_action_security_test.go
-
20weed/s3api/s3_iam_middleware.go
-
282weed/s3api/s3api_bucket_policy_engine.go
@ -0,0 +1,337 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"net/http" |
|||
"net/url" |
|||
"strings" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|||
) |
|||
|
|||
// ResolveS3Action determines the specific S3 action from HTTP request context.
|
|||
// This is the unified implementation used by both the bucket policy engine
|
|||
// and the IAM integration for consistent action resolution.
|
|||
//
|
|||
// It examines the HTTP method, path, query parameters, and headers to determine
|
|||
// the most specific S3 action string (e.g., "s3:DeleteObject", "s3:PutObjectTagging").
|
|||
//
|
|||
// Parameters:
|
|||
// - r: HTTP request containing method, URL, query params, and headers
|
|||
// - baseAction: Coarse-grained action constant (e.g., ACTION_WRITE, ACTION_READ)
|
|||
// - bucket: Bucket name from the request path
|
|||
// - object: Object key from the request path (may be empty for bucket operations)
|
|||
//
|
|||
// Returns:
|
|||
// - Specific S3 action string (e.g., "s3:DeleteObject")
|
|||
// - Empty string if no specific resolution is possible
|
|||
func ResolveS3Action(r *http.Request, baseAction string, bucket string, object string) string { |
|||
if r == nil { |
|||
return "" |
|||
} |
|||
|
|||
method := r.Method |
|||
query := r.URL.Query() |
|||
|
|||
// Determine if this is an object or bucket operation
|
|||
// Note: "/" is treated as bucket-level, not object-level
|
|||
hasObject := object != "" && object != "/" |
|||
|
|||
// Priority 1: Check for specific query parameters that indicate specific actions
|
|||
// These override everything else because they explicitly indicate the operation type
|
|||
if action := resolveFromQueryParameters(query, method, hasObject); action != "" { |
|||
return action |
|||
} |
|||
|
|||
// Priority 2: Handle basic operations based on method and resource type
|
|||
if hasObject { |
|||
return resolveObjectLevelAction(method, baseAction, r) |
|||
} else if bucket != "" { |
|||
return resolveBucketLevelAction(method, baseAction) |
|||
} |
|||
|
|||
// Priority 3: Fallback to legacy action mapping
|
|||
return mapBaseActionToS3Format(baseAction) |
|||
} |
|||
|
|||
// resolveFromQueryParameters checks query parameters to determine specific S3 actions
|
|||
func resolveFromQueryParameters(query url.Values, method string, hasObject bool) string { |
|||
// Multipart upload operations
|
|||
if query.Has("uploadId") && query.Has("partNumber") { |
|||
if method == http.MethodPut { |
|||
return s3_constants.S3_ACTION_UPLOAD_PART |
|||
} |
|||
} |
|||
|
|||
if query.Has("uploadId") { |
|||
switch method { |
|||
case http.MethodPost: |
|||
return s3_constants.S3_ACTION_COMPLETE_MULTIPART |
|||
case http.MethodDelete: |
|||
return s3_constants.S3_ACTION_ABORT_MULTIPART |
|||
case http.MethodGet: |
|||
return s3_constants.S3_ACTION_LIST_PARTS |
|||
} |
|||
} |
|||
|
|||
if query.Has("uploads") { |
|||
if method == http.MethodPost { |
|||
return s3_constants.S3_ACTION_CREATE_MULTIPART |
|||
} else if method == http.MethodGet { |
|||
return s3_constants.S3_ACTION_LIST_MULTIPART_UPLOADS |
|||
} |
|||
} |
|||
|
|||
// ACL operations
|
|||
if query.Has("acl") { |
|||
if hasObject { |
|||
if method == http.MethodGet || method == http.MethodHead { |
|||
return s3_constants.S3_ACTION_GET_OBJECT_ACL |
|||
} else if method == http.MethodPut { |
|||
return s3_constants.S3_ACTION_PUT_OBJECT_ACL |
|||
} |
|||
} else { |
|||
if method == http.MethodGet || method == http.MethodHead { |
|||
return s3_constants.S3_ACTION_GET_BUCKET_ACL |
|||
} else if method == http.MethodPut { |
|||
return s3_constants.S3_ACTION_PUT_BUCKET_ACL |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Tagging operations
|
|||
if query.Has("tagging") { |
|||
if hasObject { |
|||
if method == http.MethodGet { |
|||
return s3_constants.S3_ACTION_GET_OBJECT_TAGGING |
|||
} else if method == http.MethodPut { |
|||
return s3_constants.S3_ACTION_PUT_OBJECT_TAGGING |
|||
} else if method == http.MethodDelete { |
|||
return s3_constants.S3_ACTION_DELETE_OBJECT_TAGGING |
|||
} |
|||
} else { |
|||
if method == http.MethodGet { |
|||
return s3_constants.S3_ACTION_GET_BUCKET_TAGGING |
|||
} else if method == http.MethodPut { |
|||
return s3_constants.S3_ACTION_PUT_BUCKET_TAGGING |
|||
} else if method == http.MethodDelete { |
|||
return s3_constants.S3_ACTION_DELETE_BUCKET_TAGGING |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 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 { |
|||
// If there's an object, this is GetObjectVersion
|
|||
// If there's no object (bucket level), this is ListBucketVersions
|
|||
if hasObject { |
|||
return s3_constants.S3_ACTION_GET_OBJECT_VERSION |
|||
} else { |
|||
return s3_constants.S3_ACTION_LIST_BUCKET_VERSIONS |
|||
} |
|||
} |
|||
// DELETE with versions could be DeleteObjectVersion
|
|||
if method == http.MethodDelete && hasObject { |
|||
return s3_constants.S3_ACTION_DELETE_OBJECT_VERSION |
|||
} |
|||
} |
|||
|
|||
// 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 |
|||
} |
|||
} |
|||
|
|||
// Location
|
|||
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 |
|||
} |
|||
} |
|||
|
|||
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 |
|||
} |
|||
} |
|||
|
|||
// Batch delete - works on bucket level
|
|||
if query.Has("delete") { |
|||
return s3_constants.S3_ACTION_DELETE_OBJECT |
|||
} |
|||
|
|||
return "" |
|||
} |
|||
|
|||
// resolveObjectLevelAction determines the S3 action for object-level operations
|
|||
func resolveObjectLevelAction(method string, baseAction string, r *http.Request) string { |
|||
switch method { |
|||
case http.MethodGet, http.MethodHead: |
|||
if baseAction == s3_constants.ACTION_READ { |
|||
return s3_constants.S3_ACTION_GET_OBJECT |
|||
} |
|||
|
|||
case http.MethodPut: |
|||
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_PUT_OBJECT |
|||
} |
|||
|
|||
case http.MethodDelete: |
|||
// CRITICAL: Map DELETE method to s3:DeleteObject
|
|||
// This fixes the architectural limitation where ACTION_WRITE was mapped to s3:PutObject
|
|||
if baseAction == s3_constants.ACTION_WRITE { |
|||
return s3_constants.S3_ACTION_DELETE_OBJECT |
|||
} |
|||
|
|||
case http.MethodPost: |
|||
// POST without query params is typically multipart or form upload
|
|||
if baseAction == s3_constants.ACTION_WRITE { |
|||
return s3_constants.S3_ACTION_PUT_OBJECT |
|||
} |
|||
} |
|||
|
|||
return "" |
|||
} |
|||
|
|||
// resolveBucketLevelAction determines the S3 action for bucket-level operations
|
|||
func resolveBucketLevelAction(method string, baseAction string) string { |
|||
switch method { |
|||
case http.MethodGet, http.MethodHead: |
|||
if baseAction == s3_constants.ACTION_LIST { |
|||
return s3_constants.S3_ACTION_LIST_BUCKET |
|||
} else if baseAction == s3_constants.ACTION_READ { |
|||
return s3_constants.S3_ACTION_LIST_BUCKET |
|||
} |
|||
|
|||
case http.MethodPut: |
|||
if baseAction == s3_constants.ACTION_WRITE { |
|||
return s3_constants.S3_ACTION_CREATE_BUCKET |
|||
} |
|||
|
|||
case http.MethodDelete: |
|||
if baseAction == s3_constants.ACTION_DELETE_BUCKET { |
|||
return s3_constants.S3_ACTION_DELETE_BUCKET |
|||
} |
|||
|
|||
case http.MethodPost: |
|||
// POST to bucket is typically form upload
|
|||
if baseAction == s3_constants.ACTION_WRITE { |
|||
return s3_constants.S3_ACTION_PUT_OBJECT |
|||
} |
|||
} |
|||
|
|||
return "" |
|||
} |
|||
|
|||
// mapBaseActionToS3Format converts coarse-grained base actions to S3 format
|
|||
// This is the fallback when no specific resolution is found
|
|||
func mapBaseActionToS3Format(baseAction string) string { |
|||
// Handle actions that already have s3: prefix
|
|||
if strings.HasPrefix(baseAction, "s3:") { |
|||
return baseAction |
|||
} |
|||
|
|||
// Map coarse-grained actions to their most common S3 equivalent
|
|||
// Note: The s3_constants values ARE the string values (e.g., ACTION_READ = "Read")
|
|||
switch baseAction { |
|||
case s3_constants.ACTION_READ: // "Read"
|
|||
return s3_constants.S3_ACTION_GET_OBJECT |
|||
case s3_constants.ACTION_WRITE: // "Write"
|
|||
return s3_constants.S3_ACTION_PUT_OBJECT |
|||
case s3_constants.ACTION_LIST: // "List"
|
|||
return s3_constants.S3_ACTION_LIST_BUCKET |
|||
case s3_constants.ACTION_TAGGING: // "Tagging"
|
|||
return s3_constants.S3_ACTION_PUT_OBJECT_TAGGING |
|||
case s3_constants.ACTION_ADMIN: // "Admin"
|
|||
return s3_constants.S3_ACTION_ALL |
|||
case s3_constants.ACTION_READ_ACP: // "ReadAcp"
|
|||
return s3_constants.S3_ACTION_GET_OBJECT_ACL |
|||
case s3_constants.ACTION_WRITE_ACP: // "WriteAcp"
|
|||
return s3_constants.S3_ACTION_PUT_OBJECT_ACL |
|||
case s3_constants.ACTION_DELETE_BUCKET: // "DeleteBucket"
|
|||
return s3_constants.S3_ACTION_DELETE_BUCKET |
|||
case s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION: |
|||
return s3_constants.S3_ACTION_BYPASS_GOVERNANCE |
|||
case s3_constants.ACTION_GET_OBJECT_RETENTION: |
|||
return s3_constants.S3_ACTION_GET_OBJECT_RETENTION |
|||
case s3_constants.ACTION_PUT_OBJECT_RETENTION: |
|||
return s3_constants.S3_ACTION_PUT_OBJECT_RETENTION |
|||
case s3_constants.ACTION_GET_OBJECT_LEGAL_HOLD: |
|||
return s3_constants.S3_ACTION_GET_OBJECT_LEGAL_HOLD |
|||
case s3_constants.ACTION_PUT_OBJECT_LEGAL_HOLD: |
|||
return s3_constants.S3_ACTION_PUT_OBJECT_LEGAL_HOLD |
|||
case s3_constants.ACTION_GET_BUCKET_OBJECT_LOCK_CONFIG: |
|||
return s3_constants.S3_ACTION_GET_BUCKET_OBJECT_LOCK |
|||
case s3_constants.ACTION_PUT_BUCKET_OBJECT_LOCK_CONFIG: |
|||
return s3_constants.S3_ACTION_PUT_BUCKET_OBJECT_LOCK |
|||
default: |
|||
// For unknown actions, prefix with s3: to maintain format consistency
|
|||
return "s3:" + baseAction |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,85 @@ |
|||
package s3_constants |
|||
|
|||
// S3 action strings for bucket policy evaluation
|
|||
// These match the official AWS S3 action format used in IAM and bucket policies
|
|||
const ( |
|||
// Object operations
|
|||
S3_ACTION_GET_OBJECT = "s3:GetObject" |
|||
S3_ACTION_PUT_OBJECT = "s3:PutObject" |
|||
S3_ACTION_DELETE_OBJECT = "s3:DeleteObject" |
|||
S3_ACTION_DELETE_OBJECT_VERSION = "s3:DeleteObjectVersion" |
|||
S3_ACTION_GET_OBJECT_VERSION = "s3:GetObjectVersion" |
|||
|
|||
// Object ACL operations
|
|||
S3_ACTION_GET_OBJECT_ACL = "s3:GetObjectAcl" |
|||
S3_ACTION_PUT_OBJECT_ACL = "s3:PutObjectAcl" |
|||
|
|||
// Object tagging operations
|
|||
S3_ACTION_GET_OBJECT_TAGGING = "s3:GetObjectTagging" |
|||
S3_ACTION_PUT_OBJECT_TAGGING = "s3:PutObjectTagging" |
|||
S3_ACTION_DELETE_OBJECT_TAGGING = "s3:DeleteObjectTagging" |
|||
|
|||
// Object retention and legal hold
|
|||
S3_ACTION_GET_OBJECT_RETENTION = "s3:GetObjectRetention" |
|||
S3_ACTION_PUT_OBJECT_RETENTION = "s3:PutObjectRetention" |
|||
S3_ACTION_GET_OBJECT_LEGAL_HOLD = "s3:GetObjectLegalHold" |
|||
S3_ACTION_PUT_OBJECT_LEGAL_HOLD = "s3:PutObjectLegalHold" |
|||
S3_ACTION_BYPASS_GOVERNANCE = "s3:BypassGovernanceRetention" |
|||
|
|||
// Multipart upload operations
|
|||
S3_ACTION_CREATE_MULTIPART = "s3:CreateMultipartUpload" |
|||
S3_ACTION_UPLOAD_PART = "s3:UploadPart" |
|||
S3_ACTION_COMPLETE_MULTIPART = "s3:CompleteMultipartUpload" |
|||
S3_ACTION_ABORT_MULTIPART = "s3:AbortMultipartUpload" |
|||
S3_ACTION_LIST_PARTS = "s3:ListParts" |
|||
|
|||
// Bucket operations
|
|||
S3_ACTION_CREATE_BUCKET = "s3:CreateBucket" |
|||
S3_ACTION_DELETE_BUCKET = "s3:DeleteBucket" |
|||
S3_ACTION_LIST_BUCKET = "s3:ListBucket" |
|||
S3_ACTION_LIST_BUCKET_VERSIONS = "s3:ListBucketVersions" |
|||
S3_ACTION_LIST_MULTIPART_UPLOADS = "s3:ListMultipartUploads" |
|||
|
|||
// Bucket ACL operations
|
|||
S3_ACTION_GET_BUCKET_ACL = "s3:GetBucketAcl" |
|||
S3_ACTION_PUT_BUCKET_ACL = "s3:PutBucketAcl" |
|||
|
|||
// Bucket policy operations
|
|||
S3_ACTION_GET_BUCKET_POLICY = "s3:GetBucketPolicy" |
|||
S3_ACTION_PUT_BUCKET_POLICY = "s3:PutBucketPolicy" |
|||
S3_ACTION_DELETE_BUCKET_POLICY = "s3:DeleteBucketPolicy" |
|||
|
|||
// Bucket tagging operations
|
|||
S3_ACTION_GET_BUCKET_TAGGING = "s3:GetBucketTagging" |
|||
S3_ACTION_PUT_BUCKET_TAGGING = "s3:PutBucketTagging" |
|||
S3_ACTION_DELETE_BUCKET_TAGGING = "s3:DeleteBucketTagging" |
|||
|
|||
// Bucket CORS operations
|
|||
S3_ACTION_GET_BUCKET_CORS = "s3:GetBucketCors" |
|||
S3_ACTION_PUT_BUCKET_CORS = "s3:PutBucketCors" |
|||
S3_ACTION_DELETE_BUCKET_CORS = "s3:DeleteBucketCors" |
|||
|
|||
// Bucket lifecycle operations
|
|||
S3_ACTION_GET_BUCKET_LIFECYCLE = "s3:GetBucketLifecycleConfiguration" |
|||
S3_ACTION_PUT_BUCKET_LIFECYCLE = "s3:PutBucketLifecycleConfiguration" |
|||
S3_ACTION_DELETE_BUCKET_LIFECYCLE = "s3:DeleteBucketLifecycle" |
|||
|
|||
// Bucket versioning operations
|
|||
S3_ACTION_GET_BUCKET_VERSIONING = "s3:GetBucketVersioning" |
|||
S3_ACTION_PUT_BUCKET_VERSIONING = "s3:PutBucketVersioning" |
|||
|
|||
// Bucket location
|
|||
S3_ACTION_GET_BUCKET_LOCATION = "s3:GetBucketLocation" |
|||
|
|||
// Bucket notification
|
|||
S3_ACTION_GET_BUCKET_NOTIFICATION = "s3:GetBucketNotification" |
|||
S3_ACTION_PUT_BUCKET_NOTIFICATION = "s3:PutBucketNotification" |
|||
|
|||
// Bucket object lock operations
|
|||
S3_ACTION_GET_BUCKET_OBJECT_LOCK = "s3:GetBucketObjectLockConfiguration" |
|||
S3_ACTION_PUT_BUCKET_OBJECT_LOCK = "s3:PutBucketObjectLockConfiguration" |
|||
|
|||
// Wildcard for all S3 actions
|
|||
S3_ACTION_ALL = "s3:*" |
|||
) |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue