policy_engine_distributed_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. package policy
  2. import (
  3. "context"
  4. "fmt"
  5. "testing"
  6. "time"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. )
  10. // TestDistributedPolicyEngine verifies that multiple PolicyEngine instances with identical configurations
  11. // behave consistently across distributed environments
  12. func TestDistributedPolicyEngine(t *testing.T) {
  13. ctx := context.Background()
  14. // Common configuration for all instances
  15. commonConfig := &PolicyEngineConfig{
  16. DefaultEffect: "Deny",
  17. StoreType: "memory", // For testing - would be "filer" in production
  18. StoreConfig: map[string]interface{}{},
  19. }
  20. // Create multiple PolicyEngine instances simulating distributed deployment
  21. instance1 := NewPolicyEngine()
  22. instance2 := NewPolicyEngine()
  23. instance3 := NewPolicyEngine()
  24. // Initialize all instances with identical configuration
  25. err := instance1.Initialize(commonConfig)
  26. require.NoError(t, err, "Instance 1 should initialize successfully")
  27. err = instance2.Initialize(commonConfig)
  28. require.NoError(t, err, "Instance 2 should initialize successfully")
  29. err = instance3.Initialize(commonConfig)
  30. require.NoError(t, err, "Instance 3 should initialize successfully")
  31. // Test policy consistency across instances
  32. t.Run("policy_storage_consistency", func(t *testing.T) {
  33. // Define a test policy
  34. testPolicy := &PolicyDocument{
  35. Version: "2012-10-17",
  36. Statement: []Statement{
  37. {
  38. Sid: "AllowS3Read",
  39. Effect: "Allow",
  40. Action: []string{"s3:GetObject", "s3:ListBucket"},
  41. Resource: []string{"arn:seaweed:s3:::test-bucket/*", "arn:seaweed:s3:::test-bucket"},
  42. },
  43. {
  44. Sid: "DenyS3Write",
  45. Effect: "Deny",
  46. Action: []string{"s3:PutObject", "s3:DeleteObject"},
  47. Resource: []string{"arn:seaweed:s3:::test-bucket/*"},
  48. },
  49. },
  50. }
  51. // Store policy on instance 1
  52. err := instance1.AddPolicy("", "TestPolicy", testPolicy)
  53. require.NoError(t, err, "Should be able to store policy on instance 1")
  54. // For memory storage, each instance has separate storage
  55. // In production with filer storage, all instances would share the same policies
  56. // Verify policy exists on instance 1
  57. storedPolicy1, err := instance1.store.GetPolicy(ctx, "", "TestPolicy")
  58. require.NoError(t, err, "Policy should exist on instance 1")
  59. assert.Equal(t, "2012-10-17", storedPolicy1.Version)
  60. assert.Len(t, storedPolicy1.Statement, 2)
  61. // For demonstration: store same policy on other instances
  62. err = instance2.AddPolicy("", "TestPolicy", testPolicy)
  63. require.NoError(t, err, "Should be able to store policy on instance 2")
  64. err = instance3.AddPolicy("", "TestPolicy", testPolicy)
  65. require.NoError(t, err, "Should be able to store policy on instance 3")
  66. })
  67. // Test policy evaluation consistency
  68. t.Run("evaluation_consistency", func(t *testing.T) {
  69. // Create evaluation context
  70. evalCtx := &EvaluationContext{
  71. Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
  72. Action: "s3:GetObject",
  73. Resource: "arn:seaweed:s3:::test-bucket/file.txt",
  74. RequestContext: map[string]interface{}{
  75. "sourceIp": "192.168.1.100",
  76. },
  77. }
  78. // Evaluate policy on all instances
  79. result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  80. result2, err2 := instance2.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  81. result3, err3 := instance3.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  82. require.NoError(t, err1, "Evaluation should succeed on instance 1")
  83. require.NoError(t, err2, "Evaluation should succeed on instance 2")
  84. require.NoError(t, err3, "Evaluation should succeed on instance 3")
  85. // All instances should return identical results
  86. assert.Equal(t, result1.Effect, result2.Effect, "Instance 1 and 2 should have same effect")
  87. assert.Equal(t, result2.Effect, result3.Effect, "Instance 2 and 3 should have same effect")
  88. assert.Equal(t, EffectAllow, result1.Effect, "Should allow s3:GetObject")
  89. // Matching statements should be identical
  90. assert.Len(t, result1.MatchingStatements, 1, "Should have one matching statement")
  91. assert.Len(t, result2.MatchingStatements, 1, "Should have one matching statement")
  92. assert.Len(t, result3.MatchingStatements, 1, "Should have one matching statement")
  93. assert.Equal(t, "AllowS3Read", result1.MatchingStatements[0].StatementSid)
  94. assert.Equal(t, "AllowS3Read", result2.MatchingStatements[0].StatementSid)
  95. assert.Equal(t, "AllowS3Read", result3.MatchingStatements[0].StatementSid)
  96. })
  97. // Test explicit deny precedence
  98. t.Run("deny_precedence_consistency", func(t *testing.T) {
  99. evalCtx := &EvaluationContext{
  100. Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
  101. Action: "s3:PutObject",
  102. Resource: "arn:seaweed:s3:::test-bucket/newfile.txt",
  103. }
  104. // All instances should consistently apply deny precedence
  105. result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  106. result2, err2 := instance2.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  107. result3, err3 := instance3.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  108. require.NoError(t, err1)
  109. require.NoError(t, err2)
  110. require.NoError(t, err3)
  111. // All should deny due to explicit deny statement
  112. assert.Equal(t, EffectDeny, result1.Effect, "Instance 1 should deny write operation")
  113. assert.Equal(t, EffectDeny, result2.Effect, "Instance 2 should deny write operation")
  114. assert.Equal(t, EffectDeny, result3.Effect, "Instance 3 should deny write operation")
  115. // Should have matching deny statement
  116. assert.Len(t, result1.MatchingStatements, 1)
  117. assert.Equal(t, "DenyS3Write", result1.MatchingStatements[0].StatementSid)
  118. assert.Equal(t, EffectDeny, result1.MatchingStatements[0].Effect)
  119. })
  120. // Test default effect consistency
  121. t.Run("default_effect_consistency", func(t *testing.T) {
  122. evalCtx := &EvaluationContext{
  123. Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
  124. Action: "filer:CreateEntry", // Action not covered by any policy
  125. Resource: "arn:seaweed:filer::path/test",
  126. }
  127. result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  128. result2, err2 := instance2.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  129. result3, err3 := instance3.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
  130. require.NoError(t, err1)
  131. require.NoError(t, err2)
  132. require.NoError(t, err3)
  133. // All should use default effect (Deny)
  134. assert.Equal(t, EffectDeny, result1.Effect, "Should use default effect")
  135. assert.Equal(t, EffectDeny, result2.Effect, "Should use default effect")
  136. assert.Equal(t, EffectDeny, result3.Effect, "Should use default effect")
  137. // No matching statements
  138. assert.Empty(t, result1.MatchingStatements, "Should have no matching statements")
  139. assert.Empty(t, result2.MatchingStatements, "Should have no matching statements")
  140. assert.Empty(t, result3.MatchingStatements, "Should have no matching statements")
  141. })
  142. }
  143. // TestPolicyEngineConfigurationConsistency tests configuration validation for distributed deployments
  144. func TestPolicyEngineConfigurationConsistency(t *testing.T) {
  145. t.Run("consistent_default_effects_required", func(t *testing.T) {
  146. // Different default effects could lead to inconsistent authorization
  147. config1 := &PolicyEngineConfig{
  148. DefaultEffect: "Allow",
  149. StoreType: "memory",
  150. }
  151. config2 := &PolicyEngineConfig{
  152. DefaultEffect: "Deny", // Different default!
  153. StoreType: "memory",
  154. }
  155. instance1 := NewPolicyEngine()
  156. instance2 := NewPolicyEngine()
  157. err1 := instance1.Initialize(config1)
  158. err2 := instance2.Initialize(config2)
  159. require.NoError(t, err1)
  160. require.NoError(t, err2)
  161. // Test with an action not covered by any policy
  162. evalCtx := &EvaluationContext{
  163. Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
  164. Action: "uncovered:action",
  165. Resource: "arn:seaweed:test:::resource",
  166. }
  167. result1, _ := instance1.Evaluate(context.Background(), "", evalCtx, []string{})
  168. result2, _ := instance2.Evaluate(context.Background(), "", evalCtx, []string{})
  169. // Results should be different due to different default effects
  170. assert.NotEqual(t, result1.Effect, result2.Effect, "Different default effects should produce different results")
  171. assert.Equal(t, EffectAllow, result1.Effect, "Instance 1 should allow by default")
  172. assert.Equal(t, EffectDeny, result2.Effect, "Instance 2 should deny by default")
  173. })
  174. t.Run("invalid_configuration_handling", func(t *testing.T) {
  175. invalidConfigs := []*PolicyEngineConfig{
  176. {
  177. DefaultEffect: "Maybe", // Invalid effect
  178. StoreType: "memory",
  179. },
  180. {
  181. DefaultEffect: "Allow",
  182. StoreType: "nonexistent", // Invalid store type
  183. },
  184. }
  185. for i, config := range invalidConfigs {
  186. t.Run(fmt.Sprintf("invalid_config_%d", i), func(t *testing.T) {
  187. instance := NewPolicyEngine()
  188. err := instance.Initialize(config)
  189. assert.Error(t, err, "Should reject invalid configuration")
  190. })
  191. }
  192. })
  193. }
  194. // TestPolicyStoreDistributed tests policy store behavior in distributed scenarios
  195. func TestPolicyStoreDistributed(t *testing.T) {
  196. ctx := context.Background()
  197. t.Run("memory_store_isolation", func(t *testing.T) {
  198. // Memory stores are isolated per instance (not suitable for distributed)
  199. store1 := NewMemoryPolicyStore()
  200. store2 := NewMemoryPolicyStore()
  201. policy := &PolicyDocument{
  202. Version: "2012-10-17",
  203. Statement: []Statement{
  204. {
  205. Effect: "Allow",
  206. Action: []string{"s3:GetObject"},
  207. Resource: []string{"*"},
  208. },
  209. },
  210. }
  211. // Store policy in store1
  212. err := store1.StorePolicy(ctx, "", "TestPolicy", policy)
  213. require.NoError(t, err)
  214. // Policy should exist in store1
  215. _, err = store1.GetPolicy(ctx, "", "TestPolicy")
  216. assert.NoError(t, err, "Policy should exist in store1")
  217. // Policy should NOT exist in store2 (different instance)
  218. _, err = store2.GetPolicy(ctx, "", "TestPolicy")
  219. assert.Error(t, err, "Policy should not exist in store2")
  220. assert.Contains(t, err.Error(), "not found", "Should be a not found error")
  221. })
  222. t.Run("policy_loading_error_handling", func(t *testing.T) {
  223. engine := NewPolicyEngine()
  224. config := &PolicyEngineConfig{
  225. DefaultEffect: "Deny",
  226. StoreType: "memory",
  227. }
  228. err := engine.Initialize(config)
  229. require.NoError(t, err)
  230. evalCtx := &EvaluationContext{
  231. Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
  232. Action: "s3:GetObject",
  233. Resource: "arn:seaweed:s3:::bucket/key",
  234. }
  235. // Evaluate with non-existent policies
  236. result, err := engine.Evaluate(ctx, "", evalCtx, []string{"NonExistentPolicy1", "NonExistentPolicy2"})
  237. require.NoError(t, err, "Should not error on missing policies")
  238. // Should use default effect when no policies can be loaded
  239. assert.Equal(t, EffectDeny, result.Effect, "Should use default effect")
  240. assert.Empty(t, result.MatchingStatements, "Should have no matching statements")
  241. })
  242. }
  243. // TestFilerPolicyStoreConfiguration tests filer policy store configuration for distributed deployments
  244. func TestFilerPolicyStoreConfiguration(t *testing.T) {
  245. t.Run("filer_store_creation", func(t *testing.T) {
  246. // Test with minimal configuration
  247. config := map[string]interface{}{
  248. "filerAddress": "localhost:8888",
  249. }
  250. store, err := NewFilerPolicyStore(config, nil)
  251. require.NoError(t, err, "Should create filer policy store with minimal config")
  252. assert.NotNil(t, store)
  253. })
  254. t.Run("filer_store_custom_path", func(t *testing.T) {
  255. config := map[string]interface{}{
  256. "filerAddress": "prod-filer:8888",
  257. "basePath": "/custom/iam/policies",
  258. }
  259. store, err := NewFilerPolicyStore(config, nil)
  260. require.NoError(t, err, "Should create filer policy store with custom path")
  261. assert.NotNil(t, store)
  262. })
  263. t.Run("filer_store_missing_address", func(t *testing.T) {
  264. config := map[string]interface{}{
  265. "basePath": "/seaweedfs/iam/policies",
  266. }
  267. store, err := NewFilerPolicyStore(config, nil)
  268. assert.NoError(t, err, "Should create filer store without filerAddress in config")
  269. assert.NotNil(t, store, "Store should be created successfully")
  270. })
  271. }
  272. // TestPolicyEvaluationPerformance tests performance considerations for distributed policy evaluation
  273. func TestPolicyEvaluationPerformance(t *testing.T) {
  274. ctx := context.Background()
  275. // Create engine with memory store (for performance baseline)
  276. engine := NewPolicyEngine()
  277. config := &PolicyEngineConfig{
  278. DefaultEffect: "Deny",
  279. StoreType: "memory",
  280. }
  281. err := engine.Initialize(config)
  282. require.NoError(t, err)
  283. // Add multiple policies
  284. for i := 0; i < 10; i++ {
  285. policy := &PolicyDocument{
  286. Version: "2012-10-17",
  287. Statement: []Statement{
  288. {
  289. Sid: fmt.Sprintf("Statement%d", i),
  290. Effect: "Allow",
  291. Action: []string{"s3:GetObject", "s3:ListBucket"},
  292. Resource: []string{fmt.Sprintf("arn:seaweed:s3:::bucket%d/*", i)},
  293. },
  294. },
  295. }
  296. err := engine.AddPolicy("", fmt.Sprintf("Policy%d", i), policy)
  297. require.NoError(t, err)
  298. }
  299. // Test evaluation performance
  300. evalCtx := &EvaluationContext{
  301. Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
  302. Action: "s3:GetObject",
  303. Resource: "arn:seaweed:s3:::bucket5/file.txt",
  304. }
  305. policyNames := make([]string, 10)
  306. for i := 0; i < 10; i++ {
  307. policyNames[i] = fmt.Sprintf("Policy%d", i)
  308. }
  309. // Measure evaluation time
  310. start := time.Now()
  311. for i := 0; i < 100; i++ {
  312. _, err := engine.Evaluate(ctx, "", evalCtx, policyNames)
  313. require.NoError(t, err)
  314. }
  315. duration := time.Since(start)
  316. // Should be reasonably fast (less than 10ms per evaluation on average)
  317. avgDuration := duration / 100
  318. t.Logf("Average policy evaluation time: %v", avgDuration)
  319. assert.Less(t, avgDuration, 10*time.Millisecond, "Policy evaluation should be fast")
  320. }