mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-12 01:07:35 +02:00
* s3: require a bucket-policy action to write a bucket policy PutBucketPolicy and DeleteBucketPolicy were gated on ACTION_WRITE, the same action that grants object writes. An explicit Allow in a bucket policy short-circuits IAM entirely -- authRequestWithAuthType sets policyAllows and skips VerifyActionPermission -- so anyone who could write an object could author a policy granting itself, or anonymous, anything on the bucket. That is what separates a bucket policy from the sibling bucket controls also gated on ACTION_WRITE: rewriting cors or lifecycle can destroy data, but only a policy hands out access. Give the two verbs their own actions, mapped to the AWS names that were already defined but unrouted. ACTION_ADMIN would also have closed it, but it resolves to s3:* for IAM identities, forcing a blanket grant on a user holding a precise s3:PutBucketPolicy. Admins are unaffected, since isAdmin short-circuits CanDo, and an operator can delegate with PutBucketPolicy:bucket. The route binding is asserted from the router source: checking the action constants alone still passes when the route says ACTION_WRITE. * s3: also read the action from a direct iam.Auth call in the route test Routes read iam.Auth(cb.Limit(handler, ACTION)), a multi-value pass-through: Limit returns (http.HandlerFunc, Action) and those become Auth's parameters, so the action Auth authorizes on is Limit's second argument and the two cannot disagree -- Auth(Limit(h, X), Y) does not compile. A route that skipped Limit and called Auth with its own action would compile, though, and the test reported that as a missing route rather than as the wrong action. Recognise the two-argument Auth form so it names the action instead. * s3: make the bucket-policy actions grantable through an IAM policy The new actions close the escalation only if an operator can grant them, and they were not reachable: MapToStatementAction had no entry for PutBucketPolicy, so an IAM policy naming s3:PutBucketPolicy was rejected outright with "not a valid action". GetBucketPolicy was unmapped the same way. DeleteBucketPolicy was mapped, but to ACTION_ADMIN -- granting an identity permission to delete a bucket policy handed it full administrative access. Map all three to the actions the router now uses, and add the reverse direction so an identity holding them renders back as a policy statement instead of a bare "s3:". * admin: offer the bucket-policy permissions in the user editor The two new actions are otherwise only grantable by hand-editing identity JSON or by calling the IAM API, so an operator using the UI cannot delegate bucket policy management without granting Admin. Regenerating this file also picks up codegen the repo has not taken yet: the checked-in _templ.go files were produced by templ v0.3.1001 while go.mod pins v0.3.1020, so the generator rewrites the attribute-value calls. That churn is confined to this one file; running `make generate` in weed/admin reproduces it across all 36.
346 lines
12 KiB
Go
346 lines
12 KiB
Go
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
|
|
}
|
|
}
|
|
|
|
// GetObjectAttributes (object-level only)
|
|
// Must be checked before versionId, because GET /bucket/key?attributes&versionId=xyz
|
|
// is a GetObjectAttributes request, not a GetObjectVersion request
|
|
if hasObject && query.Has("attributes") && method == http.MethodGet {
|
|
return s3_constants.S3_ACTION_GET_OBJECT_ATTRIBUTES
|
|
}
|
|
|
|
// 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 a known service prefix
|
|
if strings.HasPrefix(baseAction, "s3:") || strings.HasPrefix(baseAction, "iam:") || strings.HasPrefix(baseAction, "sts:") {
|
|
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
|
|
case s3_constants.ACTION_PUT_BUCKET_POLICY:
|
|
return s3_constants.S3_ACTION_PUT_BUCKET_POLICY
|
|
case s3_constants.ACTION_DELETE_BUCKET_POLICY:
|
|
return s3_constants.S3_ACTION_DELETE_BUCKET_POLICY
|
|
default:
|
|
// For unknown actions, prefix with s3: to maintain format consistency
|
|
return "s3:" + baseAction
|
|
}
|
|
}
|