diff options
Diffstat (limited to 'weed/s3api/s3_action_resolver.go')
| -rw-r--r-- | weed/s3api/s3_action_resolver.go | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/weed/s3api/s3_action_resolver.go b/weed/s3api/s3_action_resolver.go new file mode 100644 index 000000000..83431424c --- /dev/null +++ b/weed/s3api/s3_action_resolver.go @@ -0,0 +1,334 @@ +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") +// - Falls back to base action mapping if no specific resolution is possible +// - Always returns a valid S3 action string (never empty) +func ResolveS3Action(r *http.Request, baseAction string, bucket string, object string) string { + if r == nil || r.URL == nil { + // No HTTP context available: fall back to coarse-grained mapping + // This ensures consistent behavior and avoids returning empty strings + return mapBaseActionToS3Format(baseAction) + } + + 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 + // Only use the result if a specific action was resolved; otherwise fall through to Priority 3 + if hasObject { + if action := resolveObjectLevelAction(method, baseAction); action != "" { + return action + } + } else if bucket != "" { + if action := resolveBucketLevelAction(method, baseAction); action != "" { + return action + } + } + + // Priority 3: Fallback to legacy action mapping + 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_PUT_BUCKET_LIFECYCLE, // DELETE uses same permission as PUT + }, + "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 with uploadId parameter (object-level only) + // All multipart operations require an object in the path + if hasObject && query.Has("uploadId") { + switch method { + case http.MethodPut: + if query.Has("partNumber") { + return s3_constants.S3_ACTION_UPLOAD_PART + } + 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 + } + } + + // Multipart upload operations + // CreateMultipartUpload: POST /bucket/object?uploads (object-level) + // ListMultipartUploads: GET /bucket?uploads (bucket-level) + if query.Has("uploads") { + if method == http.MethodPost && hasObject { + return s3_constants.S3_ACTION_CREATE_MULTIPART + } else if method == http.MethodGet && !hasObject { + return s3_constants.S3_ACTION_LIST_MULTIPART_UPLOADS + } + } + + // ACL operations + if query.Has("acl") { + switch method { + case http.MethodGet, http.MethodHead: + if hasObject { + return s3_constants.S3_ACTION_GET_OBJECT_ACL + } + return s3_constants.S3_ACTION_GET_BUCKET_ACL + case http.MethodPut: + if hasObject { + return s3_constants.S3_ACTION_PUT_OBJECT_ACL + } + return s3_constants.S3_ACTION_PUT_BUCKET_ACL + } + } + + // Tagging operations + if query.Has("tagging") { + switch method { + case http.MethodGet: + if hasObject { + return s3_constants.S3_ACTION_GET_OBJECT_TAGGING + } + return s3_constants.S3_ACTION_GET_BUCKET_TAGGING + case http.MethodPut: + if hasObject { + return s3_constants.S3_ACTION_PUT_OBJECT_TAGGING + } + return s3_constants.S3_ACTION_PUT_BUCKET_TAGGING + case http.MethodDelete: + if hasObject { + return s3_constants.S3_ACTION_DELETE_OBJECT_TAGGING + } + return s3_constants.S3_ACTION_DELETE_BUCKET_TAGGING + } + } + + // Versioning operations - distinguish between versionId (specific version) and versions (list versions) + // versionId: Used to access/delete a specific version of an object (e.g., GET /bucket/key?versionId=xyz) + if query.Has("versionId") { + if hasObject { + switch method { + case http.MethodGet, http.MethodHead: + return s3_constants.S3_ACTION_GET_OBJECT_VERSION + case http.MethodDelete: + return s3_constants.S3_ACTION_DELETE_OBJECT_VERSION + } + } + } + + // versions: Used to list all versions of objects in a bucket (e.g., GET /bucket?versions) + if query.Has("versions") { + if method == http.MethodGet && !hasObject { + return s3_constants.S3_ACTION_LIST_BUCKET_VERSIONS + } + } + + // Check bucket-level query parameters using data-driven approach + // These are strictly bucket-level operations, so only apply when !hasObject + if !hasObject { + for param, actions := range bucketQueryActions { + if query.Has(param) { + if action, ok := actions[method]; ok { + return action + } + } + } + } + + // Location (GET only, bucket-level) + if query.Has("location") && method == http.MethodGet && !hasObject { + return s3_constants.S3_ACTION_GET_BUCKET_LOCATION + } + + // Object retention and legal hold operations (object-level only) + if hasObject { + if query.Has("retention") { + switch method { + case http.MethodGet: + return s3_constants.S3_ACTION_GET_OBJECT_RETENTION + case http.MethodPut: + return s3_constants.S3_ACTION_PUT_OBJECT_RETENTION + } + } + + if query.Has("legal-hold") { + switch method { + case http.MethodGet: + return s3_constants.S3_ACTION_GET_OBJECT_LEGAL_HOLD + case http.MethodPut: + return s3_constants.S3_ACTION_PUT_OBJECT_LEGAL_HOLD + } + } + } + + // Batch delete - POST request with delete query parameter (bucket-level operation) + // Example: POST /bucket?delete (not POST /bucket/object?delete) + if query.Has("delete") && method == http.MethodPost && !hasObject { + return s3_constants.S3_ACTION_DELETE_OBJECT + } + + return "" +} + +// resolveObjectLevelAction determines the S3 action for object-level operations +func resolveObjectLevelAction(method string, baseAction string) 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 { + // Note: CopyObject operations also use s3:PutObject permission (same as MinIO/AWS) + // Copy requires s3:PutObject on destination and s3:GetObject on source + 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 || 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 + } +} |
