| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- package integration
- import (
- "context"
- "testing"
- "time"
- "github.com/golang-jwt/jwt/v5"
- "github.com/seaweedfs/seaweedfs/weed/iam/ldap"
- "github.com/seaweedfs/seaweedfs/weed/iam/oidc"
- "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)
- // Create JWT tokens for testing with the correct issuer
- validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
- invalidJWTToken := createTestJWT(t, "https://invalid-issuer.com", "test-user", "wrong-key")
- 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: validJWTToken,
- 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: validJWTToken,
- expectedAllow: false,
- },
- {
- name: "invalid token rejected",
- roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
- sessionName: "oidc-session",
- webToken: invalidJWTToken,
- 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, 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, &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, 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, &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 valid JWT token for testing
- validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
- // Create a session for testing
- ctx := context.Background()
- assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
- RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
- WebIdentityToken: validJWTToken,
- RoleSessionName: "policy-test-session",
- }
- response, err := iamManager.AssumeRoleWithWebIdentity(ctx, 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, &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 valid JWT token for testing
- validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
- // Create a short-lived session
- assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
- RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
- WebIdentityToken: validJWTToken,
- RoleSessionName: "expiration-test",
- DurationSeconds: int64Ptr(900), // 15 minutes
- }
- response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
- require.NoError(t, err)
- sessionToken := response.Credentials.SessionToken
- // Verify session is initially valid
- allowed, err := iamManager.IsActionAllowed(ctx, &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 session expiration behavior in stateless JWT system
- // In a stateless system, manual expiration is not supported
- err = iamManager.ExpireSessionForTesting(ctx, sessionToken)
- require.Error(t, err, "Manual session expiration should not be supported in stateless system")
- assert.Contains(t, err.Error(), "manual session expiration not supported")
- // Verify session is still valid (since it hasn't naturally expired)
- allowed, err = iamManager.IsActionAllowed(ctx, &ActionRequest{
- Principal: response.AssumedRoleUser.Arn,
- Action: "s3:GetObject",
- Resource: "arn:seaweed:s3:::test-bucket/file.txt",
- SessionToken: sessionToken,
- })
- require.NoError(t, err, "Session should still be valid in stateless system")
- assert.True(t, allowed, "Access should still be allowed since token hasn't naturally 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
- // createTestJWT creates a test JWT token with the specified issuer, subject and signing key
- func createTestJWT(t *testing.T, issuer, subject, signingKey string) string {
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
- "iss": issuer,
- "sub": subject,
- "aud": "test-client-id",
- "exp": time.Now().Add(time.Hour).Unix(),
- "iat": time.Now().Unix(),
- // Add claims that trust policy validation expects
- "idp": "test-oidc", // Identity provider claim for trust policy matching
- })
- tokenString, err := token.SignedString([]byte(signingKey))
- require.NoError(t, err)
- return tokenString
- }
- func setupIntegratedIAMSystem(t *testing.T) *IAMManager {
- // Create IAM manager with all components
- manager := NewIAMManager()
- // Configure and initialize
- config := &IAMConfig{
- STS: &sts.STSConfig{
- TokenDuration: sts.FlexibleDuration{time.Hour},
- MaxSessionLength: sts.FlexibleDuration{time.Hour * 12},
- Issuer: "test-sts",
- SigningKey: []byte("test-signing-key-32-characters-long"),
- },
- 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, func() string {
- return "localhost:8888" // Mock filer address for testing
- })
- 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 mock provider (no config needed for mock)
- ldapProvider := ldap.NewMockLDAPProvider("test-ldap")
- err = ldapProvider.Initialize(nil) // Mock doesn't need real config
- 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
- }
|