diff options
Diffstat (limited to 'weed/s3api/s3_jwt_auth_test.go')
| -rw-r--r-- | weed/s3api/s3_jwt_auth_test.go | 557 |
1 files changed, 557 insertions, 0 deletions
diff --git a/weed/s3api/s3_jwt_auth_test.go b/weed/s3api/s3_jwt_auth_test.go new file mode 100644 index 000000000..f6b2774d7 --- /dev/null +++ b/weed/s3api/s3_jwt_auth_test.go @@ -0,0 +1,557 @@ +package s3api + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/seaweedfs/seaweedfs/weed/iam/integration" + "github.com/seaweedfs/seaweedfs/weed/iam/ldap" + "github.com/seaweedfs/seaweedfs/weed/iam/oidc" + "github.com/seaweedfs/seaweedfs/weed/iam/policy" + "github.com/seaweedfs/seaweedfs/weed/iam/sts" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// createTestJWTAuth creates a test JWT token with the specified issuer, subject and signing key +func createTestJWTAuth(t *testing.T, issuer, subject, signingKey string) string { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iss": issuer, + "sub": subject, + "aud": "test-client-id", + "exp": time.Now().Add(time.Hour).Unix(), + "iat": time.Now().Unix(), + // Add claims that trust policy validation expects + "idp": "test-oidc", // Identity provider claim for trust policy matching + }) + + tokenString, err := token.SignedString([]byte(signingKey)) + require.NoError(t, err) + return tokenString +} + +// TestJWTAuthenticationFlow tests the JWT authentication flow without full S3 server +func TestJWTAuthenticationFlow(t *testing.T) { + // Set up IAM system + iamManager := setupTestIAMManager(t) + + // Create IAM integration + s3iam := NewS3IAMIntegration(iamManager, "localhost:8888") + + // Create IAM server with integration + iamServer := setupIAMWithIntegration(t, iamManager, s3iam) + + // Test scenarios + tests := []struct { + name string + roleArn string + setupRole func(ctx context.Context, mgr *integration.IAMManager) + testOperations []JWTTestOperation + }{ + { + name: "Read-Only JWT Authentication", + roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole", + setupRole: setupTestReadOnlyRole, + testOperations: []JWTTestOperation{ + {Action: s3_constants.ACTION_READ, Bucket: "test-bucket", Object: "test-file.txt", ExpectedAllow: true}, + {Action: s3_constants.ACTION_WRITE, Bucket: "test-bucket", Object: "new-file.txt", ExpectedAllow: false}, + {Action: s3_constants.ACTION_LIST, Bucket: "test-bucket", Object: "", ExpectedAllow: true}, + }, + }, + { + name: "Admin JWT Authentication", + roleArn: "arn:seaweed:iam::role/S3AdminRole", + setupRole: setupTestAdminRole, + testOperations: []JWTTestOperation{ + {Action: s3_constants.ACTION_READ, Bucket: "admin-bucket", Object: "admin-file.txt", ExpectedAllow: true}, + {Action: s3_constants.ACTION_WRITE, Bucket: "admin-bucket", Object: "new-admin-file.txt", ExpectedAllow: true}, + {Action: s3_constants.ACTION_DELETE_BUCKET, Bucket: "admin-bucket", Object: "", ExpectedAllow: true}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + // Set up role + tt.setupRole(ctx, iamManager) + + // Create a valid JWT token for testing + validJWTToken := createTestJWTAuth(t, "https://test-issuer.com", "test-user-123", "test-signing-key") + + // Assume role to get JWT + response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{ + RoleArn: tt.roleArn, + WebIdentityToken: validJWTToken, + RoleSessionName: "jwt-auth-test", + }) + require.NoError(t, err) + + jwtToken := response.Credentials.SessionToken + + // Test each operation + for _, op := range tt.testOperations { + t.Run(string(op.Action), func(t *testing.T) { + // Test JWT authentication + identity, errCode := testJWTAuthentication(t, iamServer, jwtToken) + require.Equal(t, s3err.ErrNone, errCode, "JWT authentication should succeed") + require.NotNil(t, identity) + + // Test authorization with appropriate role based on test case + var testRoleName string + if tt.name == "Read-Only JWT Authentication" { + testRoleName = "TestReadRole" + } else { + testRoleName = "TestAdminRole" + } + allowed := testJWTAuthorizationWithRole(t, iamServer, identity, op.Action, op.Bucket, op.Object, jwtToken, testRoleName) + assert.Equal(t, op.ExpectedAllow, allowed, "Operation %s should have expected result", op.Action) + }) + } + }) + } +} + +// TestJWTTokenValidation tests JWT token validation edge cases +func TestJWTTokenValidation(t *testing.T) { + iamManager := setupTestIAMManager(t) + s3iam := NewS3IAMIntegration(iamManager, "localhost:8888") + iamServer := setupIAMWithIntegration(t, iamManager, s3iam) + + tests := []struct { + name string + token string + expectedErr s3err.ErrorCode + }{ + { + name: "Empty token", + token: "", + expectedErr: s3err.ErrAccessDenied, + }, + { + name: "Invalid token format", + token: "invalid-token", + expectedErr: s3err.ErrAccessDenied, + }, + { + name: "Expired token", + token: "expired-session-token", + expectedErr: s3err.ErrAccessDenied, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + identity, errCode := testJWTAuthentication(t, iamServer, tt.token) + + assert.Equal(t, tt.expectedErr, errCode) + assert.Nil(t, identity) + }) + } +} + +// TestRequestContextExtraction tests context extraction for policy conditions +func TestRequestContextExtraction(t *testing.T) { + tests := []struct { + name string + setupRequest func() *http.Request + expectedIP string + expectedUA string + }{ + { + name: "Standard request with IP", + setupRequest: func() *http.Request { + req := httptest.NewRequest("GET", "/test-bucket/test-file.txt", http.NoBody) + req.Header.Set("X-Forwarded-For", "192.168.1.100") + req.Header.Set("User-Agent", "aws-sdk-go/1.0") + return req + }, + expectedIP: "192.168.1.100", + expectedUA: "aws-sdk-go/1.0", + }, + { + name: "Request with X-Real-IP", + setupRequest: func() *http.Request { + req := httptest.NewRequest("GET", "/test-bucket/test-file.txt", http.NoBody) + req.Header.Set("X-Real-IP", "10.0.0.1") + req.Header.Set("User-Agent", "boto3/1.0") + return req + }, + expectedIP: "10.0.0.1", + expectedUA: "boto3/1.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := tt.setupRequest() + + // Extract request context + context := extractRequestContext(req) + + if tt.expectedIP != "" { + assert.Equal(t, tt.expectedIP, context["sourceIP"]) + } + + if tt.expectedUA != "" { + assert.Equal(t, tt.expectedUA, context["userAgent"]) + } + }) + } +} + +// TestIPBasedPolicyEnforcement tests IP-based conditional policies +func TestIPBasedPolicyEnforcement(t *testing.T) { + iamManager := setupTestIAMManager(t) + s3iam := NewS3IAMIntegration(iamManager, "localhost:8888") + ctx := context.Background() + + // Set up IP-restricted role + setupTestIPRestrictedRole(ctx, iamManager) + + // Create a valid JWT token for testing + validJWTToken := createTestJWTAuth(t, "https://test-issuer.com", "test-user-123", "test-signing-key") + + // Assume role + response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{ + RoleArn: "arn:seaweed:iam::role/S3IPRestrictedRole", + WebIdentityToken: validJWTToken, + RoleSessionName: "ip-test-session", + }) + require.NoError(t, err) + + tests := []struct { + name string + sourceIP string + shouldAllow bool + }{ + { + name: "Allow from office IP", + sourceIP: "192.168.1.100", + shouldAllow: true, + }, + { + name: "Block from external IP", + sourceIP: "8.8.8.8", + shouldAllow: false, + }, + { + name: "Allow from internal range", + sourceIP: "10.0.0.1", + shouldAllow: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create request with specific IP + req := httptest.NewRequest("GET", "/restricted-bucket/file.txt", http.NoBody) + req.Header.Set("Authorization", "Bearer "+response.Credentials.SessionToken) + req.Header.Set("X-Forwarded-For", tt.sourceIP) + + // Create IAM identity for testing + identity := &IAMIdentity{ + Name: "test-user", + Principal: response.AssumedRoleUser.Arn, + SessionToken: response.Credentials.SessionToken, + } + + // Test authorization with IP condition + errCode := s3iam.AuthorizeAction(ctx, identity, s3_constants.ACTION_READ, "restricted-bucket", "file.txt", req) + + if tt.shouldAllow { + assert.Equal(t, s3err.ErrNone, errCode, "Should allow access from IP %s", tt.sourceIP) + } else { + assert.Equal(t, s3err.ErrAccessDenied, errCode, "Should deny access from IP %s", tt.sourceIP) + } + }) + } +} + +// JWTTestOperation represents a test operation for JWT testing +type JWTTestOperation struct { + Action Action + Bucket string + Object string + ExpectedAllow bool +} + +// Helper functions + +func setupTestIAMManager(t *testing.T) *integration.IAMManager { + // Create IAM manager + manager := 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 := manager.Initialize(config, func() string { + return "localhost:8888" // Mock filer address for testing + }) + require.NoError(t, err) + + // Set up test identity providers + setupTestIdentityProviders(t, manager) + + return manager +} + +func setupTestIdentityProviders(t *testing.T, manager *integration.IAMManager) { + // Set up OIDC provider + oidcProvider := oidc.NewMockOIDCProvider("test-oidc") + oidcConfig := &oidc.OIDCConfig{ + Issuer: "https://test-issuer.com", + ClientID: "test-client-id", + } + err := oidcProvider.Initialize(oidcConfig) + require.NoError(t, err) + oidcProvider.SetupDefaultTestData() + + // Set up LDAP provider + ldapProvider := ldap.NewMockLDAPProvider("test-ldap") + err = ldapProvider.Initialize(nil) // Mock doesn't need real config + require.NoError(t, err) + ldapProvider.SetupDefaultTestData() + + // Register providers + err = manager.RegisterIdentityProvider(oidcProvider) + require.NoError(t, err) + err = manager.RegisterIdentityProvider(ldapProvider) + require.NoError(t, err) +} + +func setupIAMWithIntegration(t *testing.T, iamManager *integration.IAMManager, s3iam *S3IAMIntegration) *IdentityAccessManagement { + // Create a minimal IdentityAccessManagement for testing + iam := &IdentityAccessManagement{ + isAuthEnabled: true, + } + + // Set IAM integration + iam.SetIAMIntegration(s3iam) + + return iam +} + +func setupTestReadOnlyRole(ctx context.Context, manager *integration.IAMManager) { + // Create read-only policy + readPolicy := &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "AllowS3Read", + Effect: "Allow", + Action: []string{"s3:GetObject", "s3:ListBucket"}, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + }, + { + Sid: "AllowSTSSessionValidation", + Effect: "Allow", + Action: []string{"sts:ValidateSession"}, + Resource: []string{"*"}, + }, + }, + } + + manager.CreatePolicy(ctx, "", "S3ReadOnlyPolicy", readPolicy) + + // Create role + manager.CreateRole(ctx, "", "S3ReadOnlyRole", &integration.RoleDefinition{ + RoleName: "S3ReadOnlyRole", + TrustPolicy: &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Effect: "Allow", + Principal: map[string]interface{}{ + "Federated": "test-oidc", + }, + Action: []string{"sts:AssumeRoleWithWebIdentity"}, + }, + }, + }, + AttachedPolicies: []string{"S3ReadOnlyPolicy"}, + }) + + // Also create a TestReadRole for read-only authorization testing + manager.CreateRole(ctx, "", "TestReadRole", &integration.RoleDefinition{ + RoleName: "TestReadRole", + TrustPolicy: &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Effect: "Allow", + Principal: map[string]interface{}{ + "Federated": "test-oidc", + }, + Action: []string{"sts:AssumeRoleWithWebIdentity"}, + }, + }, + }, + AttachedPolicies: []string{"S3ReadOnlyPolicy"}, + }) +} + +func setupTestAdminRole(ctx context.Context, manager *integration.IAMManager) { + // Create admin policy + adminPolicy := &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "AllowAllS3", + Effect: "Allow", + Action: []string{"s3:*"}, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + }, + { + Sid: "AllowSTSSessionValidation", + Effect: "Allow", + Action: []string{"sts:ValidateSession"}, + Resource: []string{"*"}, + }, + }, + } + + manager.CreatePolicy(ctx, "", "S3AdminPolicy", adminPolicy) + + // Create role + manager.CreateRole(ctx, "", "S3AdminRole", &integration.RoleDefinition{ + RoleName: "S3AdminRole", + TrustPolicy: &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Effect: "Allow", + Principal: map[string]interface{}{ + "Federated": "test-oidc", + }, + Action: []string{"sts:AssumeRoleWithWebIdentity"}, + }, + }, + }, + AttachedPolicies: []string{"S3AdminPolicy"}, + }) + + // Also create a TestAdminRole with admin policy for authorization testing + manager.CreateRole(ctx, "", "TestAdminRole", &integration.RoleDefinition{ + RoleName: "TestAdminRole", + TrustPolicy: &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Effect: "Allow", + Principal: map[string]interface{}{ + "Federated": "test-oidc", + }, + Action: []string{"sts:AssumeRoleWithWebIdentity"}, + }, + }, + }, + AttachedPolicies: []string{"S3AdminPolicy"}, // Admin gets full access + }) +} + +func setupTestIPRestrictedRole(ctx context.Context, manager *integration.IAMManager) { + // Create IP-restricted policy + restrictedPolicy := &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "AllowFromOffice", + Effect: "Allow", + Action: []string{"s3:GetObject", "s3:ListBucket"}, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + Condition: map[string]map[string]interface{}{ + "IpAddress": { + "seaweed:SourceIP": []string{"192.168.1.0/24", "10.0.0.0/8"}, + }, + }, + }, + }, + } + + manager.CreatePolicy(ctx, "", "S3IPRestrictedPolicy", restrictedPolicy) + + // Create role + manager.CreateRole(ctx, "", "S3IPRestrictedRole", &integration.RoleDefinition{ + RoleName: "S3IPRestrictedRole", + TrustPolicy: &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Effect: "Allow", + Principal: map[string]interface{}{ + "Federated": "test-oidc", + }, + Action: []string{"sts:AssumeRoleWithWebIdentity"}, + }, + }, + }, + AttachedPolicies: []string{"S3IPRestrictedPolicy"}, + }) +} + +func testJWTAuthentication(t *testing.T, iam *IdentityAccessManagement, token string) (*Identity, s3err.ErrorCode) { + // Create test request with JWT + req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody) + req.Header.Set("Authorization", "Bearer "+token) + + // Test authentication + if iam.iamIntegration == nil { + return nil, s3err.ErrNotImplemented + } + + return iam.authenticateJWTWithIAM(req) +} + +func testJWTAuthorization(t *testing.T, iam *IdentityAccessManagement, identity *Identity, action Action, bucket, object, token string) bool { + return testJWTAuthorizationWithRole(t, iam, identity, action, bucket, object, token, "TestRole") +} + +func testJWTAuthorizationWithRole(t *testing.T, iam *IdentityAccessManagement, identity *Identity, action Action, bucket, object, token, roleName string) bool { + // Create test request + req := httptest.NewRequest("GET", "/"+bucket+"/"+object, http.NoBody) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("X-SeaweedFS-Session-Token", token) + + // Use a proper principal ARN format that matches what STS would generate + principalArn := "arn:seaweed:sts::assumed-role/" + roleName + "/test-session" + req.Header.Set("X-SeaweedFS-Principal", principalArn) + + // Test authorization + if iam.iamIntegration == nil { + return false + } + + errCode := iam.authorizeWithIAM(req, identity, action, bucket, object) + return errCode == s3err.ErrNone +} |
