Files
seaweedfs/weed/iam/integration/iam_integration_test.go
T
chrislu 72c20cf379 feat: implement stateless JWT-only STS architecture
This major refactoring eliminates all session storage complexity and enables
true distributed operation without shared state. All session information is
now embedded directly into JWT tokens.

Key Changes:

Enhanced JWT Claims Structure:
- New STSSessionClaims struct with comprehensive session information
- Embedded role info, identity provider details, policies, and context
- Backward-compatible SessionInfo conversion methods
- Built-in validation and utility methods

Stateless Token Generator:
- Enhanced TokenGenerator with rich JWT claims support
- New GenerateJWTWithClaims method for comprehensive tokens
- Updated ValidateJWTWithClaims for full session extraction
- Maintains backward compatibility with existing methods

Completely Stateless STS Service:
- Removed SessionStore dependency entirely
- Updated all methods to be stateless JWT-only operations
- AssumeRoleWithWebIdentity embeds all session info in JWT
- AssumeRoleWithCredentials embeds all session info in JWT
- ValidateSessionToken extracts everything from JWT token
- RevokeSession now validates tokens but cannot truly revoke them

Updated Method Signatures:
- Removed filerAddress parameters from all STS methods
- Simplified AssumeRoleWithWebIdentity, AssumeRoleWithCredentials
- Simplified ValidateSessionToken, RevokeSession
- Simplified ExpireSessionForTesting

Benefits:
- True distributed compatibility without shared state
- Simplified architecture, no session storage layer
- Better performance, no database lookups
- Improved security with cryptographically signed tokens
- Perfect horizontal scaling

Notes:
- Stateless tokens cannot be revoked without blacklist
- Recommend short-lived tokens for security
- All tests updated and passing
- Backward compatibility maintained where possible
2025-08-24 18:26:02 -07:00

488 lines
13 KiB
Go

package integration
import (
"context"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
"github.com/seaweedfs/seaweedfs/weed/iam/ldap"
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestFullOIDCWorkflow tests the complete OIDC → STS → Policy workflow
func TestFullOIDCWorkflow(t *testing.T) {
// Set up integrated IAM system
iamManager := setupIntegratedIAMSystem(t)
tests := []struct {
name string
roleArn string
sessionName string
webToken string
expectedAllow bool
testAction string
testResource string
}{
{
name: "successful role assumption with policy validation",
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
sessionName: "oidc-session",
webToken: "valid-oidc-token",
expectedAllow: true,
testAction: "s3:GetObject",
testResource: "arn:seaweed:s3:::test-bucket/file.txt",
},
{
name: "role assumption denied by trust policy",
roleArn: "arn:seaweed:iam::role/RestrictedRole",
sessionName: "oidc-session",
webToken: "valid-oidc-token",
expectedAllow: false,
},
{
name: "invalid token rejected",
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
sessionName: "oidc-session",
webToken: "invalid-token",
expectedAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
// Step 1: Attempt role assumption
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
RoleArn: tt.roleArn,
WebIdentityToken: tt.webToken,
RoleSessionName: tt.sessionName,
}
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, "localhost:8888", assumeRequest)
if !tt.expectedAllow {
assert.Error(t, err)
assert.Nil(t, response)
return
}
// Should succeed if expectedAllow is true
require.NoError(t, err)
require.NotNil(t, response)
require.NotNil(t, response.Credentials)
// Step 2: Test policy enforcement with assumed credentials
if tt.testAction != "" && tt.testResource != "" {
allowed, err := iamManager.IsActionAllowed(ctx, "localhost:8888", &ActionRequest{
Principal: response.AssumedRoleUser.Arn,
Action: tt.testAction,
Resource: tt.testResource,
SessionToken: response.Credentials.SessionToken,
})
require.NoError(t, err)
assert.True(t, allowed, "Action should be allowed by role policy")
}
})
}
}
// TestFullLDAPWorkflow tests the complete LDAP → STS → Policy workflow
func TestFullLDAPWorkflow(t *testing.T) {
iamManager := setupIntegratedIAMSystem(t)
tests := []struct {
name string
roleArn string
sessionName string
username string
password string
expectedAllow bool
testAction string
testResource string
}{
{
name: "successful LDAP role assumption",
roleArn: "arn:seaweed:iam::role/LDAPUserRole",
sessionName: "ldap-session",
username: "testuser",
password: "testpass",
expectedAllow: true,
testAction: "filer:CreateEntry",
testResource: "arn:seaweed:filer::path/user-docs/*",
},
{
name: "invalid LDAP credentials",
roleArn: "arn:seaweed:iam::role/LDAPUserRole",
sessionName: "ldap-session",
username: "testuser",
password: "wrongpass",
expectedAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
// Step 1: Attempt role assumption with LDAP credentials
assumeRequest := &sts.AssumeRoleWithCredentialsRequest{
RoleArn: tt.roleArn,
Username: tt.username,
Password: tt.password,
RoleSessionName: tt.sessionName,
ProviderName: "test-ldap",
}
response, err := iamManager.AssumeRoleWithCredentials(ctx, "localhost:8888", assumeRequest)
if !tt.expectedAllow {
assert.Error(t, err)
assert.Nil(t, response)
return
}
require.NoError(t, err)
require.NotNil(t, response)
// Step 2: Test policy enforcement
if tt.testAction != "" && tt.testResource != "" {
allowed, err := iamManager.IsActionAllowed(ctx, "localhost:8888", &ActionRequest{
Principal: response.AssumedRoleUser.Arn,
Action: tt.testAction,
Resource: tt.testResource,
SessionToken: response.Credentials.SessionToken,
})
require.NoError(t, err)
assert.True(t, allowed)
}
})
}
}
// TestPolicyEnforcement tests policy evaluation for various scenarios
func TestPolicyEnforcement(t *testing.T) {
iamManager := setupIntegratedIAMSystem(t)
// Create a session for testing
ctx := context.Background()
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
WebIdentityToken: "valid-oidc-token",
RoleSessionName: "policy-test-session",
}
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, "localhost:8888", assumeRequest)
require.NoError(t, err)
sessionToken := response.Credentials.SessionToken
principal := response.AssumedRoleUser.Arn
tests := []struct {
name string
action string
resource string
shouldAllow bool
reason string
}{
{
name: "allow read access",
action: "s3:GetObject",
resource: "arn:seaweed:s3:::test-bucket/file.txt",
shouldAllow: true,
reason: "S3ReadOnlyRole should allow GetObject",
},
{
name: "allow list bucket",
action: "s3:ListBucket",
resource: "arn:seaweed:s3:::test-bucket",
shouldAllow: true,
reason: "S3ReadOnlyRole should allow ListBucket",
},
{
name: "deny write access",
action: "s3:PutObject",
resource: "arn:seaweed:s3:::test-bucket/newfile.txt",
shouldAllow: false,
reason: "S3ReadOnlyRole should deny write operations",
},
{
name: "deny delete access",
action: "s3:DeleteObject",
resource: "arn:seaweed:s3:::test-bucket/file.txt",
shouldAllow: false,
reason: "S3ReadOnlyRole should deny delete operations",
},
{
name: "deny filer access",
action: "filer:CreateEntry",
resource: "arn:seaweed:filer::path/test",
shouldAllow: false,
reason: "S3ReadOnlyRole should not allow filer operations",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
allowed, err := iamManager.IsActionAllowed(ctx, "localhost:8888", &ActionRequest{
Principal: principal,
Action: tt.action,
Resource: tt.resource,
SessionToken: sessionToken,
})
require.NoError(t, err)
assert.Equal(t, tt.shouldAllow, allowed, tt.reason)
})
}
}
// TestSessionExpiration tests session expiration and cleanup
func TestSessionExpiration(t *testing.T) {
iamManager := setupIntegratedIAMSystem(t)
ctx := context.Background()
// Create a short-lived session
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
WebIdentityToken: "valid-oidc-token",
RoleSessionName: "expiration-test",
DurationSeconds: int64Ptr(900), // 15 minutes
}
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, "localhost:8888", assumeRequest)
require.NoError(t, err)
sessionToken := response.Credentials.SessionToken
// Verify session is initially valid
allowed, err := iamManager.IsActionAllowed(ctx, "localhost:8888", &ActionRequest{
Principal: response.AssumedRoleUser.Arn,
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::test-bucket/file.txt",
SessionToken: sessionToken,
})
require.NoError(t, err)
assert.True(t, allowed)
// Verify the expiration time is set correctly
assert.True(t, response.Credentials.Expiration.After(time.Now()))
assert.True(t, response.Credentials.Expiration.Before(time.Now().Add(16*time.Minute)))
// Test actual session expiration
err = iamManager.ExpireSessionForTesting(ctx, "localhost:8888", sessionToken)
require.NoError(t, err)
// Verify session is now expired and access is denied
allowed, err = iamManager.IsActionAllowed(ctx, "localhost:8888", &ActionRequest{
Principal: response.AssumedRoleUser.Arn,
Action: "s3:GetObject",
Resource: "arn:seaweed:s3:::test-bucket/file.txt",
SessionToken: sessionToken,
})
require.Error(t, err)
assert.False(t, allowed)
assert.Contains(t, err.Error(), "session has expired")
}
// TestTrustPolicyValidation tests role trust policy validation
func TestTrustPolicyValidation(t *testing.T) {
iamManager := setupIntegratedIAMSystem(t)
ctx := context.Background()
tests := []struct {
name string
roleArn string
provider string
userID string
shouldAllow bool
reason string
}{
{
name: "OIDC user allowed by trust policy",
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
provider: "oidc",
userID: "test-user-id",
shouldAllow: true,
reason: "Trust policy should allow OIDC users",
},
{
name: "LDAP user allowed by different role",
roleArn: "arn:seaweed:iam::role/LDAPUserRole",
provider: "ldap",
userID: "testuser",
shouldAllow: true,
reason: "Trust policy should allow LDAP users for LDAP role",
},
{
name: "Wrong provider for role",
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
provider: "ldap",
userID: "testuser",
shouldAllow: false,
reason: "S3ReadOnlyRole trust policy should reject LDAP users",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// This would test trust policy evaluation
// For now, we'll implement this as part of the IAM manager
result := iamManager.ValidateTrustPolicy(ctx, tt.roleArn, tt.provider, tt.userID)
assert.Equal(t, tt.shouldAllow, result, tt.reason)
})
}
}
// Helper functions and test setup
func setupIntegratedIAMSystem(t *testing.T) *IAMManager {
// Create IAM manager with all components
manager := NewIAMManager()
// Configure and initialize
config := &IAMConfig{
STS: &sts.STSConfig{
TokenDuration: time.Hour,
MaxSessionLength: time.Hour * 12,
Issuer: "test-sts",
SigningKey: []byte("test-signing-key-32-characters-long"),
SessionStoreType: "memory", // Use memory for unit tests
},
Policy: &policy.PolicyEngineConfig{
DefaultEffect: "Deny",
StoreType: "memory", // Use memory for unit tests
},
Roles: &RoleStoreConfig{
StoreType: "memory", // Use memory for unit tests
},
}
err := manager.Initialize(config)
require.NoError(t, err)
// Set up test providers
setupTestProviders(t, manager)
// Set up test policies and roles
setupTestPoliciesAndRoles(t, manager)
return manager
}
func setupTestProviders(t *testing.T, manager *IAMManager) {
// Set up OIDC provider
oidcProvider := oidc.NewMockOIDCProvider("test-oidc")
oidcConfig := &oidc.OIDCConfig{
Issuer: "https://test-issuer.com",
ClientID: "test-client-id",
}
err := oidcProvider.Initialize(oidcConfig)
require.NoError(t, err)
oidcProvider.SetupDefaultTestData()
// Set up LDAP provider
ldapProvider := ldap.NewMockLDAPProvider("test-ldap")
ldapConfig := &ldap.LDAPConfig{
Server: "ldap://test-server:389",
BaseDN: "DC=test,DC=com",
}
err = ldapProvider.Initialize(ldapConfig)
require.NoError(t, err)
ldapProvider.SetupDefaultTestData()
// Register providers
err = manager.RegisterIdentityProvider(oidcProvider)
require.NoError(t, err)
err = manager.RegisterIdentityProvider(ldapProvider)
require.NoError(t, err)
}
func setupTestPoliciesAndRoles(t *testing.T, manager *IAMManager) {
ctx := context.Background()
// Create S3 read-only policy
s3ReadPolicy := &policy.PolicyDocument{
Version: "2012-10-17",
Statement: []policy.Statement{
{
Sid: "S3ReadAccess",
Effect: "Allow",
Action: []string{"s3:GetObject", "s3:ListBucket"},
Resource: []string{
"arn:seaweed:s3:::*",
"arn:seaweed:s3:::*/*",
},
},
},
}
err := manager.CreatePolicy(ctx, "S3ReadOnlyPolicy", s3ReadPolicy)
require.NoError(t, err)
// Create LDAP user policy
ldapUserPolicy := &policy.PolicyDocument{
Version: "2012-10-17",
Statement: []policy.Statement{
{
Sid: "FilerAccess",
Effect: "Allow",
Action: []string{"filer:*"},
Resource: []string{
"arn:seaweed:filer::path/user-docs/*",
},
},
},
}
err = manager.CreatePolicy(ctx, "LDAPUserPolicy", ldapUserPolicy)
require.NoError(t, err)
// Create roles with trust policies
err = manager.CreateRole(ctx, "S3ReadOnlyRole", &RoleDefinition{
RoleName: "S3ReadOnlyRole",
TrustPolicy: &policy.PolicyDocument{
Version: "2012-10-17",
Statement: []policy.Statement{
{
Effect: "Allow",
Principal: map[string]interface{}{
"Federated": "test-oidc",
},
Action: []string{"sts:AssumeRoleWithWebIdentity"},
},
},
},
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
})
require.NoError(t, err)
err = manager.CreateRole(ctx, "LDAPUserRole", &RoleDefinition{
RoleName: "LDAPUserRole",
TrustPolicy: &policy.PolicyDocument{
Version: "2012-10-17",
Statement: []policy.Statement{
{
Effect: "Allow",
Principal: map[string]interface{}{
"Federated": "test-ldap",
},
Action: []string{"sts:AssumeRoleWithCredentials"},
},
},
},
AttachedPolicies: []string{"LDAPUserPolicy"},
})
require.NoError(t, err)
}
func int64Ptr(v int64) *int64 {
return &v
}