s3api_object_lock_headers_test.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. package s3api
  2. import (
  3. "fmt"
  4. "net/http/httptest"
  5. "strconv"
  6. "testing"
  7. "time"
  8. "errors"
  9. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  10. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  11. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  12. "github.com/stretchr/testify/assert"
  13. )
  14. // TestExtractObjectLockMetadataFromRequest tests the function that extracts
  15. // object lock headers from PUT requests and stores them in Extended attributes.
  16. // This test would have caught the bug where object lock headers were ignored.
  17. func TestExtractObjectLockMetadataFromRequest(t *testing.T) {
  18. s3a := &S3ApiServer{}
  19. t.Run("Extract COMPLIANCE mode and retention date", func(t *testing.T) {
  20. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  21. retainUntilDate := time.Now().Add(24 * time.Hour)
  22. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  23. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  24. entry := &filer_pb.Entry{
  25. Extended: make(map[string][]byte),
  26. }
  27. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  28. assert.NoError(t, err)
  29. // Verify mode was stored
  30. assert.Contains(t, entry.Extended, s3_constants.ExtObjectLockModeKey)
  31. assert.Equal(t, "COMPLIANCE", string(entry.Extended[s3_constants.ExtObjectLockModeKey]))
  32. // Verify retention date was stored
  33. assert.Contains(t, entry.Extended, s3_constants.ExtRetentionUntilDateKey)
  34. storedTimestamp, err := strconv.ParseInt(string(entry.Extended[s3_constants.ExtRetentionUntilDateKey]), 10, 64)
  35. assert.NoError(t, err)
  36. storedTime := time.Unix(storedTimestamp, 0)
  37. assert.WithinDuration(t, retainUntilDate, storedTime, 1*time.Second)
  38. })
  39. t.Run("Extract GOVERNANCE mode and retention date", func(t *testing.T) {
  40. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  41. retainUntilDate := time.Now().Add(12 * time.Hour)
  42. req.Header.Set(s3_constants.AmzObjectLockMode, "GOVERNANCE")
  43. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  44. entry := &filer_pb.Entry{
  45. Extended: make(map[string][]byte),
  46. }
  47. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  48. assert.NoError(t, err)
  49. assert.Equal(t, "GOVERNANCE", string(entry.Extended[s3_constants.ExtObjectLockModeKey]))
  50. assert.Contains(t, entry.Extended, s3_constants.ExtRetentionUntilDateKey)
  51. })
  52. t.Run("Extract legal hold ON", func(t *testing.T) {
  53. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  54. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "ON")
  55. entry := &filer_pb.Entry{
  56. Extended: make(map[string][]byte),
  57. }
  58. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  59. assert.NoError(t, err)
  60. assert.Contains(t, entry.Extended, s3_constants.ExtLegalHoldKey)
  61. assert.Equal(t, "ON", string(entry.Extended[s3_constants.ExtLegalHoldKey]))
  62. })
  63. t.Run("Extract legal hold OFF", func(t *testing.T) {
  64. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  65. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "OFF")
  66. entry := &filer_pb.Entry{
  67. Extended: make(map[string][]byte),
  68. }
  69. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  70. assert.NoError(t, err)
  71. assert.Contains(t, entry.Extended, s3_constants.ExtLegalHoldKey)
  72. assert.Equal(t, "OFF", string(entry.Extended[s3_constants.ExtLegalHoldKey]))
  73. })
  74. t.Run("Handle all object lock headers together", func(t *testing.T) {
  75. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  76. retainUntilDate := time.Now().Add(24 * time.Hour)
  77. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  78. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  79. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "ON")
  80. entry := &filer_pb.Entry{
  81. Extended: make(map[string][]byte),
  82. }
  83. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  84. assert.NoError(t, err)
  85. // All metadata should be stored
  86. assert.Equal(t, "COMPLIANCE", string(entry.Extended[s3_constants.ExtObjectLockModeKey]))
  87. assert.Contains(t, entry.Extended, s3_constants.ExtRetentionUntilDateKey)
  88. assert.Equal(t, "ON", string(entry.Extended[s3_constants.ExtLegalHoldKey]))
  89. })
  90. t.Run("Handle no object lock headers", func(t *testing.T) {
  91. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  92. // No object lock headers set
  93. entry := &filer_pb.Entry{
  94. Extended: make(map[string][]byte),
  95. }
  96. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  97. assert.NoError(t, err)
  98. // No object lock metadata should be stored
  99. assert.NotContains(t, entry.Extended, s3_constants.ExtObjectLockModeKey)
  100. assert.NotContains(t, entry.Extended, s3_constants.ExtRetentionUntilDateKey)
  101. assert.NotContains(t, entry.Extended, s3_constants.ExtLegalHoldKey)
  102. })
  103. t.Run("Handle invalid retention date - should return error", func(t *testing.T) {
  104. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  105. req.Header.Set(s3_constants.AmzObjectLockMode, "GOVERNANCE")
  106. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, "invalid-date")
  107. entry := &filer_pb.Entry{
  108. Extended: make(map[string][]byte),
  109. }
  110. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  111. assert.Error(t, err)
  112. assert.True(t, errors.Is(err, ErrInvalidRetentionDateFormat))
  113. // Mode should be stored but not invalid date
  114. assert.Equal(t, "GOVERNANCE", string(entry.Extended[s3_constants.ExtObjectLockModeKey]))
  115. assert.NotContains(t, entry.Extended, s3_constants.ExtRetentionUntilDateKey)
  116. })
  117. t.Run("Handle invalid legal hold value - should return error", func(t *testing.T) {
  118. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  119. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "INVALID")
  120. entry := &filer_pb.Entry{
  121. Extended: make(map[string][]byte),
  122. }
  123. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  124. assert.Error(t, err)
  125. assert.True(t, errors.Is(err, ErrInvalidLegalHoldStatus))
  126. // No legal hold metadata should be stored due to error
  127. assert.NotContains(t, entry.Extended, s3_constants.ExtLegalHoldKey)
  128. })
  129. }
  130. // TestAddObjectLockHeadersToResponse tests the function that adds object lock
  131. // metadata from Extended attributes to HTTP response headers.
  132. // This test would have caught the bug where HEAD responses didn't include object lock metadata.
  133. func TestAddObjectLockHeadersToResponse(t *testing.T) {
  134. s3a := &S3ApiServer{}
  135. t.Run("Add COMPLIANCE mode and retention date to response", func(t *testing.T) {
  136. w := httptest.NewRecorder()
  137. retainUntilTime := time.Now().Add(24 * time.Hour)
  138. entry := &filer_pb.Entry{
  139. Extended: map[string][]byte{
  140. s3_constants.ExtObjectLockModeKey: []byte("COMPLIANCE"),
  141. s3_constants.ExtRetentionUntilDateKey: []byte(strconv.FormatInt(retainUntilTime.Unix(), 10)),
  142. },
  143. }
  144. s3a.addObjectLockHeadersToResponse(w, entry)
  145. // Verify headers were set
  146. assert.Equal(t, "COMPLIANCE", w.Header().Get(s3_constants.AmzObjectLockMode))
  147. assert.NotEmpty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  148. // Verify the date format is correct
  149. returnedDate := w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate)
  150. parsedTime, err := time.Parse(time.RFC3339, returnedDate)
  151. assert.NoError(t, err)
  152. assert.WithinDuration(t, retainUntilTime, parsedTime, 1*time.Second)
  153. })
  154. t.Run("Add GOVERNANCE mode to response", func(t *testing.T) {
  155. w := httptest.NewRecorder()
  156. entry := &filer_pb.Entry{
  157. Extended: map[string][]byte{
  158. s3_constants.ExtObjectLockModeKey: []byte("GOVERNANCE"),
  159. },
  160. }
  161. s3a.addObjectLockHeadersToResponse(w, entry)
  162. assert.Equal(t, "GOVERNANCE", w.Header().Get(s3_constants.AmzObjectLockMode))
  163. })
  164. t.Run("Add legal hold ON to response", func(t *testing.T) {
  165. w := httptest.NewRecorder()
  166. entry := &filer_pb.Entry{
  167. Extended: map[string][]byte{
  168. s3_constants.ExtLegalHoldKey: []byte("ON"),
  169. },
  170. }
  171. s3a.addObjectLockHeadersToResponse(w, entry)
  172. assert.Equal(t, "ON", w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  173. })
  174. t.Run("Add legal hold OFF to response", func(t *testing.T) {
  175. w := httptest.NewRecorder()
  176. entry := &filer_pb.Entry{
  177. Extended: map[string][]byte{
  178. s3_constants.ExtLegalHoldKey: []byte("OFF"),
  179. },
  180. }
  181. s3a.addObjectLockHeadersToResponse(w, entry)
  182. assert.Equal(t, "OFF", w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  183. })
  184. t.Run("Add all object lock headers to response", func(t *testing.T) {
  185. w := httptest.NewRecorder()
  186. retainUntilTime := time.Now().Add(12 * time.Hour)
  187. entry := &filer_pb.Entry{
  188. Extended: map[string][]byte{
  189. s3_constants.ExtObjectLockModeKey: []byte("GOVERNANCE"),
  190. s3_constants.ExtRetentionUntilDateKey: []byte(strconv.FormatInt(retainUntilTime.Unix(), 10)),
  191. s3_constants.ExtLegalHoldKey: []byte("ON"),
  192. },
  193. }
  194. s3a.addObjectLockHeadersToResponse(w, entry)
  195. // All headers should be set
  196. assert.Equal(t, "GOVERNANCE", w.Header().Get(s3_constants.AmzObjectLockMode))
  197. assert.NotEmpty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  198. assert.Equal(t, "ON", w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  199. })
  200. t.Run("Handle entry with no object lock metadata", func(t *testing.T) {
  201. w := httptest.NewRecorder()
  202. entry := &filer_pb.Entry{
  203. Extended: map[string][]byte{
  204. "other-metadata": []byte("some-value"),
  205. },
  206. }
  207. s3a.addObjectLockHeadersToResponse(w, entry)
  208. // No object lock headers should be set for entries without object lock metadata
  209. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockMode))
  210. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  211. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  212. })
  213. t.Run("Handle entry with object lock mode but no legal hold - should default to OFF", func(t *testing.T) {
  214. w := httptest.NewRecorder()
  215. entry := &filer_pb.Entry{
  216. Extended: map[string][]byte{
  217. s3_constants.ExtObjectLockModeKey: []byte("GOVERNANCE"),
  218. },
  219. }
  220. s3a.addObjectLockHeadersToResponse(w, entry)
  221. // Should set mode and default legal hold to OFF
  222. assert.Equal(t, "GOVERNANCE", w.Header().Get(s3_constants.AmzObjectLockMode))
  223. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  224. assert.Equal(t, "OFF", w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  225. })
  226. t.Run("Handle entry with retention date but no legal hold - should default to OFF", func(t *testing.T) {
  227. w := httptest.NewRecorder()
  228. retainUntilTime := time.Now().Add(24 * time.Hour)
  229. entry := &filer_pb.Entry{
  230. Extended: map[string][]byte{
  231. s3_constants.ExtRetentionUntilDateKey: []byte(strconv.FormatInt(retainUntilTime.Unix(), 10)),
  232. },
  233. }
  234. s3a.addObjectLockHeadersToResponse(w, entry)
  235. // Should set retention date and default legal hold to OFF
  236. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockMode))
  237. assert.NotEmpty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  238. assert.Equal(t, "OFF", w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  239. })
  240. t.Run("Handle nil entry gracefully", func(t *testing.T) {
  241. w := httptest.NewRecorder()
  242. // Should not panic
  243. s3a.addObjectLockHeadersToResponse(w, nil)
  244. // No headers should be set
  245. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockMode))
  246. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  247. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  248. })
  249. t.Run("Handle entry with nil Extended map gracefully", func(t *testing.T) {
  250. w := httptest.NewRecorder()
  251. entry := &filer_pb.Entry{
  252. Extended: nil,
  253. }
  254. // Should not panic
  255. s3a.addObjectLockHeadersToResponse(w, entry)
  256. // No headers should be set
  257. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockMode))
  258. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  259. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  260. })
  261. t.Run("Handle invalid retention timestamp gracefully", func(t *testing.T) {
  262. w := httptest.NewRecorder()
  263. entry := &filer_pb.Entry{
  264. Extended: map[string][]byte{
  265. s3_constants.ExtObjectLockModeKey: []byte("COMPLIANCE"),
  266. s3_constants.ExtRetentionUntilDateKey: []byte("invalid-timestamp"),
  267. },
  268. }
  269. s3a.addObjectLockHeadersToResponse(w, entry)
  270. // Mode should be set but not retention date due to invalid timestamp
  271. assert.Equal(t, "COMPLIANCE", w.Header().Get(s3_constants.AmzObjectLockMode))
  272. assert.Empty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  273. })
  274. }
  275. // TestObjectLockHeaderRoundTrip tests the complete round trip:
  276. // extract from request → store in Extended attributes → add to response
  277. func TestObjectLockHeaderRoundTrip(t *testing.T) {
  278. s3a := &S3ApiServer{}
  279. t.Run("Complete round trip for COMPLIANCE mode", func(t *testing.T) {
  280. // 1. Create request with object lock headers
  281. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  282. retainUntilDate := time.Now().Add(24 * time.Hour)
  283. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  284. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  285. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "ON")
  286. // 2. Extract and store in Extended attributes
  287. entry := &filer_pb.Entry{
  288. Extended: make(map[string][]byte),
  289. }
  290. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  291. assert.NoError(t, err)
  292. // 3. Add to response headers
  293. w := httptest.NewRecorder()
  294. s3a.addObjectLockHeadersToResponse(w, entry)
  295. // 4. Verify round trip preserved all data
  296. assert.Equal(t, "COMPLIANCE", w.Header().Get(s3_constants.AmzObjectLockMode))
  297. assert.Equal(t, "ON", w.Header().Get(s3_constants.AmzObjectLockLegalHold))
  298. returnedDate := w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate)
  299. parsedTime, err := time.Parse(time.RFC3339, returnedDate)
  300. assert.NoError(t, err)
  301. assert.WithinDuration(t, retainUntilDate, parsedTime, 1*time.Second)
  302. })
  303. t.Run("Complete round trip for GOVERNANCE mode", func(t *testing.T) {
  304. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  305. retainUntilDate := time.Now().Add(12 * time.Hour)
  306. req.Header.Set(s3_constants.AmzObjectLockMode, "GOVERNANCE")
  307. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  308. entry := &filer_pb.Entry{Extended: make(map[string][]byte)}
  309. err := s3a.extractObjectLockMetadataFromRequest(req, entry)
  310. assert.NoError(t, err)
  311. w := httptest.NewRecorder()
  312. s3a.addObjectLockHeadersToResponse(w, entry)
  313. assert.Equal(t, "GOVERNANCE", w.Header().Get(s3_constants.AmzObjectLockMode))
  314. assert.NotEmpty(t, w.Header().Get(s3_constants.AmzObjectLockRetainUntilDate))
  315. })
  316. }
  317. // TestValidateObjectLockHeaders tests the validateObjectLockHeaders function
  318. // to ensure proper validation of object lock headers in PUT requests
  319. func TestValidateObjectLockHeaders(t *testing.T) {
  320. s3a := &S3ApiServer{}
  321. t.Run("Valid COMPLIANCE mode with retention date on versioned bucket", func(t *testing.T) {
  322. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  323. retainUntilDate := time.Now().Add(24 * time.Hour)
  324. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  325. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  326. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  327. assert.NoError(t, err)
  328. })
  329. t.Run("Valid GOVERNANCE mode with retention date on versioned bucket", func(t *testing.T) {
  330. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  331. retainUntilDate := time.Now().Add(12 * time.Hour)
  332. req.Header.Set(s3_constants.AmzObjectLockMode, "GOVERNANCE")
  333. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  334. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  335. assert.NoError(t, err)
  336. })
  337. t.Run("Valid legal hold ON on versioned bucket", func(t *testing.T) {
  338. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  339. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "ON")
  340. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  341. assert.NoError(t, err)
  342. })
  343. t.Run("Valid legal hold OFF on versioned bucket", func(t *testing.T) {
  344. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  345. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "OFF")
  346. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  347. assert.NoError(t, err)
  348. })
  349. t.Run("Invalid object lock mode", func(t *testing.T) {
  350. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  351. req.Header.Set(s3_constants.AmzObjectLockMode, "INVALID_MODE")
  352. retainUntilDate := time.Now().Add(24 * time.Hour)
  353. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  354. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  355. assert.Error(t, err)
  356. assert.True(t, errors.Is(err, ErrInvalidObjectLockMode))
  357. })
  358. t.Run("Invalid legal hold status", func(t *testing.T) {
  359. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  360. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "INVALID_STATUS")
  361. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  362. assert.Error(t, err)
  363. assert.True(t, errors.Is(err, ErrInvalidLegalHoldStatus))
  364. })
  365. t.Run("Object lock headers on non-versioned bucket", func(t *testing.T) {
  366. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  367. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  368. retainUntilDate := time.Now().Add(24 * time.Hour)
  369. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  370. err := s3a.validateObjectLockHeaders(req, false) // non-versioned bucket
  371. assert.Error(t, err)
  372. assert.True(t, errors.Is(err, ErrObjectLockVersioningRequired))
  373. })
  374. t.Run("Invalid retention date format", func(t *testing.T) {
  375. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  376. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  377. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, "invalid-date-format")
  378. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  379. assert.Error(t, err)
  380. assert.True(t, errors.Is(err, ErrInvalidRetentionDateFormat))
  381. })
  382. t.Run("Retention date in the past", func(t *testing.T) {
  383. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  384. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  385. pastDate := time.Now().Add(-24 * time.Hour)
  386. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, pastDate.Format(time.RFC3339))
  387. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  388. assert.Error(t, err)
  389. assert.True(t, errors.Is(err, ErrRetentionDateMustBeFuture))
  390. })
  391. t.Run("Mode without retention date", func(t *testing.T) {
  392. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  393. req.Header.Set(s3_constants.AmzObjectLockMode, "COMPLIANCE")
  394. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  395. assert.Error(t, err)
  396. assert.True(t, errors.Is(err, ErrObjectLockModeRequiresDate))
  397. })
  398. t.Run("Retention date without mode", func(t *testing.T) {
  399. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  400. retainUntilDate := time.Now().Add(24 * time.Hour)
  401. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  402. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  403. assert.Error(t, err)
  404. assert.True(t, errors.Is(err, ErrRetentionDateRequiresMode))
  405. })
  406. t.Run("Governance bypass header on non-versioned bucket", func(t *testing.T) {
  407. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  408. req.Header.Set("x-amz-bypass-governance-retention", "true")
  409. err := s3a.validateObjectLockHeaders(req, false) // non-versioned bucket
  410. assert.Error(t, err)
  411. assert.True(t, errors.Is(err, ErrGovernanceBypassVersioningRequired))
  412. })
  413. t.Run("Governance bypass header on versioned bucket should pass", func(t *testing.T) {
  414. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  415. req.Header.Set("x-amz-bypass-governance-retention", "true")
  416. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  417. assert.NoError(t, err)
  418. })
  419. t.Run("No object lock headers should pass", func(t *testing.T) {
  420. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  421. // No object lock headers set
  422. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  423. assert.NoError(t, err)
  424. })
  425. t.Run("Mixed valid headers should pass", func(t *testing.T) {
  426. req := httptest.NewRequest("PUT", "/bucket/object", nil)
  427. retainUntilDate := time.Now().Add(48 * time.Hour)
  428. req.Header.Set(s3_constants.AmzObjectLockMode, "GOVERNANCE")
  429. req.Header.Set(s3_constants.AmzObjectLockRetainUntilDate, retainUntilDate.Format(time.RFC3339))
  430. req.Header.Set(s3_constants.AmzObjectLockLegalHold, "ON")
  431. err := s3a.validateObjectLockHeaders(req, true) // versioned bucket
  432. assert.NoError(t, err)
  433. })
  434. }
  435. // TestMapValidationErrorToS3Error tests the error mapping function
  436. func TestMapValidationErrorToS3Error(t *testing.T) {
  437. tests := []struct {
  438. name string
  439. inputError error
  440. expectedCode s3err.ErrorCode
  441. }{
  442. {
  443. name: "ErrObjectLockVersioningRequired",
  444. inputError: ErrObjectLockVersioningRequired,
  445. expectedCode: s3err.ErrInvalidRequest,
  446. },
  447. {
  448. name: "ErrInvalidObjectLockMode",
  449. inputError: ErrInvalidObjectLockMode,
  450. expectedCode: s3err.ErrInvalidRequest,
  451. },
  452. {
  453. name: "ErrInvalidLegalHoldStatus",
  454. inputError: ErrInvalidLegalHoldStatus,
  455. expectedCode: s3err.ErrMalformedXML,
  456. },
  457. {
  458. name: "ErrInvalidRetentionDateFormat",
  459. inputError: ErrInvalidRetentionDateFormat,
  460. expectedCode: s3err.ErrMalformedDate,
  461. },
  462. {
  463. name: "ErrRetentionDateMustBeFuture",
  464. inputError: ErrRetentionDateMustBeFuture,
  465. expectedCode: s3err.ErrInvalidRequest,
  466. },
  467. {
  468. name: "ErrObjectLockModeRequiresDate",
  469. inputError: ErrObjectLockModeRequiresDate,
  470. expectedCode: s3err.ErrInvalidRequest,
  471. },
  472. {
  473. name: "ErrRetentionDateRequiresMode",
  474. inputError: ErrRetentionDateRequiresMode,
  475. expectedCode: s3err.ErrInvalidRequest,
  476. },
  477. {
  478. name: "ErrGovernanceBypassVersioningRequired",
  479. inputError: ErrGovernanceBypassVersioningRequired,
  480. expectedCode: s3err.ErrInvalidRequest,
  481. },
  482. {
  483. name: "Unknown error defaults to ErrInvalidRequest",
  484. inputError: fmt.Errorf("unknown error"),
  485. expectedCode: s3err.ErrInvalidRequest,
  486. },
  487. }
  488. for _, tt := range tests {
  489. t.Run(tt.name, func(t *testing.T) {
  490. result := mapValidationErrorToS3Error(tt.inputError)
  491. assert.Equal(t, tt.expectedCode, result)
  492. })
  493. }
  494. }
  495. // TestObjectLockPermissionLogic documents the correct behavior for object lock permission checks
  496. // in PUT operations for both versioned and non-versioned buckets
  497. func TestObjectLockPermissionLogic(t *testing.T) {
  498. t.Run("Non-versioned bucket PUT operation logic", func(t *testing.T) {
  499. // In non-versioned buckets, PUT operations overwrite existing objects
  500. // Therefore, we MUST check if the existing object has object lock protections
  501. // that would prevent overwrite before allowing the PUT operation.
  502. //
  503. // This test documents the expected behavior:
  504. // 1. Check object lock headers validity (handled by validateObjectLockHeaders)
  505. // 2. Check if existing object has object lock protections (handled by checkObjectLockPermissions)
  506. // 3. If existing object is under retention/legal hold, deny the PUT unless governance bypass is valid
  507. t.Log("For non-versioned buckets:")
  508. t.Log("- PUT operations overwrite existing objects")
  509. t.Log("- Must check existing object lock protections before allowing overwrite")
  510. t.Log("- Governance bypass headers can be used to override GOVERNANCE mode retention")
  511. t.Log("- COMPLIANCE mode retention and legal holds cannot be bypassed")
  512. })
  513. t.Run("Versioned bucket PUT operation logic", func(t *testing.T) {
  514. // In versioned buckets, PUT operations create new versions without overwriting existing ones
  515. // Therefore, we do NOT need to check existing object permissions since we're not modifying them.
  516. // We only need to validate the object lock headers for the new version being created.
  517. //
  518. // This test documents the expected behavior:
  519. // 1. Check object lock headers validity (handled by validateObjectLockHeaders)
  520. // 2. Skip checking existing object permissions (since we're creating a new version)
  521. // 3. Apply object lock metadata to the new version being created
  522. t.Log("For versioned buckets:")
  523. t.Log("- PUT operations create new versions without overwriting existing objects")
  524. t.Log("- No need to check existing object lock protections")
  525. t.Log("- Only validate object lock headers for the new version being created")
  526. t.Log("- Each version has independent object lock settings")
  527. })
  528. t.Run("Governance bypass header validation", func(t *testing.T) {
  529. // Governance bypass headers should only be used in specific scenarios:
  530. // 1. Only valid on versioned buckets (consistent with object lock headers)
  531. // 2. For non-versioned buckets: Used to override existing object's GOVERNANCE retention
  532. // 3. For versioned buckets: Not typically needed since new versions don't conflict with existing ones
  533. t.Log("Governance bypass behavior:")
  534. t.Log("- Only valid on versioned buckets (header validation)")
  535. t.Log("- For non-versioned buckets: Allows overwriting objects under GOVERNANCE retention")
  536. t.Log("- For versioned buckets: Not typically needed for PUT operations")
  537. t.Log("- Must have s3:BypassGovernanceRetention permission")
  538. })
  539. }