1 changed files with 419 additions and 0 deletions
@ -0,0 +1,419 @@ |
|||
package sts |
|||
|
|||
import ( |
|||
"context" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
// TestCrossInstanceTokenUsage verifies that tokens generated by one STS instance
|
|||
// can be used and validated by other STS instances in a distributed environment
|
|||
func TestCrossInstanceTokenUsage(t *testing.T) { |
|||
ctx := context.Background() |
|||
|
|||
// Common configuration that would be shared across all instances in production
|
|||
sharedConfig := &STSConfig{ |
|||
TokenDuration: time.Hour, |
|||
MaxSessionLength: 12 * time.Hour, |
|||
Issuer: "distributed-sts-cluster", // SAME across all instances
|
|||
SigningKey: []byte("shared-signing-key-32-characters-long"), // SAME across all instances
|
|||
SessionStoreType: "memory", // In production, this would be "filer" for true sharing
|
|||
SessionStoreConfig: map[string]interface{}{ |
|||
"filerAddress": "shared-filer:8888", |
|||
"basePath": "/seaweedfs/iam/sessions", |
|||
}, |
|||
Providers: []*ProviderConfig{ |
|||
{ |
|||
Name: "company-oidc", |
|||
Type: "oidc", |
|||
Enabled: true, |
|||
Config: map[string]interface{}{ |
|||
"issuer": "https://sso.company.com/realms/production", |
|||
"clientId": "seaweedfs-cluster", |
|||
"jwksUri": "https://sso.company.com/realms/production/protocol/openid-connect/certs", |
|||
}, |
|||
}, |
|||
{ |
|||
Name: "test-mock", |
|||
Type: "mock", |
|||
Enabled: true, |
|||
Config: map[string]interface{}{ |
|||
"issuer": "http://test-mock:9999", |
|||
"clientId": "test-client", |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Create multiple STS instances simulating different S3 gateway instances
|
|||
instanceA := NewSTSService() // e.g., s3-gateway-1
|
|||
instanceB := NewSTSService() // e.g., s3-gateway-2
|
|||
instanceC := NewSTSService() // e.g., s3-gateway-3
|
|||
|
|||
// Initialize all instances with IDENTICAL configuration
|
|||
err := instanceA.Initialize(sharedConfig) |
|||
require.NoError(t, err, "Instance A should initialize") |
|||
|
|||
err = instanceB.Initialize(sharedConfig) |
|||
require.NoError(t, err, "Instance B should initialize") |
|||
|
|||
err = instanceC.Initialize(sharedConfig) |
|||
require.NoError(t, err, "Instance C should initialize") |
|||
|
|||
// Test 1: Token generated on Instance A can be validated on Instance B & C
|
|||
t.Run("cross_instance_token_validation", func(t *testing.T) { |
|||
// Generate session token on Instance A
|
|||
sessionId := "cross-instance-session-123" |
|||
expiresAt := time.Now().Add(time.Hour) |
|||
|
|||
tokenFromA, err := instanceA.tokenGenerator.GenerateSessionToken(sessionId, expiresAt) |
|||
require.NoError(t, err, "Instance A should generate token") |
|||
|
|||
// Validate token on Instance B
|
|||
claimsFromB, err := instanceB.tokenGenerator.ValidateSessionToken(tokenFromA) |
|||
require.NoError(t, err, "Instance B should validate token from Instance A") |
|||
assert.Equal(t, sessionId, claimsFromB.SessionId, "Session ID should match") |
|||
|
|||
// Validate same token on Instance C
|
|||
claimsFromC, err := instanceC.tokenGenerator.ValidateSessionToken(tokenFromA) |
|||
require.NoError(t, err, "Instance C should validate token from Instance A") |
|||
assert.Equal(t, sessionId, claimsFromC.SessionId, "Session ID should match") |
|||
|
|||
// All instances should extract identical claims
|
|||
assert.Equal(t, claimsFromB.SessionId, claimsFromC.SessionId) |
|||
assert.Equal(t, claimsFromB.ExpiresAt.Unix(), claimsFromC.ExpiresAt.Unix()) |
|||
assert.Equal(t, claimsFromB.IssuedAt.Unix(), claimsFromC.IssuedAt.Unix()) |
|||
}) |
|||
|
|||
// Test 2: Complete assume role flow across instances
|
|||
t.Run("cross_instance_assume_role_flow", func(t *testing.T) { |
|||
// Step 1: User authenticates and assumes role on Instance A
|
|||
assumeRequest := &AssumeRoleWithWebIdentityRequest{ |
|||
RoleArn: "arn:seaweed:iam::role/CrossInstanceTestRole", |
|||
WebIdentityToken: "valid_test_token", // Mock provider token
|
|||
RoleSessionName: "cross-instance-test-session", |
|||
DurationSeconds: int64ToPtr(3600), |
|||
} |
|||
|
|||
// Instance A processes assume role request
|
|||
responseFromA, err := instanceA.AssumeRoleWithWebIdentity(ctx, assumeRequest) |
|||
require.NoError(t, err, "Instance A should process assume role") |
|||
|
|||
sessionToken := responseFromA.Credentials.SessionToken |
|||
accessKeyId := responseFromA.Credentials.AccessKeyId |
|||
secretAccessKey := responseFromA.Credentials.SecretAccessKey |
|||
|
|||
// Verify response structure
|
|||
assert.NotEmpty(t, sessionToken, "Should have session token") |
|||
assert.NotEmpty(t, accessKeyId, "Should have access key ID") |
|||
assert.NotEmpty(t, secretAccessKey, "Should have secret access key") |
|||
assert.NotNil(t, responseFromA.AssumedRoleUser, "Should have assumed role user") |
|||
|
|||
// Step 2: Use session token on Instance B (different instance)
|
|||
sessionInfoFromB, err := instanceB.ValidateSessionToken(ctx, sessionToken) |
|||
require.NoError(t, err, "Instance B should validate session token from Instance A") |
|||
|
|||
assert.Equal(t, assumeRequest.RoleSessionName, sessionInfoFromB.SessionName) |
|||
assert.Equal(t, assumeRequest.RoleArn, sessionInfoFromB.RoleArn) |
|||
|
|||
// Step 3: Use same session token on Instance C (yet another instance)
|
|||
sessionInfoFromC, err := instanceC.ValidateSessionToken(ctx, sessionToken) |
|||
require.NoError(t, err, "Instance C should validate session token from Instance A") |
|||
|
|||
// All instances should return identical session information
|
|||
assert.Equal(t, sessionInfoFromB.SessionId, sessionInfoFromC.SessionId) |
|||
assert.Equal(t, sessionInfoFromB.SessionName, sessionInfoFromC.SessionName) |
|||
assert.Equal(t, sessionInfoFromB.RoleArn, sessionInfoFromC.RoleArn) |
|||
assert.Equal(t, sessionInfoFromB.Subject, sessionInfoFromC.Subject) |
|||
assert.Equal(t, sessionInfoFromB.Provider, sessionInfoFromC.Provider) |
|||
}) |
|||
|
|||
// Test 3: Session revocation across instances
|
|||
t.Run("cross_instance_session_revocation", func(t *testing.T) { |
|||
// Create session on Instance A
|
|||
assumeRequest := &AssumeRoleWithWebIdentityRequest{ |
|||
RoleArn: "arn:seaweed:iam::role/RevocationTestRole", |
|||
WebIdentityToken: "valid_test_token", |
|||
RoleSessionName: "revocation-test-session", |
|||
} |
|||
|
|||
response, err := instanceA.AssumeRoleWithWebIdentity(ctx, assumeRequest) |
|||
require.NoError(t, err) |
|||
sessionToken := response.Credentials.SessionToken |
|||
|
|||
// Verify token works on Instance B
|
|||
_, err = instanceB.ValidateSessionToken(ctx, sessionToken) |
|||
require.NoError(t, err, "Token should be valid on Instance B initially") |
|||
|
|||
// Revoke session on Instance C
|
|||
err = instanceC.RevokeSession(ctx, sessionToken) |
|||
require.NoError(t, err, "Instance C should be able to revoke session") |
|||
|
|||
// Verify token is now invalid on Instance A (revoked by Instance C)
|
|||
_, err = instanceA.ValidateSessionToken(ctx, sessionToken) |
|||
assert.Error(t, err, "Token should be invalid on Instance A after revocation") |
|||
|
|||
// Verify token is also invalid on Instance B
|
|||
_, err = instanceB.ValidateSessionToken(ctx, sessionToken) |
|||
assert.Error(t, err, "Token should be invalid on Instance B after revocation") |
|||
}) |
|||
|
|||
// Test 4: Provider consistency across instances
|
|||
t.Run("provider_consistency_affects_token_generation", func(t *testing.T) { |
|||
// All instances should have same providers and be able to process same OIDC tokens
|
|||
providerNamesA := instanceA.getProviderNames() |
|||
providerNamesB := instanceB.getProviderNames() |
|||
providerNamesC := instanceC.getProviderNames() |
|||
|
|||
assert.ElementsMatch(t, providerNamesA, providerNamesB, "Instance A and B should have same providers") |
|||
assert.ElementsMatch(t, providerNamesB, providerNamesC, "Instance B and C should have same providers") |
|||
|
|||
// All instances should be able to process same web identity token
|
|||
testToken := "valid_test_token" |
|||
|
|||
// Try to assume role with same token on different instances
|
|||
assumeRequest := &AssumeRoleWithWebIdentityRequest{ |
|||
RoleArn: "arn:seaweed:iam::role/ProviderTestRole", |
|||
WebIdentityToken: testToken, |
|||
RoleSessionName: "provider-consistency-test", |
|||
} |
|||
|
|||
// Should work on any instance
|
|||
responseA, errA := instanceA.AssumeRoleWithWebIdentity(ctx, assumeRequest) |
|||
responseB, errB := instanceB.AssumeRoleWithWebIdentity(ctx, assumeRequest) |
|||
responseC, errC := instanceC.AssumeRoleWithWebIdentity(ctx, assumeRequest) |
|||
|
|||
require.NoError(t, errA, "Instance A should process OIDC token") |
|||
require.NoError(t, errB, "Instance B should process OIDC token") |
|||
require.NoError(t, errC, "Instance C should process OIDC token") |
|||
|
|||
// All should return valid responses (sessions will have different IDs but same structure)
|
|||
assert.NotEmpty(t, responseA.Credentials.SessionToken) |
|||
assert.NotEmpty(t, responseB.Credentials.SessionToken) |
|||
assert.NotEmpty(t, responseC.Credentials.SessionToken) |
|||
}) |
|||
} |
|||
|
|||
// TestSTSDistributedConfigurationRequirements tests the configuration requirements
|
|||
// for cross-instance token compatibility
|
|||
func TestSTSDistributedConfigurationRequirements(t *testing.T) { |
|||
|
|||
t.Run("same_signing_key_required", func(t *testing.T) { |
|||
// Instance A with signing key 1
|
|||
configA := &STSConfig{ |
|||
TokenDuration: time.Hour, |
|||
MaxSessionLength: 12 * time.Hour, |
|||
Issuer: "test-sts", |
|||
SigningKey: []byte("signing-key-1-32-characters-long"), |
|||
} |
|||
|
|||
// Instance B with different signing key
|
|||
configB := &STSConfig{ |
|||
TokenDuration: time.Hour, |
|||
MaxSessionLength: 12 * time.Hour, |
|||
Issuer: "test-sts", |
|||
SigningKey: []byte("signing-key-2-32-characters-long"), // DIFFERENT!
|
|||
} |
|||
|
|||
instanceA := NewSTSService() |
|||
instanceB := NewSTSService() |
|||
|
|||
err := instanceA.Initialize(configA) |
|||
require.NoError(t, err) |
|||
|
|||
err = instanceB.Initialize(configB) |
|||
require.NoError(t, err) |
|||
|
|||
// Generate token on Instance A
|
|||
sessionId := "test-session" |
|||
expiresAt := time.Now().Add(time.Hour) |
|||
tokenFromA, err := instanceA.tokenGenerator.GenerateSessionToken(sessionId, expiresAt) |
|||
require.NoError(t, err) |
|||
|
|||
// Instance A should validate its own token
|
|||
_, err = instanceA.tokenGenerator.ValidateSessionToken(tokenFromA) |
|||
assert.NoError(t, err, "Instance A should validate own token") |
|||
|
|||
// Instance B should REJECT token due to different signing key
|
|||
_, err = instanceB.tokenGenerator.ValidateSessionToken(tokenFromA) |
|||
assert.Error(t, err, "Instance B should reject token with different signing key") |
|||
assert.Contains(t, err.Error(), "invalid token", "Should be signature validation error") |
|||
}) |
|||
|
|||
t.Run("same_issuer_required", func(t *testing.T) { |
|||
sharedSigningKey := []byte("shared-signing-key-32-characters-lo") |
|||
|
|||
// Instance A with issuer 1
|
|||
configA := &STSConfig{ |
|||
TokenDuration: time.Hour, |
|||
MaxSessionLength: 12 * time.Hour, |
|||
Issuer: "sts-cluster-1", |
|||
SigningKey: sharedSigningKey, |
|||
} |
|||
|
|||
// Instance B with different issuer
|
|||
configB := &STSConfig{ |
|||
TokenDuration: time.Hour, |
|||
MaxSessionLength: 12 * time.Hour, |
|||
Issuer: "sts-cluster-2", // DIFFERENT!
|
|||
SigningKey: sharedSigningKey, |
|||
} |
|||
|
|||
instanceA := NewSTSService() |
|||
instanceB := NewSTSService() |
|||
|
|||
err := instanceA.Initialize(configA) |
|||
require.NoError(t, err) |
|||
|
|||
err = instanceB.Initialize(configB) |
|||
require.NoError(t, err) |
|||
|
|||
// Generate token on Instance A
|
|||
sessionId := "test-session" |
|||
expiresAt := time.Now().Add(time.Hour) |
|||
tokenFromA, err := instanceA.tokenGenerator.GenerateSessionToken(sessionId, expiresAt) |
|||
require.NoError(t, err) |
|||
|
|||
// Instance B should REJECT token due to different issuer
|
|||
_, err = instanceB.tokenGenerator.ValidateSessionToken(tokenFromA) |
|||
assert.Error(t, err, "Instance B should reject token with different issuer") |
|||
assert.Contains(t, err.Error(), "invalid issuer", "Should be issuer validation error") |
|||
}) |
|||
|
|||
t.Run("identical_configuration_required", func(t *testing.T) { |
|||
// Identical configuration
|
|||
identicalConfig := &STSConfig{ |
|||
TokenDuration: time.Hour, |
|||
MaxSessionLength: 12 * time.Hour, |
|||
Issuer: "production-sts-cluster", |
|||
SigningKey: []byte("production-signing-key-32-chars-l"), |
|||
SessionStoreType: "memory", |
|||
} |
|||
|
|||
// Create multiple instances with identical config
|
|||
instances := make([]*STSService, 5) |
|||
for i := 0; i < 5; i++ { |
|||
instances[i] = NewSTSService() |
|||
err := instances[i].Initialize(identicalConfig) |
|||
require.NoError(t, err, "Instance %d should initialize", i) |
|||
} |
|||
|
|||
// Generate token on Instance 0
|
|||
sessionId := "multi-instance-test" |
|||
expiresAt := time.Now().Add(time.Hour) |
|||
token, err := instances[0].tokenGenerator.GenerateSessionToken(sessionId, expiresAt) |
|||
require.NoError(t, err) |
|||
|
|||
// All other instances should validate the token
|
|||
for i := 1; i < 5; i++ { |
|||
claims, err := instances[i].tokenGenerator.ValidateSessionToken(token) |
|||
require.NoError(t, err, "Instance %d should validate token", i) |
|||
assert.Equal(t, sessionId, claims.SessionId, "Instance %d should extract correct session ID", i) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
// TestSTSRealWorldDistributedScenarios tests realistic distributed deployment scenarios
|
|||
func TestSTSRealWorldDistributedScenarios(t *testing.T) { |
|||
ctx := context.Background() |
|||
|
|||
t.Run("load_balanced_s3_gateway_scenario", func(t *testing.T) { |
|||
// Simulate real production scenario:
|
|||
// 1. User authenticates with OIDC provider
|
|||
// 2. User calls AssumeRoleWithWebIdentity on S3 Gateway 1
|
|||
// 3. User makes S3 requests that hit S3 Gateway 2 & 3 via load balancer
|
|||
// 4. All instances should handle the session token correctly
|
|||
|
|||
productionConfig := &STSConfig{ |
|||
TokenDuration: 2 * time.Hour, |
|||
MaxSessionLength: 24 * time.Hour, |
|||
Issuer: "seaweedfs-production-sts", |
|||
SigningKey: []byte("prod-signing-key-32-characters-lon"), |
|||
SessionStoreType: "filer", |
|||
SessionStoreConfig: map[string]interface{}{ |
|||
"filerAddress": "prod-filer-cluster:8888", |
|||
"basePath": "/seaweedfs/iam/sessions", |
|||
}, |
|||
Providers: []*ProviderConfig{ |
|||
{ |
|||
Name: "corporate-oidc", |
|||
Type: "oidc", |
|||
Enabled: true, |
|||
Config: map[string]interface{}{ |
|||
"issuer": "https://sso.company.com/realms/production", |
|||
"clientId": "seaweedfs-prod-cluster", |
|||
"clientSecret": "supersecret-prod-key", |
|||
"scopes": []string{"openid", "profile", "email", "groups"}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Create 3 S3 Gateway instances behind load balancer
|
|||
gateway1 := NewSTSService() |
|||
gateway2 := NewSTSService() |
|||
gateway3 := NewSTSService() |
|||
|
|||
err := gateway1.Initialize(productionConfig) |
|||
require.NoError(t, err) |
|||
|
|||
err = gateway2.Initialize(productionConfig) |
|||
require.NoError(t, err) |
|||
|
|||
err = gateway3.Initialize(productionConfig) |
|||
require.NoError(t, err) |
|||
|
|||
// Step 1: User authenticates and hits Gateway 1 for AssumeRole
|
|||
assumeRequest := &AssumeRoleWithWebIdentityRequest{ |
|||
RoleArn: "arn:seaweed:iam::role/ProductionS3User", |
|||
WebIdentityToken: "valid_test_token", // From corporate OIDC
|
|||
RoleSessionName: "user-production-session", |
|||
DurationSeconds: int64ToPtr(7200), // 2 hours
|
|||
} |
|||
|
|||
stsResponse, err := gateway1.AssumeRoleWithWebIdentity(ctx, assumeRequest) |
|||
require.NoError(t, err, "Gateway 1 should handle AssumeRole") |
|||
|
|||
sessionToken := stsResponse.Credentials.SessionToken |
|||
accessKey := stsResponse.Credentials.AccessKeyId |
|||
secretKey := stsResponse.Credentials.SecretAccessKey |
|||
|
|||
// Step 2: User makes S3 requests that hit different gateways via load balancer
|
|||
// Simulate S3 request validation on Gateway 2
|
|||
sessionInfo2, err := gateway2.ValidateSessionToken(ctx, sessionToken) |
|||
require.NoError(t, err, "Gateway 2 should validate session from Gateway 1") |
|||
assert.Equal(t, "user-production-session", sessionInfo2.SessionName) |
|||
assert.Equal(t, "arn:seaweed:iam::role/ProductionS3User", sessionInfo2.RoleArn) |
|||
|
|||
// Simulate S3 request validation on Gateway 3
|
|||
sessionInfo3, err := gateway3.ValidateSessionToken(ctx, sessionToken) |
|||
require.NoError(t, err, "Gateway 3 should validate session from Gateway 1") |
|||
assert.Equal(t, sessionInfo2.SessionId, sessionInfo3.SessionId, "Should be same session") |
|||
|
|||
// Step 3: Verify credentials are consistent
|
|||
assert.Equal(t, accessKey, stsResponse.Credentials.AccessKeyId, "Access key should be consistent") |
|||
assert.Equal(t, secretKey, stsResponse.Credentials.SecretAccessKey, "Secret key should be consistent") |
|||
|
|||
// Step 4: Session expiration should be honored across all instances
|
|||
assert.True(t, sessionInfo2.ExpiresAt.After(time.Now()), "Session should not be expired") |
|||
assert.True(t, sessionInfo3.ExpiresAt.After(time.Now()), "Session should not be expired") |
|||
|
|||
// Step 5: Token should be identical when parsed
|
|||
claims2, err := gateway2.tokenGenerator.ValidateSessionToken(sessionToken) |
|||
require.NoError(t, err) |
|||
|
|||
claims3, err := gateway3.tokenGenerator.ValidateSessionToken(sessionToken) |
|||
require.NoError(t, err) |
|||
|
|||
assert.Equal(t, claims2.SessionId, claims3.SessionId, "Session IDs should match") |
|||
assert.Equal(t, claims2.ExpiresAt.Unix(), claims3.ExpiresAt.Unix(), "Expiration should match") |
|||
}) |
|||
} |
|||
|
|||
// Helper function to convert int64 to pointer
|
|||
func int64ToPtr(i int64) *int64 { |
|||
return &i |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue