mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-02 18:22:40 +01:00
* s3api: hash an s3vectors body up to the Vectors API limit SigV4 verification of a non-S3 request hashed at most 10 MiB of the body and silently dropped the rest, so any S3 Vectors request over 10 MiB failed its signature check with 403, although the API accepts 32 MiB. The limit now follows the signing service, and a body over it is reported as ErrEntityTooLarge instead of being truncated; the embedded IAM answers that with InvalidInput, as it already does for a body over its own limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3vectors: answer an oversized request with a ValidationException A body over 32 MiB was refused as "403 AccessDenied: authentication failed". A declared Content-Length over the limit is now refused before authentication, and a streamed body over it, whether the signature check or the handler reads it, gets the same "request body exceeds 32 MiB" ValidationException. The limit is the one SigV4 verification uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3vectors: log engine and catalog failures at the default level An engine error reached the client as a bare "internal error", and the cause was logged only with -v=1, so a misconfigured engine left nothing in a default log. Engine and catalog failures are now logged as errors, and a reported unavailability as a warning, with the operation, bucket and index. The response still leaves the engine's message out, since it can name internal hosts and paths, and points at the gateway log instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3api: hash the signed body through readRequestBody streamHashRequestBody repeated readRequestBody's read-past-the-limit check; it now reads the body with readRequestBody and hashes the bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.6 KiB
Go
96 lines
3.6 KiB
Go
package s3vectors
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
type fakeAuthenticator struct {
|
|
identity *s3api.Identity
|
|
errCode s3err.ErrorCode
|
|
calls int
|
|
}
|
|
|
|
func (f *fakeAuthenticator) AuthenticateRequest(r *http.Request) (string, interface{}, s3err.ErrorCode) {
|
|
f.calls++
|
|
if f.errCode != s3err.ErrNone {
|
|
return "", nil, f.errCode
|
|
}
|
|
return f.identity.Name, f.identity, s3err.ErrNone
|
|
}
|
|
|
|
func (f *fakeAuthenticator) DefaultAllow() bool { return true }
|
|
|
|
func TestAuthKeepsIdentityObject(t *testing.T) {
|
|
want := &s3api.Identity{Name: "alice", Actions: []s3api.Action{"Admin"}}
|
|
s := &Server{authenticator: &fakeAuthenticator{identity: want}}
|
|
var got interface{}
|
|
h := s.Auth(func(w http.ResponseWriter, r *http.Request) {
|
|
got = s3_constants.GetIdentityFromContext(r)
|
|
if name := s3_constants.GetIdentityNameFromContext(r); name != want.Name {
|
|
t.Errorf("identity name = %q, want %q", name, want.Name)
|
|
}
|
|
})
|
|
h(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/", nil))
|
|
if got != interface{}(want) {
|
|
t.Fatalf("identity object missing from context: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAuthAnswersOversizedBodyWithValidationException(t *testing.T) {
|
|
alice := &s3api.Identity{Name: "alice", Actions: []s3api.Action{"Admin"}}
|
|
for _, tc := range []struct {
|
|
name string
|
|
contentLength int64
|
|
errCode s3err.ErrorCode
|
|
wantStatus int
|
|
wantCode string
|
|
wantAuthCalls int
|
|
}{
|
|
{"declared length over the limit", maxRequestBodyBytes + 1, s3err.ErrNone, http.StatusBadRequest, "ValidationException", 0},
|
|
{"streamed body over the limit", -1, s3err.ErrEntityTooLarge, http.StatusBadRequest, "ValidationException", 1},
|
|
{"bad signature", 10, s3err.ErrSignatureDoesNotMatch, http.StatusForbidden, "AccessDenied", 1},
|
|
{"declared length at the limit", maxRequestBodyBytes, s3err.ErrNone, http.StatusOK, "", 1},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
auth := &fakeAuthenticator{identity: alice, errCode: tc.errCode}
|
|
s := &Server{authenticator: auth}
|
|
req := httptest.NewRequest(http.MethodPost, "/PutVectors", strings.NewReader("{}"))
|
|
req.ContentLength = tc.contentLength
|
|
rec := httptest.NewRecorder()
|
|
s.Auth(func(w http.ResponseWriter, r *http.Request) {})(rec, req)
|
|
|
|
if rec.Code != tc.wantStatus || auth.calls != tc.wantAuthCalls {
|
|
t.Fatalf("status %d after %d auth calls, want %d after %d: %s", rec.Code, auth.calls, tc.wantStatus, tc.wantAuthCalls, rec.Body)
|
|
}
|
|
if tc.wantCode != "" && !strings.Contains(rec.Body.String(), `"code":"`+tc.wantCode+`"`) {
|
|
t.Fatalf("body %s, want code %s", rec.Body, tc.wantCode)
|
|
}
|
|
if tc.wantCode == "ValidationException" && !strings.Contains(rec.Body.String(), "request body exceeds 32 MiB") {
|
|
t.Fatalf("body %s does not name the limit", rec.Body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReadJSONRefusesBodyOverTheLimit(t *testing.T) {
|
|
var req PutVectorsRequest
|
|
over := httptest.NewRequest(http.MethodPost, "/PutVectors", io.MultiReader(
|
|
strings.NewReader(`{"indexName":"`), strings.NewReader(strings.Repeat("a", maxRequestBodyBytes)), strings.NewReader(`"}`)))
|
|
if err := readJSON(over, &req); !errors.Is(err, errRequestBodyTooLarge) {
|
|
t.Fatalf("readJSON over the limit = %v, want %v", err, errRequestBodyTooLarge)
|
|
}
|
|
within := httptest.NewRequest(http.MethodPost, "/PutVectors", strings.NewReader(`{"indexName":"idx"}`))
|
|
if err := readJSON(within, &req); err != nil || req.IndexName != "idx" {
|
|
t.Fatalf("readJSON within the limit = %v, indexName %q", err, req.IndexName)
|
|
}
|
|
}
|