Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
531 lines
19 KiB
Go
531 lines
19 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
)
|
|
|
|
const (
|
|
proxyTestWriteKey = "cluster-write-key"
|
|
proxyTestReadKey = "cluster-read-key"
|
|
proxyTestVid = "3"
|
|
proxyTestFid = "01637037d6"
|
|
proxyTestFileId = proxyTestVid + "," + proxyTestFid
|
|
)
|
|
|
|
// proxyTestVolume is a stand-in volume server that records what the filer
|
|
// actually sent. Recording arrival separately from the header is what keeps the
|
|
// negative assertions honest: an absent Authorization and a request that never
|
|
// left the filer are otherwise indistinguishable.
|
|
type proxyTestVolume struct {
|
|
*httptest.Server
|
|
hits atomic.Int32
|
|
auth atomic.Value // string
|
|
effectiveJwt atomic.Value // string
|
|
rawQuery atomic.Value // string
|
|
}
|
|
|
|
func newProxyTestVolume(t *testing.T) *proxyTestVolume {
|
|
t.Helper()
|
|
v := &proxyTestVolume{}
|
|
v.auth.Store("")
|
|
v.effectiveJwt.Store("")
|
|
v.rawQuery.Store("")
|
|
v.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
v.hits.Add(1)
|
|
v.auth.Store(r.Header.Get("Authorization"))
|
|
v.rawQuery.Store(r.URL.RawQuery)
|
|
// The credential the volume server would actually evaluate, which is not
|
|
// necessarily the Authorization header.
|
|
v.effectiveJwt.Store(string(security.GetJwt(r)))
|
|
}))
|
|
t.Cleanup(func() {
|
|
v.Close()
|
|
// proxyToVolumeServerURL keys the semaphore map by host, and every
|
|
// httptest server binds a fresh port; drop ours so -count=N runs do not
|
|
// grow the map without bound.
|
|
if u, err := url.Parse(v.URL); err == nil {
|
|
proxySemaphores.Delete(u.Host)
|
|
}
|
|
})
|
|
return v
|
|
}
|
|
|
|
func (v *proxyTestVolume) seenAuth() string { return v.auth.Load().(string) }
|
|
|
|
// seenEffectiveJwt is the token the volume server would validate, resolved the
|
|
// same way VolumeServer.maybeCheckJwtAuthorization resolves it.
|
|
func (v *proxyTestVolume) seenEffectiveJwt() string { return v.effectiveJwt.Load().(string) }
|
|
|
|
func (v *proxyTestVolume) seenRawQuery() string { return v.rawQuery.Load().(string) }
|
|
|
|
func (v *proxyTestVolume) requireReached(t *testing.T) {
|
|
t.Helper()
|
|
if v.hits.Load() == 0 {
|
|
t.Fatal("request never reached the volume server, so the assertion below proves nothing")
|
|
}
|
|
}
|
|
|
|
// security.GetJwt reads the "jwt" query parameter before the Authorization
|
|
// header, so a caller-supplied one would outrank the token the filer attaches
|
|
// on a read -- the credential the volume server evaluates has to be the filer's.
|
|
func TestProxyReadDropsCallerJwtQueryParam(t *testing.T) {
|
|
for _, method := range []string{http.MethodGet, http.MethodHead} {
|
|
t.Run(method, func(t *testing.T) {
|
|
volume := newProxyTestVolume(t)
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
|
|
r := httptest.NewRequest(method,
|
|
"http://filer:8888/?proxyChunkId="+proxyTestFileId+"&jwt=caller-supplied&readDeleted=true", nil)
|
|
fs.proxyToVolumeServerURL(httptest.NewRecorder(), r, proxyTestFileId, volume.URL+"/"+proxyTestFileId)
|
|
|
|
volume.requireReached(t)
|
|
if got := volume.seenEffectiveJwt(); got == "caller-supplied" {
|
|
t.Fatal("caller's jwt query param outranked the filer-minted token")
|
|
}
|
|
// The credential has to be a filer-minted read token for THIS file.
|
|
// Comparing it against a separately minted token would also say that,
|
|
// but only within the second that minted both: the expiry claim has
|
|
// one-second resolution, so two mints either side of a tick differ in
|
|
// the encoded string while carrying the same authority and file id.
|
|
claims := &security.SeaweedFileIdClaims{}
|
|
if _, err := security.DecodeJwt(security.SigningKey(proxyTestReadKey),
|
|
security.EncodedJwt(volume.seenEffectiveJwt()), claims); err != nil {
|
|
t.Fatalf("volume server would evaluate %q, which does not validate against the read key: %v",
|
|
volume.seenEffectiveJwt(), err)
|
|
}
|
|
if claims.Fid != proxyTestFileId {
|
|
t.Fatalf("token authorizes file %q, want %q", claims.Fid, proxyTestFileId)
|
|
}
|
|
if q := volume.seenRawQuery(); strings.Contains(q, "jwt=") {
|
|
t.Fatalf("jwt survived in the forwarded query: %q", q)
|
|
}
|
|
// Unrelated params must still be forwarded.
|
|
if q := volume.seenRawQuery(); !strings.Contains(q, "readDeleted=true") {
|
|
t.Fatalf("readDeleted was dropped from the forwarded query: %q", q)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// On a write the query parameter carries the filer credential that got the
|
|
// caller past the gate. Relaying it would hand a volume server a filer token
|
|
// and, since security.GetJwt reads it first, hide the minted volume token
|
|
// behind one the volume server cannot validate.
|
|
func TestProxyWriteDropsCallerJwtQueryParam(t *testing.T) {
|
|
volume := newProxyTestVolume(t)
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
|
|
r := httptest.NewRequest(http.MethodPost,
|
|
"http://filer:8888/?proxyChunkId="+proxyTestFileId+"&jwt=filer-credential", nil)
|
|
fs.proxyToVolumeServerURL(httptest.NewRecorder(), r, proxyTestFileId, volume.URL+"/"+proxyTestFileId)
|
|
|
|
volume.requireReached(t)
|
|
if q := volume.seenRawQuery(); strings.Contains(q, "jwt=") {
|
|
t.Fatalf("filer credential survived in the forwarded query: %q", q)
|
|
}
|
|
claims := &security.SeaweedFileIdClaims{}
|
|
if _, err := security.DecodeJwt(security.SigningKey(proxyTestWriteKey),
|
|
security.EncodedJwt(volume.seenEffectiveJwt()), claims); err != nil {
|
|
t.Fatalf("volume server would evaluate %q, which does not validate against the write key: %v",
|
|
volume.seenEffectiveJwt(), err)
|
|
}
|
|
}
|
|
|
|
// The token is minted for the caller only once the filer has authorized them,
|
|
// so a key that is not configured means no token: on a read that is the write
|
|
// key, which would hand out more authority than the read needs.
|
|
func TestProxyMintsNothingWithoutKey(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
isWrite bool
|
|
writeKey string
|
|
readKey string
|
|
}{
|
|
{"read with only a write key", false, proxyTestWriteKey, ""},
|
|
{"write with only a read key", true, "", proxyTestReadKey},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, tc.writeKey, 10, tc.readKey, 10)}
|
|
|
|
if jwt := fs.maybeGetVolumeJwtAuthorizationToken(proxyTestFileId, tc.isWrite); jwt != "" {
|
|
t.Fatalf("minted %q with no key configured for that access level", jwt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A configured read key still yields a read token, and it stays read-only.
|
|
func TestProxyReadTokenIsReadOnly(t *testing.T) {
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
|
|
jwt := fs.maybeGetVolumeReadJwtAuthorizationToken(proxyTestFileId)
|
|
if jwt == "" {
|
|
t.Fatal("no read token minted despite a configured read key")
|
|
}
|
|
|
|
vs := &VolumeServer{guard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
|
|
read := httptest.NewRequest(http.MethodGet, "http://volume:8080/"+proxyTestFileId, nil)
|
|
read.Header.Set("Authorization", security.BearerPrefix+jwt)
|
|
if !vs.maybeCheckJwtAuthorization(read, proxyTestVid, proxyTestFid, false) {
|
|
t.Fatal("read token rejected on a read")
|
|
}
|
|
|
|
write := httptest.NewRequest(http.MethodDelete, "http://volume:8080/"+proxyTestFileId, nil)
|
|
write.Header.Set("Authorization", security.BearerPrefix+jwt)
|
|
if vs.maybeCheckJwtAuthorization(write, proxyTestVid, proxyTestFid, true) {
|
|
t.Fatal("read token authorized a write")
|
|
}
|
|
}
|
|
|
|
// A proxied write reaches the volume server on a token the filer minted for
|
|
// this file id, never on the caller's own Authorization -- that one is the
|
|
// filer credential the caller was authorized with. POST is the method every
|
|
// in-tree proxied uploader actually sends, so it leads the table.
|
|
func TestProxyWriteCarriesMintedWriteToken(t *testing.T) {
|
|
vs := &VolumeServer{guard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
|
|
for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete} {
|
|
t.Run(method, func(t *testing.T) {
|
|
volume := newProxyTestVolume(t)
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
|
|
r := httptest.NewRequest(method, "http://filer:8888/?proxyChunkId="+proxyTestFileId, nil)
|
|
r.Header.Set("Authorization", security.BearerPrefix+"filer-credential")
|
|
fs.proxyToVolumeServerURL(httptest.NewRecorder(), r, proxyTestFileId, volume.URL+"/"+proxyTestFileId)
|
|
|
|
volume.requireReached(t)
|
|
seen := volume.seenAuth()
|
|
if seen == security.BearerPrefix+"filer-credential" {
|
|
t.Fatal("caller's filer credential reached the volume server")
|
|
}
|
|
check := httptest.NewRequest(method, "http://volume:8080/"+proxyTestFileId, nil)
|
|
check.Header.Set("Authorization", seen)
|
|
if !vs.maybeCheckJwtAuthorization(check, proxyTestVid, proxyTestFid, true) {
|
|
t.Fatalf("forwarded token %q did not authorize the write", seen)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Reads keep the minted token so weed mount can read through the proxy against a
|
|
// volume server that enforces read JWTs -- and the minted token must *replace*
|
|
// whatever the caller sent, not be appended alongside it.
|
|
func TestProxyReadReplacesCallerCredential(t *testing.T) {
|
|
volume := newProxyTestVolume(t)
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "http://filer:8888/?proxyChunkId="+proxyTestFileId, nil)
|
|
r.Header.Set("Authorization", security.BearerPrefix+"caller-supplied-token")
|
|
fs.proxyToVolumeServerURL(httptest.NewRecorder(), r, proxyTestFileId, volume.URL+"/"+proxyTestFileId)
|
|
|
|
volume.requireReached(t)
|
|
seen := volume.seenAuth()
|
|
if seen == security.BearerPrefix+"caller-supplied-token" {
|
|
t.Fatal("caller's token reached the volume server instead of the minted one")
|
|
}
|
|
|
|
vs := &VolumeServer{guard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
check := httptest.NewRequest(http.MethodGet, "http://volume:8080/"+proxyTestFileId, nil)
|
|
check.Header.Set("Authorization", seen)
|
|
if !vs.maybeCheckJwtAuthorization(check, proxyTestVid, proxyTestFid, false) {
|
|
t.Fatalf("forwarded token %q did not authorize the read", seen)
|
|
}
|
|
}
|
|
|
|
// With no volume key there is nothing to mint, and the caller's Authorization
|
|
// is a filer credential -- it must be dropped, not relayed to a volume server
|
|
// that has no business seeing it.
|
|
func TestProxyDropsCallerCredentialWhenNothingMinted(t *testing.T) {
|
|
for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodDelete} {
|
|
t.Run(method, func(t *testing.T) {
|
|
volume := newProxyTestVolume(t)
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, "", 10, "", 10)}
|
|
|
|
r := httptest.NewRequest(method, "http://filer:8888/?proxyChunkId="+proxyTestFileId, nil)
|
|
r.Header.Set("Authorization", security.BearerPrefix+"filer-credential")
|
|
fs.proxyToVolumeServerURL(httptest.NewRecorder(), r, proxyTestFileId, volume.URL+"/"+proxyTestFileId)
|
|
|
|
volume.requireReached(t)
|
|
if got := volume.seenAuth(); got != "" {
|
|
t.Fatalf("volume server saw Authorization %q, want it dropped", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Writes must not queue behind the read semaphore: a proxied write carries an
|
|
// AssignVolume token that expires 10s after the assign by default, and waiting
|
|
// for a read slot can push it past expiry.
|
|
func TestProxyWriteBypassesReadSemaphore(t *testing.T) {
|
|
volume := newProxyTestVolume(t)
|
|
host := volume.Listener.Addr().String()
|
|
|
|
// Fill every read slot for this host and never release them.
|
|
for i := 0; i < proxyReadConcurrencyPerVolumeServer; i++ {
|
|
if err := acquireProxySemaphore(context.Background(), host); err != nil {
|
|
t.Fatalf("fill slot %d: %v", i, err)
|
|
}
|
|
}
|
|
defer func() {
|
|
for i := 0; i < proxyReadConcurrencyPerVolumeServer; i++ {
|
|
releaseProxySemaphore(host)
|
|
}
|
|
}()
|
|
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, proxyTestWriteKey, 10, "", 10)}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
r := httptest.NewRequest(http.MethodPost, "http://filer:8888/?proxyChunkId="+proxyTestFileId, nil).WithContext(ctx)
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
fs.proxyToVolumeServerURL(httptest.NewRecorder(), r, proxyTestFileId, volume.URL+"/"+proxyTestFileId)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("proxied write blocked on the read semaphore")
|
|
}
|
|
volume.requireReached(t)
|
|
}
|
|
|
|
// The volume server strips a _N delta suffix before comparing the fid claim, so
|
|
// a token minted for the suffixed form would never validate.
|
|
func TestProxyReadTokenMatchesDeltaFid(t *testing.T) {
|
|
const deltaFileId = proxyTestFileId + "_1"
|
|
|
|
fs := &FilerServer{volumeGuard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
jwt := fs.maybeGetVolumeReadJwtAuthorizationToken(deltaFileId)
|
|
if jwt == "" {
|
|
t.Fatal("no read token minted for a delta fid")
|
|
}
|
|
|
|
vs := &VolumeServer{guard: security.NewGuard([]string{}, proxyTestWriteKey, 10, proxyTestReadKey, 10)}
|
|
r := httptest.NewRequest(http.MethodGet, "http://volume:8080/"+deltaFileId, nil)
|
|
r.Header.Set("Authorization", security.BearerPrefix+jwt)
|
|
if !vs.maybeCheckJwtAuthorization(r, proxyTestVid, proxyTestFid+"_1", false) {
|
|
t.Fatal("token minted for a delta fid did not authorize the read")
|
|
}
|
|
}
|
|
|
|
func TestValidateProxyChunkId(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
fileId string
|
|
ok bool
|
|
}{
|
|
{"3,01637037d6", true},
|
|
{"1,0c2b3f2f0f", true},
|
|
{"12,04f0e6ba1d", true},
|
|
{"3,01637037d6_1", true}, // batch-assign delta form
|
|
{"3,01637037d6_12", true}, // multi-digit delta
|
|
{"3,x/../../status", false},
|
|
{"3,01637037d6/../../status", false},
|
|
{"3,01637037d6/../../stats/counter", false},
|
|
{"3,../../status", false},
|
|
{"3,01637037d6/../../status_1", false}, // traversal wearing a delta suffix
|
|
// The suffix must be digits only, or stripping it would reduce a
|
|
// traversal payload to a valid fid and let it through.
|
|
{"3,01637037d6_1/../../status", false},
|
|
{"3,01637037d6_../../status", false},
|
|
{"3,01637037d6_1/../../stats/counter", false},
|
|
{"3,01637037d6_", false},
|
|
{"3,01637037d6_abc", false},
|
|
{"3,01637037d6_1a", false},
|
|
{"3,01637037d6?readDeleted=true", false},
|
|
{"3,01637037d6#frag", false},
|
|
{"3,", false},
|
|
{"3,abc", false},
|
|
{"3", false},
|
|
{"", false},
|
|
} {
|
|
err := validateProxyChunkId(tc.fileId)
|
|
if tc.ok && err != nil {
|
|
t.Errorf("validateProxyChunkId(%q) rejected a valid fid: %v", tc.fileId, err)
|
|
}
|
|
if !tc.ok && err == nil {
|
|
t.Errorf("validateProxyChunkId(%q) accepted a malformed fid", tc.fileId)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A fid carrying dot segments must be rejected before the lookup, so it can
|
|
// never be pasted into a volume server URL. Asserting on 400 (not merely "no
|
|
// traversal") also proves the request never left the filer.
|
|
func TestProxyRejectsTraversalBeforeLookup(t *testing.T) {
|
|
fs := &FilerServer{}
|
|
|
|
for _, fileId := range []string{
|
|
"3,x/../../status",
|
|
"3,01637037d6/../../status",
|
|
"3,01637037d6/../../stats/counter",
|
|
"3,01637037d6_1/../../status",
|
|
"3,01637037d6_../../status",
|
|
} {
|
|
r := httptest.NewRequest(http.MethodGet, "http://filer:8888/?proxyChunkId="+fileId, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
// fs.filer is nil: reaching the lookup would panic, so surviving this
|
|
// call is itself proof the fid was rejected first.
|
|
fs.proxyToVolumeServer(w, r, fileId)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("proxyChunkId=%q returned %d, want 400", fileId, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The proxy branch reaches any needle in the cluster by file id, on the filer
|
|
// port jwt.filer_signing exists to make safe to expose, so it has to sit behind
|
|
// the same gate as every other request.
|
|
func TestFilerHandlerGatesChunkProxy(t *testing.T) {
|
|
signingKey := "secret"
|
|
fs := &FilerServer{
|
|
option: &FilerOption{},
|
|
filerGuard: security.NewGuard(nil, signingKey, 0, signingKey, 0),
|
|
}
|
|
|
|
for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPost, http.MethodDelete} {
|
|
t.Run(method, func(t *testing.T) {
|
|
r := httptest.NewRequest(method, "http://filer:8888/?proxyChunkId="+proxyTestFileId, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
// fs.filer is nil: reaching the lookup would panic, so surviving
|
|
// this call is itself proof the request was refused first.
|
|
fs.filerHandler(w, r)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("anonymous %s of a chunk returned %d, want 401", method, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ... and once past it the branch still runs. A malformed fid is refused by
|
|
// validateProxyChunkId ahead of any lookup, which is as far as this can go
|
|
// without a cluster behind the filer.
|
|
func TestFilerHandlerProxiesChunkForAuthorizedCaller(t *testing.T) {
|
|
signingKey := "secret"
|
|
fs := &FilerServer{
|
|
option: &FilerOption{},
|
|
filerGuard: security.NewGuard(nil, signingKey, 0, signingKey, 0),
|
|
}
|
|
token := security.GenJwtForFilerServer(security.SigningKey(signingKey), 60)
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "http://filer:8888/?proxyChunkId=3,not-a-fid", nil)
|
|
r.Header.Set("Authorization", security.BearerPrefix+string(token))
|
|
w := httptest.NewRecorder()
|
|
|
|
fs.filerHandler(w, r)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("authorized chunk proxy returned %d, want 400 from the fid check", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestProxySemaphore_LimitsConcurrency(t *testing.T) {
|
|
host := "test-volume:8080"
|
|
defer proxySemaphores.Delete(host)
|
|
|
|
var running atomic.Int32
|
|
var maxSeen atomic.Int32
|
|
var wg sync.WaitGroup
|
|
|
|
// Launch more goroutines than the semaphore allows
|
|
total := proxyReadConcurrencyPerVolumeServer * 3
|
|
for i := 0; i < total; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := acquireProxySemaphore(context.Background(), host); err != nil {
|
|
t.Errorf("acquire: %v", err)
|
|
return
|
|
}
|
|
defer releaseProxySemaphore(host)
|
|
|
|
cur := running.Add(1)
|
|
// Track peak concurrency
|
|
for {
|
|
old := maxSeen.Load()
|
|
if cur <= old || maxSeen.CompareAndSwap(old, cur) {
|
|
break
|
|
}
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
running.Add(-1)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
peak := maxSeen.Load()
|
|
if peak > int32(proxyReadConcurrencyPerVolumeServer) {
|
|
t.Fatalf("peak concurrency %d exceeded limit %d", peak, proxyReadConcurrencyPerVolumeServer)
|
|
}
|
|
if peak == 0 {
|
|
t.Fatal("no goroutines ran")
|
|
}
|
|
}
|
|
|
|
func TestProxySemaphore_ContextCancellation(t *testing.T) {
|
|
host := "test-cancel:8080"
|
|
defer proxySemaphores.Delete(host)
|
|
|
|
// Fill the semaphore
|
|
for i := 0; i < proxyReadConcurrencyPerVolumeServer; i++ {
|
|
if err := acquireProxySemaphore(context.Background(), host); err != nil {
|
|
t.Fatalf("fill acquire: %v", err)
|
|
}
|
|
}
|
|
|
|
// Try to acquire with a cancelled context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
err := acquireProxySemaphore(ctx, host)
|
|
if err == nil {
|
|
t.Fatal("expected error from cancelled context")
|
|
}
|
|
|
|
// Clean up
|
|
for i := 0; i < proxyReadConcurrencyPerVolumeServer; i++ {
|
|
releaseProxySemaphore(host)
|
|
}
|
|
}
|
|
|
|
func TestProxySemaphore_PerHostIsolation(t *testing.T) {
|
|
hostA := "volume-a:8080"
|
|
hostB := "volume-b:8080"
|
|
defer proxySemaphores.Delete(hostA)
|
|
defer proxySemaphores.Delete(hostB)
|
|
|
|
// Fill hostA's semaphore
|
|
for i := 0; i < proxyReadConcurrencyPerVolumeServer; i++ {
|
|
if err := acquireProxySemaphore(context.Background(), hostA); err != nil {
|
|
t.Fatalf("fill hostA: %v", err)
|
|
}
|
|
}
|
|
|
|
// hostB should still be acquirable
|
|
if err := acquireProxySemaphore(context.Background(), hostB); err != nil {
|
|
t.Fatalf("hostB should not be blocked by hostA: %v", err)
|
|
}
|
|
releaseProxySemaphore(hostB)
|
|
|
|
// Clean up hostA
|
|
for i := 0; i < proxyReadConcurrencyPerVolumeServer; i++ {
|
|
releaseProxySemaphore(hostA)
|
|
}
|
|
}
|