| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- package sts
- import (
- "context"
- "testing"
- "time"
- "github.com/golang-jwt/jwt/v5"
- "github.com/seaweedfs/seaweedfs/weed/iam/oidc"
- "github.com/seaweedfs/seaweedfs/weed/iam/providers"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // Test-only constants for mock providers
- const (
- ProviderTypeMock = "mock"
- )
- // createMockOIDCProvider creates a mock OIDC provider for testing
- // This is only available in test builds
- func createMockOIDCProvider(name string, config map[string]interface{}) (providers.IdentityProvider, error) {
- // Convert config to OIDC format
- factory := NewProviderFactory()
- oidcConfig, err := factory.convertToOIDCConfig(config)
- if err != nil {
- return nil, err
- }
- // Set default values for mock provider if not provided
- if oidcConfig.Issuer == "" {
- oidcConfig.Issuer = "http://localhost:9999"
- }
- provider := oidc.NewMockOIDCProvider(name)
- if err := provider.Initialize(oidcConfig); err != nil {
- return nil, err
- }
- // Set up default test data for the mock provider
- provider.SetupDefaultTestData()
- return provider, nil
- }
- // createMockJWT creates a test JWT token with the specified issuer for mock provider testing
- func createMockJWT(t *testing.T, issuer, subject string) string {
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
- "iss": issuer,
- "sub": subject,
- "aud": "test-client",
- "exp": time.Now().Add(time.Hour).Unix(),
- "iat": time.Now().Unix(),
- })
- tokenString, err := token.SignedString([]byte("test-signing-key"))
- require.NoError(t, err)
- return tokenString
- }
- // 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()
- // Dummy filer address for testing
- // Common configuration that would be shared across all instances in production
- sharedConfig := &STSConfig{
- TokenDuration: FlexibleDuration{time.Hour},
- MaxSessionLength: FlexibleDuration{12 * time.Hour},
- Issuer: "distributed-sts-cluster", // SAME across all instances
- SigningKey: []byte(TestSigningKey32Chars), // SAME across all instances
- Providers: []*ProviderConfig{
- {
- Name: "company-oidc",
- Type: ProviderTypeOIDC,
- Enabled: true,
- Config: map[string]interface{}{
- ConfigFieldIssuer: "https://sso.company.com/realms/production",
- ConfigFieldClientID: "seaweedfs-cluster",
- ConfigFieldJWKSUri: "https://sso.company.com/realms/production/protocol/openid-connect/certs",
- },
- },
- },
- }
- // 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")
- // Set up mock trust policy validator for all instances (required for STS testing)
- mockValidator := &MockTrustPolicyValidator{}
- instanceA.SetTrustPolicyValidator(mockValidator)
- instanceB.SetTrustPolicyValidator(mockValidator)
- instanceC.SetTrustPolicyValidator(mockValidator)
- // Manually register mock provider for testing (not available in production)
- mockProviderConfig := map[string]interface{}{
- ConfigFieldIssuer: "http://test-mock:9999",
- ConfigFieldClientID: TestClientID,
- }
- mockProviderA, err := createMockOIDCProvider("test-mock", mockProviderConfig)
- require.NoError(t, err)
- mockProviderB, err := createMockOIDCProvider("test-mock", mockProviderConfig)
- require.NoError(t, err)
- mockProviderC, err := createMockOIDCProvider("test-mock", mockProviderConfig)
- require.NoError(t, err)
- instanceA.RegisterProvider(mockProviderA)
- instanceB.RegisterProvider(mockProviderB)
- instanceC.RegisterProvider(mockProviderC)
- // 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 := TestSessionID
- 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
- // Create a valid JWT token for the mock provider
- mockToken := createMockJWT(t, "http://test-mock:9999", "test-user")
- assumeRequest := &AssumeRoleWithWebIdentityRequest{
- RoleArn: "arn:seaweed:iam::role/CrossInstanceTestRole",
- WebIdentityToken: mockToken, // JWT token for mock provider
- 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
- mockToken := createMockJWT(t, "http://test-mock:9999", "test-user")
- assumeRequest := &AssumeRoleWithWebIdentityRequest{
- RoleArn: "arn:seaweed:iam::role/RevocationTestRole",
- WebIdentityToken: mockToken,
- 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")
- // Validate session on Instance C to verify cross-instance token compatibility
- _, err = instanceC.ValidateSessionToken(ctx, sessionToken)
- require.NoError(t, err, "Instance C should be able to validate session token")
- // In a stateless JWT system, tokens remain valid on all instances since they're self-contained
- // No revocation is possible without breaking the stateless architecture
- _, err = instanceA.ValidateSessionToken(ctx, sessionToken)
- assert.NoError(t, err, "Token should still be valid on Instance A (stateless system)")
- // Verify token is still valid on Instance B
- _, err = instanceB.ValidateSessionToken(ctx, sessionToken)
- assert.NoError(t, err, "Token should still be valid on Instance B (stateless system)")
- })
- // 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 := createMockJWT(t, "http://test-mock:9999", "test-user")
- // 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) {
- _ = "localhost:8888" // Dummy filer address for testing (not used in these tests)
- t.Run("same_signing_key_required", func(t *testing.T) {
- // Instance A with signing key 1
- configA := &STSConfig{
- TokenDuration: FlexibleDuration{time.Hour},
- MaxSessionLength: FlexibleDuration{12 * time.Hour},
- Issuer: "test-sts",
- SigningKey: []byte("signing-key-1-32-characters-long"),
- }
- // Instance B with different signing key
- configB := &STSConfig{
- TokenDuration: FlexibleDuration{time.Hour},
- MaxSessionLength: FlexibleDuration{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: FlexibleDuration{time.Hour},
- MaxSessionLength: FlexibleDuration{12 * time.Hour},
- Issuer: "sts-cluster-1",
- SigningKey: sharedSigningKey,
- }
- // Instance B with different issuer
- configB := &STSConfig{
- TokenDuration: FlexibleDuration{time.Hour},
- MaxSessionLength: FlexibleDuration{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: FlexibleDuration{time.Hour},
- MaxSessionLength: FlexibleDuration{12 * time.Hour},
- Issuer: "production-sts-cluster",
- SigningKey: []byte("production-signing-key-32-chars-l"),
- }
- // 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: FlexibleDuration{2 * time.Hour},
- MaxSessionLength: FlexibleDuration{24 * time.Hour},
- Issuer: "seaweedfs-production-sts",
- SigningKey: []byte("prod-signing-key-32-characters-lon"),
- 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)
- // Set up mock trust policy validator for all gateway instances
- mockValidator := &MockTrustPolicyValidator{}
- gateway1.SetTrustPolicyValidator(mockValidator)
- gateway2.SetTrustPolicyValidator(mockValidator)
- gateway3.SetTrustPolicyValidator(mockValidator)
- // Manually register mock provider for testing (not available in production)
- mockProviderConfig := map[string]interface{}{
- ConfigFieldIssuer: "http://test-mock:9999",
- ConfigFieldClientID: "test-client-id",
- }
- mockProvider1, err := createMockOIDCProvider("test-mock", mockProviderConfig)
- require.NoError(t, err)
- mockProvider2, err := createMockOIDCProvider("test-mock", mockProviderConfig)
- require.NoError(t, err)
- mockProvider3, err := createMockOIDCProvider("test-mock", mockProviderConfig)
- require.NoError(t, err)
- gateway1.RegisterProvider(mockProvider1)
- gateway2.RegisterProvider(mockProvider2)
- gateway3.RegisterProvider(mockProvider3)
- // Step 1: User authenticates and hits Gateway 1 for AssumeRole
- mockToken := createMockJWT(t, "http://test-mock:9999", "production-user")
- assumeRequest := &AssumeRoleWithWebIdentityRequest{
- RoleArn: "arn:seaweed:iam::role/ProductionS3User",
- WebIdentityToken: mockToken, // JWT token from mock provider
- 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
- }
|