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