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

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
package policy
// Tests for S3 bucket policy JSON round-trip idempotency.
// These validate behavior that IaC tools (Terraform, Ansible) depend on:
// PUT a policy, GET it back, and verify the JSON matches exactly.
// See https://github.com/seaweedfs/seaweedfs/issues/8657
import (
"encoding/json"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/stretchr/testify/require"
)
func waitForClusterReady(t *testing.T, s3Client *s3.S3) {
t.Helper()
// ListBuckets is a lightweight call that confirms the S3 API is serving.
_, err := s3Client.ListBuckets(&s3.ListBucketsInput{})
require.NoError(t, err, "S3 endpoint not ready")
}
func newS3ClientForCluster(t *testing.T, cluster *TestCluster) *s3.S3 {
t.Helper()
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Endpoint: aws.String(cluster.s3Endpoint),
DisableSSL: aws.Bool(true),
S3ForcePathStyle: aws.Bool(true),
Credentials: credentials.NewStaticCredentials("admin", "admin", ""),
})
require.NoError(t, err)
return s3.New(sess)
}
// TestBucketPolicyRoundTrip verifies that GetBucketPolicy returns exactly what
// was submitted via PutBucketPolicy, without adding spurious fields like
// "NotResource": null. This is the core issue from #8657.
func TestBucketPolicyRoundTrip(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
s3Client := newS3ClientForCluster(t, cluster)
waitForClusterReady(t, s3Client)
bucket := uniqueName("policy-rt")
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
tests := []struct {
name string
policy map[string]interface{}
}{
{
name: "simple allow without NotResource",
policy: map[string]interface{}{
"Version": "2012-10-17",
"Statement": []interface{}{
map[string]interface{}{
"Sid": "AllowPublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::" + bucket + "/*",
},
},
},
},
{
name: "multiple actions without NotResource",
policy: map[string]interface{}{
"Version": "2012-10-17",
"Statement": []interface{}{
map[string]interface{}{
"Sid": "ReadWrite",
"Effect": "Allow",
"Principal": "*",
"Action": []interface{}{"s3:GetObject", "s3:PutObject"},
"Resource": "arn:aws:s3:::" + bucket + "/*",
},
},
},
},
{
name: "allow with NotResource",
policy: map[string]interface{}{
"Version": "2012-10-17",
"Statement": []interface{}{
map[string]interface{}{
"Sid": "AllowOutsidePublic",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"NotResource": "arn:aws:s3:::" + bucket + "/private/*",
},
},
},
},
{
name: "multiple statements with NotResource",
policy: map[string]interface{}{
"Version": "2012-10-17",
"Statement": []interface{}{
map[string]interface{}{
"Sid": "AllowRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::" + bucket + "/*",
},
map[string]interface{}{
"Sid": "DenyPrivateObjects",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"NotResource": "arn:aws:s3:::" + bucket + "/public/*",
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
policyJSON, err := json.Marshal(tc.policy)
require.NoError(t, err)
_, err = s3Client.PutBucketPolicy(&s3.PutBucketPolicyInput{
Bucket: aws.String(bucket),
Policy: aws.String(string(policyJSON)),
})
require.NoError(t, err)
getOut, err := s3Client.GetBucketPolicy(&s3.GetBucketPolicyInput{
Bucket: aws.String(bucket),
})
require.NoError(t, err)
require.NotNil(t, getOut.Policy)
// The returned policy must not contain fields that were not submitted.
// This is the exact issue from #8657: NotResource:null was being added.
returnedJSON := *getOut.Policy
var returnedPolicy map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(returnedJSON), &returnedPolicy))
if !hasKey(tc.policy, "NotResource") {
require.False(t, hasKey(returnedPolicy, "NotResource"),
"returned policy must not contain NotResource when it was not submitted")
}
// Semantic comparison of all submitted fields
require.JSONEq(t, string(policyJSON), returnedJSON,
"GET should return semantically identical policy to what was PUT")
})
}
}
// TestBucketPolicyIdempotentPut verifies that putting the same policy twice
// does not change the returned value — the behavior IaC tools rely on.
func TestBucketPolicyIdempotentPut(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
s3Client := newS3ClientForCluster(t, cluster)
waitForClusterReady(t, s3Client)
bucket := uniqueName("policy-idem")
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
policyJSON := `{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::` + bucket + `/*"
}]
}`
// PUT, then GET, then PUT the returned value, then GET again.
// Both GETs should return the same result.
_, err = s3Client.PutBucketPolicy(&s3.PutBucketPolicyInput{
Bucket: aws.String(bucket),
Policy: aws.String(policyJSON),
})
require.NoError(t, err)
getOut1, err := s3Client.GetBucketPolicy(&s3.GetBucketPolicyInput{
Bucket: aws.String(bucket),
})
require.NoError(t, err)
// Re-PUT the policy that was returned by GET (what Terraform does on update)
_, err = s3Client.PutBucketPolicy(&s3.PutBucketPolicyInput{
Bucket: aws.String(bucket),
Policy: getOut1.Policy,
})
require.NoError(t, err)
getOut2, err := s3Client.GetBucketPolicy(&s3.GetBucketPolicyInput{
Bucket: aws.String(bucket),
})
require.NoError(t, err)
require.JSONEq(t, *getOut1.Policy, *getOut2.Policy,
"re-PUTting the GET result must produce identical output (idempotency)")
}
// TestBucketPolicyDeleteAndRecreate verifies clean lifecycle.
func TestBucketPolicyDeleteAndRecreate(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
s3Client := newS3ClientForCluster(t, cluster)
waitForClusterReady(t, s3Client)
bucket := uniqueName("policy-del")
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
policyJSON := `{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::` + bucket + `/*"
}]
}`
// PUT
_, err = s3Client.PutBucketPolicy(&s3.PutBucketPolicyInput{
Bucket: aws.String(bucket),
Policy: aws.String(policyJSON),
})
require.NoError(t, err)
// DELETE
_, err = s3Client.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{
Bucket: aws.String(bucket),
})
require.NoError(t, err)
// GET should fail with NoSuchBucketPolicy
_, err = s3Client.GetBucketPolicy(&s3.GetBucketPolicyInput{
Bucket: aws.String(bucket),
})
require.Error(t, err)
awsErr, ok := err.(awserr.Error)
require.True(t, ok, "expected AWS error, got %T: %v", err, err)
require.Equal(t, "NoSuchBucketPolicy", awsErr.Code())
// Re-PUT same policy
_, err = s3Client.PutBucketPolicy(&s3.PutBucketPolicyInput{
Bucket: aws.String(bucket),
Policy: aws.String(policyJSON),
})
require.NoError(t, err)
// GET should succeed and be clean
getOut, err := s3Client.GetBucketPolicy(&s3.GetBucketPolicyInput{
Bucket: aws.String(bucket),
})
require.NoError(t, err)
var recreatedPolicy map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(*getOut.Policy), &recreatedPolicy))
require.False(t, hasKey(recreatedPolicy, "NotResource"),
"recreated policy must not contain spurious NotResource")
}
// hasKey checks whether any Statement in the policy map contains the given key.
// Handles both single-statement objects and arrays of statements.
func hasKey(policy map[string]interface{}, key string) bool {
stmtsRaw, ok := policy["Statement"]
if !ok {
return false
}
// Single statement object
if stmt, ok := stmtsRaw.(map[string]interface{}); ok {
_, exists := stmt[key]
return exists
}
// Array of statements
if stmts, ok := stmtsRaw.([]interface{}); ok {
for _, s := range stmts {
stmt, ok := s.(map[string]interface{})
if !ok {
continue
}
if _, exists := stmt[key]; exists {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,945 @@
package policy
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/seaweedfs/seaweedfs/test/testutil"
"github.com/seaweedfs/seaweedfs/test/volume_server/framework"
"github.com/seaweedfs/seaweedfs/weed/command"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
flag "github.com/seaweedfs/seaweedfs/weed/util/fla9"
"github.com/stretchr/testify/require"
)
// TestCluster manages the weed mini instance for integration testing
type TestCluster struct {
dataDir string
ctx context.Context
cancel context.CancelFunc
isRunning bool
wg sync.WaitGroup
masterPort int
masterGrpcPort int
volumePort int
filerPort int
filerGrpcPort int
s3Port int
s3Endpoint string
rustVolumeCmd *exec.Cmd
}
func TestS3PolicyShellRevised(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
policyContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}`
tmpPolicyFile, err := os.CreateTemp("", "test_policy_*.json")
if err != nil {
t.Fatalf("Failed to create temp policy file: %v", err)
}
defer os.Remove(tmpPolicyFile.Name())
_, err = tmpPolicyFile.WriteString(policyContent)
require.NoError(t, err)
require.NoError(t, tmpPolicyFile.Close())
weedCmd := "weed"
masterAddr := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filerAddr := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
// Put
execShell(t, weedCmd, masterAddr, filerAddr, fmt.Sprintf("s3.policy -put -name=testpolicy -file=%s", tmpPolicyFile.Name()))
// List
out := execShell(t, weedCmd, masterAddr, filerAddr, "s3.policy -list")
if !contains(out, "Name: testpolicy") {
t.Errorf("List failed: %s", out)
}
// Get
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.policy -get -name=testpolicy")
if !contains(out, "Statement") {
t.Errorf("Get failed: %s", out)
}
// Delete
execShell(t, weedCmd, masterAddr, filerAddr, "s3.policy -delete -name=testpolicy")
// Verify
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.policy -list")
if contains(out, "Name: testpolicy") {
t.Errorf("delete failed, policy 'testpolicy' should not be in the list: %s", out)
}
// Verify s3.configure linking policies
execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure -user=test -actions=Read -policies=testpolicy -apply")
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure")
if !contains(out, "\"policyNames\": [\n \"testpolicy\"\n ]") {
// relaxed check
if !contains(out, "\"testpolicy\"") || !contains(out, "policyNames") {
t.Errorf("s3.configure failed to link policy: %s", out)
}
}
// 1. Update User: Add Write action
execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure -user=test -actions=Write -apply")
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure")
if !contains(out, "Write") {
t.Errorf("s3.configure failed to add Write action: %s", out)
}
// 2. Granular Delete: Delete Read action
execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure -user=test -actions=Read -delete -apply")
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure")
if contains(out, "\"Read\"") { // Quote to avoid matching partial words if any
t.Errorf("s3.configure failed to delete Read action: %s", out)
}
if !contains(out, "Write") {
t.Errorf("s3.configure deleted Write action unnecessarily: %s", out)
}
// 3. Access Key Management
execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure -user=test -access_key=testkey -secret_key=testsecret -apply")
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure")
if !contains(out, "testkey") {
t.Errorf("s3.configure failed to add access key: %s", out)
}
execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure -user=test -access_key=testkey -delete -apply")
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure")
if contains(out, "testkey") {
t.Errorf("s3.configure failed to delete access key: %s", out)
}
// 4. Delete User
execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure -user=test -delete -apply")
out = execShell(t, weedCmd, masterAddr, filerAddr, "s3.configure")
if contains(out, "\"Name\": \"test\"") {
t.Errorf("s3.configure failed to delete user: %s", out)
}
}
func TestS3IAMAttachDetachUserPolicy(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
time.Sleep(500 * time.Millisecond)
policyName := uniqueName("managed-policy")
policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
policyContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}`
tmpPolicyFile, err := os.CreateTemp("", "test_policy_attach_*.json")
require.NoError(t, err)
defer os.Remove(tmpPolicyFile.Name())
_, err = tmpPolicyFile.WriteString(policyContent)
require.NoError(t, err)
require.NoError(t, tmpPolicyFile.Close())
weedCmd := "weed"
masterAddr := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filerAddr := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
execShell(t, weedCmd, masterAddr, filerAddr, fmt.Sprintf("s3.policy -put -name=%s -file=%s", policyName, tmpPolicyFile.Name()))
iamClient := newIAMClient(t, cluster.s3Endpoint)
userName := uniqueName("iam-user")
_, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
require.NoError(t, err)
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(policyArn),
})
require.NoError(t, err)
listOut, err := iamClient.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{
UserName: aws.String(userName),
})
require.NoError(t, err)
require.True(t, attachedPolicyContains(listOut.AttachedPolicies, policyName))
_, err = iamClient.DetachUserPolicy(&iam.DetachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(policyArn),
})
require.NoError(t, err)
listOut, err = iamClient.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{
UserName: aws.String(userName),
})
require.NoError(t, err)
require.False(t, attachedPolicyContains(listOut.AttachedPolicies, policyName))
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String("arn:aws:iam:::policy/does-not-exist"),
})
require.Error(t, err)
if awsErr, ok := err.(awserr.Error); ok {
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
}
}
func TestS3IAMListPoliciesAndGetPolicy(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
time.Sleep(500 * time.Millisecond)
policyName := uniqueName("managed-policy")
policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
policyContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListAllMyBuckets","Resource":"*"}]}`
iamClient := newIAMClient(t, cluster.s3Endpoint)
_, err = iamClient.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(policyContent),
})
require.NoError(t, err)
listOut, err := iamClient.ListPolicies(&iam.ListPoliciesInput{})
require.NoError(t, err)
require.True(t, managedPolicyContains(listOut.Policies, policyName))
getOut, err := iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(policyArn)})
require.NoError(t, err)
require.NotNil(t, getOut.Policy)
require.NotNil(t, getOut.Policy.PolicyName)
require.Equal(t, policyName, *getOut.Policy.PolicyName)
missingArn := fmt.Sprintf("arn:aws:iam:::policy/%s", uniqueName("missing"))
_, err = iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(missingArn)})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
}
func TestS3IAMDeletePolicyInUse(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
time.Sleep(500 * time.Millisecond)
policyName := uniqueName("managed-delete-policy")
policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
policyContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}`
iamClient := newIAMClient(t, cluster.s3Endpoint)
_, err = iamClient.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(policyContent),
})
require.NoError(t, err)
userName := uniqueName("iam-user-delete-policy")
_, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
require.NoError(t, err)
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(policyArn),
})
require.NoError(t, err)
_, err = iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: aws.String(policyArn)})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeDeleteConflictException, awsErr.Code())
}
func TestS3MultipartOperationsInheritPutObjectPermissions(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
time.Sleep(500 * time.Millisecond)
// Create a policy with only s3:PutObject permission
// This should implicitly allow multipart upload operations (CreateMultipartUpload,
// UploadPart, CompleteMultipartUpload, AbortMultipartUpload, ListBucketMultipartUploads, ListMultipartUploadParts)
policyName := uniqueName("putobject-policy")
policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
policyContent := `{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Action":"s3:PutObject",
"Resource":"*"
}]
}`
iamClient := newIAMClient(t, cluster.s3Endpoint)
_, err = iamClient.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(policyContent),
})
require.NoError(t, err)
// Create a user and attach the policy
userName := uniqueName("multipart-user")
_, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
require.NoError(t, err)
_, err = iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(userName)})
require.NoError(t, err)
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(policyArn),
})
require.NoError(t, err)
// Create S3 client with user credentials (using admin credentials for now, test will still validate permissions)
// In a real scenario, we'd use the user's access key
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Endpoint: aws.String(cluster.s3Endpoint),
DisableSSL: aws.Bool(true),
S3ForcePathStyle: aws.Bool(true),
Credentials: credentials.NewStaticCredentials("admin", "admin", ""),
})
require.NoError(t, err)
s3Client := newS3Client(sess)
bucketName := uniqueName("multipart-test-bucket")
objectKey := "test-object"
// Create bucket
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(bucketName)})
require.NoError(t, err)
// Test multipart upload operations with s3:PutObject permission
// These operations should all succeed since multipart operations are part of s3:PutObject
// 1. CreateMultipartUpload
createOut, err := s3Client.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
require.NoError(t, err)
require.NotNil(t, createOut.UploadId)
uploadID := *createOut.UploadId
// 2. UploadPart
partBody := "test part data"
uploadPartOut, err := s3Client.UploadPart(&s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
PartNumber: aws.Int64(1),
UploadId: aws.String(uploadID),
Body: strings.NewReader(partBody),
})
require.NoError(t, err)
require.NotNil(t, uploadPartOut.ETag)
// 3. ListParts
listPartsOut, err := s3Client.ListParts(&s3.ListPartsInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
})
require.NoError(t, err)
require.Equal(t, 1, len(listPartsOut.Parts))
// 4. CompleteMultipartUpload
completeOut, err := s3Client.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
UploadId: aws.String(uploadID),
MultipartUpload: &s3.CompletedMultipartUpload{
Parts: []*s3.CompletedPart{
{
ETag: uploadPartOut.ETag,
PartNumber: aws.Int64(1),
},
},
},
})
require.NoError(t, err)
require.NotNil(t, completeOut.ETag)
// Test AbortMultipartUpload with a new upload
createOut2, err := s3Client.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey + "-abort"),
})
require.NoError(t, err)
uploadID2 := *createOut2.UploadId
_, err = s3Client.AbortMultipartUpload(&s3.AbortMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey + "-abort"),
UploadId: aws.String(uploadID2),
})
require.NoError(t, err)
// Test ListMultipartUploads
listUploadsOut, err := s3Client.ListMultipartUploads(&s3.ListMultipartUploadsInput{
Bucket: aws.String(bucketName),
})
require.NoError(t, err)
// After aborting, there should be no active uploads
require.Equal(t, 0, len(listUploadsOut.Uploads))
}
// TestS3IAMManagedPolicyLifecycle is an end-to-end integration test covering the
// user-reported use case in https://github.com/seaweedfs/seaweedfs/issues/8506
// where managed policy operations (GetPolicy, ListPolicies, DeletePolicy,
// AttachUserPolicy, DetachUserPolicy) returned 500 errors.
func TestS3IAMManagedPolicyLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
iamClient := newIAMClient(t, cluster.s3Endpoint)
// Step 1: Create a user (this already worked per the issue)
userName := uniqueName("lifecycle-user")
_, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
require.NoError(t, err, "CreateUser should succeed")
// Step 2: Create a managed policy via IAM API
policyName := uniqueName("lifecycle-policy")
policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
policyDoc := `{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::*"
}]
}`
createOut, err := iamClient.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(policyDoc),
})
require.NoError(t, err, "CreatePolicy should succeed")
require.NotNil(t, createOut.Policy)
require.Equal(t, policyName, *createOut.Policy.PolicyName)
// Step 3: ListPolicies — should include the created policy (was returning 500)
listOut, err := iamClient.ListPolicies(&iam.ListPoliciesInput{})
require.NoError(t, err, "ListPolicies should succeed (was returning 500)")
require.True(t, managedPolicyContains(listOut.Policies, policyName),
"ListPolicies should contain the newly created policy")
// Step 4: GetPolicy by ARN — should return the policy (was returning 500)
getOut, err := iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(policyArn)})
require.NoError(t, err, "GetPolicy should succeed (was returning 500)")
require.NotNil(t, getOut.Policy)
require.Equal(t, policyName, *getOut.Policy.PolicyName)
require.Equal(t, policyArn, *getOut.Policy.Arn)
// Step 5: AttachUserPolicy — should succeed (was returning 500)
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(policyArn),
})
require.NoError(t, err, "AttachUserPolicy should succeed (was returning 500)")
// Step 6: ListAttachedUserPolicies — verify the policy is attached
attachedOut, err := iamClient.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{
UserName: aws.String(userName),
})
require.NoError(t, err, "ListAttachedUserPolicies should succeed")
require.True(t, attachedPolicyContains(attachedOut.AttachedPolicies, policyName),
"Policy should appear in user's attached policies")
// Step 7: Idempotent re-attach should not fail
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(policyArn),
})
require.NoError(t, err, "Re-attaching same policy should be idempotent")
// Step 8: DeletePolicy while attached — should fail with DeleteConflict (AWS behavior)
_, err = iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: aws.String(policyArn)})
require.Error(t, err, "DeletePolicy should fail while policy is attached")
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeDeleteConflictException, awsErr.Code(),
"Should return DeleteConflict when deleting attached policy")
// Step 9: DetachUserPolicy
_, err = iamClient.DetachUserPolicy(&iam.DetachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(policyArn),
})
require.NoError(t, err, "DetachUserPolicy should succeed")
// Verify detached
attachedOut, err = iamClient.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{
UserName: aws.String(userName),
})
require.NoError(t, err)
require.False(t, attachedPolicyContains(attachedOut.AttachedPolicies, policyName),
"Policy should no longer appear in user's attached policies after detach")
// Step 10: DeletePolicy — should now succeed (was returning XML parsing error)
_, err = iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: aws.String(policyArn)})
require.NoError(t, err, "DeletePolicy should succeed after detach (was returning XML parsing error)")
// Step 11: Verify the policy is gone
listOut, err = iamClient.ListPolicies(&iam.ListPoliciesInput{})
require.NoError(t, err)
require.False(t, managedPolicyContains(listOut.Policies, policyName),
"Deleted policy should not appear in ListPolicies")
_, err = iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(policyArn)})
require.Error(t, err, "GetPolicy should fail for deleted policy")
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
}
// TestS3IAMManagedPolicyErrorCases covers error cases from the user-reported issue:
// invalid ARNs, missing policies, and missing users.
func TestS3IAMManagedPolicyErrorCases(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
iamClient := newIAMClient(t, cluster.s3Endpoint)
t.Run("GetPolicy with nonexistent ARN returns NoSuchEntity", func(t *testing.T) {
_, err := iamClient.GetPolicy(&iam.GetPolicyInput{
PolicyArn: aws.String("arn:aws:iam:::policy/does-not-exist"),
})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
})
t.Run("DeletePolicy with nonexistent ARN returns NoSuchEntity", func(t *testing.T) {
_, err := iamClient.DeletePolicy(&iam.DeletePolicyInput{
PolicyArn: aws.String("arn:aws:iam:::policy/does-not-exist"),
})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
})
t.Run("AttachUserPolicy with nonexistent policy returns NoSuchEntity", func(t *testing.T) {
userName := uniqueName("err-user")
_, err := iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
require.NoError(t, err)
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String("arn:aws:iam:::policy/does-not-exist"),
})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
})
t.Run("AttachUserPolicy with nonexistent user returns NoSuchEntity", func(t *testing.T) {
policyName := uniqueName("err-policy")
_, err := iamClient.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`),
})
require.NoError(t, err)
_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: aws.String("nonexistent-user"),
PolicyArn: aws.String(fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)),
})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
})
t.Run("DetachUserPolicy that is not attached returns NoSuchEntity", func(t *testing.T) {
userName := uniqueName("detach-user")
_, err := iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
require.NoError(t, err)
policyName := uniqueName("detach-policy")
_, err = iamClient.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`),
})
require.NoError(t, err)
_, err = iamClient.DetachUserPolicy(&iam.DetachUserPolicyInput{
UserName: aws.String(userName),
PolicyArn: aws.String(fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)),
})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
})
t.Run("ListAttachedUserPolicies for nonexistent user returns NoSuchEntity", func(t *testing.T) {
_, err := iamClient.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{
UserName: aws.String("nonexistent-user"),
})
require.Error(t, err)
var awsErr awserr.Error
require.True(t, errors.As(err, &awsErr))
require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
})
}
func execShell(t *testing.T, weedCmd, master, filer, shellCmd string) string {
// weed shell -master=... -filer=...
args := []string{"shell", "-master=" + master, "-filer=" + filer}
t.Logf("Running: %s %v <<< %s", weedCmd, args, shellCmd)
cmd := exec.Command(weedCmd, args...)
cmd.Stdin = strings.NewReader(shellCmd + "\n")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("Failed to run %s: %v\nOutput: %s", shellCmd, err, string(out))
}
return string(out)
}
func newIAMClient(t *testing.T, endpoint string) *iam.IAM {
t.Helper()
accessKey := os.Getenv("AWS_ACCESS_KEY_ID")
secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
if accessKey == "" {
accessKey = "admin"
}
if secretKey == "" {
secretKey = "admin"
}
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Endpoint: aws.String(endpoint),
DisableSSL: aws.Bool(true),
S3ForcePathStyle: aws.Bool(true),
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
})
require.NoError(t, err)
return iam.New(sess)
}
func newS3Client(sess *session.Session) *s3.S3 {
return s3.New(sess)
}
func attachedPolicyContains(policies []*iam.AttachedPolicy, policyName string) bool {
for _, policy := range policies {
if policy.PolicyName != nil && *policy.PolicyName == policyName {
return true
}
}
return false
}
func managedPolicyContains(policies []*iam.Policy, policyName string) bool {
for _, policy := range policies {
if policy.PolicyName != nil && *policy.PolicyName == policyName {
return true
}
}
return false
}
func uniqueName(prefix string) string {
return fmt.Sprintf("%s-%s", prefix, strconv.FormatInt(time.Now().UnixNano(), 36))
}
// --- Test setup helpers ---
func startMiniCluster(t *testing.T) (*TestCluster, error) {
// Allocate two extra ports for admin HTTP + gRPC. Without explicit
// values mini falls back to 23646/33646, which collide across the
// sequential subtests in this package: each subtest spins up a fresh
// mini in a goroutine, but the previous mini's admin goroutine is
// still binding 23646 — so the next test's admin can never become
// ready and mini.go fatals on its readiness check.
ports := testutil.MustAllocatePorts(t, 10)
masterPort, masterGrpcPort := ports[0], ports[1]
volumePort, volumeGrpcPort := ports[2], ports[3]
filerPort, filerGrpcPort := ports[4], ports[5]
s3Port, s3GrpcPort := ports[6], ports[7]
adminPort, adminGrpcPort := ports[8], ports[9]
// Manually-managed temp dir (not t.TempDir()) so we control removal order:
// the dir is removed inside Stop() AFTER the mini goroutine has fully
// exited. Otherwise t.TempDir()'s RemoveAll cleanup races the admin
// plugin worker, which keeps creating files under admin/plugin/job_types/
// for ~1s after subtests finish, producing flaky "directory not empty"
// failures (see seaweedfs CI run 25352039081).
testDir, err := os.MkdirTemp("", "seaweed-policy-mini-")
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
s3Endpoint := fmt.Sprintf("http://127.0.0.1:%d", s3Port)
cluster := &TestCluster{
dataDir: testDir,
ctx: ctx,
cancel: cancel,
masterPort: masterPort,
masterGrpcPort: masterGrpcPort,
volumePort: volumePort,
filerPort: filerPort,
filerGrpcPort: filerGrpcPort,
s3Port: s3Port,
s3Endpoint: s3Endpoint,
}
// Disable authentication for tests
securityToml := filepath.Join(testDir, "security.toml")
err = os.WriteFile(securityToml, []byte("# Empty security config\n"), 0644)
require.NoError(t, err)
// Configure credential store for IAM tests.
// Use filer_etc instead of memory because the memory store does not
// persist groups or service accounts through LoadConfiguration/SaveConfiguration.
credentialToml := filepath.Join(testDir, "credential.toml")
credentialConfig := `
[credential.filer_etc]
enabled = true
`
err = os.WriteFile(credentialToml, []byte(credentialConfig), 0644)
require.NoError(t, err)
// Set environment variables for admin credentials safely for this test
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
t.Setenv("AWS_ACCESS_KEY_ID", "admin")
}
if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
t.Setenv("AWS_SECRET_ACCESS_KEY", "admin")
}
cluster.wg.Add(1)
go func() {
defer cluster.wg.Done()
oldDir, _ := os.Getwd()
oldArgs := os.Args
defer func() {
os.Chdir(oldDir)
os.Args = oldArgs
}()
os.Chdir(testDir)
os.Args = []string{
"weed",
"-dir=" + testDir,
"-master.port=" + strconv.Itoa(masterPort),
"-master.port.grpc=" + strconv.Itoa(masterGrpcPort),
"-volume.port=" + strconv.Itoa(volumePort),
"-volume.port.grpc=" + strconv.Itoa(volumeGrpcPort),
"-filer.port=" + strconv.Itoa(filerPort),
"-filer.port.grpc=" + strconv.Itoa(filerGrpcPort),
"-s3.port=" + strconv.Itoa(s3Port),
"-s3.port.grpc=" + strconv.Itoa(s3GrpcPort),
"-admin.port=" + strconv.Itoa(adminPort),
"-admin.port.grpc=" + strconv.Itoa(adminGrpcPort),
"-webdav.port=0",
"-admin.ui=false",
"-master.volumeSizeLimitMB=32",
"-ip=127.0.0.1",
"-master.peers=none",
"-s3.iam.readOnly=false",
}
glog.MaxSize = 1024 * 1024
for _, cmd := range command.Commands {
if cmd.Name() == "mini" && cmd.Run != nil {
cmd.Flag.Parse(os.Args[1:])
// MiniClusterCtx makes runMini observe our cancel: master/
// volume/filer get this as their shutdownCtx, and the clients
// state (admin/s3/webdav/plugin worker) chains from it.
command.MiniClusterCtx = ctx
cmd.Run(cmd, cmd.Flag.Args())
command.MiniClusterCtx = nil
return
}
}
}()
// Wait for S3
if !testutil.WaitForService(cluster.s3Endpoint, 60*time.Second) {
cancel()
cluster.wg.Wait()
os.RemoveAll(testDir)
return nil, fmt.Errorf("timeout waiting for S3 at %s", cluster.s3Endpoint)
}
// If VOLUME_SERVER_IMPL=rust, start a Rust volume server alongside weed mini
if os.Getenv("VOLUME_SERVER_IMPL") == "rust" {
if err := cluster.startRustVolumeServer(t); err != nil {
cancel()
cluster.wg.Wait()
os.RemoveAll(testDir)
return nil, fmt.Errorf("failed to start Rust volume server: %v", err)
}
}
cluster.isRunning = true
return cluster, nil
}
// startRustVolumeServer starts a Rust volume server that registers with the same master.
func (c *TestCluster) startRustVolumeServer(t *testing.T) error {
t.Helper()
rustBinary, err := framework.FindOrBuildRustBinary()
if err != nil {
return fmt.Errorf("resolve rust volume binary: %v", err)
}
rustPorts, err := testutil.AllocatePorts(2)
if err != nil {
return fmt.Errorf("find rust volume ports: %v", err)
}
rustVolumePort := rustPorts[0]
rustVolumeGrpcPort := rustPorts[1]
rustVolumeDir := filepath.Join(c.dataDir, "rust-volume")
if err := os.MkdirAll(rustVolumeDir, 0o755); err != nil {
return fmt.Errorf("create rust volume dir: %v", err)
}
securityToml := filepath.Join(c.dataDir, "security.toml")
args := []string{
"--port", strconv.Itoa(rustVolumePort),
"--port.grpc", strconv.Itoa(rustVolumeGrpcPort),
"--port.public", strconv.Itoa(rustVolumePort),
"--ip", "127.0.0.1",
"--ip.bind", "127.0.0.1",
"--dir", rustVolumeDir,
"--max", "16",
"--master", "127.0.0.1:" + strconv.Itoa(c.masterPort),
"--securityFile", securityToml,
"--preStopSeconds", "0",
}
logFile, err := os.Create(filepath.Join(c.dataDir, "rust-volume.log"))
if err != nil {
return fmt.Errorf("create rust volume log: %v", err)
}
c.rustVolumeCmd = exec.Command(rustBinary, args...)
c.rustVolumeCmd.Dir = c.dataDir
c.rustVolumeCmd.Stdout = logFile
c.rustVolumeCmd.Stderr = logFile
if err := c.rustVolumeCmd.Start(); err != nil {
logFile.Close()
return fmt.Errorf("start rust volume: %v", err)
}
rustEndpoint := fmt.Sprintf("http://127.0.0.1:%d/healthz", rustVolumePort)
deadline := time.Now().Add(15 * time.Second)
client := &http.Client{Timeout: 1 * time.Second}
for time.Now().Before(deadline) {
resp, err := client.Get(rustEndpoint)
if err == nil {
resp.Body.Close()
t.Logf("Rust volume server ready on port %d (grpc %d)", rustVolumePort, rustVolumeGrpcPort)
return nil
}
time.Sleep(200 * time.Millisecond)
}
return fmt.Errorf("rust volume server not ready after 15s (port %d)", rustVolumePort)
}
func (c *TestCluster) Stop() {
// Stop Rust volume server first
if c.rustVolumeCmd != nil && c.rustVolumeCmd.Process != nil {
c.rustVolumeCmd.Process.Kill()
c.rustVolumeCmd.Wait()
}
if c.cancel != nil {
c.cancel()
}
// Wait for the mini goroutine to fully return before removing the data
// dir. runMini observes MiniClusterCtx and returns once cancel fires; the
// admin/s3/webdav/plugin-worker shutdown is gated by the same ctx via
// resetMiniClients. The deadline is generous because admin shutdown can
// take several seconds when graceful-stops are draining.
done := make(chan struct{})
go func() {
c.wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(15 * time.Second):
fmt.Println("Warning: TestCluster.Stop timed out waiting for mini goroutine")
}
// Reset all mini flags so a subsequent in-process startMiniCluster sees
// fresh state.
for _, cmd := range command.Commands {
if cmd.Name() == "mini" {
cmd.Flag.VisitAll(func(f *flag.Flag) {
f.Value.Set(f.DefValue)
})
break
}
}
if c.dataDir != "" {
os.RemoveAll(c.dataDir)
}
}
func contains(s, substr string) bool {
return strings.Contains(s, substr)
}

View File

@@ -0,0 +1,75 @@
package policy
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellAccessKeyLifecycle exercises s3.accesskey.* commands end-to-end.
func TestShellAccessKeyLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
userName := uniqueName("akuser")
// Create user with explicit key so we know the initial value.
initialAK := "INITIALAK1234567890X"
initialSK := "initialsecret1234567890abcdefghijklmnop"
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.user.create -name %s -access_key %s -secret_key %s", userName, initialAK, initialSK))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
t.Run("ListInitialKey", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireContains(t, out, initialAK, "accesskey.list initial")
})
var createdAK string
t.Run("CreateAdditionalKey", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.create -user %s", userName))
requireContains(t, out, "Access Key:", "accesskey.create output")
requireContains(t, out, "Secret Key:", "accesskey.create output")
createdAK = extractFieldAfter(out, "Access Key:")
if createdAK == "" {
t.Fatalf("failed to extract access key from create output:\n%s", out)
}
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireContains(t, out, initialAK, "list contains original")
requireContains(t, out, createdAK, "list contains new key")
})
t.Run("RotateKey", func(t *testing.T) {
if createdAK == "" {
t.Fatal("createdAK is empty; CreateAdditionalKey must run successfully first")
}
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.accesskey.rotate -user %s -access_key %s", userName, initialAK))
requireContains(t, out, initialAK, "rotate shows old key")
requireContains(t, out, "deleted", "rotate marks old key deleted")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireNotContains(t, out, initialAK, "old key removed")
requireContains(t, out, createdAK, "other key still present")
})
t.Run("DeleteKey", func(t *testing.T) {
if createdAK == "" {
t.Fatal("createdAK is empty; CreateAdditionalKey must run successfully first")
}
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.accesskey.delete -user %s -access_key %s", userName, createdAK))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireNotContains(t, out, createdAK, "deleted key removed from list")
})
}

View File

@@ -0,0 +1,54 @@
package policy
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellAnonymousAccess exercises s3.anonymous.* commands end-to-end.
func TestShellAnonymousAccess(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
bucketName := uniqueName("anon-bkt")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucketName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
t.Run("SetAndGet", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.anonymous.set -bucket %s -access Read,List", bucketName))
requireContains(t, out, bucketName, "anonymous.set output")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.anonymous.get -bucket %s", bucketName))
requireContains(t, out, bucketName, "anonymous.get bucket")
requireContains(t, out, "Read", "anonymous.get read action")
requireContains(t, out, "List", "anonymous.get list action")
})
t.Run("List", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer, "s3.anonymous.list")
requireContains(t, out, bucketName, "anonymous.list contains bucket")
})
t.Run("SetNone", func(t *testing.T) {
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.anonymous.set -bucket %s -access none", bucketName))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.anonymous.get -bucket %s", bucketName))
requireContains(t, out, "none", "anonymous.get after set none")
out = execShell(t, weedCmd, master, filer, "s3.anonymous.list")
requireNotContains(t, out, bucketName, "anonymous.list after clearing")
})
}

View File

@@ -0,0 +1,128 @@
package policy
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellBucketLifecycle exercises s3.bucket.* commands end-to-end:
// create/list/delete, owner, quota, versioning, lock, quota.enforce.
func TestShellBucketLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
t.Run("CreateListDelete", func(t *testing.T) {
bucketName := uniqueName("bkt")
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucketName))
requireContains(t, out, bucketName, "bucket.create output")
out = execShell(t, weedCmd, master, filer, "s3.bucket.list")
requireContains(t, out, bucketName, "bucket.list contains created")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
out = execShell(t, weedCmd, master, filer, "s3.bucket.list")
requireNotContains(t, out, bucketName, "bucket.list after delete")
})
t.Run("Owner", func(t *testing.T) {
bucketName := uniqueName("bkt-own")
ownerName := uniqueName("owner")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", ownerName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", ownerName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucketName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
// Initially no owner.
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.owner -name %s", bucketName))
requireContains(t, out, "none", "initial owner none")
// Set owner.
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.bucket.owner -name %s -owner %s", bucketName, ownerName))
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.owner -name %s", bucketName))
requireContains(t, out, ownerName, "owner set")
// Remove owner.
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.owner -name %s -delete", bucketName))
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.owner -name %s", bucketName))
requireContains(t, out, "none", "owner removed")
})
t.Run("Quota", func(t *testing.T) {
bucketName := uniqueName("bkt-quota")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucketName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.bucket.quota -name %s -op=set -sizeMB=1024", bucketName))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.quota -name %s -op=get", bucketName))
requireContains(t, out, "1024", "quota.get shows size")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.quota -name %s -op=disable", bucketName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.quota -name %s -op=enable", bucketName))
// Enforce should run on an empty bucket without error.
execShell(t, weedCmd, master, filer, "s3.bucket.quota.enforce -apply")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.quota -name %s -op=remove", bucketName))
})
t.Run("Versioning", func(t *testing.T) {
bucketName := uniqueName("bkt-ver")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucketName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.versioning -name %s -enable", bucketName))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.versioning -name %s", bucketName))
requireContains(t, out, "Enabled", "versioning enabled")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.versioning -name %s -suspend", bucketName))
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.versioning -name %s", bucketName))
requireContains(t, out, "Suspended", "versioning suspended")
})
t.Run("Lock", func(t *testing.T) {
bucketName := uniqueName("bkt-lock")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucketName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.lock -name %s", bucketName))
requireContains(t, out, "Disabled", "lock initially disabled")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.lock -name %s -enable", bucketName))
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.lock -name %s", bucketName))
requireContains(t, out, "Enabled", "lock enabled")
// Versioning should have been auto-enabled.
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.versioning -name %s", bucketName))
requireContains(t, out, "Enabled", "versioning auto-enabled by lock")
})
t.Run("CreateWithLock", func(t *testing.T) {
bucketName := uniqueName("bkt-wlock")
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.bucket.create -name %s -withLock", bucketName))
// Cleanup may fail if the bucket contains locked objects; we created none.
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
requireContains(t, out, "Object Lock", "create -withLock output")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.lock -name %s", bucketName))
requireContains(t, out, "Enabled", "lock enabled after create -withLock")
})
}

View File

@@ -0,0 +1,86 @@
package policy
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellConfigShow verifies s3.config.show outputs a summary of IAM config.
func TestShellConfigShow(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
userName := uniqueName("cfg-user")
groupName := uniqueName("cfg-grp")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", userName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.create -name %s", groupName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.delete -name %s", groupName))
out := execShell(t, weedCmd, master, filer, "s3.config.show")
requireContains(t, out, "S3 IAM Configuration Summary", "config.show header")
requireContains(t, out, userName, "config.show contains user")
requireContains(t, out, groupName, "config.show contains group")
}
// TestShellIAMExportImport does a roundtrip: create resources, export, delete, import, verify.
func TestShellIAMExportImport(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
userName := uniqueName("exp-user")
groupName := uniqueName("exp-grp")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", userName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.create -name %s", groupName))
exportFile := filepath.Join(t.TempDir(), "iam_export.txt")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.iam.export -file %s", exportFile))
data, err := os.ReadFile(exportFile)
require.NoError(t, err)
content := string(data)
requireContains(t, content, userName, "export file contains user")
requireContains(t, content, groupName, "export file contains group")
// Delete the resources.
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.delete -name %s", groupName))
out := execShell(t, weedCmd, master, filer, "s3.user.list")
requireNotContains(t, out, userName, "user gone before import")
// Import.
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.iam.import -file %s -apply", exportFile))
requireContains(t, out, "Imported IAM configuration", "import output")
// Verify resources restored.
out = execShell(t, weedCmd, master, filer, "s3.user.list")
requireContains(t, out, userName, "user restored after import")
out = execShell(t, weedCmd, master, filer, "s3.group.list")
requireContains(t, out, groupName, "group restored after import")
// Cleanup.
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.delete -name %s", groupName))
}

View File

@@ -0,0 +1,62 @@
package policy
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellGroupLifecycle exercises s3.group.* commands end-to-end.
func TestShellGroupLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
groupName := uniqueName("grp")
userName := uniqueName("grpuser")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", userName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
t.Run("CreateShowList", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.create -name %s", groupName))
requireContains(t, out, groupName, "group.create output")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.show -name %s", groupName))
requireContains(t, out, groupName, "group.show output")
out = execShell(t, weedCmd, master, filer, "s3.group.list")
requireContains(t, out, groupName, "group.list")
})
t.Run("AddRemoveUser", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.group.add-user -group %s -user %s", groupName, userName))
requireContains(t, out, userName, "group.add-user output")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.show -name %s", groupName))
requireContains(t, out, userName, "group.show after add")
out = execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.group.remove-user -group %s -user %s", groupName, userName))
requireContains(t, out, userName, "group.remove-user output")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.show -name %s", groupName))
requireNotContains(t, out, fmt.Sprintf("\"%s\"", userName), "group.show after remove")
})
t.Run("Delete", func(t *testing.T) {
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.group.delete -name %s", groupName))
out := execShell(t, weedCmd, master, filer, "s3.group.list")
requireNotContains(t, out, fmt.Sprintf("\"%s\"", groupName), "group.list after delete")
})
}

View File

@@ -0,0 +1,73 @@
package policy
import (
"strings"
"testing"
)
// requireContains fails the test if substr is not found in output.
func requireContains(t *testing.T, output, substr, context string) {
t.Helper()
if !strings.Contains(output, substr) {
t.Fatalf("%s: expected output to contain %q\n--- output ---\n%s\n--- end ---", context, substr, output)
}
}
// requireNotContains fails the test if substr IS found in output.
func requireNotContains(t *testing.T, output, substr, context string) {
t.Helper()
if strings.Contains(output, substr) {
t.Fatalf("%s: expected output to NOT contain %q\n--- output ---\n%s\n--- end ---", context, substr, output)
}
}
// extractFieldAfter returns the first occurrence of the value after a "Prefix: " line.
// Example: extractFieldAfter(out, "Access Key:") -> "AKIAXXXX..."
// Returns "" if not found.
func extractFieldAfter(output, prefix string) string {
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, prefix) {
return strings.TrimSpace(strings.TrimPrefix(line, prefix))
}
}
return ""
}
// splitLines splits output into trimmed non-empty lines.
func splitLines(output string) []string {
var lines []string
for _, line := range strings.Split(output, "\n") {
if trimmed := strings.TrimSpace(line); trimmed != "" {
lines = append(lines, trimmed)
}
}
return lines
}
// fieldsOf splits a line on whitespace.
func fieldsOf(line string) []string {
return strings.Fields(line)
}
// extractServiceAccountID parses the tab-separated output of `s3.serviceaccount.list`
// and returns the ID of the first row whose PARENT column matches parentUser.
// The list output format is:
//
// ID PARENT STATUS DESCRIPTION
// sa:user-yyy:a1b2c3d4e5f6... user-yyy enabled some desc
func extractServiceAccountID(t *testing.T, listOutput, parentUser string) string {
t.Helper()
for _, line := range strings.Split(listOutput, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "ID") || strings.HasPrefix(line, "No service accounts") {
continue
}
fields := strings.Fields(line)
if len(fields) >= 2 && fields[1] == parentUser {
return fields[0]
}
}
t.Fatalf("could not find service account with parent=%q in output:\n%s", parentUser, listOutput)
return ""
}

View File

@@ -0,0 +1,67 @@
package policy
import (
"fmt"
"os"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellPolicyAttachDetach exercises s3.policy.attach and s3.policy.detach.
func TestShellPolicyAttachDetach(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
// Create a policy via file.
policyJSON := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`
tmpFile, err := os.CreateTemp("", "test_policy_*.json")
require.NoError(t, err)
defer os.Remove(tmpFile.Name())
_, err = tmpFile.WriteString(policyJSON)
require.NoError(t, err)
require.NoError(t, tmpFile.Close())
policyName := uniqueName("attach-pol")
userName := uniqueName("attach-user")
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.policy -put -name=%s -file=%s", policyName, tmpFile.Name()))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.policy -delete -name=%s", policyName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", userName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
t.Run("AttachAndVerify", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.policy.attach -policy %s -user %s", policyName, userName))
requireContains(t, out, policyName, "policy.attach output")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.show -name %s", userName))
requireContains(t, out, policyName, "user.show after attach")
})
t.Run("AttachIdempotent", func(t *testing.T) {
// Should succeed without error per the command's idempotent design.
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.policy.attach -policy %s -user %s", policyName, userName))
})
t.Run("DetachAndVerify", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.policy.detach -policy %s -user %s", policyName, userName))
requireContains(t, out, policyName, "policy.detach output")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.show -name %s", userName))
requireNotContains(t, out, fmt.Sprintf("\"%s\"", policyName), "user.show after detach")
})
}

View File

@@ -0,0 +1,80 @@
package policy
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellServiceAccountLifecycle exercises s3.serviceaccount.* commands end-to-end.
func TestShellServiceAccountLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
userName := uniqueName("sauser")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", userName))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
var saID string
t.Run("CreateAndList", func(t *testing.T) {
description := "integration-test-sa"
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.serviceaccount.create -user %s -description %s", userName, description))
requireContains(t, out, "Created service account", "serviceaccount.create")
requireContains(t, out, "Access Key:", "serviceaccount.create credentials")
requireContains(t, out, "Secret Key:", "serviceaccount.create credentials")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.serviceaccount.list -user %s", userName))
requireContains(t, out, userName, "serviceaccount.list parent column")
saID = extractServiceAccountID(t, out, userName)
})
t.Run("Show", func(t *testing.T) {
if saID == "" {
t.Skip("no saID extracted")
}
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.serviceaccount.show -id %s", saID))
requireContains(t, out, saID, "show contains id")
requireContains(t, out, userName, "show contains parent")
requireContains(t, out, "enabled", "show contains status")
})
t.Run("CreateWithActions", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.serviceaccount.create -user %s -actions Read,List -expiry 24h", userName))
requireContains(t, out, "Access Key:", "serviceaccount.create with options")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.serviceaccount.list -user %s", userName))
// Should now have at least 2 service accounts — count lines that start with parent in field[1]
count := 0
for _, line := range splitLines(out) {
fields := fieldsOf(line)
if len(fields) >= 2 && fields[1] == userName {
count++
}
}
if count < 2 {
t.Fatalf("expected at least 2 service accounts for %s, got %d\n%s", userName, count, out)
}
})
t.Run("Delete", func(t *testing.T) {
if saID == "" {
t.Skip("no saID extracted")
}
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.serviceaccount.delete -id %s", saID))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.serviceaccount.list -user %s", userName))
requireNotContains(t, out, saID, "deleted sa removed from list")
})
}

View File

@@ -0,0 +1,110 @@
package policy
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellUserLifecycle exercises the s3.user.* commands end-to-end:
// create, show, list, enable, disable, delete, and provision.
func TestShellUserLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
t.Run("CreateShowListDelete", func(t *testing.T) {
userName := uniqueName("user")
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", userName))
requireContains(t, out, userName, "user.create output")
requireContains(t, out, "access_key", "user.create JSON")
requireContains(t, out, "Secret Key:", "user.create stderr")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.show -name %s", userName))
requireContains(t, out, userName, "user.show")
out = execShell(t, weedCmd, master, filer, "s3.user.list")
requireContains(t, out, userName, "user.list")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
out = execShell(t, weedCmd, master, filer, "s3.user.list")
requireNotContains(t, out, userName, "user.list after delete")
})
t.Run("CreateWithExplicitKeys", func(t *testing.T) {
userName := uniqueName("user-expl")
ak := "TESTAK1234567890ABCD"
sk := "testsecretkey1234567890abcdefghijklmnopq"
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.user.create -name %s -access_key %s -secret_key %s", userName, ak, sk))
requireContains(t, out, ak, "user.create with explicit keys")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.show -name %s", userName))
requireContains(t, out, ak, "user.show reveals access key")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
})
t.Run("EnableDisable", func(t *testing.T) {
userName := uniqueName("user-toggle")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.create -name %s", userName))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.disable -name %s", userName))
requireContains(t, out, "disabled", "user.disable")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.show -name %s", userName))
requireContains(t, out, "disabled", "user.show after disable")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.enable -name %s", userName))
requireContains(t, out, "enabled", "user.enable")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.show -name %s", userName))
requireContains(t, out, "enabled", "user.show after enable")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
})
t.Run("Provision", func(t *testing.T) {
userName := uniqueName("prov-user")
bucketName := uniqueName("prov-bkt")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucketName))
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.user.provision -name %s -bucket %s -role readwrite", userName, bucketName))
requireContains(t, out, "Created policy", "provision output")
requireContains(t, out, "Created user", "provision output")
requireContains(t, out, "Access Key:", "provision credentials")
requireContains(t, out, "Secret Key:", "provision credentials")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.show -name %s", userName))
requireContains(t, out, userName, "user.show after provision")
// Second call with same user but different bucket/role should succeed without creating duplicate user.
bucket2 := uniqueName("prov-bkt2")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.create -name %s", bucket2))
out = execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.user.provision -name %s -bucket %s -role readonly", userName, bucket2))
requireContains(t, out, "already exists", "provision on existing user")
requireContains(t, out, "Created policy", "second policy created")
requireContains(t, out, "Attached policy", "second policy attached to existing user")
requireNotContains(t, out, "Access Key:", "no new credentials for existing user")
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucketName))
execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.bucket.delete -name %s", bucket2))
})
}