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

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