| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- package iam
- import (
- "fmt"
- "os"
- "strings"
- "sync"
- "testing"
- "time"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/service/s3"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // TestS3IAMDistributedTests tests IAM functionality across multiple S3 gateway instances
- func TestS3IAMDistributedTests(t *testing.T) {
- // Skip if not in distributed test mode
- if os.Getenv("ENABLE_DISTRIBUTED_TESTS") != "true" {
- t.Skip("Distributed tests not enabled. Set ENABLE_DISTRIBUTED_TESTS=true")
- }
- framework := NewS3IAMTestFramework(t)
- defer framework.Cleanup()
- t.Run("distributed_session_consistency", func(t *testing.T) {
- // Test that sessions created on one instance are visible on others
- // This requires filer-based session storage
- // Create S3 clients that would connect to different gateway instances
- // In a real distributed setup, these would point to different S3 gateway ports
- client1, err := framework.CreateS3ClientWithJWT("test-user", "TestAdminRole")
- require.NoError(t, err)
- client2, err := framework.CreateS3ClientWithJWT("test-user", "TestAdminRole")
- require.NoError(t, err)
- // Both clients should be able to perform operations
- bucketName := "test-distributed-session"
- err = framework.CreateBucket(client1, bucketName)
- require.NoError(t, err)
- // Client2 should see the bucket created by client1
- listResult, err := client2.ListBuckets(&s3.ListBucketsInput{})
- require.NoError(t, err)
- found := false
- for _, bucket := range listResult.Buckets {
- if *bucket.Name == bucketName {
- found = true
- break
- }
- }
- assert.True(t, found, "Bucket should be visible across distributed instances")
- // Cleanup
- _, err = client1.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(t, err)
- })
- t.Run("distributed_role_consistency", func(t *testing.T) {
- // Test that role definitions are consistent across instances
- // This requires filer-based role storage
- // Create clients with different roles
- adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
- require.NoError(t, err)
- readOnlyClient, err := framework.CreateS3ClientWithJWT("readonly-user", "TestReadOnlyRole")
- require.NoError(t, err)
- bucketName := "test-distributed-roles"
- objectKey := "test-object.txt"
- // Admin should be able to create bucket
- err = framework.CreateBucket(adminClient, bucketName)
- require.NoError(t, err)
- // Admin should be able to put object
- err = framework.PutTestObject(adminClient, bucketName, objectKey, "test content")
- require.NoError(t, err)
- // Read-only user should be able to get object
- content, err := framework.GetTestObject(readOnlyClient, bucketName, objectKey)
- require.NoError(t, err)
- assert.Equal(t, "test content", content)
- // Read-only user should NOT be able to put object
- err = framework.PutTestObject(readOnlyClient, bucketName, "forbidden-object.txt", "forbidden content")
- require.Error(t, err, "Read-only user should not be able to put objects")
- // Cleanup
- err = framework.DeleteTestObject(adminClient, bucketName, objectKey)
- require.NoError(t, err)
- _, err = adminClient.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(t, err)
- })
- t.Run("distributed_concurrent_operations", func(t *testing.T) {
- // Test concurrent operations across distributed instances with robust retry mechanisms
- // This approach implements proper retry logic instead of tolerating errors to catch real concurrency issues
- const numGoroutines = 3 // Reduced concurrency for better CI reliability
- const numOperationsPerGoroutine = 2 // Minimal operations per goroutine
- const maxRetries = 3 // Maximum retry attempts for transient failures
- const retryDelay = 200 * time.Millisecond // Increased delay for better stability
- var wg sync.WaitGroup
- errors := make(chan error, numGoroutines*numOperationsPerGoroutine)
- // Helper function to determine if an error is retryable
- isRetryableError := func(err error) bool {
- if err == nil {
- return false
- }
- errorMsg := err.Error()
- return strings.Contains(errorMsg, "timeout") ||
- strings.Contains(errorMsg, "connection reset") ||
- strings.Contains(errorMsg, "temporary failure") ||
- strings.Contains(errorMsg, "TooManyRequests") ||
- strings.Contains(errorMsg, "ServiceUnavailable") ||
- strings.Contains(errorMsg, "InternalError")
- }
- // Helper function to execute operations with retry logic
- executeWithRetry := func(operation func() error, operationName string) error {
- var lastErr error
- for attempt := 0; attempt <= maxRetries; attempt++ {
- if attempt > 0 {
- time.Sleep(retryDelay * time.Duration(attempt)) // Linear backoff
- }
- lastErr = operation()
- if lastErr == nil {
- return nil // Success
- }
- if !isRetryableError(lastErr) {
- // Non-retryable error - fail immediately
- return fmt.Errorf("%s failed with non-retryable error: %w", operationName, lastErr)
- }
- // Retryable error - continue to next attempt
- if attempt < maxRetries {
- t.Logf("Retrying %s (attempt %d/%d) after error: %v", operationName, attempt+1, maxRetries, lastErr)
- }
- }
- // All retries exhausted
- return fmt.Errorf("%s failed after %d retries, last error: %w", operationName, maxRetries, lastErr)
- }
- for i := 0; i < numGoroutines; i++ {
- wg.Add(1)
- go func(goroutineID int) {
- defer wg.Done()
- client, err := framework.CreateS3ClientWithJWT(fmt.Sprintf("user-%d", goroutineID), "TestAdminRole")
- if err != nil {
- errors <- fmt.Errorf("failed to create S3 client for goroutine %d: %w", goroutineID, err)
- return
- }
- for j := 0; j < numOperationsPerGoroutine; j++ {
- bucketName := fmt.Sprintf("test-concurrent-%d-%d", goroutineID, j)
- objectKey := "test-object.txt"
- objectContent := fmt.Sprintf("content-%d-%d", goroutineID, j)
- // Execute full operation sequence with individual retries
- operationFailed := false
- // 1. Create bucket with retry
- if err := executeWithRetry(func() error {
- return framework.CreateBucket(client, bucketName)
- }, fmt.Sprintf("CreateBucket-%s", bucketName)); err != nil {
- errors <- err
- operationFailed = true
- }
- if !operationFailed {
- // 2. Put object with retry
- if err := executeWithRetry(func() error {
- return framework.PutTestObject(client, bucketName, objectKey, objectContent)
- }, fmt.Sprintf("PutObject-%s/%s", bucketName, objectKey)); err != nil {
- errors <- err
- operationFailed = true
- }
- }
- if !operationFailed {
- // 3. Get object with retry
- if err := executeWithRetry(func() error {
- _, err := framework.GetTestObject(client, bucketName, objectKey)
- return err
- }, fmt.Sprintf("GetObject-%s/%s", bucketName, objectKey)); err != nil {
- errors <- err
- operationFailed = true
- }
- }
- if !operationFailed {
- // 4. Delete object with retry
- if err := executeWithRetry(func() error {
- return framework.DeleteTestObject(client, bucketName, objectKey)
- }, fmt.Sprintf("DeleteObject-%s/%s", bucketName, objectKey)); err != nil {
- errors <- err
- operationFailed = true
- }
- }
- // 5. Always attempt bucket cleanup, even if previous operations failed
- if err := executeWithRetry(func() error {
- _, err := client.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- return err
- }, fmt.Sprintf("DeleteBucket-%s", bucketName)); err != nil {
- // Only log cleanup failures, don't fail the test
- t.Logf("Warning: Failed to cleanup bucket %s: %v", bucketName, err)
- }
- // Increased delay between operation sequences to reduce server load and improve stability
- time.Sleep(100 * time.Millisecond)
- }
- }(i)
- }
- wg.Wait()
- close(errors)
- // Collect and analyze errors - with retry logic, we should see very few errors
- var errorList []error
- for err := range errors {
- errorList = append(errorList, err)
- }
- totalOperations := numGoroutines * numOperationsPerGoroutine
- // Report results
- if len(errorList) == 0 {
- t.Logf("🎉 All %d concurrent operations completed successfully with retry mechanisms!", totalOperations)
- } else {
- t.Logf("Concurrent operations summary:")
- t.Logf(" Total operations: %d", totalOperations)
- t.Logf(" Failed operations: %d (%.1f%% error rate)", len(errorList), float64(len(errorList))/float64(totalOperations)*100)
- // Log first few errors for debugging
- for i, err := range errorList {
- if i >= 3 { // Limit to first 3 errors
- t.Logf(" ... and %d more errors", len(errorList)-3)
- break
- }
- t.Logf(" Error %d: %v", i+1, err)
- }
- }
- // With proper retry mechanisms, we should expect near-zero failures
- // Any remaining errors likely indicate real concurrency issues or system problems
- if len(errorList) > 0 {
- t.Errorf("❌ %d operation(s) failed even after retry mechanisms (%.1f%% failure rate). This indicates potential system issues or race conditions that need investigation.",
- len(errorList), float64(len(errorList))/float64(totalOperations)*100)
- }
- })
- }
- // TestS3IAMPerformanceTests tests IAM performance characteristics
- func TestS3IAMPerformanceTests(t *testing.T) {
- // Skip if not in performance test mode
- if os.Getenv("ENABLE_PERFORMANCE_TESTS") != "true" {
- t.Skip("Performance tests not enabled. Set ENABLE_PERFORMANCE_TESTS=true")
- }
- framework := NewS3IAMTestFramework(t)
- defer framework.Cleanup()
- t.Run("authentication_performance", func(t *testing.T) {
- // Test authentication performance
- const numRequests = 100
- client, err := framework.CreateS3ClientWithJWT("perf-user", "TestAdminRole")
- require.NoError(t, err)
- bucketName := "test-auth-performance"
- err = framework.CreateBucket(client, bucketName)
- require.NoError(t, err)
- defer func() {
- _, err := client.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(t, err)
- }()
- start := time.Now()
- for i := 0; i < numRequests; i++ {
- _, err := client.ListBuckets(&s3.ListBucketsInput{})
- require.NoError(t, err)
- }
- duration := time.Since(start)
- avgLatency := duration / numRequests
- t.Logf("Authentication performance: %d requests in %v (avg: %v per request)",
- numRequests, duration, avgLatency)
- // Performance assertion - should be under 100ms per request on average
- assert.Less(t, avgLatency, 100*time.Millisecond,
- "Average authentication latency should be under 100ms")
- })
- t.Run("authorization_performance", func(t *testing.T) {
- // Test authorization performance with different policy complexities
- const numRequests = 50
- client, err := framework.CreateS3ClientWithJWT("perf-user", "TestAdminRole")
- require.NoError(t, err)
- bucketName := "test-authz-performance"
- err = framework.CreateBucket(client, bucketName)
- require.NoError(t, err)
- defer func() {
- _, err := client.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(t, err)
- }()
- start := time.Now()
- for i := 0; i < numRequests; i++ {
- objectKey := fmt.Sprintf("perf-object-%d.txt", i)
- err := framework.PutTestObject(client, bucketName, objectKey, "performance test content")
- require.NoError(t, err)
- _, err = framework.GetTestObject(client, bucketName, objectKey)
- require.NoError(t, err)
- err = framework.DeleteTestObject(client, bucketName, objectKey)
- require.NoError(t, err)
- }
- duration := time.Since(start)
- avgLatency := duration / (numRequests * 3) // 3 operations per iteration
- t.Logf("Authorization performance: %d operations in %v (avg: %v per operation)",
- numRequests*3, duration, avgLatency)
- // Performance assertion - should be under 50ms per operation on average
- assert.Less(t, avgLatency, 50*time.Millisecond,
- "Average authorization latency should be under 50ms")
- })
- }
- // BenchmarkS3IAMAuthentication benchmarks JWT authentication
- func BenchmarkS3IAMAuthentication(b *testing.B) {
- if os.Getenv("ENABLE_PERFORMANCE_TESTS") != "true" {
- b.Skip("Performance tests not enabled. Set ENABLE_PERFORMANCE_TESTS=true")
- }
- framework := NewS3IAMTestFramework(&testing.T{})
- defer framework.Cleanup()
- client, err := framework.CreateS3ClientWithJWT("bench-user", "TestAdminRole")
- require.NoError(b, err)
- bucketName := "test-bench-auth"
- err = framework.CreateBucket(client, bucketName)
- require.NoError(b, err)
- defer func() {
- _, err := client.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(b, err)
- }()
- b.ResetTimer()
- b.RunParallel(func(pb *testing.PB) {
- for pb.Next() {
- _, err := client.ListBuckets(&s3.ListBucketsInput{})
- if err != nil {
- b.Error(err)
- }
- }
- })
- }
- // BenchmarkS3IAMAuthorization benchmarks policy evaluation
- func BenchmarkS3IAMAuthorization(b *testing.B) {
- if os.Getenv("ENABLE_PERFORMANCE_TESTS") != "true" {
- b.Skip("Performance tests not enabled. Set ENABLE_PERFORMANCE_TESTS=true")
- }
- framework := NewS3IAMTestFramework(&testing.T{})
- defer framework.Cleanup()
- client, err := framework.CreateS3ClientWithJWT("bench-user", "TestAdminRole")
- require.NoError(b, err)
- bucketName := "test-bench-authz"
- err = framework.CreateBucket(client, bucketName)
- require.NoError(b, err)
- defer func() {
- _, err := client.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(b, err)
- }()
- b.ResetTimer()
- b.RunParallel(func(pb *testing.PB) {
- i := 0
- for pb.Next() {
- objectKey := fmt.Sprintf("bench-object-%d.txt", i)
- err := framework.PutTestObject(client, bucketName, objectKey, "benchmark content")
- if err != nil {
- b.Error(err)
- }
- i++
- }
- })
- }
|