| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- package s3api
- import (
- "context"
- "net/http"
- "net/http/httptest"
- "net/url"
- "testing"
- "time"
- "github.com/seaweedfs/seaweedfs/weed/iam/integration"
- "github.com/seaweedfs/seaweedfs/weed/iam/policy"
- "github.com/seaweedfs/seaweedfs/weed/iam/sts"
- "github.com/seaweedfs/seaweedfs/weed/iam/utils"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // TestS3IAMMiddleware tests the basic S3 IAM middleware functionality
- func TestS3IAMMiddleware(t *testing.T) {
- // Create IAM manager
- iamManager := integration.NewIAMManager()
- // Initialize with test configuration
- config := &integration.IAMConfig{
- STS: &sts.STSConfig{
- TokenDuration: sts.FlexibleDuration{time.Hour},
- MaxSessionLength: sts.FlexibleDuration{time.Hour * 12},
- Issuer: "test-sts",
- SigningKey: []byte("test-signing-key-32-characters-long"),
- },
- Policy: &policy.PolicyEngineConfig{
- DefaultEffect: "Deny",
- StoreType: "memory",
- },
- Roles: &integration.RoleStoreConfig{
- StoreType: "memory",
- },
- }
- err := iamManager.Initialize(config, func() string {
- return "localhost:8888" // Mock filer address for testing
- })
- require.NoError(t, err)
- // Create S3 IAM integration
- s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
- // Test that integration is created successfully
- assert.NotNil(t, s3IAMIntegration)
- assert.True(t, s3IAMIntegration.enabled)
- }
- // TestS3IAMMiddlewareJWTAuth tests JWT authentication
- func TestS3IAMMiddlewareJWTAuth(t *testing.T) {
- // Skip for now since it requires full setup
- t.Skip("JWT authentication test requires full IAM setup")
- // Create IAM integration
- s3iam := NewS3IAMIntegration(nil, "localhost:8888") // Disabled integration
- // Create test request with JWT token
- req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
- req.Header.Set("Authorization", "Bearer test-token")
- // Test authentication (should return not implemented when disabled)
- ctx := context.Background()
- identity, errCode := s3iam.AuthenticateJWT(ctx, req)
- assert.Nil(t, identity)
- assert.NotEqual(t, errCode, 0) // Should return an error
- }
- // TestBuildS3ResourceArn tests resource ARN building
- func TestBuildS3ResourceArn(t *testing.T) {
- tests := []struct {
- name string
- bucket string
- object string
- expected string
- }{
- {
- name: "empty bucket and object",
- bucket: "",
- object: "",
- expected: "arn:seaweed:s3:::*",
- },
- {
- name: "bucket only",
- bucket: "test-bucket",
- object: "",
- expected: "arn:seaweed:s3:::test-bucket",
- },
- {
- name: "bucket and object",
- bucket: "test-bucket",
- object: "test-object.txt",
- expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
- },
- {
- name: "bucket and object with leading slash",
- bucket: "test-bucket",
- object: "/test-object.txt",
- expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
- },
- {
- name: "bucket and nested object",
- bucket: "test-bucket",
- object: "folder/subfolder/test-object.txt",
- expected: "arn:seaweed:s3:::test-bucket/folder/subfolder/test-object.txt",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := buildS3ResourceArn(tt.bucket, tt.object)
- assert.Equal(t, tt.expected, result)
- })
- }
- }
- // TestDetermineGranularS3Action tests granular S3 action determination from HTTP requests
- func TestDetermineGranularS3Action(t *testing.T) {
- tests := []struct {
- name string
- method string
- bucket string
- objectKey string
- queryParams map[string]string
- fallbackAction Action
- expected string
- description string
- }{
- // Object-level operations
- {
- name: "get_object",
- method: "GET",
- bucket: "test-bucket",
- objectKey: "test-object.txt",
- queryParams: map[string]string{},
- fallbackAction: s3_constants.ACTION_READ,
- expected: "s3:GetObject",
- description: "Basic object retrieval",
- },
- {
- name: "get_object_acl",
- method: "GET",
- bucket: "test-bucket",
- objectKey: "test-object.txt",
- queryParams: map[string]string{"acl": ""},
- fallbackAction: s3_constants.ACTION_READ_ACP,
- expected: "s3:GetObjectAcl",
- description: "Object ACL retrieval",
- },
- {
- name: "get_object_tagging",
- method: "GET",
- bucket: "test-bucket",
- objectKey: "test-object.txt",
- queryParams: map[string]string{"tagging": ""},
- fallbackAction: s3_constants.ACTION_TAGGING,
- expected: "s3:GetObjectTagging",
- description: "Object tagging retrieval",
- },
- {
- name: "put_object",
- method: "PUT",
- bucket: "test-bucket",
- objectKey: "test-object.txt",
- queryParams: map[string]string{},
- fallbackAction: s3_constants.ACTION_WRITE,
- expected: "s3:PutObject",
- description: "Basic object upload",
- },
- {
- name: "put_object_acl",
- method: "PUT",
- bucket: "test-bucket",
- objectKey: "test-object.txt",
- queryParams: map[string]string{"acl": ""},
- fallbackAction: s3_constants.ACTION_WRITE_ACP,
- expected: "s3:PutObjectAcl",
- description: "Object ACL modification",
- },
- {
- name: "delete_object",
- method: "DELETE",
- bucket: "test-bucket",
- objectKey: "test-object.txt",
- queryParams: map[string]string{},
- fallbackAction: s3_constants.ACTION_WRITE, // DELETE object uses WRITE fallback
- expected: "s3:DeleteObject",
- description: "Object deletion - correctly mapped to DeleteObject (not PutObject)",
- },
- {
- name: "delete_object_tagging",
- method: "DELETE",
- bucket: "test-bucket",
- objectKey: "test-object.txt",
- queryParams: map[string]string{"tagging": ""},
- fallbackAction: s3_constants.ACTION_TAGGING,
- expected: "s3:DeleteObjectTagging",
- description: "Object tag deletion",
- },
- // Multipart upload operations
- {
- name: "create_multipart_upload",
- method: "POST",
- bucket: "test-bucket",
- objectKey: "large-file.txt",
- queryParams: map[string]string{"uploads": ""},
- fallbackAction: s3_constants.ACTION_WRITE,
- expected: "s3:CreateMultipartUpload",
- description: "Multipart upload initiation",
- },
- {
- name: "upload_part",
- method: "PUT",
- bucket: "test-bucket",
- objectKey: "large-file.txt",
- queryParams: map[string]string{"uploadId": "12345", "partNumber": "1"},
- fallbackAction: s3_constants.ACTION_WRITE,
- expected: "s3:UploadPart",
- description: "Multipart part upload",
- },
- {
- name: "complete_multipart_upload",
- method: "POST",
- bucket: "test-bucket",
- objectKey: "large-file.txt",
- queryParams: map[string]string{"uploadId": "12345"},
- fallbackAction: s3_constants.ACTION_WRITE,
- expected: "s3:CompleteMultipartUpload",
- description: "Multipart upload completion",
- },
- {
- name: "abort_multipart_upload",
- method: "DELETE",
- bucket: "test-bucket",
- objectKey: "large-file.txt",
- queryParams: map[string]string{"uploadId": "12345"},
- fallbackAction: s3_constants.ACTION_WRITE,
- expected: "s3:AbortMultipartUpload",
- description: "Multipart upload abort",
- },
- // Bucket-level operations
- {
- name: "list_bucket",
- method: "GET",
- bucket: "test-bucket",
- objectKey: "",
- queryParams: map[string]string{},
- fallbackAction: s3_constants.ACTION_LIST,
- expected: "s3:ListBucket",
- description: "Bucket listing",
- },
- {
- name: "get_bucket_acl",
- method: "GET",
- bucket: "test-bucket",
- objectKey: "",
- queryParams: map[string]string{"acl": ""},
- fallbackAction: s3_constants.ACTION_READ_ACP,
- expected: "s3:GetBucketAcl",
- description: "Bucket ACL retrieval",
- },
- {
- name: "put_bucket_policy",
- method: "PUT",
- bucket: "test-bucket",
- objectKey: "",
- queryParams: map[string]string{"policy": ""},
- fallbackAction: s3_constants.ACTION_WRITE,
- expected: "s3:PutBucketPolicy",
- description: "Bucket policy modification",
- },
- {
- name: "delete_bucket",
- method: "DELETE",
- bucket: "test-bucket",
- objectKey: "",
- queryParams: map[string]string{},
- fallbackAction: s3_constants.ACTION_DELETE_BUCKET,
- expected: "s3:DeleteBucket",
- description: "Bucket deletion",
- },
- {
- name: "list_multipart_uploads",
- method: "GET",
- bucket: "test-bucket",
- objectKey: "",
- queryParams: map[string]string{"uploads": ""},
- fallbackAction: s3_constants.ACTION_LIST,
- expected: "s3:ListMultipartUploads",
- description: "List multipart uploads in bucket",
- },
- // Fallback scenarios
- {
- name: "legacy_read_fallback",
- method: "GET",
- bucket: "",
- objectKey: "",
- queryParams: map[string]string{},
- fallbackAction: s3_constants.ACTION_READ,
- expected: "s3:GetObject",
- description: "Legacy read action fallback",
- },
- {
- name: "already_granular_action",
- method: "GET",
- bucket: "",
- objectKey: "",
- queryParams: map[string]string{},
- fallbackAction: "s3:GetBucketLocation", // Already granular
- expected: "s3:GetBucketLocation",
- description: "Already granular action passed through",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // Create HTTP request with query parameters
- req := &http.Request{
- Method: tt.method,
- URL: &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
- }
- // Add query parameters
- query := req.URL.Query()
- for key, value := range tt.queryParams {
- query.Set(key, value)
- }
- req.URL.RawQuery = query.Encode()
- // Test the granular action determination
- result := determineGranularS3Action(req, tt.fallbackAction, tt.bucket, tt.objectKey)
- assert.Equal(t, tt.expected, result,
- "Test %s failed: %s. Expected %s but got %s",
- tt.name, tt.description, tt.expected, result)
- })
- }
- }
- // TestMapLegacyActionToIAM tests the legacy action fallback mapping
- func TestMapLegacyActionToIAM(t *testing.T) {
- tests := []struct {
- name string
- legacyAction Action
- expected string
- }{
- {
- name: "read_action_fallback",
- legacyAction: s3_constants.ACTION_READ,
- expected: "s3:GetObject",
- },
- {
- name: "write_action_fallback",
- legacyAction: s3_constants.ACTION_WRITE,
- expected: "s3:PutObject",
- },
- {
- name: "admin_action_fallback",
- legacyAction: s3_constants.ACTION_ADMIN,
- expected: "s3:*",
- },
- {
- name: "granular_multipart_action",
- legacyAction: s3_constants.ACTION_CREATE_MULTIPART_UPLOAD,
- expected: "s3:CreateMultipartUpload",
- },
- {
- name: "unknown_action_with_s3_prefix",
- legacyAction: "s3:CustomAction",
- expected: "s3:CustomAction",
- },
- {
- name: "unknown_action_without_prefix",
- legacyAction: "CustomAction",
- expected: "s3:CustomAction",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := mapLegacyActionToIAM(tt.legacyAction)
- assert.Equal(t, tt.expected, result)
- })
- }
- }
- // TestExtractSourceIP tests source IP extraction from requests
- func TestExtractSourceIP(t *testing.T) {
- tests := []struct {
- name string
- setupReq func() *http.Request
- expectedIP string
- }{
- {
- name: "X-Forwarded-For header",
- setupReq: func() *http.Request {
- req := httptest.NewRequest("GET", "/test", http.NoBody)
- req.Header.Set("X-Forwarded-For", "192.168.1.100, 10.0.0.1")
- return req
- },
- expectedIP: "192.168.1.100",
- },
- {
- name: "X-Real-IP header",
- setupReq: func() *http.Request {
- req := httptest.NewRequest("GET", "/test", http.NoBody)
- req.Header.Set("X-Real-IP", "192.168.1.200")
- return req
- },
- expectedIP: "192.168.1.200",
- },
- {
- name: "RemoteAddr fallback",
- setupReq: func() *http.Request {
- req := httptest.NewRequest("GET", "/test", http.NoBody)
- req.RemoteAddr = "192.168.1.300:12345"
- return req
- },
- expectedIP: "192.168.1.300",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := tt.setupReq()
- result := extractSourceIP(req)
- assert.Equal(t, tt.expectedIP, result)
- })
- }
- }
- // TestExtractRoleNameFromPrincipal tests role name extraction
- func TestExtractRoleNameFromPrincipal(t *testing.T) {
- tests := []struct {
- name string
- principal string
- expected string
- }{
- {
- name: "valid assumed role ARN",
- principal: "arn:seaweed:sts::assumed-role/S3ReadOnlyRole/session-123",
- expected: "S3ReadOnlyRole",
- },
- {
- name: "invalid format",
- principal: "invalid-principal",
- expected: "", // Returns empty string to signal invalid format
- },
- {
- name: "missing session name",
- principal: "arn:seaweed:sts::assumed-role/TestRole",
- expected: "TestRole", // Extracts role name even without session name
- },
- {
- name: "empty principal",
- principal: "",
- expected: "",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := utils.ExtractRoleNameFromPrincipal(tt.principal)
- assert.Equal(t, tt.expected, result)
- })
- }
- }
- // TestIAMIdentityIsAdmin tests the IsAdmin method
- func TestIAMIdentityIsAdmin(t *testing.T) {
- identity := &IAMIdentity{
- Name: "test-identity",
- Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
- SessionToken: "test-token",
- }
- // In our implementation, IsAdmin always returns false since admin status
- // is determined by policies, not identity
- result := identity.IsAdmin()
- assert.False(t, result)
- }
|