| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- 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)
- }
|