diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 1cd3268edd..7dc76ada1c 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -126,6 +126,27 @@ func normalizePayloadHash(payloadHashValue string) string { return payloadHashValue } +// requestSigningService extracts the service name from the SigV4 credential +// scope (…/region/service/aws4_request) so a failed request can be answered in +// that service's error envelope. +func requestSigningService(r *http.Request) string { + credential := r.URL.Query().Get("X-Amz-Credential") + if credential == "" { + v4Auth := strings.Replace(r.Header.Get("Authorization"), " ", "", -1) + v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm) + for _, field := range strings.Split(v4Auth, ",") { + if cred, ok := strings.CutPrefix(field, "Credential="); ok { + credential = cred + break + } + } + } + if parts := strings.Split(credential, "/"); len(parts) == 5 { + return parts[3] + } + return "" +} + // signValues data type represents structured form of AWS Signature V4 header. type signValues struct { Credential credentialHeader diff --git a/weed/s3api/s3api_embedded_iam.go b/weed/s3api/s3api_embedded_iam.go index 055754384e..bd1ea53354 100644 --- a/weed/s3api/s3api_embedded_iam.go +++ b/weed/s3api/s3api_embedded_iam.go @@ -196,10 +196,32 @@ func newIamErrorResponse(errCode string, errMsg string, requestID string) iamErr return errorResp } -func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Request, reqID string, iamErr *iamError) { +// iamAuthErrorCode maps an S3-side signature failure onto the IAM +// query-protocol code IAM clients expect for the same request. +func iamAuthErrorCode(errCode s3err.ErrorCode) string { + switch errCode { + case s3err.ErrInvalidAccessKeyID: + return "InvalidClientTokenId" + case s3err.ErrSignatureDoesNotMatch: + return "SignatureDoesNotMatch" + case s3err.ErrAccessDenied: + return "AccessDenied" + case s3err.ErrExpiredToken: + return "ExpiredToken" + case s3err.ErrRequestTimeTooSkewed: + return "RequestTimeTooSkewed" + case s3err.ErrInternalError: + return iam.ErrCodeServiceFailureException + default: + return "IncompleteSignature" + } +} + +func writeIamErrorResponse(w http.ResponseWriter, r *http.Request, reqID string, iamErr *iamError) { if iamErr == nil { glog.Errorf("writeIamErrorResponse called with nil error") internalResp := newIamErrorResponse(iam.ErrCodeServiceFailureException, "Internal server error", reqID) + internalResp.Error.Type = "Receiver" s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalResp) return } @@ -2562,15 +2584,19 @@ func (iam *IdentityAccessManagement) AuthIamManagement(f http.HandlerFunc) http. // needs to hash the body for IAM requests (service != "s3"). // The streamHashRequestBody function in auth_signature_v4.go preserves the body // after reading it, so ParseForm() will work correctly after authentication. + r, reqID := request_id.Ensure(r) identity, errCode := iam.AuthSignatureOnly(r) if errCode != s3err.ErrNone { - s3err.WriteErrorResponse(w, r, errCode) + writeIamErrorResponse(w, r, reqID, &iamError{ + Code: iamAuthErrorCode(errCode), + Error: errors.New(s3err.GetAPIError(errCode).Description), + }) return } // Now parse form to get Action and UserName (body was preserved by auth) if err := r.ParseForm(); err != nil { - s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) + writeIamErrorResponse(w, r, reqID, &iamError{Code: "InvalidInput", Error: err}) return } @@ -2578,7 +2604,10 @@ func (iam *IdentityAccessManagement) AuthIamManagement(f http.HandlerFunc) http. // from, so the authorized target and the acted-on target cannot differ. action := r.Form.Get("Action") if errCode := iam.AuthorizeIamAction(r, identity, action, iamTargetUserName(action, r)); errCode != s3err.ErrNone { - s3err.WriteErrorResponse(w, r, errCode) + writeIamErrorResponse(w, r, reqID, &iamError{ + Code: iamAuthErrorCode(errCode), + Error: errors.New(s3err.GetAPIError(errCode).Description), + }) return } @@ -3003,7 +3032,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) { r, reqID := request_id.Ensure(r) if err := r.ParseForm(); err != nil { - s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) + writeIamErrorResponse(w, r, reqID, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}) return } values := r.PostForm @@ -3019,7 +3048,7 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) { response, iamErr := e.ExecuteAction(r.Context(), values, false, reqID) if iamErr != nil { - e.writeIamErrorResponse(w, r, reqID, iamErr) + writeIamErrorResponse(w, r, reqID, iamErr) return } diff --git a/weed/s3api/s3api_iam_oidc_test.go b/weed/s3api/s3api_iam_oidc_test.go index 70807ea808..0889885ce4 100644 --- a/weed/s3api/s3api_iam_oidc_test.go +++ b/weed/s3api/s3api_iam_oidc_test.go @@ -402,9 +402,8 @@ func TestUpdateThumbprintAndTags(t *testing.T) { // A refusal to change a config-file provider reaches the client as AWS sends // it (400 UnmodifiableEntity), not as an internal error clients retry. func TestUnmodifiableEntityIsAClientError(t *testing.T) { - api := NewEmbeddedIamApiForTest() rec := httptest.NewRecorder() - api.writeIamErrorResponse(rec, httptest.NewRequest(http.MethodPost, "/", nil), "req-1", + writeIamErrorResponse(rec, httptest.NewRequest(http.MethodPost, "/", nil), "req-1", oidcMutationError(fmt.Errorf("%w: arn:aws:iam:::oidc-provider/static.example", integration.ErrOIDCProviderStatic))) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index b5c9305bcd..920d8fb47d 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "math" @@ -730,10 +731,21 @@ func (s3a *S3ApiServer) handleCORSOriginValidation(w http.ResponseWriter, r *htt // UnifiedPostHandler handles authenticated POST requests to the root path // It inspects the Action parameter to dispatch to either STS or IAM handlers func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Request) { + // Failures on this route answer in the IAM/STS query-protocol envelope + // unless the request was actually signed for s3. + r, reqID := request_id.Ensure(r) + writeQueryError := func(iamCode string, s3Code s3err.ErrorCode) { + if requestSigningService(r) == "s3" { + s3err.WriteErrorResponse(w, r, s3Code) + return + } + writeIamErrorResponse(w, r, reqID, &iamError{Code: iamCode, Error: errors.New(s3err.GetAPIError(s3Code).Description)}) + } + // 1. Authenticate (preserves body) identity, errCode := s3a.iam.AuthSignatureOnly(r) if errCode != s3err.ErrNone { - s3err.WriteErrorResponse(w, r, errCode) + writeQueryError(iamAuthErrorCode(errCode), errCode) return } @@ -745,7 +757,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques bodyBytes, err = readRequestBody(r, iamRequestBodyLimit) if err != nil { glog.Errorf("failed to read request body: %v", err) - s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) + writeQueryError("InvalidInput", s3err.ErrInvalidRequest) return } r.Body.Close() @@ -754,7 +766,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques } if err := r.ParseForm(); err != nil { - s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) + writeQueryError("InvalidInput", s3err.ErrInvalidRequest) return } @@ -781,7 +793,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques // IAM // IAM API requests must be authenticated - reject nil identity if identity == nil { - s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) + writeQueryError("AccessDenied", s3err.ErrAccessDenied) return } @@ -792,7 +804,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques // UserName comes from the body only, the same place DoActions reads it // from, so the authorized target and the acted-on target cannot differ. if s3a.iam.AuthorizeIamAction(r, identity, action, iamTargetUserName(action, r)) != s3err.ErrNone { - s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) + writeQueryError("AccessDenied", s3err.ErrAccessDenied) return }