s3_iam_simple_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. package s3api
  2. import (
  3. "context"
  4. "net/http"
  5. "net/http/httptest"
  6. "net/url"
  7. "testing"
  8. "time"
  9. "github.com/seaweedfs/seaweedfs/weed/iam/integration"
  10. "github.com/seaweedfs/seaweedfs/weed/iam/policy"
  11. "github.com/seaweedfs/seaweedfs/weed/iam/sts"
  12. "github.com/seaweedfs/seaweedfs/weed/iam/utils"
  13. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  14. "github.com/stretchr/testify/assert"
  15. "github.com/stretchr/testify/require"
  16. )
  17. // TestS3IAMMiddleware tests the basic S3 IAM middleware functionality
  18. func TestS3IAMMiddleware(t *testing.T) {
  19. // Create IAM manager
  20. iamManager := integration.NewIAMManager()
  21. // Initialize with test configuration
  22. config := &integration.IAMConfig{
  23. STS: &sts.STSConfig{
  24. TokenDuration: sts.FlexibleDuration{time.Hour},
  25. MaxSessionLength: sts.FlexibleDuration{time.Hour * 12},
  26. Issuer: "test-sts",
  27. SigningKey: []byte("test-signing-key-32-characters-long"),
  28. },
  29. Policy: &policy.PolicyEngineConfig{
  30. DefaultEffect: "Deny",
  31. StoreType: "memory",
  32. },
  33. Roles: &integration.RoleStoreConfig{
  34. StoreType: "memory",
  35. },
  36. }
  37. err := iamManager.Initialize(config, func() string {
  38. return "localhost:8888" // Mock filer address for testing
  39. })
  40. require.NoError(t, err)
  41. // Create S3 IAM integration
  42. s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
  43. // Test that integration is created successfully
  44. assert.NotNil(t, s3IAMIntegration)
  45. assert.True(t, s3IAMIntegration.enabled)
  46. }
  47. // TestS3IAMMiddlewareJWTAuth tests JWT authentication
  48. func TestS3IAMMiddlewareJWTAuth(t *testing.T) {
  49. // Skip for now since it requires full setup
  50. t.Skip("JWT authentication test requires full IAM setup")
  51. // Create IAM integration
  52. s3iam := NewS3IAMIntegration(nil, "localhost:8888") // Disabled integration
  53. // Create test request with JWT token
  54. req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
  55. req.Header.Set("Authorization", "Bearer test-token")
  56. // Test authentication (should return not implemented when disabled)
  57. ctx := context.Background()
  58. identity, errCode := s3iam.AuthenticateJWT(ctx, req)
  59. assert.Nil(t, identity)
  60. assert.NotEqual(t, errCode, 0) // Should return an error
  61. }
  62. // TestBuildS3ResourceArn tests resource ARN building
  63. func TestBuildS3ResourceArn(t *testing.T) {
  64. tests := []struct {
  65. name string
  66. bucket string
  67. object string
  68. expected string
  69. }{
  70. {
  71. name: "empty bucket and object",
  72. bucket: "",
  73. object: "",
  74. expected: "arn:seaweed:s3:::*",
  75. },
  76. {
  77. name: "bucket only",
  78. bucket: "test-bucket",
  79. object: "",
  80. expected: "arn:seaweed:s3:::test-bucket",
  81. },
  82. {
  83. name: "bucket and object",
  84. bucket: "test-bucket",
  85. object: "test-object.txt",
  86. expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
  87. },
  88. {
  89. name: "bucket and object with leading slash",
  90. bucket: "test-bucket",
  91. object: "/test-object.txt",
  92. expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
  93. },
  94. {
  95. name: "bucket and nested object",
  96. bucket: "test-bucket",
  97. object: "folder/subfolder/test-object.txt",
  98. expected: "arn:seaweed:s3:::test-bucket/folder/subfolder/test-object.txt",
  99. },
  100. }
  101. for _, tt := range tests {
  102. t.Run(tt.name, func(t *testing.T) {
  103. result := buildS3ResourceArn(tt.bucket, tt.object)
  104. assert.Equal(t, tt.expected, result)
  105. })
  106. }
  107. }
  108. // TestDetermineGranularS3Action tests granular S3 action determination from HTTP requests
  109. func TestDetermineGranularS3Action(t *testing.T) {
  110. tests := []struct {
  111. name string
  112. method string
  113. bucket string
  114. objectKey string
  115. queryParams map[string]string
  116. fallbackAction Action
  117. expected string
  118. description string
  119. }{
  120. // Object-level operations
  121. {
  122. name: "get_object",
  123. method: "GET",
  124. bucket: "test-bucket",
  125. objectKey: "test-object.txt",
  126. queryParams: map[string]string{},
  127. fallbackAction: s3_constants.ACTION_READ,
  128. expected: "s3:GetObject",
  129. description: "Basic object retrieval",
  130. },
  131. {
  132. name: "get_object_acl",
  133. method: "GET",
  134. bucket: "test-bucket",
  135. objectKey: "test-object.txt",
  136. queryParams: map[string]string{"acl": ""},
  137. fallbackAction: s3_constants.ACTION_READ_ACP,
  138. expected: "s3:GetObjectAcl",
  139. description: "Object ACL retrieval",
  140. },
  141. {
  142. name: "get_object_tagging",
  143. method: "GET",
  144. bucket: "test-bucket",
  145. objectKey: "test-object.txt",
  146. queryParams: map[string]string{"tagging": ""},
  147. fallbackAction: s3_constants.ACTION_TAGGING,
  148. expected: "s3:GetObjectTagging",
  149. description: "Object tagging retrieval",
  150. },
  151. {
  152. name: "put_object",
  153. method: "PUT",
  154. bucket: "test-bucket",
  155. objectKey: "test-object.txt",
  156. queryParams: map[string]string{},
  157. fallbackAction: s3_constants.ACTION_WRITE,
  158. expected: "s3:PutObject",
  159. description: "Basic object upload",
  160. },
  161. {
  162. name: "put_object_acl",
  163. method: "PUT",
  164. bucket: "test-bucket",
  165. objectKey: "test-object.txt",
  166. queryParams: map[string]string{"acl": ""},
  167. fallbackAction: s3_constants.ACTION_WRITE_ACP,
  168. expected: "s3:PutObjectAcl",
  169. description: "Object ACL modification",
  170. },
  171. {
  172. name: "delete_object",
  173. method: "DELETE",
  174. bucket: "test-bucket",
  175. objectKey: "test-object.txt",
  176. queryParams: map[string]string{},
  177. fallbackAction: s3_constants.ACTION_WRITE, // DELETE object uses WRITE fallback
  178. expected: "s3:DeleteObject",
  179. description: "Object deletion - correctly mapped to DeleteObject (not PutObject)",
  180. },
  181. {
  182. name: "delete_object_tagging",
  183. method: "DELETE",
  184. bucket: "test-bucket",
  185. objectKey: "test-object.txt",
  186. queryParams: map[string]string{"tagging": ""},
  187. fallbackAction: s3_constants.ACTION_TAGGING,
  188. expected: "s3:DeleteObjectTagging",
  189. description: "Object tag deletion",
  190. },
  191. // Multipart upload operations
  192. {
  193. name: "create_multipart_upload",
  194. method: "POST",
  195. bucket: "test-bucket",
  196. objectKey: "large-file.txt",
  197. queryParams: map[string]string{"uploads": ""},
  198. fallbackAction: s3_constants.ACTION_WRITE,
  199. expected: "s3:CreateMultipartUpload",
  200. description: "Multipart upload initiation",
  201. },
  202. {
  203. name: "upload_part",
  204. method: "PUT",
  205. bucket: "test-bucket",
  206. objectKey: "large-file.txt",
  207. queryParams: map[string]string{"uploadId": "12345", "partNumber": "1"},
  208. fallbackAction: s3_constants.ACTION_WRITE,
  209. expected: "s3:UploadPart",
  210. description: "Multipart part upload",
  211. },
  212. {
  213. name: "complete_multipart_upload",
  214. method: "POST",
  215. bucket: "test-bucket",
  216. objectKey: "large-file.txt",
  217. queryParams: map[string]string{"uploadId": "12345"},
  218. fallbackAction: s3_constants.ACTION_WRITE,
  219. expected: "s3:CompleteMultipartUpload",
  220. description: "Multipart upload completion",
  221. },
  222. {
  223. name: "abort_multipart_upload",
  224. method: "DELETE",
  225. bucket: "test-bucket",
  226. objectKey: "large-file.txt",
  227. queryParams: map[string]string{"uploadId": "12345"},
  228. fallbackAction: s3_constants.ACTION_WRITE,
  229. expected: "s3:AbortMultipartUpload",
  230. description: "Multipart upload abort",
  231. },
  232. // Bucket-level operations
  233. {
  234. name: "list_bucket",
  235. method: "GET",
  236. bucket: "test-bucket",
  237. objectKey: "",
  238. queryParams: map[string]string{},
  239. fallbackAction: s3_constants.ACTION_LIST,
  240. expected: "s3:ListBucket",
  241. description: "Bucket listing",
  242. },
  243. {
  244. name: "get_bucket_acl",
  245. method: "GET",
  246. bucket: "test-bucket",
  247. objectKey: "",
  248. queryParams: map[string]string{"acl": ""},
  249. fallbackAction: s3_constants.ACTION_READ_ACP,
  250. expected: "s3:GetBucketAcl",
  251. description: "Bucket ACL retrieval",
  252. },
  253. {
  254. name: "put_bucket_policy",
  255. method: "PUT",
  256. bucket: "test-bucket",
  257. objectKey: "",
  258. queryParams: map[string]string{"policy": ""},
  259. fallbackAction: s3_constants.ACTION_WRITE,
  260. expected: "s3:PutBucketPolicy",
  261. description: "Bucket policy modification",
  262. },
  263. {
  264. name: "delete_bucket",
  265. method: "DELETE",
  266. bucket: "test-bucket",
  267. objectKey: "",
  268. queryParams: map[string]string{},
  269. fallbackAction: s3_constants.ACTION_DELETE_BUCKET,
  270. expected: "s3:DeleteBucket",
  271. description: "Bucket deletion",
  272. },
  273. {
  274. name: "list_multipart_uploads",
  275. method: "GET",
  276. bucket: "test-bucket",
  277. objectKey: "",
  278. queryParams: map[string]string{"uploads": ""},
  279. fallbackAction: s3_constants.ACTION_LIST,
  280. expected: "s3:ListMultipartUploads",
  281. description: "List multipart uploads in bucket",
  282. },
  283. // Fallback scenarios
  284. {
  285. name: "legacy_read_fallback",
  286. method: "GET",
  287. bucket: "",
  288. objectKey: "",
  289. queryParams: map[string]string{},
  290. fallbackAction: s3_constants.ACTION_READ,
  291. expected: "s3:GetObject",
  292. description: "Legacy read action fallback",
  293. },
  294. {
  295. name: "already_granular_action",
  296. method: "GET",
  297. bucket: "",
  298. objectKey: "",
  299. queryParams: map[string]string{},
  300. fallbackAction: "s3:GetBucketLocation", // Already granular
  301. expected: "s3:GetBucketLocation",
  302. description: "Already granular action passed through",
  303. },
  304. }
  305. for _, tt := range tests {
  306. t.Run(tt.name, func(t *testing.T) {
  307. // Create HTTP request with query parameters
  308. req := &http.Request{
  309. Method: tt.method,
  310. URL: &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
  311. }
  312. // Add query parameters
  313. query := req.URL.Query()
  314. for key, value := range tt.queryParams {
  315. query.Set(key, value)
  316. }
  317. req.URL.RawQuery = query.Encode()
  318. // Test the granular action determination
  319. result := determineGranularS3Action(req, tt.fallbackAction, tt.bucket, tt.objectKey)
  320. assert.Equal(t, tt.expected, result,
  321. "Test %s failed: %s. Expected %s but got %s",
  322. tt.name, tt.description, tt.expected, result)
  323. })
  324. }
  325. }
  326. // TestMapLegacyActionToIAM tests the legacy action fallback mapping
  327. func TestMapLegacyActionToIAM(t *testing.T) {
  328. tests := []struct {
  329. name string
  330. legacyAction Action
  331. expected string
  332. }{
  333. {
  334. name: "read_action_fallback",
  335. legacyAction: s3_constants.ACTION_READ,
  336. expected: "s3:GetObject",
  337. },
  338. {
  339. name: "write_action_fallback",
  340. legacyAction: s3_constants.ACTION_WRITE,
  341. expected: "s3:PutObject",
  342. },
  343. {
  344. name: "admin_action_fallback",
  345. legacyAction: s3_constants.ACTION_ADMIN,
  346. expected: "s3:*",
  347. },
  348. {
  349. name: "granular_multipart_action",
  350. legacyAction: s3_constants.ACTION_CREATE_MULTIPART_UPLOAD,
  351. expected: "s3:CreateMultipartUpload",
  352. },
  353. {
  354. name: "unknown_action_with_s3_prefix",
  355. legacyAction: "s3:CustomAction",
  356. expected: "s3:CustomAction",
  357. },
  358. {
  359. name: "unknown_action_without_prefix",
  360. legacyAction: "CustomAction",
  361. expected: "s3:CustomAction",
  362. },
  363. }
  364. for _, tt := range tests {
  365. t.Run(tt.name, func(t *testing.T) {
  366. result := mapLegacyActionToIAM(tt.legacyAction)
  367. assert.Equal(t, tt.expected, result)
  368. })
  369. }
  370. }
  371. // TestExtractSourceIP tests source IP extraction from requests
  372. func TestExtractSourceIP(t *testing.T) {
  373. tests := []struct {
  374. name string
  375. setupReq func() *http.Request
  376. expectedIP string
  377. }{
  378. {
  379. name: "X-Forwarded-For header",
  380. setupReq: func() *http.Request {
  381. req := httptest.NewRequest("GET", "/test", http.NoBody)
  382. req.Header.Set("X-Forwarded-For", "192.168.1.100, 10.0.0.1")
  383. return req
  384. },
  385. expectedIP: "192.168.1.100",
  386. },
  387. {
  388. name: "X-Real-IP header",
  389. setupReq: func() *http.Request {
  390. req := httptest.NewRequest("GET", "/test", http.NoBody)
  391. req.Header.Set("X-Real-IP", "192.168.1.200")
  392. return req
  393. },
  394. expectedIP: "192.168.1.200",
  395. },
  396. {
  397. name: "RemoteAddr fallback",
  398. setupReq: func() *http.Request {
  399. req := httptest.NewRequest("GET", "/test", http.NoBody)
  400. req.RemoteAddr = "192.168.1.300:12345"
  401. return req
  402. },
  403. expectedIP: "192.168.1.300",
  404. },
  405. }
  406. for _, tt := range tests {
  407. t.Run(tt.name, func(t *testing.T) {
  408. req := tt.setupReq()
  409. result := extractSourceIP(req)
  410. assert.Equal(t, tt.expectedIP, result)
  411. })
  412. }
  413. }
  414. // TestExtractRoleNameFromPrincipal tests role name extraction
  415. func TestExtractRoleNameFromPrincipal(t *testing.T) {
  416. tests := []struct {
  417. name string
  418. principal string
  419. expected string
  420. }{
  421. {
  422. name: "valid assumed role ARN",
  423. principal: "arn:seaweed:sts::assumed-role/S3ReadOnlyRole/session-123",
  424. expected: "S3ReadOnlyRole",
  425. },
  426. {
  427. name: "invalid format",
  428. principal: "invalid-principal",
  429. expected: "", // Returns empty string to signal invalid format
  430. },
  431. {
  432. name: "missing session name",
  433. principal: "arn:seaweed:sts::assumed-role/TestRole",
  434. expected: "TestRole", // Extracts role name even without session name
  435. },
  436. {
  437. name: "empty principal",
  438. principal: "",
  439. expected: "",
  440. },
  441. }
  442. for _, tt := range tests {
  443. t.Run(tt.name, func(t *testing.T) {
  444. result := utils.ExtractRoleNameFromPrincipal(tt.principal)
  445. assert.Equal(t, tt.expected, result)
  446. })
  447. }
  448. }
  449. // TestIAMIdentityIsAdmin tests the IsAdmin method
  450. func TestIAMIdentityIsAdmin(t *testing.T) {
  451. identity := &IAMIdentity{
  452. Name: "test-identity",
  453. Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
  454. SessionToken: "test-token",
  455. }
  456. // In our implementation, IsAdmin always returns false since admin status
  457. // is determined by policies, not identity
  458. result := identity.IsAdmin()
  459. assert.False(t, result)
  460. }