You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							340 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							340 lines
						
					
					
						
							13 KiB
						
					
					
				| 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") | |
| }
 |