You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							386 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							386 lines
						
					
					
						
							13 KiB
						
					
					
				
								package policy
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"fmt"
							 | 
						|
									"testing"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/stretchr/testify/assert"
							 | 
						|
									"github.com/stretchr/testify/require"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// TestDistributedPolicyEngine verifies that multiple PolicyEngine instances with identical configurations
							 | 
						|
								// behave consistently across distributed environments
							 | 
						|
								func TestDistributedPolicyEngine(t *testing.T) {
							 | 
						|
									ctx := context.Background()
							 | 
						|
								
							 | 
						|
									// Common configuration for all instances
							 | 
						|
									commonConfig := &PolicyEngineConfig{
							 | 
						|
										DefaultEffect: "Deny",
							 | 
						|
										StoreType:     "memory", // For testing - would be "filer" in production
							 | 
						|
										StoreConfig:   map[string]interface{}{},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create multiple PolicyEngine instances simulating distributed deployment
							 | 
						|
									instance1 := NewPolicyEngine()
							 | 
						|
									instance2 := NewPolicyEngine()
							 | 
						|
									instance3 := NewPolicyEngine()
							 | 
						|
								
							 | 
						|
									// Initialize all instances with identical configuration
							 | 
						|
									err := instance1.Initialize(commonConfig)
							 | 
						|
									require.NoError(t, err, "Instance 1 should initialize successfully")
							 | 
						|
								
							 | 
						|
									err = instance2.Initialize(commonConfig)
							 | 
						|
									require.NoError(t, err, "Instance 2 should initialize successfully")
							 | 
						|
								
							 | 
						|
									err = instance3.Initialize(commonConfig)
							 | 
						|
									require.NoError(t, err, "Instance 3 should initialize successfully")
							 | 
						|
								
							 | 
						|
									// Test policy consistency across instances
							 | 
						|
									t.Run("policy_storage_consistency", func(t *testing.T) {
							 | 
						|
										// Define a test policy
							 | 
						|
										testPolicy := &PolicyDocument{
							 | 
						|
											Version: "2012-10-17",
							 | 
						|
											Statement: []Statement{
							 | 
						|
												{
							 | 
						|
													Sid:      "AllowS3Read",
							 | 
						|
													Effect:   "Allow",
							 | 
						|
													Action:   []string{"s3:GetObject", "s3:ListBucket"},
							 | 
						|
													Resource: []string{"arn:seaweed:s3:::test-bucket/*", "arn:seaweed:s3:::test-bucket"},
							 | 
						|
												},
							 | 
						|
												{
							 | 
						|
													Sid:      "DenyS3Write",
							 | 
						|
													Effect:   "Deny",
							 | 
						|
													Action:   []string{"s3:PutObject", "s3:DeleteObject"},
							 | 
						|
													Resource: []string{"arn:seaweed:s3:::test-bucket/*"},
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Store policy on instance 1
							 | 
						|
										err := instance1.AddPolicy("", "TestPolicy", testPolicy)
							 | 
						|
										require.NoError(t, err, "Should be able to store policy on instance 1")
							 | 
						|
								
							 | 
						|
										// For memory storage, each instance has separate storage
							 | 
						|
										// In production with filer storage, all instances would share the same policies
							 | 
						|
								
							 | 
						|
										// Verify policy exists on instance 1
							 | 
						|
										storedPolicy1, err := instance1.store.GetPolicy(ctx, "", "TestPolicy")
							 | 
						|
										require.NoError(t, err, "Policy should exist on instance 1")
							 | 
						|
										assert.Equal(t, "2012-10-17", storedPolicy1.Version)
							 | 
						|
										assert.Len(t, storedPolicy1.Statement, 2)
							 | 
						|
								
							 | 
						|
										// For demonstration: store same policy on other instances
							 | 
						|
										err = instance2.AddPolicy("", "TestPolicy", testPolicy)
							 | 
						|
										require.NoError(t, err, "Should be able to store policy on instance 2")
							 | 
						|
								
							 | 
						|
										err = instance3.AddPolicy("", "TestPolicy", testPolicy)
							 | 
						|
										require.NoError(t, err, "Should be able to store policy on instance 3")
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test policy evaluation consistency
							 | 
						|
									t.Run("evaluation_consistency", func(t *testing.T) {
							 | 
						|
										// Create evaluation context
							 | 
						|
										evalCtx := &EvaluationContext{
							 | 
						|
											Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
							 | 
						|
											Action:    "s3:GetObject",
							 | 
						|
											Resource:  "arn:seaweed:s3:::test-bucket/file.txt",
							 | 
						|
											RequestContext: map[string]interface{}{
							 | 
						|
												"sourceIp": "192.168.1.100",
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Evaluate policy on all instances
							 | 
						|
										result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
										result2, err2 := instance2.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
										result3, err3 := instance3.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
								
							 | 
						|
										require.NoError(t, err1, "Evaluation should succeed on instance 1")
							 | 
						|
										require.NoError(t, err2, "Evaluation should succeed on instance 2")
							 | 
						|
										require.NoError(t, err3, "Evaluation should succeed on instance 3")
							 | 
						|
								
							 | 
						|
										// All instances should return identical results
							 | 
						|
										assert.Equal(t, result1.Effect, result2.Effect, "Instance 1 and 2 should have same effect")
							 | 
						|
										assert.Equal(t, result2.Effect, result3.Effect, "Instance 2 and 3 should have same effect")
							 | 
						|
										assert.Equal(t, EffectAllow, result1.Effect, "Should allow s3:GetObject")
							 | 
						|
								
							 | 
						|
										// Matching statements should be identical
							 | 
						|
										assert.Len(t, result1.MatchingStatements, 1, "Should have one matching statement")
							 | 
						|
										assert.Len(t, result2.MatchingStatements, 1, "Should have one matching statement")
							 | 
						|
										assert.Len(t, result3.MatchingStatements, 1, "Should have one matching statement")
							 | 
						|
								
							 | 
						|
										assert.Equal(t, "AllowS3Read", result1.MatchingStatements[0].StatementSid)
							 | 
						|
										assert.Equal(t, "AllowS3Read", result2.MatchingStatements[0].StatementSid)
							 | 
						|
										assert.Equal(t, "AllowS3Read", result3.MatchingStatements[0].StatementSid)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test explicit deny precedence
							 | 
						|
									t.Run("deny_precedence_consistency", func(t *testing.T) {
							 | 
						|
										evalCtx := &EvaluationContext{
							 | 
						|
											Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
							 | 
						|
											Action:    "s3:PutObject",
							 | 
						|
											Resource:  "arn:seaweed:s3:::test-bucket/newfile.txt",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// All instances should consistently apply deny precedence
							 | 
						|
										result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
										result2, err2 := instance2.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
										result3, err3 := instance3.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
								
							 | 
						|
										require.NoError(t, err1)
							 | 
						|
										require.NoError(t, err2)
							 | 
						|
										require.NoError(t, err3)
							 | 
						|
								
							 | 
						|
										// All should deny due to explicit deny statement
							 | 
						|
										assert.Equal(t, EffectDeny, result1.Effect, "Instance 1 should deny write operation")
							 | 
						|
										assert.Equal(t, EffectDeny, result2.Effect, "Instance 2 should deny write operation")
							 | 
						|
										assert.Equal(t, EffectDeny, result3.Effect, "Instance 3 should deny write operation")
							 | 
						|
								
							 | 
						|
										// Should have matching deny statement
							 | 
						|
										assert.Len(t, result1.MatchingStatements, 1)
							 | 
						|
										assert.Equal(t, "DenyS3Write", result1.MatchingStatements[0].StatementSid)
							 | 
						|
										assert.Equal(t, EffectDeny, result1.MatchingStatements[0].Effect)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test default effect consistency
							 | 
						|
									t.Run("default_effect_consistency", func(t *testing.T) {
							 | 
						|
										evalCtx := &EvaluationContext{
							 | 
						|
											Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
							 | 
						|
											Action:    "filer:CreateEntry", // Action not covered by any policy
							 | 
						|
											Resource:  "arn:seaweed:filer::path/test",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
										result2, err2 := instance2.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
										result3, err3 := instance3.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
							 | 
						|
								
							 | 
						|
										require.NoError(t, err1)
							 | 
						|
										require.NoError(t, err2)
							 | 
						|
										require.NoError(t, err3)
							 | 
						|
								
							 | 
						|
										// All should use default effect (Deny)
							 | 
						|
										assert.Equal(t, EffectDeny, result1.Effect, "Should use default effect")
							 | 
						|
										assert.Equal(t, EffectDeny, result2.Effect, "Should use default effect")
							 | 
						|
										assert.Equal(t, EffectDeny, result3.Effect, "Should use default effect")
							 | 
						|
								
							 | 
						|
										// No matching statements
							 | 
						|
										assert.Empty(t, result1.MatchingStatements, "Should have no matching statements")
							 | 
						|
										assert.Empty(t, result2.MatchingStatements, "Should have no matching statements")
							 | 
						|
										assert.Empty(t, result3.MatchingStatements, "Should have no matching statements")
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestPolicyEngineConfigurationConsistency tests configuration validation for distributed deployments
							 | 
						|
								func TestPolicyEngineConfigurationConsistency(t *testing.T) {
							 | 
						|
									t.Run("consistent_default_effects_required", func(t *testing.T) {
							 | 
						|
										// Different default effects could lead to inconsistent authorization
							 | 
						|
										config1 := &PolicyEngineConfig{
							 | 
						|
											DefaultEffect: "Allow",
							 | 
						|
											StoreType:     "memory",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										config2 := &PolicyEngineConfig{
							 | 
						|
											DefaultEffect: "Deny", // Different default!
							 | 
						|
											StoreType:     "memory",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										instance1 := NewPolicyEngine()
							 | 
						|
										instance2 := NewPolicyEngine()
							 | 
						|
								
							 | 
						|
										err1 := instance1.Initialize(config1)
							 | 
						|
										err2 := instance2.Initialize(config2)
							 | 
						|
								
							 | 
						|
										require.NoError(t, err1)
							 | 
						|
										require.NoError(t, err2)
							 | 
						|
								
							 | 
						|
										// Test with an action not covered by any policy
							 | 
						|
										evalCtx := &EvaluationContext{
							 | 
						|
											Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
							 | 
						|
											Action:    "uncovered:action",
							 | 
						|
											Resource:  "arn:seaweed:test:::resource",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										result1, _ := instance1.Evaluate(context.Background(), "", evalCtx, []string{})
							 | 
						|
										result2, _ := instance2.Evaluate(context.Background(), "", evalCtx, []string{})
							 | 
						|
								
							 | 
						|
										// Results should be different due to different default effects
							 | 
						|
										assert.NotEqual(t, result1.Effect, result2.Effect, "Different default effects should produce different results")
							 | 
						|
										assert.Equal(t, EffectAllow, result1.Effect, "Instance 1 should allow by default")
							 | 
						|
										assert.Equal(t, EffectDeny, result2.Effect, "Instance 2 should deny by default")
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("invalid_configuration_handling", func(t *testing.T) {
							 | 
						|
										invalidConfigs := []*PolicyEngineConfig{
							 | 
						|
											{
							 | 
						|
												DefaultEffect: "Maybe", // Invalid effect
							 | 
						|
												StoreType:     "memory",
							 | 
						|
											},
							 | 
						|
											{
							 | 
						|
												DefaultEffect: "Allow",
							 | 
						|
												StoreType:     "nonexistent", // Invalid store type
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										for i, config := range invalidConfigs {
							 | 
						|
											t.Run(fmt.Sprintf("invalid_config_%d", i), func(t *testing.T) {
							 | 
						|
												instance := NewPolicyEngine()
							 | 
						|
												err := instance.Initialize(config)
							 | 
						|
												assert.Error(t, err, "Should reject invalid configuration")
							 | 
						|
											})
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestPolicyStoreDistributed tests policy store behavior in distributed scenarios
							 | 
						|
								func TestPolicyStoreDistributed(t *testing.T) {
							 | 
						|
									ctx := context.Background()
							 | 
						|
								
							 | 
						|
									t.Run("memory_store_isolation", func(t *testing.T) {
							 | 
						|
										// Memory stores are isolated per instance (not suitable for distributed)
							 | 
						|
										store1 := NewMemoryPolicyStore()
							 | 
						|
										store2 := NewMemoryPolicyStore()
							 | 
						|
								
							 | 
						|
										policy := &PolicyDocument{
							 | 
						|
											Version: "2012-10-17",
							 | 
						|
											Statement: []Statement{
							 | 
						|
												{
							 | 
						|
													Effect:   "Allow",
							 | 
						|
													Action:   []string{"s3:GetObject"},
							 | 
						|
													Resource: []string{"*"},
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Store policy in store1
							 | 
						|
										err := store1.StorePolicy(ctx, "", "TestPolicy", policy)
							 | 
						|
										require.NoError(t, err)
							 | 
						|
								
							 | 
						|
										// Policy should exist in store1
							 | 
						|
										_, err = store1.GetPolicy(ctx, "", "TestPolicy")
							 | 
						|
										assert.NoError(t, err, "Policy should exist in store1")
							 | 
						|
								
							 | 
						|
										// Policy should NOT exist in store2 (different instance)
							 | 
						|
										_, err = store2.GetPolicy(ctx, "", "TestPolicy")
							 | 
						|
										assert.Error(t, err, "Policy should not exist in store2")
							 | 
						|
										assert.Contains(t, err.Error(), "not found", "Should be a not found error")
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("policy_loading_error_handling", func(t *testing.T) {
							 | 
						|
										engine := NewPolicyEngine()
							 | 
						|
										config := &PolicyEngineConfig{
							 | 
						|
											DefaultEffect: "Deny",
							 | 
						|
											StoreType:     "memory",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										err := engine.Initialize(config)
							 | 
						|
										require.NoError(t, err)
							 | 
						|
								
							 | 
						|
										evalCtx := &EvaluationContext{
							 | 
						|
											Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
							 | 
						|
											Action:    "s3:GetObject",
							 | 
						|
											Resource:  "arn:seaweed:s3:::bucket/key",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Evaluate with non-existent policies
							 | 
						|
										result, err := engine.Evaluate(ctx, "", evalCtx, []string{"NonExistentPolicy1", "NonExistentPolicy2"})
							 | 
						|
										require.NoError(t, err, "Should not error on missing policies")
							 | 
						|
								
							 | 
						|
										// Should use default effect when no policies can be loaded
							 | 
						|
										assert.Equal(t, EffectDeny, result.Effect, "Should use default effect")
							 | 
						|
										assert.Empty(t, result.MatchingStatements, "Should have no matching statements")
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestFilerPolicyStoreConfiguration tests filer policy store configuration for distributed deployments
							 | 
						|
								func TestFilerPolicyStoreConfiguration(t *testing.T) {
							 | 
						|
									t.Run("filer_store_creation", func(t *testing.T) {
							 | 
						|
										// Test with minimal configuration
							 | 
						|
										config := map[string]interface{}{
							 | 
						|
											"filerAddress": "localhost:8888",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										store, err := NewFilerPolicyStore(config)
							 | 
						|
										require.NoError(t, err, "Should create filer policy store with minimal config")
							 | 
						|
										assert.NotNil(t, store)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("filer_store_custom_path", func(t *testing.T) {
							 | 
						|
										config := map[string]interface{}{
							 | 
						|
											"filerAddress": "prod-filer:8888",
							 | 
						|
											"basePath":     "/custom/iam/policies",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										store, err := NewFilerPolicyStore(config)
							 | 
						|
										require.NoError(t, err, "Should create filer policy store with custom path")
							 | 
						|
										assert.NotNil(t, store)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("filer_store_missing_address", func(t *testing.T) {
							 | 
						|
										config := map[string]interface{}{
							 | 
						|
											"basePath": "/seaweedfs/iam/policies",
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										store, err := NewFilerPolicyStore(config)
							 | 
						|
										assert.NoError(t, err, "Should create filer store without filerAddress in config")
							 | 
						|
										assert.NotNil(t, store, "Store should be created successfully")
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestPolicyEvaluationPerformance tests performance considerations for distributed policy evaluation
							 | 
						|
								func TestPolicyEvaluationPerformance(t *testing.T) {
							 | 
						|
									ctx := context.Background()
							 | 
						|
								
							 | 
						|
									// Create engine with memory store (for performance baseline)
							 | 
						|
									engine := NewPolicyEngine()
							 | 
						|
									config := &PolicyEngineConfig{
							 | 
						|
										DefaultEffect: "Deny",
							 | 
						|
										StoreType:     "memory",
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									err := engine.Initialize(config)
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Add multiple policies
							 | 
						|
									for i := 0; i < 10; i++ {
							 | 
						|
										policy := &PolicyDocument{
							 | 
						|
											Version: "2012-10-17",
							 | 
						|
											Statement: []Statement{
							 | 
						|
												{
							 | 
						|
													Sid:      fmt.Sprintf("Statement%d", i),
							 | 
						|
													Effect:   "Allow",
							 | 
						|
													Action:   []string{"s3:GetObject", "s3:ListBucket"},
							 | 
						|
													Resource: []string{fmt.Sprintf("arn:seaweed:s3:::bucket%d/*", i)},
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										err := engine.AddPolicy("", fmt.Sprintf("Policy%d", i), policy)
							 | 
						|
										require.NoError(t, err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Test evaluation performance
							 | 
						|
									evalCtx := &EvaluationContext{
							 | 
						|
										Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
							 | 
						|
										Action:    "s3:GetObject",
							 | 
						|
										Resource:  "arn:seaweed:s3:::bucket5/file.txt",
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									policyNames := make([]string, 10)
							 | 
						|
									for i := 0; i < 10; i++ {
							 | 
						|
										policyNames[i] = fmt.Sprintf("Policy%d", i)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Measure evaluation time
							 | 
						|
									start := time.Now()
							 | 
						|
									for i := 0; i < 100; i++ {
							 | 
						|
										_, err := engine.Evaluate(ctx, "", evalCtx, policyNames)
							 | 
						|
										require.NoError(t, err)
							 | 
						|
									}
							 | 
						|
									duration := time.Since(start)
							 | 
						|
								
							 | 
						|
									// Should be reasonably fast (less than 10ms per evaluation on average)
							 | 
						|
									avgDuration := duration / 100
							 | 
						|
									t.Logf("Average policy evaluation time: %v", avgDuration)
							 | 
						|
									assert.Less(t, avgDuration, 10*time.Millisecond, "Policy evaluation should be fast")
							 | 
						|
								}
							 |