s3_multipart_iam_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. package s3api
  2. import (
  3. "context"
  4. "net/http"
  5. "net/http/httptest"
  6. "testing"
  7. "time"
  8. "github.com/golang-jwt/jwt/v5"
  9. "github.com/seaweedfs/seaweedfs/weed/iam/integration"
  10. "github.com/seaweedfs/seaweedfs/weed/iam/ldap"
  11. "github.com/seaweedfs/seaweedfs/weed/iam/oidc"
  12. "github.com/seaweedfs/seaweedfs/weed/iam/policy"
  13. "github.com/seaweedfs/seaweedfs/weed/iam/sts"
  14. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  15. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. )
  19. // createTestJWTMultipart creates a test JWT token with the specified issuer, subject and signing key
  20. func createTestJWTMultipart(t *testing.T, issuer, subject, signingKey string) string {
  21. token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
  22. "iss": issuer,
  23. "sub": subject,
  24. "aud": "test-client-id",
  25. "exp": time.Now().Add(time.Hour).Unix(),
  26. "iat": time.Now().Unix(),
  27. // Add claims that trust policy validation expects
  28. "idp": "test-oidc", // Identity provider claim for trust policy matching
  29. })
  30. tokenString, err := token.SignedString([]byte(signingKey))
  31. require.NoError(t, err)
  32. return tokenString
  33. }
  34. // TestMultipartIAMValidation tests IAM validation for multipart operations
  35. func TestMultipartIAMValidation(t *testing.T) {
  36. // Set up IAM system
  37. iamManager := setupTestIAMManagerForMultipart(t)
  38. s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
  39. s3iam.enabled = true
  40. // Create IAM with integration
  41. iam := &IdentityAccessManagement{
  42. isAuthEnabled: true,
  43. }
  44. iam.SetIAMIntegration(s3iam)
  45. // Set up roles
  46. ctx := context.Background()
  47. setupTestRolesForMultipart(ctx, iamManager)
  48. // Create a valid JWT token for testing
  49. validJWTToken := createTestJWTMultipart(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
  50. // Get session token
  51. response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
  52. RoleArn: "arn:seaweed:iam::role/S3WriteRole",
  53. WebIdentityToken: validJWTToken,
  54. RoleSessionName: "multipart-test-session",
  55. })
  56. require.NoError(t, err)
  57. sessionToken := response.Credentials.SessionToken
  58. tests := []struct {
  59. name string
  60. operation MultipartOperation
  61. method string
  62. path string
  63. sessionToken string
  64. expectedResult s3err.ErrorCode
  65. }{
  66. {
  67. name: "Initiate multipart upload",
  68. operation: MultipartOpInitiate,
  69. method: "POST",
  70. path: "/test-bucket/test-file.txt?uploads",
  71. sessionToken: sessionToken,
  72. expectedResult: s3err.ErrNone,
  73. },
  74. {
  75. name: "Upload part",
  76. operation: MultipartOpUploadPart,
  77. method: "PUT",
  78. path: "/test-bucket/test-file.txt?partNumber=1&uploadId=test-upload-id",
  79. sessionToken: sessionToken,
  80. expectedResult: s3err.ErrNone,
  81. },
  82. {
  83. name: "Complete multipart upload",
  84. operation: MultipartOpComplete,
  85. method: "POST",
  86. path: "/test-bucket/test-file.txt?uploadId=test-upload-id",
  87. sessionToken: sessionToken,
  88. expectedResult: s3err.ErrNone,
  89. },
  90. {
  91. name: "Abort multipart upload",
  92. operation: MultipartOpAbort,
  93. method: "DELETE",
  94. path: "/test-bucket/test-file.txt?uploadId=test-upload-id",
  95. sessionToken: sessionToken,
  96. expectedResult: s3err.ErrNone,
  97. },
  98. {
  99. name: "List multipart uploads",
  100. operation: MultipartOpList,
  101. method: "GET",
  102. path: "/test-bucket?uploads",
  103. sessionToken: sessionToken,
  104. expectedResult: s3err.ErrNone,
  105. },
  106. {
  107. name: "Upload part without session token",
  108. operation: MultipartOpUploadPart,
  109. method: "PUT",
  110. path: "/test-bucket/test-file.txt?partNumber=1&uploadId=test-upload-id",
  111. sessionToken: "",
  112. expectedResult: s3err.ErrNone, // Falls back to standard auth
  113. },
  114. {
  115. name: "Upload part with invalid session token",
  116. operation: MultipartOpUploadPart,
  117. method: "PUT",
  118. path: "/test-bucket/test-file.txt?partNumber=1&uploadId=test-upload-id",
  119. sessionToken: "invalid-token",
  120. expectedResult: s3err.ErrAccessDenied,
  121. },
  122. }
  123. for _, tt := range tests {
  124. t.Run(tt.name, func(t *testing.T) {
  125. // Create request for multipart operation
  126. req := createMultipartRequest(t, tt.method, tt.path, tt.sessionToken)
  127. // Create identity for testing
  128. identity := &Identity{
  129. Name: "test-user",
  130. Account: &AccountAdmin,
  131. }
  132. // Test validation
  133. result := iam.ValidateMultipartOperationWithIAM(req, identity, tt.operation)
  134. assert.Equal(t, tt.expectedResult, result, "Multipart IAM validation result should match expected")
  135. })
  136. }
  137. }
  138. // TestMultipartUploadPolicy tests multipart upload security policies
  139. func TestMultipartUploadPolicy(t *testing.T) {
  140. policy := &MultipartUploadPolicy{
  141. MaxPartSize: 10 * 1024 * 1024, // 10MB for testing
  142. MinPartSize: 5 * 1024 * 1024, // 5MB minimum
  143. MaxParts: 100, // 100 parts max for testing
  144. AllowedContentTypes: []string{"application/json", "text/plain"},
  145. RequiredHeaders: []string{"Content-Type"},
  146. }
  147. tests := []struct {
  148. name string
  149. request *MultipartUploadRequest
  150. expectedError string
  151. }{
  152. {
  153. name: "Valid upload part request",
  154. request: &MultipartUploadRequest{
  155. Bucket: "test-bucket",
  156. ObjectKey: "test-file.txt",
  157. PartNumber: 1,
  158. Operation: string(MultipartOpUploadPart),
  159. ContentSize: 8 * 1024 * 1024, // 8MB
  160. Headers: map[string]string{
  161. "Content-Type": "application/json",
  162. },
  163. },
  164. expectedError: "",
  165. },
  166. {
  167. name: "Part size too large",
  168. request: &MultipartUploadRequest{
  169. Bucket: "test-bucket",
  170. ObjectKey: "test-file.txt",
  171. PartNumber: 1,
  172. Operation: string(MultipartOpUploadPart),
  173. ContentSize: 15 * 1024 * 1024, // 15MB exceeds limit
  174. Headers: map[string]string{
  175. "Content-Type": "application/json",
  176. },
  177. },
  178. expectedError: "part size",
  179. },
  180. {
  181. name: "Invalid part number (too high)",
  182. request: &MultipartUploadRequest{
  183. Bucket: "test-bucket",
  184. ObjectKey: "test-file.txt",
  185. PartNumber: 150, // Exceeds max parts
  186. Operation: string(MultipartOpUploadPart),
  187. ContentSize: 8 * 1024 * 1024,
  188. Headers: map[string]string{
  189. "Content-Type": "application/json",
  190. },
  191. },
  192. expectedError: "part number",
  193. },
  194. {
  195. name: "Invalid part number (too low)",
  196. request: &MultipartUploadRequest{
  197. Bucket: "test-bucket",
  198. ObjectKey: "test-file.txt",
  199. PartNumber: 0, // Must be >= 1
  200. Operation: string(MultipartOpUploadPart),
  201. ContentSize: 8 * 1024 * 1024,
  202. Headers: map[string]string{
  203. "Content-Type": "application/json",
  204. },
  205. },
  206. expectedError: "part number",
  207. },
  208. {
  209. name: "Content type not allowed",
  210. request: &MultipartUploadRequest{
  211. Bucket: "test-bucket",
  212. ObjectKey: "test-file.txt",
  213. PartNumber: 1,
  214. Operation: string(MultipartOpUploadPart),
  215. ContentSize: 8 * 1024 * 1024,
  216. Headers: map[string]string{
  217. "Content-Type": "video/mp4", // Not in allowed list
  218. },
  219. },
  220. expectedError: "content type video/mp4 is not allowed",
  221. },
  222. {
  223. name: "Missing required header",
  224. request: &MultipartUploadRequest{
  225. Bucket: "test-bucket",
  226. ObjectKey: "test-file.txt",
  227. PartNumber: 1,
  228. Operation: string(MultipartOpUploadPart),
  229. ContentSize: 8 * 1024 * 1024,
  230. Headers: map[string]string{}, // Missing Content-Type
  231. },
  232. expectedError: "required header Content-Type is missing",
  233. },
  234. {
  235. name: "Non-upload operation (should not validate size)",
  236. request: &MultipartUploadRequest{
  237. Bucket: "test-bucket",
  238. ObjectKey: "test-file.txt",
  239. Operation: string(MultipartOpInitiate),
  240. Headers: map[string]string{
  241. "Content-Type": "application/json",
  242. },
  243. },
  244. expectedError: "",
  245. },
  246. }
  247. for _, tt := range tests {
  248. t.Run(tt.name, func(t *testing.T) {
  249. err := policy.ValidateMultipartRequestWithPolicy(tt.request)
  250. if tt.expectedError == "" {
  251. assert.NoError(t, err, "Policy validation should succeed")
  252. } else {
  253. assert.Error(t, err, "Policy validation should fail")
  254. assert.Contains(t, err.Error(), tt.expectedError, "Error message should contain expected text")
  255. }
  256. })
  257. }
  258. }
  259. // TestMultipartS3ActionMapping tests the mapping of multipart operations to S3 actions
  260. func TestMultipartS3ActionMapping(t *testing.T) {
  261. tests := []struct {
  262. operation MultipartOperation
  263. expectedAction Action
  264. }{
  265. {MultipartOpInitiate, s3_constants.ACTION_CREATE_MULTIPART_UPLOAD},
  266. {MultipartOpUploadPart, s3_constants.ACTION_UPLOAD_PART},
  267. {MultipartOpComplete, s3_constants.ACTION_COMPLETE_MULTIPART},
  268. {MultipartOpAbort, s3_constants.ACTION_ABORT_MULTIPART},
  269. {MultipartOpList, s3_constants.ACTION_LIST_MULTIPART_UPLOADS},
  270. {MultipartOpListParts, s3_constants.ACTION_LIST_PARTS},
  271. {MultipartOperation("unknown"), "s3:InternalErrorUnknownMultipartAction"}, // Fail-closed for security
  272. }
  273. for _, tt := range tests {
  274. t.Run(string(tt.operation), func(t *testing.T) {
  275. action := determineMultipartS3Action(tt.operation)
  276. assert.Equal(t, tt.expectedAction, action, "S3 action mapping should match expected")
  277. })
  278. }
  279. }
  280. // TestSessionTokenExtraction tests session token extraction from various sources
  281. func TestSessionTokenExtraction(t *testing.T) {
  282. tests := []struct {
  283. name string
  284. setupRequest func() *http.Request
  285. expectedToken string
  286. }{
  287. {
  288. name: "Bearer token in Authorization header",
  289. setupRequest: func() *http.Request {
  290. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt", nil)
  291. req.Header.Set("Authorization", "Bearer test-session-token-123")
  292. return req
  293. },
  294. expectedToken: "test-session-token-123",
  295. },
  296. {
  297. name: "X-Amz-Security-Token header",
  298. setupRequest: func() *http.Request {
  299. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt", nil)
  300. req.Header.Set("X-Amz-Security-Token", "security-token-456")
  301. return req
  302. },
  303. expectedToken: "security-token-456",
  304. },
  305. {
  306. name: "X-Amz-Security-Token query parameter",
  307. setupRequest: func() *http.Request {
  308. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt?X-Amz-Security-Token=query-token-789", nil)
  309. return req
  310. },
  311. expectedToken: "query-token-789",
  312. },
  313. {
  314. name: "No token present",
  315. setupRequest: func() *http.Request {
  316. return httptest.NewRequest("PUT", "/test-bucket/test-file.txt", nil)
  317. },
  318. expectedToken: "",
  319. },
  320. {
  321. name: "Authorization header without Bearer",
  322. setupRequest: func() *http.Request {
  323. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt", nil)
  324. req.Header.Set("Authorization", "AWS access_key:signature")
  325. return req
  326. },
  327. expectedToken: "",
  328. },
  329. }
  330. for _, tt := range tests {
  331. t.Run(tt.name, func(t *testing.T) {
  332. req := tt.setupRequest()
  333. token := extractSessionTokenFromRequest(req)
  334. assert.Equal(t, tt.expectedToken, token, "Extracted token should match expected")
  335. })
  336. }
  337. }
  338. // TestUploadPartValidation tests upload part request validation
  339. func TestUploadPartValidation(t *testing.T) {
  340. s3Server := &S3ApiServer{}
  341. tests := []struct {
  342. name string
  343. setupRequest func() *http.Request
  344. expectedError string
  345. }{
  346. {
  347. name: "Valid upload part request",
  348. setupRequest: func() *http.Request {
  349. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt?partNumber=1&uploadId=test-123", nil)
  350. req.Header.Set("Content-Type", "application/octet-stream")
  351. req.ContentLength = 6 * 1024 * 1024 // 6MB
  352. return req
  353. },
  354. expectedError: "",
  355. },
  356. {
  357. name: "Missing partNumber parameter",
  358. setupRequest: func() *http.Request {
  359. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt?uploadId=test-123", nil)
  360. req.Header.Set("Content-Type", "application/octet-stream")
  361. req.ContentLength = 6 * 1024 * 1024
  362. return req
  363. },
  364. expectedError: "missing partNumber parameter",
  365. },
  366. {
  367. name: "Invalid partNumber format",
  368. setupRequest: func() *http.Request {
  369. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt?partNumber=abc&uploadId=test-123", nil)
  370. req.Header.Set("Content-Type", "application/octet-stream")
  371. req.ContentLength = 6 * 1024 * 1024
  372. return req
  373. },
  374. expectedError: "invalid partNumber",
  375. },
  376. {
  377. name: "Part size too large",
  378. setupRequest: func() *http.Request {
  379. req := httptest.NewRequest("PUT", "/test-bucket/test-file.txt?partNumber=1&uploadId=test-123", nil)
  380. req.Header.Set("Content-Type", "application/octet-stream")
  381. req.ContentLength = 6 * 1024 * 1024 * 1024 // 6GB exceeds 5GB limit
  382. return req
  383. },
  384. expectedError: "part size",
  385. },
  386. }
  387. for _, tt := range tests {
  388. t.Run(tt.name, func(t *testing.T) {
  389. req := tt.setupRequest()
  390. err := s3Server.validateUploadPartRequest(req)
  391. if tt.expectedError == "" {
  392. assert.NoError(t, err, "Upload part validation should succeed")
  393. } else {
  394. assert.Error(t, err, "Upload part validation should fail")
  395. assert.Contains(t, err.Error(), tt.expectedError, "Error message should contain expected text")
  396. }
  397. })
  398. }
  399. }
  400. // TestDefaultMultipartUploadPolicy tests the default policy configuration
  401. func TestDefaultMultipartUploadPolicy(t *testing.T) {
  402. policy := DefaultMultipartUploadPolicy()
  403. assert.Equal(t, int64(5*1024*1024*1024), policy.MaxPartSize, "Max part size should be 5GB")
  404. assert.Equal(t, int64(5*1024*1024), policy.MinPartSize, "Min part size should be 5MB")
  405. assert.Equal(t, 10000, policy.MaxParts, "Max parts should be 10,000")
  406. assert.Equal(t, 7*24*time.Hour, policy.MaxUploadDuration, "Max upload duration should be 7 days")
  407. assert.Empty(t, policy.AllowedContentTypes, "Should allow all content types by default")
  408. assert.Empty(t, policy.RequiredHeaders, "Should have no required headers by default")
  409. assert.Empty(t, policy.IPWhitelist, "Should have no IP restrictions by default")
  410. }
  411. // TestMultipartUploadSession tests multipart upload session structure
  412. func TestMultipartUploadSession(t *testing.T) {
  413. session := &MultipartUploadSession{
  414. UploadID: "test-upload-123",
  415. Bucket: "test-bucket",
  416. ObjectKey: "test-file.txt",
  417. Initiator: "arn:seaweed:iam::user/testuser",
  418. Owner: "arn:seaweed:iam::user/testuser",
  419. CreatedAt: time.Now(),
  420. Parts: []MultipartUploadPart{
  421. {
  422. PartNumber: 1,
  423. Size: 5 * 1024 * 1024,
  424. ETag: "abc123",
  425. LastModified: time.Now(),
  426. Checksum: "sha256:def456",
  427. },
  428. },
  429. Metadata: map[string]string{
  430. "Content-Type": "application/octet-stream",
  431. "x-amz-meta-custom": "value",
  432. },
  433. Policy: DefaultMultipartUploadPolicy(),
  434. SessionToken: "session-token-789",
  435. }
  436. assert.NotEmpty(t, session.UploadID, "Upload ID should not be empty")
  437. assert.NotEmpty(t, session.Bucket, "Bucket should not be empty")
  438. assert.NotEmpty(t, session.ObjectKey, "Object key should not be empty")
  439. assert.Len(t, session.Parts, 1, "Should have one part")
  440. assert.Equal(t, 1, session.Parts[0].PartNumber, "Part number should be 1")
  441. assert.NotNil(t, session.Policy, "Policy should not be nil")
  442. }
  443. // Helper functions for tests
  444. func setupTestIAMManagerForMultipart(t *testing.T) *integration.IAMManager {
  445. // Create IAM manager
  446. manager := integration.NewIAMManager()
  447. // Initialize with test configuration
  448. config := &integration.IAMConfig{
  449. STS: &sts.STSConfig{
  450. TokenDuration: sts.FlexibleDuration{time.Hour},
  451. MaxSessionLength: sts.FlexibleDuration{time.Hour * 12},
  452. Issuer: "test-sts",
  453. SigningKey: []byte("test-signing-key-32-characters-long"),
  454. },
  455. Policy: &policy.PolicyEngineConfig{
  456. DefaultEffect: "Deny",
  457. StoreType: "memory",
  458. },
  459. Roles: &integration.RoleStoreConfig{
  460. StoreType: "memory",
  461. },
  462. }
  463. err := manager.Initialize(config, func() string {
  464. return "localhost:8888" // Mock filer address for testing
  465. })
  466. require.NoError(t, err)
  467. // Set up test identity providers
  468. setupTestProvidersForMultipart(t, manager)
  469. return manager
  470. }
  471. func setupTestProvidersForMultipart(t *testing.T, manager *integration.IAMManager) {
  472. // Set up OIDC provider
  473. oidcProvider := oidc.NewMockOIDCProvider("test-oidc")
  474. oidcConfig := &oidc.OIDCConfig{
  475. Issuer: "https://test-issuer.com",
  476. ClientID: "test-client-id",
  477. }
  478. err := oidcProvider.Initialize(oidcConfig)
  479. require.NoError(t, err)
  480. oidcProvider.SetupDefaultTestData()
  481. // Set up LDAP provider
  482. ldapProvider := ldap.NewMockLDAPProvider("test-ldap")
  483. err = ldapProvider.Initialize(nil) // Mock doesn't need real config
  484. require.NoError(t, err)
  485. ldapProvider.SetupDefaultTestData()
  486. // Register providers
  487. err = manager.RegisterIdentityProvider(oidcProvider)
  488. require.NoError(t, err)
  489. err = manager.RegisterIdentityProvider(ldapProvider)
  490. require.NoError(t, err)
  491. }
  492. func setupTestRolesForMultipart(ctx context.Context, manager *integration.IAMManager) {
  493. // Create write policy for multipart operations
  494. writePolicy := &policy.PolicyDocument{
  495. Version: "2012-10-17",
  496. Statement: []policy.Statement{
  497. {
  498. Sid: "AllowS3MultipartOperations",
  499. Effect: "Allow",
  500. Action: []string{
  501. "s3:PutObject",
  502. "s3:GetObject",
  503. "s3:ListBucket",
  504. "s3:DeleteObject",
  505. "s3:CreateMultipartUpload",
  506. "s3:UploadPart",
  507. "s3:CompleteMultipartUpload",
  508. "s3:AbortMultipartUpload",
  509. "s3:ListMultipartUploads",
  510. "s3:ListParts",
  511. },
  512. Resource: []string{
  513. "arn:seaweed:s3:::*",
  514. "arn:seaweed:s3:::*/*",
  515. },
  516. },
  517. },
  518. }
  519. manager.CreatePolicy(ctx, "", "S3WritePolicy", writePolicy)
  520. // Create write role
  521. manager.CreateRole(ctx, "", "S3WriteRole", &integration.RoleDefinition{
  522. RoleName: "S3WriteRole",
  523. TrustPolicy: &policy.PolicyDocument{
  524. Version: "2012-10-17",
  525. Statement: []policy.Statement{
  526. {
  527. Effect: "Allow",
  528. Principal: map[string]interface{}{
  529. "Federated": "test-oidc",
  530. },
  531. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  532. },
  533. },
  534. },
  535. AttachedPolicies: []string{"S3WritePolicy"},
  536. })
  537. // Create a role for multipart users
  538. manager.CreateRole(ctx, "", "MultipartUser", &integration.RoleDefinition{
  539. RoleName: "MultipartUser",
  540. TrustPolicy: &policy.PolicyDocument{
  541. Version: "2012-10-17",
  542. Statement: []policy.Statement{
  543. {
  544. Effect: "Allow",
  545. Principal: map[string]interface{}{
  546. "Federated": "test-oidc",
  547. },
  548. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  549. },
  550. },
  551. },
  552. AttachedPolicies: []string{"S3WritePolicy"},
  553. })
  554. }
  555. func createMultipartRequest(t *testing.T, method, path, sessionToken string) *http.Request {
  556. req := httptest.NewRequest(method, path, nil)
  557. // Add session token if provided
  558. if sessionToken != "" {
  559. req.Header.Set("Authorization", "Bearer "+sessionToken)
  560. // Set the principal ARN header that matches the assumed role from the test setup
  561. // This corresponds to the role "arn:seaweed:iam::role/S3WriteRole" with session name "multipart-test-session"
  562. req.Header.Set("X-SeaweedFS-Principal", "arn:seaweed:sts::assumed-role/S3WriteRole/multipart-test-session")
  563. }
  564. // Add common headers
  565. req.Header.Set("Content-Type", "application/octet-stream")
  566. return req
  567. }