s3_granular_action_security_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package s3api
  2. import (
  3. "net/http"
  4. "net/url"
  5. "testing"
  6. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  7. "github.com/stretchr/testify/assert"
  8. )
  9. // TestGranularActionMappingSecurity demonstrates how the new granular action mapping
  10. // fixes critical security issues that existed with the previous coarse mapping
  11. func TestGranularActionMappingSecurity(t *testing.T) {
  12. tests := []struct {
  13. name string
  14. method string
  15. bucket string
  16. objectKey string
  17. queryParams map[string]string
  18. description string
  19. problemWithOldMapping string
  20. granularActionResult string
  21. }{
  22. {
  23. name: "delete_object_security_fix",
  24. method: "DELETE",
  25. bucket: "sensitive-bucket",
  26. objectKey: "confidential-file.txt",
  27. queryParams: map[string]string{},
  28. description: "DELETE object operations should map to s3:DeleteObject, not s3:PutObject",
  29. problemWithOldMapping: "Old mapping incorrectly mapped DELETE object to s3:PutObject, " +
  30. "allowing users with only PUT permissions to delete objects - a critical security flaw",
  31. granularActionResult: "s3:DeleteObject",
  32. },
  33. {
  34. name: "get_object_acl_precision",
  35. method: "GET",
  36. bucket: "secure-bucket",
  37. objectKey: "private-file.pdf",
  38. queryParams: map[string]string{"acl": ""},
  39. description: "GET object ACL should map to s3:GetObjectAcl, not generic s3:GetObject",
  40. problemWithOldMapping: "Old mapping would allow users with s3:GetObject permission to " +
  41. "read ACLs, potentially exposing sensitive permission information",
  42. granularActionResult: "s3:GetObjectAcl",
  43. },
  44. {
  45. name: "put_object_tagging_precision",
  46. method: "PUT",
  47. bucket: "data-bucket",
  48. objectKey: "business-document.xlsx",
  49. queryParams: map[string]string{"tagging": ""},
  50. description: "PUT object tagging should map to s3:PutObjectTagging, not generic s3:PutObject",
  51. problemWithOldMapping: "Old mapping couldn't distinguish between actual object uploads and " +
  52. "metadata operations like tagging, making fine-grained permissions impossible",
  53. granularActionResult: "s3:PutObjectTagging",
  54. },
  55. {
  56. name: "multipart_upload_precision",
  57. method: "POST",
  58. bucket: "large-files",
  59. objectKey: "video.mp4",
  60. queryParams: map[string]string{"uploads": ""},
  61. description: "Multipart upload initiation should map to s3:CreateMultipartUpload",
  62. problemWithOldMapping: "Old mapping would treat multipart operations as generic s3:PutObject, " +
  63. "preventing policies that allow regular uploads but restrict large multipart operations",
  64. granularActionResult: "s3:CreateMultipartUpload",
  65. },
  66. {
  67. name: "bucket_policy_vs_bucket_creation",
  68. method: "PUT",
  69. bucket: "corporate-bucket",
  70. objectKey: "",
  71. queryParams: map[string]string{"policy": ""},
  72. description: "Bucket policy modifications should map to s3:PutBucketPolicy, not s3:CreateBucket",
  73. problemWithOldMapping: "Old mapping couldn't distinguish between creating buckets and " +
  74. "modifying bucket policies, potentially allowing unauthorized policy changes",
  75. granularActionResult: "s3:PutBucketPolicy",
  76. },
  77. {
  78. name: "list_vs_read_distinction",
  79. method: "GET",
  80. bucket: "inventory-bucket",
  81. objectKey: "",
  82. queryParams: map[string]string{"uploads": ""},
  83. description: "Listing multipart uploads should map to s3:ListMultipartUploads",
  84. problemWithOldMapping: "Old mapping would use generic s3:ListBucket for all bucket operations, " +
  85. "preventing fine-grained control over who can see ongoing multipart operations",
  86. granularActionResult: "s3:ListMultipartUploads",
  87. },
  88. {
  89. name: "delete_object_tagging_precision",
  90. method: "DELETE",
  91. bucket: "metadata-bucket",
  92. objectKey: "tagged-file.json",
  93. queryParams: map[string]string{"tagging": ""},
  94. description: "Delete object tagging should map to s3:DeleteObjectTagging, not s3:DeleteObject",
  95. problemWithOldMapping: "Old mapping couldn't distinguish between deleting objects and " +
  96. "deleting tags, preventing policies that allow tag management but not object deletion",
  97. granularActionResult: "s3:DeleteObjectTagging",
  98. },
  99. }
  100. for _, tt := range tests {
  101. t.Run(tt.name, func(t *testing.T) {
  102. // Create HTTP request with query parameters
  103. req := &http.Request{
  104. Method: tt.method,
  105. URL: &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
  106. }
  107. // Add query parameters
  108. query := req.URL.Query()
  109. for key, value := range tt.queryParams {
  110. query.Set(key, value)
  111. }
  112. req.URL.RawQuery = query.Encode()
  113. // Test the new granular action determination
  114. result := determineGranularS3Action(req, s3_constants.ACTION_WRITE, tt.bucket, tt.objectKey)
  115. assert.Equal(t, tt.granularActionResult, result,
  116. "Security Fix Test: %s\n"+
  117. "Description: %s\n"+
  118. "Problem with old mapping: %s\n"+
  119. "Expected: %s, Got: %s",
  120. tt.name, tt.description, tt.problemWithOldMapping, tt.granularActionResult, result)
  121. // Log the security improvement
  122. t.Logf("✅ SECURITY IMPROVEMENT: %s", tt.description)
  123. t.Logf(" Problem Fixed: %s", tt.problemWithOldMapping)
  124. t.Logf(" Granular Action: %s", result)
  125. })
  126. }
  127. }
  128. // TestBackwardCompatibilityFallback tests that the new system maintains backward compatibility
  129. // with existing generic actions while providing enhanced granularity
  130. func TestBackwardCompatibilityFallback(t *testing.T) {
  131. tests := []struct {
  132. name string
  133. method string
  134. bucket string
  135. objectKey string
  136. fallbackAction Action
  137. expectedResult string
  138. description string
  139. }{
  140. {
  141. name: "generic_read_fallback",
  142. method: "GET", // Generic method without specific query params
  143. bucket: "", // Edge case: no bucket specified
  144. objectKey: "", // Edge case: no object specified
  145. fallbackAction: s3_constants.ACTION_READ,
  146. expectedResult: "s3:GetObject",
  147. description: "Generic read operations should fall back to s3:GetObject for compatibility",
  148. },
  149. {
  150. name: "generic_write_fallback",
  151. method: "PUT", // Generic method without specific query params
  152. bucket: "", // Edge case: no bucket specified
  153. objectKey: "", // Edge case: no object specified
  154. fallbackAction: s3_constants.ACTION_WRITE,
  155. expectedResult: "s3:PutObject",
  156. description: "Generic write operations should fall back to s3:PutObject for compatibility",
  157. },
  158. {
  159. name: "already_granular_passthrough",
  160. method: "GET",
  161. bucket: "",
  162. objectKey: "",
  163. fallbackAction: "s3:GetBucketLocation", // Already specific
  164. expectedResult: "s3:GetBucketLocation",
  165. description: "Already granular actions should pass through unchanged",
  166. },
  167. {
  168. name: "unknown_action_conversion",
  169. method: "GET",
  170. bucket: "",
  171. objectKey: "",
  172. fallbackAction: "CustomAction", // Not S3-prefixed
  173. expectedResult: "s3:CustomAction",
  174. description: "Unknown actions should be converted to S3 format for consistency",
  175. },
  176. }
  177. for _, tt := range tests {
  178. t.Run(tt.name, func(t *testing.T) {
  179. req := &http.Request{
  180. Method: tt.method,
  181. URL: &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
  182. }
  183. result := determineGranularS3Action(req, tt.fallbackAction, tt.bucket, tt.objectKey)
  184. assert.Equal(t, tt.expectedResult, result,
  185. "Backward Compatibility Test: %s\nDescription: %s\nExpected: %s, Got: %s",
  186. tt.name, tt.description, tt.expectedResult, result)
  187. t.Logf("✅ COMPATIBILITY: %s - %s", tt.description, result)
  188. })
  189. }
  190. }
  191. // TestPolicyEnforcementScenarios demonstrates how granular actions enable
  192. // more precise and secure IAM policy enforcement
  193. func TestPolicyEnforcementScenarios(t *testing.T) {
  194. scenarios := []struct {
  195. name string
  196. policyExample string
  197. method string
  198. bucket string
  199. objectKey string
  200. queryParams map[string]string
  201. expectedAction string
  202. securityBenefit string
  203. }{
  204. {
  205. name: "allow_read_deny_acl_access",
  206. policyExample: `{
  207. "Version": "2012-10-17",
  208. "Statement": [
  209. {
  210. "Effect": "Allow",
  211. "Action": "s3:GetObject",
  212. "Resource": "arn:aws:s3:::sensitive-bucket/*"
  213. }
  214. ]
  215. }`,
  216. method: "GET",
  217. bucket: "sensitive-bucket",
  218. objectKey: "document.pdf",
  219. queryParams: map[string]string{"acl": ""},
  220. expectedAction: "s3:GetObjectAcl",
  221. securityBenefit: "Policy allows reading objects but denies ACL access - granular actions enable this distinction",
  222. },
  223. {
  224. name: "allow_tagging_deny_object_modification",
  225. policyExample: `{
  226. "Version": "2012-10-17",
  227. "Statement": [
  228. {
  229. "Effect": "Allow",
  230. "Action": ["s3:PutObjectTagging", "s3:DeleteObjectTagging"],
  231. "Resource": "arn:aws:s3:::data-bucket/*"
  232. }
  233. ]
  234. }`,
  235. method: "PUT",
  236. bucket: "data-bucket",
  237. objectKey: "metadata-file.json",
  238. queryParams: map[string]string{"tagging": ""},
  239. expectedAction: "s3:PutObjectTagging",
  240. securityBenefit: "Policy allows tag management but prevents actual object uploads - critical for metadata-only roles",
  241. },
  242. {
  243. name: "restrict_multipart_uploads",
  244. policyExample: `{
  245. "Version": "2012-10-17",
  246. "Statement": [
  247. {
  248. "Effect": "Allow",
  249. "Action": "s3:PutObject",
  250. "Resource": "arn:aws:s3:::uploads/*"
  251. },
  252. {
  253. "Effect": "Deny",
  254. "Action": ["s3:CreateMultipartUpload", "s3:UploadPart"],
  255. "Resource": "arn:aws:s3:::uploads/*"
  256. }
  257. ]
  258. }`,
  259. method: "POST",
  260. bucket: "uploads",
  261. objectKey: "large-file.zip",
  262. queryParams: map[string]string{"uploads": ""},
  263. expectedAction: "s3:CreateMultipartUpload",
  264. securityBenefit: "Policy allows regular uploads but blocks large multipart uploads - prevents resource abuse",
  265. },
  266. }
  267. for _, scenario := range scenarios {
  268. t.Run(scenario.name, func(t *testing.T) {
  269. req := &http.Request{
  270. Method: scenario.method,
  271. URL: &url.URL{Path: "/" + scenario.bucket + "/" + scenario.objectKey},
  272. }
  273. query := req.URL.Query()
  274. for key, value := range scenario.queryParams {
  275. query.Set(key, value)
  276. }
  277. req.URL.RawQuery = query.Encode()
  278. result := determineGranularS3Action(req, s3_constants.ACTION_WRITE, scenario.bucket, scenario.objectKey)
  279. assert.Equal(t, scenario.expectedAction, result,
  280. "Policy Enforcement Scenario: %s\nExpected Action: %s, Got: %s",
  281. scenario.name, scenario.expectedAction, result)
  282. t.Logf("🔒 SECURITY SCENARIO: %s", scenario.name)
  283. t.Logf(" Expected Action: %s", result)
  284. t.Logf(" Security Benefit: %s", scenario.securityBenefit)
  285. t.Logf(" Policy Example:\n%s", scenario.policyExample)
  286. })
  287. }
  288. }