mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-09 21:38:19 +01:00
* s3vectors: attribute request and traffic metrics to authorized buckets * s3vectors: emit resource and identity audit events * s3vectors: apply existing S3 request limits and preserve JSON errors * s3vectors: select limit resources by the dispatched operation * s3vectors: count decoded chunked request bodies against byte limits
126 lines
4.9 KiB
Go
126 lines
4.9 KiB
Go
package s3vectors
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
func TestVectorLimitsResolveResourcesAndPreserveBodies(t *testing.T) {
|
|
s := NewServer(nil, headerAuth{}, nil)
|
|
s.SetRegion("r")
|
|
s.SetAccountID("a")
|
|
for _, tc := range []struct {
|
|
op, action string
|
|
body interface{}
|
|
}{
|
|
{"PutVectors", s3_constants.ACTION_WRITE, PutVectorsRequest{VectorBucketName: "embeddings", IndexName: "documents", Vectors: []*PutInputVector{{Key: "a", Data: &VectorData{Float32: []float32{1, 2}}}}}},
|
|
{"QueryVectors", s3_constants.ACTION_READ, QueryVectorsRequest{IndexArn: s.indexArn("embeddings", "documents"), TopK: 1}},
|
|
{"GetVectorBucket", s3_constants.ACTION_READ, GetVectorBucketRequest{VectorBucketArn: s.vectorBucketArn("embeddings")}},
|
|
} {
|
|
t.Run(tc.op, func(t *testing.T) {
|
|
payload, err := json.Marshal(tc.body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := httptest.NewRequest(http.MethodPost, "/"+tc.op, bytes.NewReader(payload))
|
|
var called bool
|
|
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if mux.Vars(r)["bucket"] != "embeddings" || action != tc.action {
|
|
t.Fatalf("limit target %v, action %s", mux.Vars(r), action)
|
|
}
|
|
next(w, r)
|
|
}
|
|
})
|
|
s.instrument(func(w http.ResponseWriter, r *http.Request) {
|
|
s.limitRequest(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
var got json.RawMessage
|
|
if err := readJSON(r, &got); err != nil || !bytes.Equal(got, payload) {
|
|
t.Fatalf("handler body %s: %v", got, err)
|
|
}
|
|
}, w, r)
|
|
})(httptest.NewRecorder(), r)
|
|
if !called {
|
|
t.Fatal("limited handler was not called")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVectorLimitsReturnJSONWithoutCallingHandler(t *testing.T) {
|
|
s := NewServer(nil, headerAuth{}, nil)
|
|
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrTooManyRequest)
|
|
}
|
|
})
|
|
r := httptest.NewRequest(http.MethodPost, "/PutVectors", bytes.NewBufferString(`{"vectorBucketName":"embeddings","indexName":"documents"}`))
|
|
w := httptest.NewRecorder()
|
|
s.instrument(func(w http.ResponseWriter, r *http.Request) {
|
|
s.limitRequest(func(http.ResponseWriter, *http.Request) { t.Fatal("rejected handler ran") }, w, r)
|
|
})(w, r)
|
|
var failure Error
|
|
if json.Unmarshal(w.Body.Bytes(), &failure) != nil || failure.Code == "" || w.Code < http.StatusBadRequest || w.Header().Get("Content-Type") != "application/x-amz-json-1.1" {
|
|
t.Fatalf("limit response: %d %v %s", w.Code, w.Header(), w.Body)
|
|
}
|
|
}
|
|
|
|
func TestVectorLimitsIgnoreUnrelatedResourceFields(t *testing.T) {
|
|
s := NewServer(nil, headerAuth{}, nil)
|
|
for _, tc := range []struct{ op, body, bucket string }{
|
|
{"PutVectors", `{"vectorBucketName":"bucket-a","indexName":"idx","resourceArn":"arn:aws:s3vectors:r:a:bucket/bucket-b"}`, "bucket-a"},
|
|
{"PutVectors", `{"vectorBucketName":"bucket-a","indexName":"idx","resourceArn":{"ignored":true}}`, "bucket-a"},
|
|
{"GetVectorBucket", `{"vectorBucketName":"bucket-a","indexArn":"invalid","indexName":42}`, "bucket-a"},
|
|
{"CreateIndex", `{"vectorBucketName":"bucket-a","indexName":"idx","resourceArn":"ignored"}`, "bucket-a"},
|
|
{"ListVectorBuckets", `{"vectorBucketName":"ignored","resourceArn":"ignored"}`, ""},
|
|
} {
|
|
t.Run(tc.op+tc.body, func(t *testing.T) {
|
|
called := false
|
|
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
if got := mux.Vars(r)["bucket"]; got != tc.bucket {
|
|
t.Fatalf("limited bucket %q, want %q", got, tc.bucket)
|
|
}
|
|
next(w, r)
|
|
}
|
|
})
|
|
r := httptest.NewRequest(http.MethodPost, "/"+tc.op, bytes.NewBufferString(tc.body))
|
|
s.instrument(func(w http.ResponseWriter, r *http.Request) {
|
|
s.limitRequest(func(http.ResponseWriter, *http.Request) {}, w, r)
|
|
})(httptest.NewRecorder(), r)
|
|
if !called {
|
|
t.Fatal("limiter was not invoked")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVectorLimitsCountChunkedBodyBytes(t *testing.T) {
|
|
s := NewServer(nil, headerAuth{}, nil)
|
|
body := `{"vectorBucketName":"embeddings","indexName":"documents"}`
|
|
var got int64
|
|
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) { got = r.ContentLength; next(w, r) }
|
|
})
|
|
r := httptest.NewRequest(http.MethodPost, "/PutVectors", bytes.NewBufferString(body))
|
|
r.ContentLength = -1
|
|
s.instrument(func(w http.ResponseWriter, r *http.Request) {
|
|
s.limitRequest(func(http.ResponseWriter, *http.Request) {}, w, r)
|
|
})(httptest.NewRecorder(), r)
|
|
if got != int64(len(body)) {
|
|
t.Fatalf("limited body length = %d, want %d", got, len(body))
|
|
}
|
|
if r.ContentLength != -1 {
|
|
t.Fatal("mutated original request")
|
|
}
|
|
}
|