diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 49f2acf87..0cbed72a2 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -529,73 +529,120 @@ func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) htt identity, errCode := iam.authRequest(r, action) glog.V(3).Infof("auth error: %v", errCode) - if errCode == s3err.ErrNone { - // Store the authenticated identity in request context (secure, cannot be spoofed) - if identity != nil && identity.Name != "" { - ctx := s3_constants.SetIdentityNameInContext(r.Context(), identity.Name) - // Also store the full identity object for handlers that need it (e.g., ListBuckets) - // This is especially important for JWT users whose identity is not in the identities list - ctx = s3_constants.SetIdentityInContext(ctx, identity) - r = r.WithContext(ctx) - } + iam.handleAuthResult(w, r, identity, errCode, f) + } +} + +// AuthPostPolicy is a specialized authentication wrapper for PostPolicy requests. +// It allows requests with multipart/form-data to proceed even if classified as Anonymous, +// because the actual authentication (signature verification) for ALL PostPolicy requests is +// performed unconditionally in PostPolicyBucketHandler.doesPolicySignatureMatch(). +// This delegation only defers the initial authentication classification; it does NOT bypass +// signature verification, which is mandatory for all PostPolicy uploads. +func (iam *IdentityAccessManagement) AuthPostPolicy(f http.HandlerFunc, action Action) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !iam.isEnabled() { f(w, r) return } - s3err.WriteErrorResponse(w, r, errCode) + + // Optimization: Use authRequestWithAuthType to avoid re-parsing headers for classification + identity, errCode, authType := iam.authRequestWithAuthType(r, action) + + // Special handling for PostPolicy: if AccessDenied (likely because Anonymous to private bucket) + // AND it looks like a PostPolicy request, allow it to proceed to handler for verification. + if errCode == s3err.ErrAccessDenied { + if authType == authTypeAnonymous && + r.Method == http.MethodPost && + strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") { + + glog.V(3).Infof("Delegating PostPolicy auth to handler") + r.Header.Set(s3_constants.AmzAuthType, "PostPolicy") + f(w, r) + return + } + } + + glog.V(3).Infof("auth error: %v", errCode) + + iam.handleAuthResult(w, r, identity, errCode, f) } } -// check whether the request has valid access keys +func (iam *IdentityAccessManagement) handleAuthResult(w http.ResponseWriter, r *http.Request, identity *Identity, errCode s3err.ErrorCode, f http.HandlerFunc) { + if errCode == s3err.ErrNone { + // Store the authenticated identity in request context (secure, cannot be spoofed) + if identity != nil && identity.Name != "" { + ctx := s3_constants.SetIdentityNameInContext(r.Context(), identity.Name) + // Also store the full identity object for handlers that need it (e.g., ListBuckets) + // This is especially important for JWT users whose identity is not in the identities list + ctx = s3_constants.SetIdentityInContext(ctx, identity) + r = r.WithContext(ctx) + } + f(w, r) + return + } + s3err.WriteErrorResponse(w, r, errCode) +} + +// Wrapper to maintain backward compatibility func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) (*Identity, s3err.ErrorCode) { + identity, err, _ := iam.authRequestWithAuthType(r, action) + return identity, err +} + +// check whether the request has valid access keys +func (iam *IdentityAccessManagement) authRequestWithAuthType(r *http.Request, action Action) (*Identity, s3err.ErrorCode, authType) { var identity *Identity var s3Err s3err.ErrorCode var found bool - var authType string - switch getRequestAuthType(r) { + var amzAuthType string + + reqAuthType := getRequestAuthType(r) + + switch reqAuthType { case authTypeUnknown: glog.V(3).Infof("unknown auth type") r.Header.Set(s3_constants.AmzAuthType, "Unknown") - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType case authTypePresignedV2, authTypeSignedV2: glog.V(3).Infof("v2 auth type") identity, s3Err = iam.isReqAuthenticatedV2(r) - authType = "SigV2" + amzAuthType = "SigV2" case authTypeStreamingSigned, authTypeSigned, authTypePresigned: glog.V(3).Infof("v4 auth type") identity, s3Err = iam.reqSignatureV4Verify(r) - authType = "SigV4" - case authTypePostPolicy: - glog.V(3).Infof("post policy auth type") - r.Header.Set(s3_constants.AmzAuthType, "PostPolicy") - return identity, s3err.ErrNone + amzAuthType = "SigV4" case authTypeStreamingUnsigned: glog.V(3).Infof("unsigned streaming upload") - return identity, s3err.ErrNone + // no amzAuthType set for this case in original code? + // Actually original explicitly returned ErrNone without setting identity + return identity, s3err.ErrNone, reqAuthType case authTypeJWT: glog.V(3).Infof("jwt auth type detected, iamIntegration != nil? %t", iam.iamIntegration != nil) r.Header.Set(s3_constants.AmzAuthType, "Jwt") if iam.iamIntegration != nil { identity, s3Err = iam.authenticateJWTWithIAM(r) - authType = "Jwt" + amzAuthType = "Jwt" } else { glog.V(2).Infof("IAM integration is nil, returning ErrNotImplemented") - return identity, s3err.ErrNotImplemented + return identity, s3err.ErrNotImplemented, reqAuthType } case authTypeAnonymous: - authType = "Anonymous" + amzAuthType = "Anonymous" if identity, found = iam.lookupAnonymous(); !found { - r.Header.Set(s3_constants.AmzAuthType, authType) - return identity, s3err.ErrAccessDenied + r.Header.Set(s3_constants.AmzAuthType, amzAuthType) + return identity, s3err.ErrAccessDenied, reqAuthType } default: - return identity, s3err.ErrNotImplemented + return identity, s3err.ErrNotImplemented, reqAuthType } - if len(authType) > 0 { - r.Header.Set(s3_constants.AmzAuthType, authType) + if len(amzAuthType) > 0 { + r.Header.Set(s3_constants.AmzAuthType, amzAuthType) } if s3Err != s3err.ErrNone { - return identity, s3Err + return identity, s3Err, reqAuthType } glog.V(3).Infof("user name: %v actions: %v, action: %v", identity.Name, identity.Actions, action) @@ -636,7 +683,7 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // SECURITY: Fail-close on policy evaluation errors // If we can't evaluate the policy, deny access rather than falling through to IAM glog.Errorf("Error evaluating bucket policy for %s/%s: %v - denying access", bucket, object, err) - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } else if evaluated { // A bucket policy exists and was evaluated with a matching statement if allowed { @@ -648,7 +695,7 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // Policy explicitly denies this action - deny access immediately // Note: Explicit Deny in bucket policy overrides all other permissions glog.V(3).Infof("Bucket policy explicitly denies %s to %s on %s/%s", identity.Name, action, bucket, object) - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } } // If not evaluated (no policy or no matching statements), fall through to IAM/identity checks @@ -660,21 +707,21 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // JWT/STS identities (no Actions) use IAM authorization if len(identity.Actions) > 0 { if !identity.canDo(action, bucket, object) { - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } } else if iam.iamIntegration != nil { if errCode := iam.authorizeWithIAM(r, identity, action, bucket, object); errCode != s3err.ErrNone { - return identity, errCode + return identity, errCode, reqAuthType } } else { - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } } } r.Header.Set(s3_constants.AmzAccountId, identity.Account.Id) - return identity, s3err.ErrNone + return identity, s3err.ErrNone, reqAuthType } @@ -699,10 +746,7 @@ func (iam *IdentityAccessManagement) AuthSignatureOnly(r *http.Request) (*Identi glog.V(3).Infof("v4 auth type") identity, s3Err = iam.reqSignatureV4Verify(r) authType = "SigV4" - case authTypePostPolicy: - glog.V(3).Infof("post policy auth type") - r.Header.Set(s3_constants.AmzAuthType, "PostPolicy") - return identity, s3err.ErrNone + case authTypeStreamingUnsigned: glog.V(3).Infof("unsigned streaming upload") return identity, s3err.ErrNone diff --git a/weed/s3api/s3api_auth.go b/weed/s3api/s3api_auth.go index 5592fe939..6963373cd 100644 --- a/weed/s3api/s3api_auth.go +++ b/weed/s3api/s3api_auth.go @@ -41,12 +41,6 @@ func isRequestPresignedSignatureV2(r *http.Request) bool { return ok } -// Verify if request has AWS Post policy Signature Version '4'. -func isRequestPostPolicySignatureV4(r *http.Request) bool { - return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && - r.Method == http.MethodPost -} - // Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation. // Supports both with and without trailer variants: // - STREAMING-AWS4-HMAC-SHA256-PAYLOAD (original) @@ -101,8 +95,6 @@ func getRequestAuthType(r *http.Request) authType { authType = authTypePresigned } else if isRequestJWT(r) { authType = authTypeJWT - } else if isRequestPostPolicySignatureV4(r) { - authType = authTypePostPolicy } else if _, ok := r.Header["Authorization"]; !ok { authType = authTypeAnonymous } else { diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index c811d668b..5917b5195 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -573,7 +573,7 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) { // raw buckets // PostPolicy - bucket.Methods(http.MethodPost).HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST")) + bucket.Methods(http.MethodPost).HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.AuthPostPolicy(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST")) // HeadBucket bucket.Methods(http.MethodHead).HandlerFunc(track(s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) {