Files
seaweedfs/weed/s3api/s3vectors/auth_test.go
T
Chris LuandClaude Opus 5.5 d431c1b35a s3api: let the vectors route, not the credential scope, raise the body limit (#12326)
SigV4 verification picked the 32 MiB S3 Vectors body limit from the
credential scope, which is not verified until the body is hashed. A request
claiming the s3vectors scope on any other route could make the gateway buffer
32 MiB before authentication instead of 10 MiB. The vectors handler now sets
the limit on the request context, and every other route keeps 10 MiB.


Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 01:15:42 -08:00

101 lines
3.9 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
bodyLimit int64
}
func (f *fakeAuthenticator) AuthenticateRequest(r *http.Request) (string, interface{}, s3err.ErrorCode) {
f.calls++
f.bodyLimit = s3_constants.GetRequestBodyLimitFromContext(r, 0)
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 auth.calls > 0 && auth.bodyLimit != maxRequestBodyBytes {
t.Fatalf("signature check reads up to %d bytes, want the route's %d", auth.bodyLimit, maxRequestBodyBytes)
}
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)
}
}