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