policy_variable_matching_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package policy
  2. import (
  3. "context"
  4. "testing"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // TestPolicyVariableMatchingInActionsAndResources tests that Actions and Resources
  9. // now support policy variables like ${aws:username} just like string conditions do
  10. func TestPolicyVariableMatchingInActionsAndResources(t *testing.T) {
  11. engine := NewPolicyEngine()
  12. config := &PolicyEngineConfig{
  13. DefaultEffect: "Deny",
  14. StoreType: "memory",
  15. }
  16. err := engine.Initialize(config)
  17. require.NoError(t, err)
  18. ctx := context.Background()
  19. filerAddress := ""
  20. // Create a policy that uses policy variables in Action and Resource fields
  21. policyDoc := &PolicyDocument{
  22. Version: "2012-10-17",
  23. Statement: []Statement{
  24. {
  25. Sid: "AllowUserSpecificActions",
  26. Effect: "Allow",
  27. Action: []string{
  28. "s3:Get*", // Regular wildcard
  29. "s3:${aws:principaltype}*", // Policy variable in action
  30. },
  31. Resource: []string{
  32. "arn:aws:s3:::user-${aws:username}/*", // Policy variable in resource
  33. "arn:aws:s3:::shared/${saml:username}/*", // Different policy variable
  34. },
  35. },
  36. },
  37. }
  38. err = engine.AddPolicy(filerAddress, "user-specific-policy", policyDoc)
  39. require.NoError(t, err)
  40. tests := []struct {
  41. name string
  42. principal string
  43. action string
  44. resource string
  45. requestContext map[string]interface{}
  46. expectedEffect Effect
  47. description string
  48. }{
  49. {
  50. name: "policy_variable_in_action_matches",
  51. principal: "test-user",
  52. action: "s3:AssumedRole", // Should match s3:${aws:principaltype}* when principaltype=AssumedRole
  53. resource: "arn:aws:s3:::user-testuser/file.txt",
  54. requestContext: map[string]interface{}{
  55. "aws:username": "testuser",
  56. "aws:principaltype": "AssumedRole",
  57. },
  58. expectedEffect: EffectAllow,
  59. description: "Action with policy variable should match when variable is expanded",
  60. },
  61. {
  62. name: "policy_variable_in_resource_matches",
  63. principal: "alice",
  64. action: "s3:GetObject",
  65. resource: "arn:aws:s3:::user-alice/document.pdf", // Should match user-${aws:username}/*
  66. requestContext: map[string]interface{}{
  67. "aws:username": "alice",
  68. },
  69. expectedEffect: EffectAllow,
  70. description: "Resource with policy variable should match when variable is expanded",
  71. },
  72. {
  73. name: "saml_username_variable_in_resource",
  74. principal: "bob",
  75. action: "s3:GetObject",
  76. resource: "arn:aws:s3:::shared/bob/data.json", // Should match shared/${saml:username}/*
  77. requestContext: map[string]interface{}{
  78. "saml:username": "bob",
  79. },
  80. expectedEffect: EffectAllow,
  81. description: "SAML username variable should be expanded in resource patterns",
  82. },
  83. {
  84. name: "policy_variable_no_match_wrong_user",
  85. principal: "charlie",
  86. action: "s3:GetObject",
  87. resource: "arn:aws:s3:::user-alice/file.txt", // charlie trying to access alice's files
  88. requestContext: map[string]interface{}{
  89. "aws:username": "charlie",
  90. },
  91. expectedEffect: EffectDeny,
  92. description: "Policy variable should prevent access when username doesn't match",
  93. },
  94. {
  95. name: "missing_policy_variable_context",
  96. principal: "dave",
  97. action: "s3:GetObject",
  98. resource: "arn:aws:s3:::user-dave/file.txt",
  99. requestContext: map[string]interface{}{
  100. // Missing aws:username context
  101. },
  102. expectedEffect: EffectDeny,
  103. description: "Missing policy variable context should result in no match",
  104. },
  105. }
  106. for _, tt := range tests {
  107. t.Run(tt.name, func(t *testing.T) {
  108. evalCtx := &EvaluationContext{
  109. Principal: tt.principal,
  110. Action: tt.action,
  111. Resource: tt.resource,
  112. RequestContext: tt.requestContext,
  113. }
  114. result, err := engine.Evaluate(ctx, filerAddress, evalCtx, []string{"user-specific-policy"})
  115. require.NoError(t, err, "Policy evaluation should not error")
  116. assert.Equal(t, tt.expectedEffect, result.Effect,
  117. "Test %s: %s. Expected %s but got %s",
  118. tt.name, tt.description, tt.expectedEffect, result.Effect)
  119. })
  120. }
  121. }
  122. // TestActionResourceConsistencyWithStringConditions verifies that Actions, Resources,
  123. // and string conditions all use the same AWS IAM-compliant matching logic
  124. func TestActionResourceConsistencyWithStringConditions(t *testing.T) {
  125. engine := NewPolicyEngine()
  126. config := &PolicyEngineConfig{
  127. DefaultEffect: "Deny",
  128. StoreType: "memory",
  129. }
  130. err := engine.Initialize(config)
  131. require.NoError(t, err)
  132. ctx := context.Background()
  133. filerAddress := ""
  134. // Policy that uses case-insensitive matching in all three areas
  135. policyDoc := &PolicyDocument{
  136. Version: "2012-10-17",
  137. Statement: []Statement{
  138. {
  139. Sid: "CaseInsensitiveMatching",
  140. Effect: "Allow",
  141. Action: []string{"S3:GET*"}, // Uppercase action pattern
  142. Resource: []string{"arn:aws:s3:::TEST-BUCKET/*"}, // Uppercase resource pattern
  143. Condition: map[string]map[string]interface{}{
  144. "StringLike": {
  145. "s3:RequestedRegion": "US-*", // Uppercase condition pattern
  146. },
  147. },
  148. },
  149. },
  150. }
  151. err = engine.AddPolicy(filerAddress, "case-insensitive-policy", policyDoc)
  152. require.NoError(t, err)
  153. evalCtx := &EvaluationContext{
  154. Principal: "test-user",
  155. Action: "s3:getobject", // lowercase action
  156. Resource: "arn:aws:s3:::test-bucket/file.txt", // lowercase resource
  157. RequestContext: map[string]interface{}{
  158. "s3:RequestedRegion": "us-east-1", // lowercase condition value
  159. },
  160. }
  161. result, err := engine.Evaluate(ctx, filerAddress, evalCtx, []string{"case-insensitive-policy"})
  162. require.NoError(t, err)
  163. // All should match due to case-insensitive AWS IAM-compliant matching
  164. assert.Equal(t, EffectAllow, result.Effect,
  165. "Actions, Resources, and Conditions should all use case-insensitive AWS IAM matching")
  166. // Verify that matching statements were found
  167. assert.Len(t, result.MatchingStatements, 1,
  168. "Should have exactly one matching statement")
  169. assert.Equal(t, "Allow", string(result.MatchingStatements[0].Effect),
  170. "Matching statement should have Allow effect")
  171. }