package iam import ( "bytes" "fmt" "io" "strings" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/s3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( testEndpoint = "http://localhost:8333" testRegion = "us-west-2" testBucketPrefix = "test-iam-bucket" testObjectKey = "test-object.txt" testObjectData = "Hello, SeaweedFS IAM Integration!" ) var ( testBucket = testBucketPrefix ) // TestS3IAMAuthentication tests S3 API authentication with IAM JWT tokens func TestS3IAMAuthentication(t *testing.T) { framework := NewS3IAMTestFramework(t) defer framework.Cleanup() t.Run("valid_jwt_token_authentication", func(t *testing.T) { // Create S3 client with valid JWT token s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole") require.NoError(t, err) // Test bucket operations err = framework.CreateBucket(s3Client, testBucket) require.NoError(t, err) // Verify bucket exists buckets, err := s3Client.ListBuckets(&s3.ListBucketsInput{}) require.NoError(t, err) found := false for _, bucket := range buckets.Buckets { if *bucket.Name == testBucket { found = true break } } assert.True(t, found, "Created bucket should be listed") }) t.Run("invalid_jwt_token_authentication", func(t *testing.T) { // Create S3 client with invalid JWT token s3Client, err := framework.CreateS3ClientWithInvalidJWT() require.NoError(t, err) // Attempt bucket operations - should fail err = framework.CreateBucket(s3Client, testBucket+"-invalid") require.Error(t, err) // Verify it's an access denied error if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } else { t.Error("Expected AWS error with AccessDenied code") } }) t.Run("expired_jwt_token_authentication", func(t *testing.T) { // Create S3 client with expired JWT token s3Client, err := framework.CreateS3ClientWithExpiredJWT("expired-user", "TestAdminRole") require.NoError(t, err) // Attempt bucket operations - should fail err = framework.CreateBucket(s3Client, testBucket+"-expired") require.Error(t, err) // Verify it's an access denied error if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } else { t.Error("Expected AWS error with AccessDenied code") } }) } // TestS3IAMPolicyEnforcement tests policy enforcement for different S3 operations func TestS3IAMPolicyEnforcement(t *testing.T) { framework := NewS3IAMTestFramework(t) defer framework.Cleanup() // Setup test bucket with admin client adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole") require.NoError(t, err) err = framework.CreateBucket(adminClient, testBucket) require.NoError(t, err) // Put test object with admin client _, err = adminClient.PutObject(&s3.PutObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), Body: strings.NewReader(testObjectData), }) require.NoError(t, err) t.Run("read_only_policy_enforcement", func(t *testing.T) { // Create S3 client with read-only role readOnlyClient, err := framework.CreateS3ClientWithJWT("read-user", "TestReadOnlyRole") require.NoError(t, err) // Should be able to read objects result, err := readOnlyClient.GetObject(&s3.GetObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), }) require.NoError(t, err) data, err := io.ReadAll(result.Body) require.NoError(t, err) assert.Equal(t, testObjectData, string(data)) result.Body.Close() // Should be able to list objects listResult, err := readOnlyClient.ListObjects(&s3.ListObjectsInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) assert.Len(t, listResult.Contents, 1) assert.Equal(t, testObjectKey, *listResult.Contents[0].Key) // Should NOT be able to put objects _, err = readOnlyClient.PutObject(&s3.PutObjectInput{ Bucket: aws.String(testBucket), Key: aws.String("forbidden-object.txt"), Body: strings.NewReader("This should fail"), }) require.Error(t, err) if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } // Should NOT be able to delete objects _, err = readOnlyClient.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), }) require.Error(t, err) if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } }) t.Run("write_only_policy_enforcement", func(t *testing.T) { // Create S3 client with write-only role writeOnlyClient, err := framework.CreateS3ClientWithJWT("write-user", "TestWriteOnlyRole") require.NoError(t, err) // Should be able to put objects testWriteKey := "write-test-object.txt" testWriteData := "Write-only test data" _, err = writeOnlyClient.PutObject(&s3.PutObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testWriteKey), Body: strings.NewReader(testWriteData), }) require.NoError(t, err) // Should be able to delete objects _, err = writeOnlyClient.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testWriteKey), }) require.NoError(t, err) // Should NOT be able to read objects _, err = writeOnlyClient.GetObject(&s3.GetObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), }) require.Error(t, err) if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } // Should NOT be able to list objects _, err = writeOnlyClient.ListObjects(&s3.ListObjectsInput{ Bucket: aws.String(testBucket), }) require.Error(t, err) if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } }) t.Run("admin_policy_enforcement", func(t *testing.T) { // Admin client should be able to do everything testAdminKey := "admin-test-object.txt" testAdminData := "Admin test data" // Should be able to put objects _, err = adminClient.PutObject(&s3.PutObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testAdminKey), Body: strings.NewReader(testAdminData), }) require.NoError(t, err) // Should be able to read objects result, err := adminClient.GetObject(&s3.GetObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testAdminKey), }) require.NoError(t, err) data, err := io.ReadAll(result.Body) require.NoError(t, err) assert.Equal(t, testAdminData, string(data)) result.Body.Close() // Should be able to list objects listResult, err := adminClient.ListObjects(&s3.ListObjectsInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) assert.GreaterOrEqual(t, len(listResult.Contents), 1) // Should be able to delete objects _, err = adminClient.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testAdminKey), }) require.NoError(t, err) // Should be able to delete buckets // First delete remaining objects _, err = adminClient.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), }) require.NoError(t, err) // Then delete the bucket _, err = adminClient.DeleteBucket(&s3.DeleteBucketInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) }) } // TestS3IAMSessionExpiration tests session expiration handling func TestS3IAMSessionExpiration(t *testing.T) { framework := NewS3IAMTestFramework(t) defer framework.Cleanup() t.Run("session_expiration_enforcement", func(t *testing.T) { // Create S3 client with valid JWT token s3Client, err := framework.CreateS3ClientWithJWT("session-user", "TestAdminRole") require.NoError(t, err) // Initially should work err = framework.CreateBucket(s3Client, testBucket+"-session") require.NoError(t, err) // Create S3 client with expired JWT token expiredClient, err := framework.CreateS3ClientWithExpiredJWT("session-user", "TestAdminRole") require.NoError(t, err) // Now operations should fail with expired token err = framework.CreateBucket(expiredClient, testBucket+"-session-expired") require.Error(t, err) if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } // Cleanup the successful bucket adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole") require.NoError(t, err) _, err = adminClient.DeleteBucket(&s3.DeleteBucketInput{ Bucket: aws.String(testBucket + "-session"), }) require.NoError(t, err) }) } // TestS3IAMMultipartUploadPolicyEnforcement tests multipart upload with IAM policies func TestS3IAMMultipartUploadPolicyEnforcement(t *testing.T) { framework := NewS3IAMTestFramework(t) defer framework.Cleanup() // Setup test bucket with admin client adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole") require.NoError(t, err) err = framework.CreateBucket(adminClient, testBucket) require.NoError(t, err) t.Run("multipart_upload_with_write_permissions", func(t *testing.T) { // Create S3 client with admin role (has multipart permissions) s3Client := adminClient // Initiate multipart upload multipartKey := "large-test-file.txt" initResult, err := s3Client.CreateMultipartUpload(&s3.CreateMultipartUploadInput{ Bucket: aws.String(testBucket), Key: aws.String(multipartKey), }) require.NoError(t, err) uploadId := initResult.UploadId // Upload a part partNumber := int64(1) partData := strings.Repeat("Test data for multipart upload. ", 1000) // ~30KB uploadResult, err := s3Client.UploadPart(&s3.UploadPartInput{ Bucket: aws.String(testBucket), Key: aws.String(multipartKey), PartNumber: aws.Int64(partNumber), UploadId: uploadId, Body: strings.NewReader(partData), }) require.NoError(t, err) // Complete multipart upload _, err = s3Client.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{ Bucket: aws.String(testBucket), Key: aws.String(multipartKey), UploadId: uploadId, MultipartUpload: &s3.CompletedMultipartUpload{ Parts: []*s3.CompletedPart{ { ETag: uploadResult.ETag, PartNumber: aws.Int64(partNumber), }, }, }, }) require.NoError(t, err) // Verify object was created result, err := s3Client.GetObject(&s3.GetObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(multipartKey), }) require.NoError(t, err) data, err := io.ReadAll(result.Body) require.NoError(t, err) assert.Equal(t, partData, string(data)) result.Body.Close() // Cleanup _, err = s3Client.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(multipartKey), }) require.NoError(t, err) }) t.Run("multipart_upload_denied_for_read_only", func(t *testing.T) { // Create S3 client with read-only role readOnlyClient, err := framework.CreateS3ClientWithJWT("read-user", "TestReadOnlyRole") require.NoError(t, err) // Attempt to initiate multipart upload - should fail multipartKey := "denied-multipart-file.txt" _, err = readOnlyClient.CreateMultipartUpload(&s3.CreateMultipartUploadInput{ Bucket: aws.String(testBucket), Key: aws.String(multipartKey), }) require.Error(t, err) if awsErr, ok := err.(awserr.Error); ok { assert.Equal(t, "AccessDenied", awsErr.Code()) } }) // Cleanup _, err = adminClient.DeleteBucket(&s3.DeleteBucketInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) } // TestS3IAMBucketPolicyIntegration tests bucket policy integration with IAM func TestS3IAMBucketPolicyIntegration(t *testing.T) { framework := NewS3IAMTestFramework(t) defer framework.Cleanup() // Setup test bucket with admin client adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole") require.NoError(t, err) err = framework.CreateBucket(adminClient, testBucket) require.NoError(t, err) t.Run("bucket_policy_allows_public_read", func(t *testing.T) { // Set bucket policy to allow public read access bucketPolicy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": ["s3:GetObject"], "Resource": ["arn:seaweed:s3:::%s/*"] } ] }`, testBucket) _, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{ Bucket: aws.String(testBucket), Policy: aws.String(bucketPolicy), }) require.NoError(t, err) // Put test object _, err = adminClient.PutObject(&s3.PutObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), Body: strings.NewReader(testObjectData), }) require.NoError(t, err) // Test with read-only client - should now be allowed due to bucket policy readOnlyClient, err := framework.CreateS3ClientWithJWT("read-user", "TestReadOnlyRole") require.NoError(t, err) result, err := readOnlyClient.GetObject(&s3.GetObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), }) require.NoError(t, err) data, err := io.ReadAll(result.Body) require.NoError(t, err) assert.Equal(t, testObjectData, string(data)) result.Body.Close() }) t.Run("bucket_policy_denies_specific_action", func(t *testing.T) { // Set bucket policy to deny delete operations bucketPolicy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Sid": "DenyDelete", "Effect": "Deny", "Principal": "*", "Action": ["s3:DeleteObject"], "Resource": ["arn:seaweed:s3:::%s/*"] } ] }`, testBucket) _, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{ Bucket: aws.String(testBucket), Policy: aws.String(bucketPolicy), }) require.NoError(t, err) // Verify that the bucket policy was stored successfully by retrieving it policyResult, err := adminClient.GetBucketPolicy(&s3.GetBucketPolicyInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) assert.Contains(t, *policyResult.Policy, "s3:DeleteObject") assert.Contains(t, *policyResult.Policy, "Deny") // IMPLEMENTATION NOTE: Bucket policy enforcement in authorization flow // is planned for a future phase. Currently, this test validates policy // storage and retrieval. When enforcement is implemented, this test // should be extended to verify that delete operations are actually denied. }) // Cleanup - delete bucket policy first, then objects and bucket _, err = adminClient.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) _, err = adminClient.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), }) require.NoError(t, err) _, err = adminClient.DeleteBucket(&s3.DeleteBucketInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) } // TestS3IAMContextualPolicyEnforcement tests context-aware policy enforcement func TestS3IAMContextualPolicyEnforcement(t *testing.T) { framework := NewS3IAMTestFramework(t) defer framework.Cleanup() // This test would verify IP-based restrictions, time-based restrictions, // and other context-aware policy conditions // For now, we'll focus on the basic structure t.Run("ip_based_policy_enforcement", func(t *testing.T) { // IMPLEMENTATION NOTE: IP-based policy testing framework planned for future release // Requirements: // - Configure IAM policies with IpAddress/NotIpAddress conditions // - Multi-container test setup with controlled source IP addresses // - Test policy enforcement from allowed vs denied IP ranges t.Skip("IP-based policy testing requires advanced network configuration and multi-container setup") }) t.Run("time_based_policy_enforcement", func(t *testing.T) { // IMPLEMENTATION NOTE: Time-based policy testing framework planned for future release // Requirements: // - Configure IAM policies with DateGreaterThan/DateLessThan conditions // - Time manipulation capabilities for testing different time windows // - Test policy enforcement during allowed vs restricted time periods t.Skip("Time-based policy testing requires time manipulation capabilities") }) } // Helper function to create test content of specific size func createTestContent(size int) *bytes.Reader { content := make([]byte, size) for i := range content { content[i] = byte(i % 256) } return bytes.NewReader(content) } // TestS3IAMPresignedURLIntegration tests presigned URL generation with IAM func TestS3IAMPresignedURLIntegration(t *testing.T) { framework := NewS3IAMTestFramework(t) defer framework.Cleanup() // Setup test bucket with admin client adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole") require.NoError(t, err) // Use static bucket name but with cleanup to handle conflicts err = framework.CreateBucketWithCleanup(adminClient, testBucketPrefix) require.NoError(t, err) // Put test object _, err = adminClient.PutObject(&s3.PutObjectInput{ Bucket: aws.String(testBucketPrefix), Key: aws.String(testObjectKey), Body: strings.NewReader(testObjectData), }) require.NoError(t, err) t.Run("presigned_url_generation_and_usage", func(t *testing.T) { // ARCHITECTURAL NOTE: AWS SDK presigned URLs are incompatible with JWT Bearer authentication // // AWS SDK presigned URLs use AWS Signature Version 4 (SigV4) which requires: // - Access Key ID and Secret Access Key for signing // - Query parameter-based authentication in the URL // // SeaweedFS JWT authentication uses: // - Bearer tokens in the Authorization header // - Stateless JWT validation without AWS-style signing // // RECOMMENDATION: For JWT-authenticated applications, use direct API calls // with Bearer tokens rather than presigned URLs. // Test direct object access with JWT Bearer token (recommended approach) _, err := adminClient.GetObject(&s3.GetObjectInput{ Bucket: aws.String(testBucketPrefix), Key: aws.String(testObjectKey), }) require.NoError(t, err, "Direct object access with JWT Bearer token works correctly") t.Log("✅ JWT Bearer token authentication confirmed working for direct S3 API calls") t.Log("ℹ️ Note: Presigned URLs are not supported with JWT Bearer authentication by design") }) // Cleanup _, err = adminClient.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(testBucket), Key: aws.String(testObjectKey), }) require.NoError(t, err) _, err = adminClient.DeleteBucket(&s3.DeleteBucketInput{ Bucket: aws.String(testBucket), }) require.NoError(t, err) }