snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
41
test/sftp/Makefile
Normal file
41
test/sftp/Makefile
Normal file
@@ -0,0 +1,41 @@
|
||||
.PHONY: all build test test-verbose test-short test-homedir test-debug clean deps tidy
|
||||
|
||||
all: build test
|
||||
|
||||
# Build the weed binary first
|
||||
build:
|
||||
cd ../../weed && go build -o weed .
|
||||
|
||||
# Install test dependencies
|
||||
deps:
|
||||
go mod download
|
||||
|
||||
# Run all tests
|
||||
test: build deps
|
||||
go test -timeout 5m ./...
|
||||
|
||||
# Run tests with verbose output
|
||||
test-verbose: build deps
|
||||
go test -v -timeout 5m ./...
|
||||
|
||||
# Run quick tests only (skip integration tests)
|
||||
test-short: deps
|
||||
go test -short -v ./...
|
||||
|
||||
# Run specific test
|
||||
test-homedir: build deps
|
||||
go test -v -timeout 5m -run TestHomeDirPathTranslation ./...
|
||||
|
||||
# Run tests with debug output from SeaweedFS
|
||||
test-debug: build deps
|
||||
go test -v -timeout 5m ./... 2>&1 | tee test.log
|
||||
|
||||
# Clean up test artifacts
|
||||
clean:
|
||||
rm -f test.log
|
||||
go clean -testcache
|
||||
|
||||
# Update go.sum
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
93
test/sftp/README.md
Normal file
93
test/sftp/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# SeaweedFS SFTP Integration Tests
|
||||
|
||||
This directory contains integration tests for the SeaweedFS SFTP server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Build the SeaweedFS binary:
|
||||
```bash
|
||||
cd ../../weed
|
||||
go build -o weed .
|
||||
```
|
||||
|
||||
2. Ensure `ssh-keygen` is available (for generating test SSH host keys)
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run all tests
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
### Run tests with verbose output
|
||||
```bash
|
||||
make test-verbose
|
||||
```
|
||||
|
||||
### Run a specific test
|
||||
```bash
|
||||
go test -v -run TestHomeDirPathTranslation
|
||||
```
|
||||
|
||||
### Skip long-running tests
|
||||
```bash
|
||||
go test -short ./...
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
- `framework.go` - Test framework that starts SeaweedFS cluster with SFTP
|
||||
- `basic_test.go` - Basic SFTP operation tests including:
|
||||
- HomeDir path translation (fixes issue #7470)
|
||||
- File upload/download
|
||||
- Directory operations
|
||||
- Large file handling
|
||||
- Edge cases
|
||||
|
||||
## Test Configuration
|
||||
|
||||
Tests use `testdata/userstore.json` which defines test users:
|
||||
|
||||
| Username | Password | HomeDir | Permissions |
|
||||
|----------|----------|---------|-------------|
|
||||
| admin | adminpassword | / | Full access |
|
||||
| testuser | testuserpassword | /sftp/testuser | Full access to home |
|
||||
| readonly | readonlypassword | /public | Read-only |
|
||||
|
||||
## Key Tests
|
||||
|
||||
### TestHomeDirPathTranslation
|
||||
|
||||
Tests the fix for [issue #7470](https://github.com/seaweedfs/seaweedfs/issues/7470) where
|
||||
users with a non-root HomeDir (e.g., `/sftp/testuser`) could not upload files to `/`
|
||||
because the path wasn't being translated to their home directory.
|
||||
|
||||
The test verifies:
|
||||
- Uploading to `/` correctly maps to the user's HomeDir
|
||||
- Creating directories at `/` works
|
||||
- Listing `/` shows the user's home directory contents
|
||||
- All path operations respect the HomeDir translation
|
||||
|
||||
## Debugging
|
||||
|
||||
To debug test failures:
|
||||
|
||||
1. Enable verbose output:
|
||||
```bash
|
||||
go test -v -run TestName
|
||||
```
|
||||
|
||||
2. Keep test artifacts (don't cleanup):
|
||||
```go
|
||||
config := DefaultTestConfig()
|
||||
config.SkipCleanup = true
|
||||
```
|
||||
|
||||
3. Enable debug logging:
|
||||
```go
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = true
|
||||
```
|
||||
|
||||
|
||||
|
||||
651
test/sftp/basic_test.go
Normal file
651
test/sftp/basic_test.go
Normal file
@@ -0,0 +1,651 @@
|
||||
package sftp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestHomeDirPathTranslation tests that SFTP operations correctly translate
|
||||
// paths relative to the user's HomeDir.
|
||||
// This is the fix for https://github.com/seaweedfs/seaweedfs/issues/7470
|
||||
func TestHomeDirPathTranslation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
// Test with user "testuser" who has HomeDir="/sftp/testuser"
|
||||
// When they upload to "/", it should actually go to "/sftp/testuser"
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
|
||||
require.NoError(t, err, "failed to connect as testuser")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
// Test 1: Upload file to "/" (should map to /sftp/testuser/)
|
||||
t.Run("UploadToRoot", func(t *testing.T) {
|
||||
testContent := []byte("Hello from SFTP test!")
|
||||
filename := "test_upload.txt"
|
||||
|
||||
// Create file at "/" from user's perspective
|
||||
file, err := sftpClient.Create("/" + filename)
|
||||
require.NoError(t, err, "should be able to create file at /")
|
||||
|
||||
_, err = file.Write(testContent)
|
||||
require.NoError(t, err, "should be able to write to file")
|
||||
err = file.Close()
|
||||
require.NoError(t, err, "should be able to close file")
|
||||
|
||||
// Verify file exists and has correct content
|
||||
readFile, err := sftpClient.Open("/" + filename)
|
||||
require.NoError(t, err, "should be able to open file")
|
||||
defer readFile.Close()
|
||||
|
||||
content, err := io.ReadAll(readFile)
|
||||
require.NoError(t, err, "should be able to read file")
|
||||
require.Equal(t, testContent, content, "file content should match")
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove("/" + filename)
|
||||
require.NoError(t, err, "should be able to remove file")
|
||||
})
|
||||
|
||||
// Test 2: Create directory at "/" (should map to /sftp/testuser/)
|
||||
t.Run("CreateDirAtRoot", func(t *testing.T) {
|
||||
dirname := "test_dir"
|
||||
|
||||
err := sftpClient.Mkdir("/" + dirname)
|
||||
require.NoError(t, err, "should be able to create directory at /")
|
||||
|
||||
// Verify directory exists
|
||||
info, err := sftpClient.Stat("/" + dirname)
|
||||
require.NoError(t, err, "should be able to stat directory")
|
||||
require.True(t, info.IsDir(), "should be a directory")
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.RemoveDirectory("/" + dirname)
|
||||
require.NoError(t, err, "should be able to remove directory")
|
||||
})
|
||||
|
||||
// Test 3: List directory at "/" (should list /sftp/testuser/)
|
||||
t.Run("ListRoot", func(t *testing.T) {
|
||||
// Create a test file first
|
||||
testContent := []byte("list test content")
|
||||
filename := "list_test.txt"
|
||||
|
||||
file, err := sftpClient.Create("/" + filename)
|
||||
require.NoError(t, err)
|
||||
_, err = file.Write(testContent)
|
||||
require.NoError(t, err)
|
||||
file.Close()
|
||||
|
||||
// List root directory
|
||||
files, err := sftpClient.ReadDir("/")
|
||||
require.NoError(t, err, "should be able to list root directory")
|
||||
|
||||
// Should find our test file
|
||||
found := false
|
||||
for _, f := range files {
|
||||
if f.Name() == filename {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "should find test file in listing")
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove("/" + filename)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Test 4: Nested directory operations
|
||||
t.Run("NestedOperations", func(t *testing.T) {
|
||||
// Create nested directory structure
|
||||
err := sftpClient.MkdirAll("/nested/dir/structure")
|
||||
require.NoError(t, err, "should be able to create nested directories")
|
||||
|
||||
// Create file in nested directory
|
||||
testContent := []byte("nested file content")
|
||||
file, err := sftpClient.Create("/nested/dir/structure/file.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = file.Write(testContent)
|
||||
require.NoError(t, err)
|
||||
file.Close()
|
||||
|
||||
// Verify file exists
|
||||
readFile, err := sftpClient.Open("/nested/dir/structure/file.txt")
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(readFile)
|
||||
require.NoError(t, err)
|
||||
readFile.Close()
|
||||
require.Equal(t, testContent, content)
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove("/nested/dir/structure/file.txt")
|
||||
require.NoError(t, err)
|
||||
err = sftpClient.RemoveDirectory("/nested/dir/structure")
|
||||
require.NoError(t, err)
|
||||
err = sftpClient.RemoveDirectory("/nested/dir")
|
||||
require.NoError(t, err)
|
||||
err = sftpClient.RemoveDirectory("/nested")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Test 5: Rename operation
|
||||
t.Run("RenameFile", func(t *testing.T) {
|
||||
testContent := []byte("rename test content")
|
||||
|
||||
file, err := sftpClient.Create("/original.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = file.Write(testContent)
|
||||
require.NoError(t, err)
|
||||
file.Close()
|
||||
|
||||
// Rename file
|
||||
err = sftpClient.Rename("/original.txt", "/renamed.txt")
|
||||
require.NoError(t, err, "should be able to rename file")
|
||||
|
||||
// Verify old file doesn't exist
|
||||
_, err = sftpClient.Stat("/original.txt")
|
||||
require.Error(t, err, "original file should not exist")
|
||||
|
||||
// Verify new file exists with correct content
|
||||
readFile, err := sftpClient.Open("/renamed.txt")
|
||||
require.NoError(t, err, "renamed file should exist")
|
||||
content, err := io.ReadAll(readFile)
|
||||
require.NoError(t, err)
|
||||
readFile.Close()
|
||||
require.Equal(t, testContent, content)
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove("/renamed.txt")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAdminRootAccess tests that admin user with HomeDir="/" can access everything
|
||||
func TestAdminRootAccess(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
// Connect as admin with HomeDir="/"
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("admin", "adminpassword")
|
||||
require.NoError(t, err, "failed to connect as admin")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
// Admin should be able to create directories anywhere
|
||||
t.Run("CreateAnyDirectory", func(t *testing.T) {
|
||||
// Create the user's home directory structure
|
||||
err := sftpClient.MkdirAll("/sftp/testuser")
|
||||
require.NoError(t, err, "admin should be able to create any directory")
|
||||
|
||||
// Create file in that directory
|
||||
testContent := []byte("admin created this")
|
||||
file, err := sftpClient.Create("/sftp/testuser/admin_file.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = file.Write(testContent)
|
||||
require.NoError(t, err)
|
||||
file.Close()
|
||||
|
||||
// Verify file exists
|
||||
info, err := sftpClient.Stat("/sftp/testuser/admin_file.txt")
|
||||
require.NoError(t, err)
|
||||
require.False(t, info.IsDir())
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove("/sftp/testuser/admin_file.txt")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLargeFileUpload tests uploading larger files through SFTP
|
||||
func TestLargeFileUpload(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
|
||||
require.NoError(t, err, "failed to connect as testuser")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
// Create a 1MB file
|
||||
t.Run("Upload1MB", func(t *testing.T) {
|
||||
size := 1024 * 1024 // 1MB
|
||||
testData := bytes.Repeat([]byte("A"), size)
|
||||
|
||||
file, err := sftpClient.Create("/large_file.bin")
|
||||
require.NoError(t, err)
|
||||
n, err := file.Write(testData)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, size, n)
|
||||
file.Close()
|
||||
|
||||
// Verify file size
|
||||
info, err := sftpClient.Stat("/large_file.bin")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(size), info.Size())
|
||||
|
||||
// Verify content
|
||||
readFile, err := sftpClient.Open("/large_file.bin")
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(readFile)
|
||||
require.NoError(t, err)
|
||||
readFile.Close()
|
||||
require.Equal(t, testData, content)
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove("/large_file.bin")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestStatOperations tests Stat and Lstat operations
|
||||
func TestStatOperations(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
|
||||
require.NoError(t, err, "failed to connect as testuser")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
// Create a test file
|
||||
testContent := []byte("stat test content")
|
||||
file, err := sftpClient.Create("/stat_test.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = file.Write(testContent)
|
||||
require.NoError(t, err)
|
||||
file.Close()
|
||||
|
||||
t.Run("StatFile", func(t *testing.T) {
|
||||
info, err := sftpClient.Stat("/stat_test.txt")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "stat_test.txt", info.Name())
|
||||
require.Equal(t, int64(len(testContent)), info.Size())
|
||||
require.False(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("StatDirectory", func(t *testing.T) {
|
||||
err := sftpClient.Mkdir("/stat_dir")
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := sftpClient.Stat("/stat_dir")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "stat_dir", info.Name())
|
||||
require.True(t, info.IsDir())
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.RemoveDirectory("/stat_dir")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("StatRoot", func(t *testing.T) {
|
||||
// Should be able to stat "/" which maps to user's home directory
|
||||
info, err := sftpClient.Stat("/")
|
||||
require.NoError(t, err, "should be able to stat root (home) directory")
|
||||
require.True(t, info.IsDir(), "root should be a directory")
|
||||
})
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove("/stat_test.txt")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestWalk tests walking directory trees
|
||||
func TestWalk(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
|
||||
require.NoError(t, err, "failed to connect as testuser")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
// Create directory structure
|
||||
err = sftpClient.MkdirAll("/walk/a/b")
|
||||
require.NoError(t, err)
|
||||
err = sftpClient.MkdirAll("/walk/c")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create files
|
||||
for _, p := range []string{"/walk/file1.txt", "/walk/a/file2.txt", "/walk/a/b/file3.txt", "/walk/c/file4.txt"} {
|
||||
file, err := sftpClient.Create(p)
|
||||
require.NoError(t, err)
|
||||
file.Write([]byte("test"))
|
||||
file.Close()
|
||||
}
|
||||
|
||||
t.Run("WalkEntireTree", func(t *testing.T) {
|
||||
var paths []string
|
||||
walker := sftpClient.Walk("/walk")
|
||||
for walker.Step() {
|
||||
if walker.Err() != nil {
|
||||
continue
|
||||
}
|
||||
paths = append(paths, walker.Path())
|
||||
}
|
||||
|
||||
// Should find all directories and files
|
||||
require.Contains(t, paths, "/walk")
|
||||
require.Contains(t, paths, "/walk/a")
|
||||
require.Contains(t, paths, "/walk/a/b")
|
||||
require.Contains(t, paths, "/walk/c")
|
||||
})
|
||||
|
||||
// Clean up
|
||||
for _, p := range []string{"/walk/file1.txt", "/walk/a/file2.txt", "/walk/a/b/file3.txt", "/walk/c/file4.txt"} {
|
||||
require.NoError(t, sftpClient.Remove(p))
|
||||
}
|
||||
for _, p := range []string{"/walk/a/b", "/walk/a", "/walk/c", "/walk"} {
|
||||
require.NoError(t, sftpClient.RemoveDirectory(p))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCurrentWorkingDirectory tests that Getwd and Chdir work correctly
|
||||
func TestCurrentWorkingDirectory(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
|
||||
require.NoError(t, err, "failed to connect as testuser")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
// Create test directory
|
||||
err = sftpClient.Mkdir("/cwd_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("GetCurrentDir", func(t *testing.T) {
|
||||
cwd, err := sftpClient.Getwd()
|
||||
require.NoError(t, err)
|
||||
// The initial working directory should be the user's home directory
|
||||
// which from the user's perspective is "/"
|
||||
require.Equal(t, "/", cwd, "initial working directory should be the virtual root")
|
||||
})
|
||||
|
||||
t.Run("ChangeAndCreate", func(t *testing.T) {
|
||||
// Create file in subdirectory using relative path after chdir
|
||||
// Note: pkg/sftp doesn't support Chdir, so we test using absolute paths
|
||||
file, err := sftpClient.Create("/cwd_test/relative_file.txt")
|
||||
require.NoError(t, err)
|
||||
file.Write([]byte("test"))
|
||||
file.Close()
|
||||
|
||||
// Verify using absolute path
|
||||
_, err = sftpClient.Stat("/cwd_test/relative_file.txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Clean up
|
||||
sftpClient.Remove("/cwd_test/relative_file.txt")
|
||||
})
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.RemoveDirectory("/cwd_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestPathEdgeCases tests various edge cases in path handling
|
||||
func TestPathEdgeCases(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
|
||||
require.NoError(t, err, "failed to connect as testuser")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
t.Run("PathWithDotDot", func(t *testing.T) {
|
||||
// Create directory structure
|
||||
err := sftpClient.MkdirAll("/edge/subdir")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create file using path with ..
|
||||
file, err := sftpClient.Create("/edge/subdir/../file.txt")
|
||||
require.NoError(t, err)
|
||||
file.Write([]byte("test"))
|
||||
file.Close()
|
||||
|
||||
// Verify file was created in /edge
|
||||
_, err = sftpClient.Stat("/edge/file.txt")
|
||||
require.NoError(t, err, "file should be created in parent directory")
|
||||
|
||||
// Clean up
|
||||
sftpClient.Remove("/edge/file.txt")
|
||||
sftpClient.RemoveDirectory("/edge/subdir")
|
||||
sftpClient.RemoveDirectory("/edge")
|
||||
})
|
||||
|
||||
t.Run("PathWithTrailingSlash", func(t *testing.T) {
|
||||
err := sftpClient.Mkdir("/trailing")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Stat with trailing slash
|
||||
info, err := sftpClient.Stat("/trailing/")
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.IsDir())
|
||||
|
||||
// Clean up
|
||||
sftpClient.RemoveDirectory("/trailing")
|
||||
})
|
||||
|
||||
t.Run("CreateFileAtRootPath", func(t *testing.T) {
|
||||
// This is the exact scenario from issue #7470
|
||||
// User with HomeDir="/sftp/testuser" uploads to "/"
|
||||
file, err := sftpClient.Create("/issue7470.txt")
|
||||
require.NoError(t, err, "should be able to create file at / (issue #7470)")
|
||||
file.Write([]byte("This tests the fix for issue #7470"))
|
||||
file.Close()
|
||||
|
||||
// Verify
|
||||
_, err = sftpClient.Stat("/issue7470.txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Clean up
|
||||
sftpClient.Remove("/issue7470.txt")
|
||||
})
|
||||
|
||||
// Security test: path traversal attacks should be blocked
|
||||
t.Run("PathTraversalPrevention", func(t *testing.T) {
|
||||
// User's HomeDir is "/sftp/testuser"
|
||||
// Attempting to escape via "../.." should NOT create files outside home directory
|
||||
|
||||
// First, create a valid file to ensure we can write
|
||||
validFile, err := sftpClient.Create("/valid.txt")
|
||||
require.NoError(t, err)
|
||||
validFile.Write([]byte("valid"))
|
||||
validFile.Close()
|
||||
|
||||
// Try various path traversal attempts
|
||||
// These should either:
|
||||
// 1. Be blocked (error returned), OR
|
||||
// 2. Be safely resolved to stay within home directory
|
||||
|
||||
traversalPaths := []string{
|
||||
"/../escape.txt",
|
||||
"/../../escape.txt",
|
||||
"/../../../escape.txt",
|
||||
"/subdir/../../escape.txt",
|
||||
"/./../../escape.txt",
|
||||
}
|
||||
|
||||
for _, traversalPath := range traversalPaths {
|
||||
t.Run(traversalPath, func(t *testing.T) {
|
||||
// Note: The pkg/sftp client sanitizes paths locally before sending them to the server.
|
||||
// So "/../escape.txt" becomes "/escape.txt" on the wire.
|
||||
// Therefore, we cannot trigger the server-side path traversal block with this client.
|
||||
// Instead, we verify that the file is created successfully within the jail (contained).
|
||||
// The server-side protection logic is verified in unit tests (sftpd/sftp_server_test.go).
|
||||
|
||||
file, err := sftpClient.Create(traversalPath)
|
||||
require.NoError(t, err, "creation should succeed because client sanitizes path")
|
||||
file.Close()
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove(traversalPath)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up
|
||||
sftpClient.Remove("/valid.txt")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFileContent tests reading and writing file content correctly
|
||||
func TestFileContent(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
err := fw.Setup(config)
|
||||
require.NoError(t, err, "failed to setup test framework")
|
||||
defer fw.Cleanup()
|
||||
|
||||
sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
|
||||
require.NoError(t, err, "failed to connect as testuser")
|
||||
defer sshConn.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
t.Run("BinaryContent", func(t *testing.T) {
|
||||
// Create binary data with all byte values
|
||||
data := make([]byte, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
data[i] = byte(i)
|
||||
}
|
||||
|
||||
file, err := sftpClient.Create("/binary.bin")
|
||||
require.NoError(t, err)
|
||||
n, err := file.Write(data)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 256, n)
|
||||
file.Close()
|
||||
|
||||
// Read back
|
||||
readFile, err := sftpClient.Open("/binary.bin")
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(readFile)
|
||||
require.NoError(t, err)
|
||||
readFile.Close()
|
||||
|
||||
require.Equal(t, data, content, "binary content should match")
|
||||
|
||||
// Clean up
|
||||
sftpClient.Remove("/binary.bin")
|
||||
})
|
||||
|
||||
t.Run("EmptyFile", func(t *testing.T) {
|
||||
file, err := sftpClient.Create("/empty.txt")
|
||||
require.NoError(t, err)
|
||||
file.Close()
|
||||
|
||||
info, err := sftpClient.Stat("/empty.txt")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), info.Size())
|
||||
|
||||
// Clean up
|
||||
sftpClient.Remove("/empty.txt")
|
||||
})
|
||||
|
||||
t.Run("UnicodeFilename", func(t *testing.T) {
|
||||
filename := "/文件名.txt"
|
||||
content := []byte("Unicode content: 你好世界")
|
||||
|
||||
file, err := sftpClient.Create(filename)
|
||||
require.NoError(t, err)
|
||||
file.Write(content)
|
||||
file.Close()
|
||||
|
||||
// Read back
|
||||
readFile, err := sftpClient.Open(filename)
|
||||
require.NoError(t, err)
|
||||
readContent, err := io.ReadAll(readFile)
|
||||
require.NoError(t, err)
|
||||
readFile.Close()
|
||||
|
||||
require.Equal(t, content, readContent)
|
||||
|
||||
// Verify in listing
|
||||
files, err := sftpClient.ReadDir("/")
|
||||
require.NoError(t, err)
|
||||
found := false
|
||||
for _, f := range files {
|
||||
if f.Name() == path.Base(filename) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "should find unicode filename in listing")
|
||||
|
||||
// Clean up
|
||||
sftpClient.Remove(filename)
|
||||
})
|
||||
}
|
||||
422
test/sftp/framework.go
Normal file
422
test/sftp/framework.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package sftp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SftpTestFramework provides utilities for SFTP integration testing
|
||||
type SftpTestFramework struct {
|
||||
t *testing.T
|
||||
tempDir string
|
||||
dataDir string
|
||||
masterProcess *os.Process
|
||||
volumeProcess *os.Process
|
||||
filerProcess *os.Process
|
||||
sftpProcess *os.Process
|
||||
masterAddr string
|
||||
volumeAddr string
|
||||
filerAddr string
|
||||
sftpAddr string
|
||||
weedBinary string
|
||||
userStoreFile string
|
||||
hostKeyFile string
|
||||
isSetup bool
|
||||
skipCleanup bool
|
||||
}
|
||||
|
||||
// TestConfig holds configuration for SFTP tests
|
||||
type TestConfig struct {
|
||||
NumVolumes int
|
||||
EnableDebug bool
|
||||
SkipCleanup bool // for debugging failed tests
|
||||
UserStoreFile string
|
||||
}
|
||||
|
||||
// DefaultTestConfig returns a default configuration for SFTP tests
|
||||
func DefaultTestConfig() *TestConfig {
|
||||
return &TestConfig{
|
||||
NumVolumes: 3,
|
||||
EnableDebug: false,
|
||||
SkipCleanup: false,
|
||||
UserStoreFile: "",
|
||||
}
|
||||
}
|
||||
|
||||
// NewSftpTestFramework creates a new SFTP testing framework
|
||||
func NewSftpTestFramework(t *testing.T, config *TestConfig) *SftpTestFramework {
|
||||
if config == nil {
|
||||
config = DefaultTestConfig()
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "seaweedfs_sftp_test_")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate SSH host key for SFTP server
|
||||
hostKeyFile := filepath.Join(tempDir, "ssh_host_key")
|
||||
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", hostKeyFile, "-N", "")
|
||||
err = cmd.Run()
|
||||
require.NoError(t, err, "failed to generate SSH host key")
|
||||
|
||||
// Use provided userstore or copy the test one
|
||||
userStoreFile := config.UserStoreFile
|
||||
if userStoreFile == "" {
|
||||
// Copy test userstore to temp dir
|
||||
userStoreFile = filepath.Join(tempDir, "userstore.json")
|
||||
testDataPath := findTestDataPath()
|
||||
input, err := os.ReadFile(filepath.Join(testDataPath, "userstore.json"))
|
||||
require.NoError(t, err, "failed to read test userstore.json")
|
||||
err = os.WriteFile(userStoreFile, input, 0644)
|
||||
require.NoError(t, err, "failed to write userstore.json")
|
||||
}
|
||||
|
||||
return &SftpTestFramework{
|
||||
t: t,
|
||||
tempDir: tempDir,
|
||||
dataDir: filepath.Join(tempDir, "data"),
|
||||
masterAddr: "127.0.0.1:19333",
|
||||
volumeAddr: "127.0.0.1:18080",
|
||||
filerAddr: "127.0.0.1:18888",
|
||||
sftpAddr: "127.0.0.1:12022",
|
||||
weedBinary: findWeedBinary(),
|
||||
userStoreFile: userStoreFile,
|
||||
hostKeyFile: hostKeyFile,
|
||||
isSetup: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Setup starts SeaweedFS cluster with SFTP server
|
||||
func (f *SftpTestFramework) Setup(config *TestConfig) error {
|
||||
if f.isSetup {
|
||||
return fmt.Errorf("framework already setup")
|
||||
}
|
||||
|
||||
// Create all data directories
|
||||
dirs := []string{
|
||||
f.dataDir,
|
||||
filepath.Join(f.dataDir, "master"),
|
||||
filepath.Join(f.dataDir, "volume"),
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start master
|
||||
if err := f.startMaster(config); err != nil {
|
||||
return fmt.Errorf("failed to start master: %v", err)
|
||||
}
|
||||
|
||||
// Wait for master to be ready
|
||||
if err := f.waitForService(f.masterAddr, 30*time.Second); err != nil {
|
||||
return fmt.Errorf("master not ready: %v", err)
|
||||
}
|
||||
|
||||
// Start volume server
|
||||
if err := f.startVolumeServer(config); err != nil {
|
||||
return fmt.Errorf("failed to start volume server: %v", err)
|
||||
}
|
||||
|
||||
// Wait for volume server to be ready
|
||||
if err := f.waitForService(f.volumeAddr, 30*time.Second); err != nil {
|
||||
return fmt.Errorf("volume server not ready: %v", err)
|
||||
}
|
||||
|
||||
// Start filer
|
||||
if err := f.startFiler(config); err != nil {
|
||||
return fmt.Errorf("failed to start filer: %v", err)
|
||||
}
|
||||
|
||||
// Wait for filer to be ready
|
||||
if err := f.waitForService(f.filerAddr, 30*time.Second); err != nil {
|
||||
return fmt.Errorf("filer not ready: %v", err)
|
||||
}
|
||||
|
||||
// Start SFTP server
|
||||
if err := f.startSftpServer(config); err != nil {
|
||||
return fmt.Errorf("failed to start SFTP server: %v", err)
|
||||
}
|
||||
|
||||
// Wait for SFTP server to be ready
|
||||
if err := f.waitForService(f.sftpAddr, 30*time.Second); err != nil {
|
||||
return fmt.Errorf("SFTP server not ready: %v", err)
|
||||
}
|
||||
|
||||
// Additional wait for all services to stabilize (gRPC endpoints)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
f.skipCleanup = config.SkipCleanup
|
||||
f.isSetup = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup stops all processes and removes temporary files
|
||||
func (f *SftpTestFramework) Cleanup() {
|
||||
// Stop processes in reverse order
|
||||
processes := []*os.Process{f.sftpProcess, f.filerProcess, f.volumeProcess, f.masterProcess}
|
||||
for _, proc := range processes {
|
||||
if proc != nil {
|
||||
proc.Signal(syscall.SIGTERM)
|
||||
proc.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temp directory
|
||||
if !f.skipCleanup {
|
||||
os.RemoveAll(f.tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSftpAddr returns the SFTP server address
|
||||
func (f *SftpTestFramework) GetSftpAddr() string {
|
||||
return f.sftpAddr
|
||||
}
|
||||
|
||||
// GetFilerAddr returns the filer address
|
||||
func (f *SftpTestFramework) GetFilerAddr() string {
|
||||
return f.filerAddr
|
||||
}
|
||||
|
||||
// ConnectSFTP creates an SFTP client connection with the given credentials
|
||||
func (f *SftpTestFramework) ConnectSFTP(username, password string) (*sftp.Client, *ssh.Client, error) {
|
||||
// Load the known host public key for verification
|
||||
hostKeyCallback, err := f.getHostKeyCallback()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get host key callback: %v", err)
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password(password),
|
||||
},
|
||||
HostKeyCallback: hostKeyCallback,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
sshConn, err := ssh.Dial("tcp", f.sftpAddr, config)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to connect SSH: %v", err)
|
||||
}
|
||||
|
||||
sftpClient, err := sftp.NewClient(sshConn)
|
||||
if err != nil {
|
||||
sshConn.Close()
|
||||
return nil, nil, fmt.Errorf("failed to create SFTP client: %v", err)
|
||||
}
|
||||
|
||||
return sftpClient, sshConn, nil
|
||||
}
|
||||
|
||||
// getHostKeyCallback returns a callback that verifies the server's host key
|
||||
// matches the known test server key we generated
|
||||
func (f *SftpTestFramework) getHostKeyCallback() (ssh.HostKeyCallback, error) {
|
||||
// Read the public key file generated alongside the private key
|
||||
pubKeyFile := f.hostKeyFile + ".pub"
|
||||
pubKeyBytes, err := os.ReadFile(pubKeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read host public key: %v", err)
|
||||
}
|
||||
|
||||
// Parse the public key
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse host public key: %v", err)
|
||||
}
|
||||
|
||||
// Return a callback that verifies the server key matches our known key
|
||||
return ssh.FixedHostKey(pubKey), nil
|
||||
}
|
||||
|
||||
// startMaster starts the SeaweedFS master server
|
||||
func (f *SftpTestFramework) startMaster(config *TestConfig) error {
|
||||
args := []string{
|
||||
"master",
|
||||
"-ip=127.0.0.1",
|
||||
"-port=19333",
|
||||
"-mdir=" + filepath.Join(f.dataDir, "master"),
|
||||
"-raftBootstrap",
|
||||
"-peers=none",
|
||||
}
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if config.EnableDebug {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.masterProcess = cmd.Process
|
||||
return nil
|
||||
}
|
||||
|
||||
// startVolumeServer starts SeaweedFS volume server
|
||||
func (f *SftpTestFramework) startVolumeServer(config *TestConfig) error {
|
||||
args := []string{
|
||||
"volume",
|
||||
"-master=" + f.masterAddr,
|
||||
"-ip=127.0.0.1",
|
||||
"-port=18080",
|
||||
"-dir=" + filepath.Join(f.dataDir, "volume"),
|
||||
fmt.Sprintf("-max=%d", config.NumVolumes),
|
||||
}
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if config.EnableDebug {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.volumeProcess = cmd.Process
|
||||
return nil
|
||||
}
|
||||
|
||||
// startFiler starts the SeaweedFS filer server
|
||||
func (f *SftpTestFramework) startFiler(config *TestConfig) error {
|
||||
args := []string{
|
||||
"filer",
|
||||
"-master=" + f.masterAddr,
|
||||
"-ip=127.0.0.1",
|
||||
"-port=18888",
|
||||
}
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if config.EnableDebug {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.filerProcess = cmd.Process
|
||||
return nil
|
||||
}
|
||||
|
||||
// startSftpServer starts the SeaweedFS SFTP server
|
||||
func (f *SftpTestFramework) startSftpServer(config *TestConfig) error {
|
||||
args := []string{
|
||||
"sftp",
|
||||
"-filer=" + f.filerAddr,
|
||||
"-ip.bind=127.0.0.1",
|
||||
"-port=12022",
|
||||
"-sshPrivateKey=" + f.hostKeyFile,
|
||||
"-userStoreFile=" + f.userStoreFile,
|
||||
}
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if config.EnableDebug {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.sftpProcess = cmd.Process
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForService waits for a service to be available
|
||||
func (f *SftpTestFramework) waitForService(addr string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("service at %s not ready within timeout", addr)
|
||||
}
|
||||
|
||||
// findWeedBinary locates the weed binary
|
||||
// Prefers local build over system-installed weed to ensure we test the latest code
|
||||
func findWeedBinary() string {
|
||||
// Get the directory where this source file is located
|
||||
// This ensures we find the locally built weed binary first
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if ok {
|
||||
thisDir := filepath.Dir(thisFile)
|
||||
// From test/sftp/, the weed binary should be at ../../weed/weed
|
||||
candidates := []string{
|
||||
filepath.Join(thisDir, "../../weed/weed"),
|
||||
filepath.Join(thisDir, "../weed/weed"),
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
abs, _ := filepath.Abs(candidate)
|
||||
return abs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try relative paths from current working directory
|
||||
cwd, _ := os.Getwd()
|
||||
candidates := []string{
|
||||
filepath.Join(cwd, "../../weed/weed"),
|
||||
filepath.Join(cwd, "../weed/weed"),
|
||||
filepath.Join(cwd, "./weed"),
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
abs, _ := filepath.Abs(candidate)
|
||||
return abs
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to PATH only if local build not found
|
||||
if path, err := exec.LookPath("weed"); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return "weed"
|
||||
}
|
||||
|
||||
// findTestDataPath locates the testdata directory
|
||||
func findTestDataPath() string {
|
||||
// Get the directory where this source file is located
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if ok {
|
||||
thisDir := filepath.Dir(thisFile)
|
||||
testDataPath := filepath.Join(thisDir, "testdata")
|
||||
if _, err := os.Stat(testDataPath); err == nil {
|
||||
return testDataPath
|
||||
}
|
||||
}
|
||||
|
||||
// Try relative paths from current working directory
|
||||
cwd, _ := os.Getwd()
|
||||
candidates := []string{
|
||||
filepath.Join(cwd, "testdata"),
|
||||
filepath.Join(cwd, "../sftp/testdata"),
|
||||
filepath.Join(cwd, "test/sftp/testdata"),
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return "./testdata"
|
||||
}
|
||||
17
test/sftp/go.mod
Normal file
17
test/sftp/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module seaweedfs-sftp-tests
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/pkg/sftp v1.13.7
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
64
test/sftp/go.sum
Normal file
64
test/sftp/go.sum
Normal file
@@ -0,0 +1,64 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM=
|
||||
github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
72
test/sftp/sibling_prefix_test.go
Normal file
72
test/sftp/sibling_prefix_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package sftp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestScopedUserCannotAccessSiblingPrefix verifies that a permission entry
|
||||
// for /tenants/alice does not leak onto sibling paths such as
|
||||
// /tenants/alice-archive that merely share the string prefix.
|
||||
func TestScopedUserCannotAccessSiblingPrefix(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := DefaultTestConfig()
|
||||
config.EnableDebug = testing.Verbose()
|
||||
|
||||
fw := NewSftpTestFramework(t, config)
|
||||
require.NoError(t, fw.Setup(config))
|
||||
defer fw.Cleanup()
|
||||
|
||||
// admin seeds both tenants
|
||||
admin, adminConn, err := fw.ConnectSFTP("admin", "adminpassword")
|
||||
require.NoError(t, err)
|
||||
defer adminConn.Close()
|
||||
defer admin.Close()
|
||||
|
||||
require.NoError(t, admin.MkdirAll("/tenants/alice"))
|
||||
require.NoError(t, admin.MkdirAll("/tenants/alice-archive"))
|
||||
secret, err := admin.Create("/tenants/alice-archive/secret.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = secret.Write([]byte("SIBLING-SECRET"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, secret.Close())
|
||||
|
||||
scoped, scopedConn, err := fw.ConnectSFTP("scoped", "scopedpassword")
|
||||
require.NoError(t, err)
|
||||
defer scopedConn.Close()
|
||||
defer scoped.Close()
|
||||
|
||||
// positive control: the scoped user can write within its own prefix
|
||||
own, err := scoped.Create("/tenants/alice/own.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = own.Write([]byte("ok"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, own.Close())
|
||||
|
||||
// reading the sibling tenant's file must fail
|
||||
if f, err := scoped.Open("/tenants/alice-archive/secret.txt"); err == nil {
|
||||
data, _ := io.ReadAll(f)
|
||||
f.Close()
|
||||
t.Fatalf("ACL for /tenants/alice read sibling /tenants/alice-archive: %q", data)
|
||||
}
|
||||
|
||||
// overwriting the sibling tenant's file must fail
|
||||
if f, err := scoped.Create("/tenants/alice-archive/secret.txt"); err == nil {
|
||||
f.Write([]byte("OVERWRITTEN-BY-SCOPED-USER"))
|
||||
f.Close()
|
||||
t.Fatal("ACL for /tenants/alice overwrote sibling /tenants/alice-archive")
|
||||
}
|
||||
|
||||
// the sibling file is intact
|
||||
f, err := admin.Open("/tenants/alice-archive/secret.txt")
|
||||
require.NoError(t, err)
|
||||
data, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
require.Equal(t, "SIBLING-SECRET", string(data))
|
||||
}
|
||||
49
test/sftp/testdata/userstore.json
vendored
Normal file
49
test/sftp/testdata/userstore.json
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
[
|
||||
{
|
||||
"Username": "admin",
|
||||
"Password": "adminpassword",
|
||||
"PublicKeys": [],
|
||||
"HomeDir": "/",
|
||||
"Permissions": {
|
||||
"/": ["*"]
|
||||
},
|
||||
"Uid": 0,
|
||||
"Gid": 0
|
||||
},
|
||||
{
|
||||
"Username": "testuser",
|
||||
"Password": "testuserpassword",
|
||||
"PublicKeys": [],
|
||||
"HomeDir": "/sftp/testuser",
|
||||
"Permissions": {
|
||||
"/sftp/testuser": ["*"]
|
||||
},
|
||||
"Uid": 1001,
|
||||
"Gid": 1001
|
||||
},
|
||||
{
|
||||
"Username": "readonly",
|
||||
"Password": "readonlypassword",
|
||||
"PublicKeys": [],
|
||||
"HomeDir": "/public",
|
||||
"Permissions": {
|
||||
"/public": ["read", "list"]
|
||||
},
|
||||
"Uid": 1002,
|
||||
"Gid": 1002
|
||||
},
|
||||
{
|
||||
"Username": "scoped",
|
||||
"Password": "scopedpassword",
|
||||
"PublicKeys": [],
|
||||
"HomeDir": "/",
|
||||
"Permissions": {
|
||||
"/tenants/alice": ["read", "write", "list"]
|
||||
},
|
||||
"Uid": 1003,
|
||||
"Gid": 1003
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user