aboutsummaryrefslogtreecommitdiff
path: root/weed/iam/sts/distributed_sts_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/iam/sts/distributed_sts_test.go')
-rw-r--r--weed/iam/sts/distributed_sts_test.go340
1 files changed, 340 insertions, 0 deletions
diff --git a/weed/iam/sts/distributed_sts_test.go b/weed/iam/sts/distributed_sts_test.go
new file mode 100644
index 000000000..133f3a669
--- /dev/null
+++ b/weed/iam/sts/distributed_sts_test.go
@@ -0,0 +1,340 @@
+package sts
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestDistributedSTSService verifies that multiple STS instances with identical configurations
+// behave consistently across distributed environments
+func TestDistributedSTSService(t *testing.T) {
+ ctx := context.Background()
+
+ // Common configuration for all instances
+ commonConfig := &STSConfig{
+ TokenDuration: FlexibleDuration{time.Hour},
+ MaxSessionLength: FlexibleDuration{12 * time.Hour},
+ Issuer: "distributed-sts-test",
+ SigningKey: []byte("test-signing-key-32-characters-long"),
+
+ Providers: []*ProviderConfig{
+ {
+ Name: "keycloak-oidc",
+ Type: "oidc",
+ Enabled: true,
+ Config: map[string]interface{}{
+ "issuer": "http://keycloak:8080/realms/seaweedfs-test",
+ "clientId": "seaweedfs-s3",
+ "jwksUri": "http://keycloak:8080/realms/seaweedfs-test/protocol/openid-connect/certs",
+ },
+ },
+
+ {
+ Name: "disabled-ldap",
+ Type: "oidc", // Use OIDC as placeholder since LDAP isn't implemented
+ Enabled: false,
+ Config: map[string]interface{}{
+ "issuer": "ldap://company.com",
+ "clientId": "ldap-client",
+ },
+ },
+ },
+ }
+
+ // Create multiple STS instances simulating distributed deployment
+ instance1 := NewSTSService()
+ instance2 := NewSTSService()
+ instance3 := NewSTSService()
+
+ // Initialize all instances with identical configuration
+ err := instance1.Initialize(commonConfig)
+ require.NoError(t, err, "Instance 1 should initialize successfully")
+
+ err = instance2.Initialize(commonConfig)
+ require.NoError(t, err, "Instance 2 should initialize successfully")
+
+ err = instance3.Initialize(commonConfig)
+ require.NoError(t, err, "Instance 3 should initialize successfully")
+
+ // Manually register mock providers for testing (not available in production)
+ mockProviderConfig := map[string]interface{}{
+ "issuer": "http://localhost:9999",
+ "clientId": "test-client",
+ }
+ mockProvider1, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
+ require.NoError(t, err)
+ mockProvider2, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
+ require.NoError(t, err)
+ mockProvider3, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
+ require.NoError(t, err)
+
+ instance1.RegisterProvider(mockProvider1)
+ instance2.RegisterProvider(mockProvider2)
+ instance3.RegisterProvider(mockProvider3)
+
+ // Verify all instances have identical provider configurations
+ t.Run("provider_consistency", func(t *testing.T) {
+ // All instances should have same number of providers
+ assert.Len(t, instance1.providers, 2, "Instance 1 should have 2 enabled providers")
+ assert.Len(t, instance2.providers, 2, "Instance 2 should have 2 enabled providers")
+ assert.Len(t, instance3.providers, 2, "Instance 3 should have 2 enabled providers")
+
+ // All instances should have same provider names
+ instance1Names := instance1.getProviderNames()
+ instance2Names := instance2.getProviderNames()
+ instance3Names := instance3.getProviderNames()
+
+ assert.ElementsMatch(t, instance1Names, instance2Names, "Instance 1 and 2 should have same providers")
+ assert.ElementsMatch(t, instance2Names, instance3Names, "Instance 2 and 3 should have same providers")
+
+ // Verify specific providers exist on all instances
+ expectedProviders := []string{"keycloak-oidc", "test-mock-provider"}
+ assert.ElementsMatch(t, instance1Names, expectedProviders, "Instance 1 should have expected providers")
+ assert.ElementsMatch(t, instance2Names, expectedProviders, "Instance 2 should have expected providers")
+ assert.ElementsMatch(t, instance3Names, expectedProviders, "Instance 3 should have expected providers")
+
+ // Verify disabled providers are not loaded
+ assert.NotContains(t, instance1Names, "disabled-ldap", "Disabled providers should not be loaded")
+ assert.NotContains(t, instance2Names, "disabled-ldap", "Disabled providers should not be loaded")
+ assert.NotContains(t, instance3Names, "disabled-ldap", "Disabled providers should not be loaded")
+ })
+
+ // Test token generation consistency across instances
+ t.Run("token_generation_consistency", func(t *testing.T) {
+ sessionId := "test-session-123"
+ expiresAt := time.Now().Add(time.Hour)
+
+ // Generate tokens from different instances
+ token1, err1 := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
+ token2, err2 := instance2.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
+ token3, err3 := instance3.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
+
+ require.NoError(t, err1, "Instance 1 token generation should succeed")
+ require.NoError(t, err2, "Instance 2 token generation should succeed")
+ require.NoError(t, err3, "Instance 3 token generation should succeed")
+
+ // All tokens should be different (due to timestamp variations)
+ // But they should all be valid JWTs with same signing key
+ assert.NotEmpty(t, token1)
+ assert.NotEmpty(t, token2)
+ assert.NotEmpty(t, token3)
+ })
+
+ // Test token validation consistency - any instance should validate tokens from any other instance
+ t.Run("cross_instance_token_validation", func(t *testing.T) {
+ sessionId := "cross-validation-session"
+ expiresAt := time.Now().Add(time.Hour)
+
+ // Generate token on instance 1
+ token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
+ require.NoError(t, err)
+
+ // Validate on all instances
+ claims1, err1 := instance1.tokenGenerator.ValidateSessionToken(token)
+ claims2, err2 := instance2.tokenGenerator.ValidateSessionToken(token)
+ claims3, err3 := instance3.tokenGenerator.ValidateSessionToken(token)
+
+ require.NoError(t, err1, "Instance 1 should validate token from instance 1")
+ require.NoError(t, err2, "Instance 2 should validate token from instance 1")
+ require.NoError(t, err3, "Instance 3 should validate token from instance 1")
+
+ // All instances should extract same session ID
+ assert.Equal(t, sessionId, claims1.SessionId)
+ assert.Equal(t, sessionId, claims2.SessionId)
+ assert.Equal(t, sessionId, claims3.SessionId)
+
+ assert.Equal(t, claims1.SessionId, claims2.SessionId)
+ assert.Equal(t, claims2.SessionId, claims3.SessionId)
+ })
+
+ // Test provider access consistency
+ t.Run("provider_access_consistency", func(t *testing.T) {
+ // All instances should be able to access the same providers
+ provider1, exists1 := instance1.providers["test-mock-provider"]
+ provider2, exists2 := instance2.providers["test-mock-provider"]
+ provider3, exists3 := instance3.providers["test-mock-provider"]
+
+ assert.True(t, exists1, "Instance 1 should have test-mock-provider")
+ assert.True(t, exists2, "Instance 2 should have test-mock-provider")
+ assert.True(t, exists3, "Instance 3 should have test-mock-provider")
+
+ assert.Equal(t, provider1.Name(), provider2.Name())
+ assert.Equal(t, provider2.Name(), provider3.Name())
+
+ // Test authentication with the mock provider on all instances
+ testToken := "valid_test_token"
+
+ identity1, err1 := provider1.Authenticate(ctx, testToken)
+ identity2, err2 := provider2.Authenticate(ctx, testToken)
+ identity3, err3 := provider3.Authenticate(ctx, testToken)
+
+ require.NoError(t, err1, "Instance 1 provider should authenticate successfully")
+ require.NoError(t, err2, "Instance 2 provider should authenticate successfully")
+ require.NoError(t, err3, "Instance 3 provider should authenticate successfully")
+
+ // All instances should return identical identity information
+ assert.Equal(t, identity1.UserID, identity2.UserID)
+ assert.Equal(t, identity2.UserID, identity3.UserID)
+ assert.Equal(t, identity1.Email, identity2.Email)
+ assert.Equal(t, identity2.Email, identity3.Email)
+ assert.Equal(t, identity1.Provider, identity2.Provider)
+ assert.Equal(t, identity2.Provider, identity3.Provider)
+ })
+}
+
+// TestSTSConfigurationValidation tests configuration validation for distributed deployments
+func TestSTSConfigurationValidation(t *testing.T) {
+ t.Run("consistent_signing_keys_required", func(t *testing.T) {
+ // Different signing keys should result in incompatible token validation
+ config1 := &STSConfig{
+ TokenDuration: FlexibleDuration{time.Hour},
+ MaxSessionLength: FlexibleDuration{12 * time.Hour},
+ Issuer: "test-sts",
+ SigningKey: []byte("signing-key-1-32-characters-long"),
+ }
+
+ config2 := &STSConfig{
+ TokenDuration: FlexibleDuration{time.Hour},
+ MaxSessionLength: FlexibleDuration{12 * time.Hour},
+ Issuer: "test-sts",
+ SigningKey: []byte("signing-key-2-32-characters-long"), // Different key!
+ }
+
+ instance1 := NewSTSService()
+ instance2 := NewSTSService()
+
+ err1 := instance1.Initialize(config1)
+ err2 := instance2.Initialize(config2)
+
+ require.NoError(t, err1)
+ require.NoError(t, err2)
+
+ // Generate token on instance 1
+ sessionId := "test-session"
+ expiresAt := time.Now().Add(time.Hour)
+ token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
+ require.NoError(t, err)
+
+ // Instance 1 should validate its own token
+ _, err = instance1.tokenGenerator.ValidateSessionToken(token)
+ assert.NoError(t, err, "Instance 1 should validate its own token")
+
+ // Instance 2 should reject token from instance 1 (different signing key)
+ _, err = instance2.tokenGenerator.ValidateSessionToken(token)
+ assert.Error(t, err, "Instance 2 should reject token with different signing key")
+ })
+
+ t.Run("consistent_issuer_required", func(t *testing.T) {
+ // Different issuers should result in incompatible tokens
+ commonSigningKey := []byte("shared-signing-key-32-characters-lo")
+
+ config1 := &STSConfig{
+ TokenDuration: FlexibleDuration{time.Hour},
+ MaxSessionLength: FlexibleDuration{12 * time.Hour},
+ Issuer: "sts-instance-1",
+ SigningKey: commonSigningKey,
+ }
+
+ config2 := &STSConfig{
+ TokenDuration: FlexibleDuration{time.Hour},
+ MaxSessionLength: FlexibleDuration{12 * time.Hour},
+ Issuer: "sts-instance-2", // Different issuer!
+ SigningKey: commonSigningKey,
+ }
+
+ instance1 := NewSTSService()
+ instance2 := NewSTSService()
+
+ err1 := instance1.Initialize(config1)
+ err2 := instance2.Initialize(config2)
+
+ require.NoError(t, err1)
+ require.NoError(t, err2)
+
+ // Generate token on instance 1
+ sessionId := "test-session"
+ expiresAt := time.Now().Add(time.Hour)
+ token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
+ require.NoError(t, err)
+
+ // Instance 2 should reject token due to issuer mismatch
+ // (Even though signing key is the same, issuer validation will fail)
+ _, err = instance2.tokenGenerator.ValidateSessionToken(token)
+ assert.Error(t, err, "Instance 2 should reject token with different issuer")
+ })
+}
+
+// TestProviderFactoryDistributed tests the provider factory in distributed scenarios
+func TestProviderFactoryDistributed(t *testing.T) {
+ factory := NewProviderFactory()
+
+ // Simulate configuration that would be identical across all instances
+ configs := []*ProviderConfig{
+ {
+ Name: "production-keycloak",
+ Type: "oidc",
+ Enabled: true,
+ Config: map[string]interface{}{
+ "issuer": "https://keycloak.company.com/realms/seaweedfs",
+ "clientId": "seaweedfs-prod",
+ "clientSecret": "super-secret-key",
+ "jwksUri": "https://keycloak.company.com/realms/seaweedfs/protocol/openid-connect/certs",
+ "scopes": []string{"openid", "profile", "email", "roles"},
+ },
+ },
+ {
+ Name: "backup-oidc",
+ Type: "oidc",
+ Enabled: false, // Disabled by default
+ Config: map[string]interface{}{
+ "issuer": "https://backup-oidc.company.com",
+ "clientId": "seaweedfs-backup",
+ },
+ },
+ }
+
+ // Create providers multiple times (simulating multiple instances)
+ providers1, err1 := factory.LoadProvidersFromConfig(configs)
+ providers2, err2 := factory.LoadProvidersFromConfig(configs)
+ providers3, err3 := factory.LoadProvidersFromConfig(configs)
+
+ require.NoError(t, err1, "First load should succeed")
+ require.NoError(t, err2, "Second load should succeed")
+ require.NoError(t, err3, "Third load should succeed")
+
+ // All instances should have same provider counts
+ assert.Len(t, providers1, 1, "First instance should have 1 enabled provider")
+ assert.Len(t, providers2, 1, "Second instance should have 1 enabled provider")
+ assert.Len(t, providers3, 1, "Third instance should have 1 enabled provider")
+
+ // All instances should have same provider names
+ names1 := make([]string, 0, len(providers1))
+ names2 := make([]string, 0, len(providers2))
+ names3 := make([]string, 0, len(providers3))
+
+ for name := range providers1 {
+ names1 = append(names1, name)
+ }
+ for name := range providers2 {
+ names2 = append(names2, name)
+ }
+ for name := range providers3 {
+ names3 = append(names3, name)
+ }
+
+ assert.ElementsMatch(t, names1, names2, "Instance 1 and 2 should have same provider names")
+ assert.ElementsMatch(t, names2, names3, "Instance 2 and 3 should have same provider names")
+
+ // Verify specific providers
+ expectedProviders := []string{"production-keycloak"}
+ assert.ElementsMatch(t, names1, expectedProviders, "Should have expected enabled providers")
+
+ // Verify disabled providers are not included
+ assert.NotContains(t, names1, "backup-oidc", "Disabled providers should not be loaded")
+ assert.NotContains(t, names2, "backup-oidc", "Disabled providers should not be loaded")
+ assert.NotContains(t, names3, "backup-oidc", "Disabled providers should not be loaded")
+}