s3api_governance_permissions_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. package s3api
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "strings"
  6. "testing"
  7. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  8. )
  9. // TestCheckGovernanceBypassPermissionResourceGeneration tests that the function
  10. // correctly generates resource paths for the permission check
  11. func TestCheckGovernanceBypassPermissionResourceGeneration(t *testing.T) {
  12. tests := []struct {
  13. name string
  14. bucket string
  15. object string
  16. expectedPath string
  17. description string
  18. }{
  19. {
  20. name: "simple_object",
  21. bucket: "test-bucket",
  22. object: "test-object.txt",
  23. expectedPath: "test-bucket/test-object.txt",
  24. description: "Simple bucket and object should be joined with slash",
  25. },
  26. {
  27. name: "object_with_leading_slash",
  28. bucket: "test-bucket",
  29. object: "/test-object.txt",
  30. expectedPath: "test-bucket/test-object.txt",
  31. description: "Leading slash should be trimmed from object name",
  32. },
  33. {
  34. name: "nested_object",
  35. bucket: "test-bucket",
  36. object: "/folder/subfolder/test-object.txt",
  37. expectedPath: "test-bucket/folder/subfolder/test-object.txt",
  38. description: "Nested object path should be handled correctly",
  39. },
  40. {
  41. name: "empty_object",
  42. bucket: "test-bucket",
  43. object: "",
  44. expectedPath: "test-bucket/",
  45. description: "Empty object should result in bucket with trailing slash",
  46. },
  47. {
  48. name: "root_object",
  49. bucket: "test-bucket",
  50. object: "/",
  51. expectedPath: "test-bucket/",
  52. description: "Root object should result in bucket with trailing slash",
  53. },
  54. }
  55. for _, tt := range tests {
  56. t.Run(tt.name, func(t *testing.T) {
  57. // Test the resource generation logic used in checkGovernanceBypassPermission
  58. resource := strings.TrimPrefix(tt.object, "/")
  59. actualPath := tt.bucket + "/" + resource
  60. if actualPath != tt.expectedPath {
  61. t.Errorf("Resource path generation failed. Expected: %s, Got: %s. %s",
  62. tt.expectedPath, actualPath, tt.description)
  63. }
  64. })
  65. }
  66. }
  67. // TestCheckGovernanceBypassPermissionActionGeneration tests that the function
  68. // correctly generates action strings for IAM checking
  69. func TestCheckGovernanceBypassPermissionActionGeneration(t *testing.T) {
  70. tests := []struct {
  71. name string
  72. bucket string
  73. object string
  74. expectedBypassAction string
  75. expectedAdminAction string
  76. description string
  77. }{
  78. {
  79. name: "bypass_action_generation",
  80. bucket: "test-bucket",
  81. object: "test-object.txt",
  82. expectedBypassAction: "BypassGovernanceRetention:test-bucket/test-object.txt",
  83. expectedAdminAction: "Admin:test-bucket/test-object.txt",
  84. description: "Actions should be properly formatted with resource path",
  85. },
  86. {
  87. name: "leading_slash_handling",
  88. bucket: "test-bucket",
  89. object: "/test-object.txt",
  90. expectedBypassAction: "BypassGovernanceRetention:test-bucket/test-object.txt",
  91. expectedAdminAction: "Admin:test-bucket/test-object.txt",
  92. description: "Leading slash should be trimmed in action generation",
  93. },
  94. }
  95. for _, tt := range tests {
  96. t.Run(tt.name, func(t *testing.T) {
  97. // Test the action generation logic used in checkGovernanceBypassPermission
  98. resource := strings.TrimPrefix(tt.object, "/")
  99. resourcePath := tt.bucket + "/" + resource
  100. bypassAction := s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION + ":" + resourcePath
  101. adminAction := s3_constants.ACTION_ADMIN + ":" + resourcePath
  102. if bypassAction != tt.expectedBypassAction {
  103. t.Errorf("Bypass action generation failed. Expected: %s, Got: %s. %s",
  104. tt.expectedBypassAction, bypassAction, tt.description)
  105. }
  106. if adminAction != tt.expectedAdminAction {
  107. t.Errorf("Admin action generation failed. Expected: %s, Got: %s. %s",
  108. tt.expectedAdminAction, adminAction, tt.description)
  109. }
  110. })
  111. }
  112. }
  113. // TestCheckGovernanceBypassPermissionErrorHandling tests error handling scenarios
  114. func TestCheckGovernanceBypassPermissionErrorHandling(t *testing.T) {
  115. // Note: This test demonstrates the expected behavior for different error scenarios
  116. // without requiring full IAM setup
  117. tests := []struct {
  118. name string
  119. bucket string
  120. object string
  121. description string
  122. }{
  123. {
  124. name: "empty_bucket",
  125. bucket: "",
  126. object: "test-object.txt",
  127. description: "Empty bucket should be handled gracefully",
  128. },
  129. {
  130. name: "special_characters",
  131. bucket: "test-bucket",
  132. object: "test object with spaces.txt",
  133. description: "Objects with special characters should be handled",
  134. },
  135. {
  136. name: "unicode_characters",
  137. bucket: "test-bucket",
  138. object: "测试文件.txt",
  139. description: "Objects with unicode characters should be handled",
  140. },
  141. }
  142. for _, tt := range tests {
  143. t.Run(tt.name, func(t *testing.T) {
  144. // Test that the function doesn't panic with various inputs
  145. // This would normally call checkGovernanceBypassPermission
  146. // but since we don't have a full S3ApiServer setup, we just test
  147. // that the resource generation logic works without panicking
  148. resource := strings.TrimPrefix(tt.object, "/")
  149. resourcePath := tt.bucket + "/" + resource
  150. // Verify the resource path is generated
  151. if resourcePath == "" {
  152. t.Errorf("Resource path should not be empty for test case: %s", tt.description)
  153. }
  154. t.Logf("Generated resource path for %s: %s", tt.description, resourcePath)
  155. })
  156. }
  157. }
  158. // TestCheckGovernanceBypassPermissionIntegrationBehavior documents the expected behavior
  159. // when integrated with a full IAM system
  160. func TestCheckGovernanceBypassPermissionIntegrationBehavior(t *testing.T) {
  161. t.Skip("Documentation test - describes expected behavior with full IAM integration")
  162. // This test documents the expected behavior when checkGovernanceBypassPermission
  163. // is called with a full IAM system:
  164. //
  165. // 1. Function calls s3a.iam.authRequest() with the bypass action
  166. // 2. If authRequest returns errCode != s3err.ErrNone, function returns false
  167. // 3. If authRequest succeeds, function checks identity.canDo() with the bypass action
  168. // 4. If canDo() returns true, function returns true
  169. // 5. If bypass permission fails, function checks admin action with identity.canDo()
  170. // 6. If admin action succeeds, function returns true and logs admin access
  171. // 7. If all checks fail, function returns false
  172. //
  173. // The function correctly uses:
  174. // - s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION for bypass permission
  175. // - s3_constants.ACTION_ADMIN for admin permission
  176. // - Proper resource path generation with bucket/object format
  177. // - Trimming of leading slashes from object names
  178. }
  179. // TestGovernanceBypassPermission was removed because it tested the old
  180. // insecure behavior of trusting the AmzIsAdmin header. The new implementation
  181. // uses proper IAM authentication instead of relying on client-provided headers.
  182. // Test specifically for users with IAM bypass permission
  183. func TestGovernanceBypassWithIAMPermission(t *testing.T) {
  184. // This test demonstrates the expected behavior for non-admin users with bypass permission
  185. // In a real implementation, this would integrate with the full IAM system
  186. t.Skip("Integration test requires full IAM setup - demonstrates expected behavior")
  187. // The expected behavior would be:
  188. // 1. Non-admin user makes request with bypass header
  189. // 2. checkGovernanceBypassPermission calls s3a.iam.authRequest
  190. // 3. authRequest validates user identity and checks permissions
  191. // 4. If user has s3:BypassGovernanceRetention permission, return true
  192. // 5. Otherwise return false
  193. // For now, the function correctly returns false for non-admin users
  194. // when the IAM system doesn't have the user configured with bypass permission
  195. }
  196. func TestGovernancePermissionIntegration(t *testing.T) {
  197. // Note: This test demonstrates the expected integration behavior
  198. // In a real implementation, this would require setting up a proper IAM mock
  199. // with identities that have the bypass governance permission
  200. t.Skip("Integration test requires full IAM setup - demonstrates expected behavior")
  201. // This test would verify:
  202. // 1. User with BypassGovernanceRetention permission can bypass governance
  203. // 2. User without permission cannot bypass governance
  204. // 3. Admin users can always bypass governance
  205. // 4. Anonymous users cannot bypass governance
  206. }
  207. func TestGovernanceBypassHeader(t *testing.T) {
  208. tests := []struct {
  209. name string
  210. headerValue string
  211. expectedResult bool
  212. description string
  213. }{
  214. {
  215. name: "bypass_header_true",
  216. headerValue: "true",
  217. expectedResult: true,
  218. description: "Header with 'true' value should enable bypass",
  219. },
  220. {
  221. name: "bypass_header_false",
  222. headerValue: "false",
  223. expectedResult: false,
  224. description: "Header with 'false' value should not enable bypass",
  225. },
  226. {
  227. name: "bypass_header_empty",
  228. headerValue: "",
  229. expectedResult: false,
  230. description: "Empty header should not enable bypass",
  231. },
  232. {
  233. name: "bypass_header_invalid",
  234. headerValue: "invalid",
  235. expectedResult: false,
  236. description: "Invalid header value should not enable bypass",
  237. },
  238. }
  239. for _, tt := range tests {
  240. t.Run(tt.name, func(t *testing.T) {
  241. req := httptest.NewRequest("DELETE", "/bucket/object", nil)
  242. if tt.headerValue != "" {
  243. req.Header.Set("x-amz-bypass-governance-retention", tt.headerValue)
  244. }
  245. result := req.Header.Get("x-amz-bypass-governance-retention") == "true"
  246. if result != tt.expectedResult {
  247. t.Errorf("bypass header check = %v, want %v. %s", result, tt.expectedResult, tt.description)
  248. }
  249. })
  250. }
  251. }
  252. func TestGovernanceRetentionModeChecking(t *testing.T) {
  253. tests := []struct {
  254. name string
  255. retentionMode string
  256. bypassGovernance bool
  257. hasPermission bool
  258. expectedError bool
  259. expectedErrorType string
  260. description string
  261. }{
  262. {
  263. name: "compliance_mode_cannot_bypass",
  264. retentionMode: s3_constants.RetentionModeCompliance,
  265. bypassGovernance: true,
  266. hasPermission: true,
  267. expectedError: true,
  268. expectedErrorType: "compliance mode",
  269. description: "Compliance mode should not be bypassable even with permission",
  270. },
  271. {
  272. name: "governance_mode_without_bypass",
  273. retentionMode: s3_constants.RetentionModeGovernance,
  274. bypassGovernance: false,
  275. hasPermission: false,
  276. expectedError: true,
  277. expectedErrorType: "governance mode",
  278. description: "Governance mode should be blocked without bypass",
  279. },
  280. {
  281. name: "governance_mode_with_bypass_no_permission",
  282. retentionMode: s3_constants.RetentionModeGovernance,
  283. bypassGovernance: true,
  284. hasPermission: false,
  285. expectedError: true,
  286. expectedErrorType: "permission",
  287. description: "Governance mode bypass should fail without permission",
  288. },
  289. {
  290. name: "governance_mode_with_bypass_and_permission",
  291. retentionMode: s3_constants.RetentionModeGovernance,
  292. bypassGovernance: true,
  293. hasPermission: true,
  294. expectedError: false,
  295. expectedErrorType: "",
  296. description: "Governance mode bypass should succeed with permission",
  297. },
  298. }
  299. for _, tt := range tests {
  300. t.Run(tt.name, func(t *testing.T) {
  301. // Test validates the logic without actually needing the full implementation
  302. // This demonstrates the expected behavior patterns
  303. var hasError bool
  304. var errorType string
  305. if tt.retentionMode == s3_constants.RetentionModeCompliance {
  306. hasError = true
  307. errorType = "compliance mode"
  308. } else if tt.retentionMode == s3_constants.RetentionModeGovernance {
  309. if !tt.bypassGovernance {
  310. hasError = true
  311. errorType = "governance mode"
  312. } else if !tt.hasPermission {
  313. hasError = true
  314. errorType = "permission"
  315. }
  316. }
  317. if hasError != tt.expectedError {
  318. t.Errorf("expected error: %v, got error: %v. %s", tt.expectedError, hasError, tt.description)
  319. }
  320. if tt.expectedError && !strings.Contains(errorType, tt.expectedErrorType) {
  321. t.Errorf("expected error type containing '%s', got '%s'. %s", tt.expectedErrorType, errorType, tt.description)
  322. }
  323. })
  324. }
  325. }
  326. func TestGovernancePermissionActionGeneration(t *testing.T) {
  327. tests := []struct {
  328. name string
  329. bucket string
  330. object string
  331. expectedAction string
  332. description string
  333. }{
  334. {
  335. name: "bucket_and_object_action",
  336. bucket: "test-bucket",
  337. object: "/test-object", // Object has "/" prefix from GetBucketAndObject
  338. expectedAction: "BypassGovernanceRetention:test-bucket/test-object",
  339. description: "Action should be generated correctly for bucket and object",
  340. },
  341. {
  342. name: "bucket_only_action",
  343. bucket: "test-bucket",
  344. object: "",
  345. expectedAction: "BypassGovernanceRetention:test-bucket",
  346. description: "Action should be generated correctly for bucket only",
  347. },
  348. {
  349. name: "nested_object_action",
  350. bucket: "test-bucket",
  351. object: "/folder/subfolder/object", // Object has "/" prefix from GetBucketAndObject
  352. expectedAction: "BypassGovernanceRetention:test-bucket/folder/subfolder/object",
  353. description: "Action should be generated correctly for nested objects",
  354. },
  355. }
  356. for _, tt := range tests {
  357. t.Run(tt.name, func(t *testing.T) {
  358. action := s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION + ":" + tt.bucket + tt.object
  359. if action != tt.expectedAction {
  360. t.Errorf("generated action: %s, expected: %s. %s", action, tt.expectedAction, tt.description)
  361. }
  362. })
  363. }
  364. }
  365. // TestGovernancePermissionEndToEnd tests the complete object lock permission flow
  366. func TestGovernancePermissionEndToEnd(t *testing.T) {
  367. t.Skip("End-to-end testing requires full S3 API server setup - demonstrates expected behavior")
  368. // This test demonstrates the end-to-end flow that would be tested in a full integration test
  369. // The checkObjectLockPermissions method is called by:
  370. // 1. DeleteObjectHandler - when versioning is enabled and object lock is configured
  371. // 2. DeleteMultipleObjectsHandler - for each object in versioned buckets
  372. // 3. PutObjectHandler - via checkObjectLockPermissionsForPut for versioned buckets
  373. // 4. PutObjectRetentionHandler - when setting retention on objects
  374. //
  375. // Each handler:
  376. // - Extracts bypassGovernance from "x-amz-bypass-governance-retention" header
  377. // - Calls checkObjectLockPermissions with the appropriate parameters
  378. // - Handles the returned errors appropriately (ErrAccessDenied, etc.)
  379. //
  380. // The method integrates with the IAM system through checkGovernanceBypassPermission
  381. // which validates the s3:BypassGovernanceRetention permission
  382. }
  383. // TestGovernancePermissionHTTPFlow tests the HTTP header processing and method calls
  384. func TestGovernancePermissionHTTPFlow(t *testing.T) {
  385. tests := []struct {
  386. name string
  387. headerValue string
  388. expectedBypassGovernance bool
  389. }{
  390. {
  391. name: "bypass_header_true",
  392. headerValue: "true",
  393. expectedBypassGovernance: true,
  394. },
  395. {
  396. name: "bypass_header_false",
  397. headerValue: "false",
  398. expectedBypassGovernance: false,
  399. },
  400. {
  401. name: "bypass_header_missing",
  402. headerValue: "",
  403. expectedBypassGovernance: false,
  404. },
  405. }
  406. for _, tt := range tests {
  407. t.Run(tt.name, func(t *testing.T) {
  408. // Create a mock HTTP request
  409. req, _ := http.NewRequest("DELETE", "/bucket/test-object", nil)
  410. if tt.headerValue != "" {
  411. req.Header.Set("x-amz-bypass-governance-retention", tt.headerValue)
  412. }
  413. // Test the header processing logic used in handlers
  414. bypassGovernance := req.Header.Get("x-amz-bypass-governance-retention") == "true"
  415. if bypassGovernance != tt.expectedBypassGovernance {
  416. t.Errorf("Expected bypassGovernance to be %v, got %v", tt.expectedBypassGovernance, bypassGovernance)
  417. }
  418. })
  419. }
  420. }
  421. // TestGovernancePermissionMethodCalls tests that the governance permission methods are called correctly
  422. func TestGovernancePermissionMethodCalls(t *testing.T) {
  423. // Test that demonstrates the method call pattern used in handlers
  424. // This is the pattern used in DeleteObjectHandler:
  425. t.Run("delete_object_handler_pattern", func(t *testing.T) {
  426. req, _ := http.NewRequest("DELETE", "/bucket/test-object", nil)
  427. req.Header.Set("x-amz-bypass-governance-retention", "true")
  428. // Extract parameters as done in the handler
  429. bucket, object := s3_constants.GetBucketAndObject(req)
  430. versionId := req.URL.Query().Get("versionId")
  431. bypassGovernance := req.Header.Get("x-amz-bypass-governance-retention") == "true"
  432. // Verify the parameters are extracted correctly
  433. // Note: The actual bucket and object extraction depends on the URL structure
  434. t.Logf("Extracted bucket: %s, object: %s", bucket, object)
  435. if versionId != "" {
  436. t.Errorf("Expected versionId to be empty, got %v", versionId)
  437. }
  438. if !bypassGovernance {
  439. t.Errorf("Expected bypassGovernance to be true")
  440. }
  441. })
  442. // This is the pattern used in PutObjectHandler:
  443. t.Run("put_object_handler_pattern", func(t *testing.T) {
  444. req, _ := http.NewRequest("PUT", "/bucket/test-object", nil)
  445. req.Header.Set("x-amz-bypass-governance-retention", "true")
  446. // Extract parameters as done in the handler
  447. bucket, object := s3_constants.GetBucketAndObject(req)
  448. bypassGovernance := req.Header.Get("x-amz-bypass-governance-retention") == "true"
  449. versioningEnabled := true // Would be determined by isVersioningEnabled(bucket)
  450. // Verify the parameters are extracted correctly
  451. // Note: The actual bucket and object extraction depends on the URL structure
  452. t.Logf("Extracted bucket: %s, object: %s", bucket, object)
  453. if !bypassGovernance {
  454. t.Errorf("Expected bypassGovernance to be true")
  455. }
  456. if !versioningEnabled {
  457. t.Errorf("Expected versioningEnabled to be true")
  458. }
  459. })
  460. }
  461. // TestGovernanceBypassNotPermittedError tests that ErrGovernanceBypassNotPermitted
  462. // is returned when bypass is requested but the user lacks permission
  463. func TestGovernanceBypassNotPermittedError(t *testing.T) {
  464. // Test the error constant itself
  465. if ErrGovernanceBypassNotPermitted == nil {
  466. t.Error("ErrGovernanceBypassNotPermitted should be defined")
  467. }
  468. // Verify the error message
  469. expectedMessage := "user does not have permission to bypass governance retention"
  470. if ErrGovernanceBypassNotPermitted.Error() != expectedMessage {
  471. t.Errorf("expected error message '%s', got '%s'",
  472. expectedMessage, ErrGovernanceBypassNotPermitted.Error())
  473. }
  474. // Test the scenario where this error should be returned
  475. // This documents the expected behavior when:
  476. // 1. Object is under governance retention
  477. // 2. bypassGovernance is true
  478. // 3. checkGovernanceBypassPermission returns false
  479. testCases := []struct {
  480. name string
  481. retentionMode string
  482. bypassGovernance bool
  483. hasPermission bool
  484. expectedError error
  485. description string
  486. }{
  487. {
  488. name: "governance_bypass_without_permission",
  489. retentionMode: s3_constants.RetentionModeGovernance,
  490. bypassGovernance: true,
  491. hasPermission: false,
  492. expectedError: ErrGovernanceBypassNotPermitted,
  493. description: "Should return ErrGovernanceBypassNotPermitted when bypass is requested but user lacks permission",
  494. },
  495. {
  496. name: "governance_bypass_with_permission",
  497. retentionMode: s3_constants.RetentionModeGovernance,
  498. bypassGovernance: true,
  499. hasPermission: true,
  500. expectedError: nil,
  501. description: "Should succeed when bypass is requested and user has permission",
  502. },
  503. {
  504. name: "governance_no_bypass",
  505. retentionMode: s3_constants.RetentionModeGovernance,
  506. bypassGovernance: false,
  507. hasPermission: false,
  508. expectedError: ErrGovernanceModeActive,
  509. description: "Should return ErrGovernanceModeActive when bypass is not requested",
  510. },
  511. }
  512. for _, tc := range testCases {
  513. t.Run(tc.name, func(t *testing.T) {
  514. // This test documents the expected behavior pattern
  515. // The actual checkObjectLockPermissions method implements this logic:
  516. // if retention.Mode == s3_constants.RetentionModeGovernance {
  517. // if !bypassGovernance {
  518. // return ErrGovernanceModeActive
  519. // }
  520. // if !s3a.checkGovernanceBypassPermission(request, bucket, object) {
  521. // return ErrGovernanceBypassNotPermitted
  522. // }
  523. // }
  524. var simulatedError error
  525. if tc.retentionMode == s3_constants.RetentionModeGovernance {
  526. if !tc.bypassGovernance {
  527. simulatedError = ErrGovernanceModeActive
  528. } else if !tc.hasPermission {
  529. simulatedError = ErrGovernanceBypassNotPermitted
  530. }
  531. }
  532. if simulatedError != tc.expectedError {
  533. t.Errorf("expected error %v, got %v. %s", tc.expectedError, simulatedError, tc.description)
  534. }
  535. // Verify ErrGovernanceBypassNotPermitted is returned in the right case
  536. if tc.name == "governance_bypass_without_permission" && simulatedError != ErrGovernanceBypassNotPermitted {
  537. t.Errorf("Test case should return ErrGovernanceBypassNotPermitted but got %v", simulatedError)
  538. }
  539. })
  540. }
  541. }