s3api_object_retention.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. package s3api
  2. import (
  3. "encoding/xml"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/seaweedfs/seaweedfs/weed/glog"
  11. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  12. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  13. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  14. )
  15. // ====================================================================
  16. // ERROR DEFINITIONS
  17. // ====================================================================
  18. // Sentinel errors for proper error handling instead of string matching
  19. var (
  20. ErrNoRetentionConfiguration = errors.New("no retention configuration found")
  21. ErrNoLegalHoldConfiguration = errors.New("no legal hold configuration found")
  22. ErrBucketNotFound = errors.New("bucket not found")
  23. ErrObjectNotFound = errors.New("object not found")
  24. ErrVersionNotFound = errors.New("version not found")
  25. ErrLatestVersionNotFound = errors.New("latest version not found")
  26. ErrComplianceModeActive = errors.New("object is under COMPLIANCE mode retention and cannot be deleted or modified")
  27. ErrGovernanceModeActive = errors.New("object is under GOVERNANCE mode retention and cannot be deleted or modified without bypass")
  28. )
  29. // Error definitions for Object Lock
  30. var (
  31. ErrObjectUnderLegalHold = errors.New("object is under legal hold and cannot be deleted or modified")
  32. ErrGovernanceBypassNotPermitted = errors.New("user does not have permission to bypass governance retention")
  33. ErrInvalidRetentionPeriod = errors.New("invalid retention period specified")
  34. ErrBothDaysAndYearsSpecified = errors.New("both days and years cannot be specified in the same retention configuration")
  35. ErrMalformedXML = errors.New("malformed XML in request body")
  36. // Validation error constants with specific messages for tests
  37. ErrRetentionMissingMode = errors.New("retention configuration must specify Mode")
  38. ErrRetentionMissingRetainUntilDate = errors.New("retention configuration must specify RetainUntilDate")
  39. ErrInvalidRetentionModeValue = errors.New("invalid retention mode")
  40. )
  41. const (
  42. // Maximum retention period limits according to AWS S3 specifications
  43. MaxRetentionDays = 36500 // Maximum number of days for object retention (100 years)
  44. MaxRetentionYears = 100 // Maximum number of years for object retention
  45. )
  46. // ====================================================================
  47. // DATA STRUCTURES
  48. // ====================================================================
  49. // ObjectRetention represents S3 Object Retention configuration
  50. type ObjectRetention struct {
  51. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Retention"`
  52. Mode string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Mode,omitempty"`
  53. RetainUntilDate *time.Time `xml:"http://s3.amazonaws.com/doc/2006-03-01/ RetainUntilDate,omitempty"`
  54. }
  55. // ObjectLegalHold represents S3 Object Legal Hold configuration
  56. type ObjectLegalHold struct {
  57. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LegalHold"`
  58. Status string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Status,omitempty"`
  59. }
  60. // ObjectLockConfiguration represents S3 Object Lock Configuration
  61. type ObjectLockConfiguration struct {
  62. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ObjectLockConfiguration"`
  63. ObjectLockEnabled string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ObjectLockEnabled,omitempty"`
  64. Rule *ObjectLockRule `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Rule,omitempty"`
  65. }
  66. // ObjectLockRule represents an Object Lock Rule
  67. type ObjectLockRule struct {
  68. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Rule"`
  69. DefaultRetention *DefaultRetention `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DefaultRetention,omitempty"`
  70. }
  71. // DefaultRetention represents default retention settings
  72. // Implements custom XML unmarshal to track if Days/Years were present in XML
  73. type DefaultRetention struct {
  74. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DefaultRetention"`
  75. Mode string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Mode,omitempty"`
  76. Days int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Days,omitempty"`
  77. Years int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Years,omitempty"`
  78. DaysSet bool `xml:"-"`
  79. YearsSet bool `xml:"-"`
  80. }
  81. // ====================================================================
  82. // XML PARSING
  83. // ====================================================================
  84. // UnmarshalXML implements custom XML unmarshaling for DefaultRetention
  85. // to track whether Days/Years fields were explicitly present in the XML
  86. func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
  87. type Alias DefaultRetention
  88. aux := &struct {
  89. *Alias
  90. Days *int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Days,omitempty"`
  91. Years *int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Years,omitempty"`
  92. }{Alias: (*Alias)(dr)}
  93. if err := d.DecodeElement(aux, &start); err != nil {
  94. glog.V(2).Infof("DefaultRetention.UnmarshalXML: decode error: %v", err)
  95. return err
  96. }
  97. if aux.Days != nil {
  98. dr.Days = *aux.Days
  99. dr.DaysSet = true
  100. glog.V(4).Infof("DefaultRetention.UnmarshalXML: Days present, value=%d", dr.Days)
  101. } else {
  102. glog.V(4).Infof("DefaultRetention.UnmarshalXML: Days not present")
  103. }
  104. if aux.Years != nil {
  105. dr.Years = *aux.Years
  106. dr.YearsSet = true
  107. glog.V(4).Infof("DefaultRetention.UnmarshalXML: Years present, value=%d", dr.Years)
  108. } else {
  109. glog.V(4).Infof("DefaultRetention.UnmarshalXML: Years not present")
  110. }
  111. return nil
  112. }
  113. // parseXML is a generic helper function to parse XML from an HTTP request body.
  114. // It uses xml.Decoder for streaming XML parsing, which is more memory-efficient
  115. // and avoids loading the entire request body into memory.
  116. //
  117. // The function assumes:
  118. // - The request body is not nil (returns error if it is)
  119. // - The request body will be closed after parsing (deferred close)
  120. // - The XML content matches the structure of the provided result type T
  121. //
  122. // This approach is optimized for small XML payloads typical in S3 API requests
  123. // (retention configurations, legal hold settings, etc.) where the overhead of
  124. // streaming parsing is acceptable for the memory efficiency benefits.
  125. func parseXML[T any](request *http.Request, result *T) error {
  126. if request.Body == nil {
  127. return fmt.Errorf("error parsing XML: empty request body")
  128. }
  129. defer request.Body.Close()
  130. decoder := xml.NewDecoder(request.Body)
  131. if err := decoder.Decode(result); err != nil {
  132. return fmt.Errorf("error parsing XML: %w", err)
  133. }
  134. return nil
  135. }
  136. // parseObjectRetention parses XML retention configuration from request body
  137. func parseObjectRetention(request *http.Request) (*ObjectRetention, error) {
  138. var retention ObjectRetention
  139. if err := parseXML(request, &retention); err != nil {
  140. return nil, err
  141. }
  142. return &retention, nil
  143. }
  144. // parseObjectLegalHold parses XML legal hold configuration from request body
  145. func parseObjectLegalHold(request *http.Request) (*ObjectLegalHold, error) {
  146. var legalHold ObjectLegalHold
  147. if err := parseXML(request, &legalHold); err != nil {
  148. return nil, err
  149. }
  150. return &legalHold, nil
  151. }
  152. // parseObjectLockConfiguration parses XML object lock configuration from request body
  153. func parseObjectLockConfiguration(request *http.Request) (*ObjectLockConfiguration, error) {
  154. var config ObjectLockConfiguration
  155. if err := parseXML(request, &config); err != nil {
  156. return nil, err
  157. }
  158. return &config, nil
  159. }
  160. // ====================================================================
  161. // OBJECT ENTRY OPERATIONS
  162. // ====================================================================
  163. // getObjectEntry retrieves the appropriate object entry based on versioning and versionId
  164. func (s3a *S3ApiServer) getObjectEntry(bucket, object, versionId string) (*filer_pb.Entry, error) {
  165. var entry *filer_pb.Entry
  166. var err error
  167. if versionId != "" {
  168. entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
  169. } else {
  170. // Check if versioning is enabled
  171. versioningEnabled, vErr := s3a.isVersioningEnabled(bucket)
  172. if vErr != nil {
  173. return nil, fmt.Errorf("error checking versioning: %w", vErr)
  174. }
  175. if versioningEnabled {
  176. entry, err = s3a.getLatestObjectVersion(bucket, object)
  177. } else {
  178. bucketDir := s3a.option.BucketsPath + "/" + bucket
  179. entry, err = s3a.getEntry(bucketDir, object)
  180. }
  181. }
  182. if err != nil {
  183. return nil, fmt.Errorf("failed to retrieve object %s/%s: %w", bucket, object, ErrObjectNotFound)
  184. }
  185. return entry, nil
  186. }
  187. // ====================================================================
  188. // RETENTION OPERATIONS
  189. // ====================================================================
  190. // getObjectRetention retrieves object retention configuration
  191. func (s3a *S3ApiServer) getObjectRetention(bucket, object, versionId string) (*ObjectRetention, error) {
  192. entry, err := s3a.getObjectEntry(bucket, object, versionId)
  193. if err != nil {
  194. return nil, err
  195. }
  196. if entry.Extended == nil {
  197. return nil, ErrNoRetentionConfiguration
  198. }
  199. retention := &ObjectRetention{}
  200. if modeBytes, exists := entry.Extended[s3_constants.ExtObjectLockModeKey]; exists {
  201. retention.Mode = string(modeBytes)
  202. }
  203. if dateBytes, exists := entry.Extended[s3_constants.ExtRetentionUntilDateKey]; exists {
  204. if timestamp, err := strconv.ParseInt(string(dateBytes), 10, 64); err == nil {
  205. t := time.Unix(timestamp, 0)
  206. retention.RetainUntilDate = &t
  207. } else {
  208. return nil, fmt.Errorf("failed to parse retention timestamp for %s/%s: corrupted timestamp data", bucket, object)
  209. }
  210. }
  211. if retention.Mode == "" || retention.RetainUntilDate == nil {
  212. return nil, ErrNoRetentionConfiguration
  213. }
  214. return retention, nil
  215. }
  216. // setObjectRetention sets object retention configuration
  217. func (s3a *S3ApiServer) setObjectRetention(bucket, object, versionId string, retention *ObjectRetention, bypassGovernance bool) error {
  218. var entry *filer_pb.Entry
  219. var err error
  220. var entryPath string
  221. if versionId != "" {
  222. entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
  223. if err != nil {
  224. return fmt.Errorf("failed to get version %s for object %s/%s: %w", versionId, bucket, object, ErrVersionNotFound)
  225. }
  226. entryPath = object + ".versions/" + s3a.getVersionFileName(versionId)
  227. } else {
  228. // Check if versioning is enabled
  229. versioningEnabled, vErr := s3a.isVersioningEnabled(bucket)
  230. if vErr != nil {
  231. return fmt.Errorf("error checking versioning: %w", vErr)
  232. }
  233. if versioningEnabled {
  234. entry, err = s3a.getLatestObjectVersion(bucket, object)
  235. if err != nil {
  236. return fmt.Errorf("failed to get latest version for object %s/%s: %w", bucket, object, ErrLatestVersionNotFound)
  237. }
  238. // Extract version ID from entry metadata
  239. if entry.Extended != nil {
  240. if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists {
  241. versionId = string(versionIdBytes)
  242. entryPath = object + ".versions/" + s3a.getVersionFileName(versionId)
  243. }
  244. }
  245. } else {
  246. bucketDir := s3a.option.BucketsPath + "/" + bucket
  247. entry, err = s3a.getEntry(bucketDir, object)
  248. if err != nil {
  249. return fmt.Errorf("failed to get object %s/%s: %w", bucket, object, ErrObjectNotFound)
  250. }
  251. entryPath = object
  252. }
  253. }
  254. // Check if object is already under retention
  255. if entry.Extended != nil {
  256. if existingMode, exists := entry.Extended[s3_constants.ExtObjectLockModeKey]; exists {
  257. // Check if attempting to change retention mode
  258. if retention.Mode != "" && string(existingMode) != retention.Mode {
  259. // Attempting to change retention mode
  260. if string(existingMode) == s3_constants.RetentionModeCompliance {
  261. // Cannot change compliance mode retention without bypass
  262. return ErrComplianceModeActive
  263. }
  264. if string(existingMode) == s3_constants.RetentionModeGovernance && !bypassGovernance {
  265. // Cannot change governance mode retention without bypass
  266. return ErrGovernanceModeActive
  267. }
  268. }
  269. if existingDateBytes, dateExists := entry.Extended[s3_constants.ExtRetentionUntilDateKey]; dateExists {
  270. if timestamp, err := strconv.ParseInt(string(existingDateBytes), 10, 64); err == nil {
  271. existingDate := time.Unix(timestamp, 0)
  272. // Check if the new retention date is earlier than the existing one
  273. if retention.RetainUntilDate != nil && retention.RetainUntilDate.Before(existingDate) {
  274. // Attempting to decrease retention period
  275. if string(existingMode) == s3_constants.RetentionModeCompliance {
  276. // Cannot decrease compliance mode retention without bypass
  277. return ErrComplianceModeActive
  278. }
  279. if string(existingMode) == s3_constants.RetentionModeGovernance && !bypassGovernance {
  280. // Cannot decrease governance mode retention without bypass
  281. return ErrGovernanceModeActive
  282. }
  283. }
  284. // If new retention date is later or same, allow the operation
  285. // This covers both increasing retention period and overriding with same/later date
  286. }
  287. }
  288. }
  289. }
  290. // Update retention metadata
  291. if entry.Extended == nil {
  292. entry.Extended = make(map[string][]byte)
  293. }
  294. if retention.Mode != "" {
  295. entry.Extended[s3_constants.ExtObjectLockModeKey] = []byte(retention.Mode)
  296. }
  297. if retention.RetainUntilDate != nil {
  298. entry.Extended[s3_constants.ExtRetentionUntilDateKey] = []byte(strconv.FormatInt(retention.RetainUntilDate.Unix(), 10))
  299. // Also update the existing WORM fields for compatibility
  300. entry.WormEnforcedAtTsNs = time.Now().UnixNano()
  301. }
  302. // Update the entry
  303. // NOTE: Potential race condition exists if concurrent calls to PutObjectRetention
  304. // and PutObjectLegalHold update the same object simultaneously, as they might
  305. // overwrite each other's Extended map changes. This is mitigated by the fact
  306. // that mkFile operations are typically serialized at the filer level, but
  307. // future implementations might consider using atomic update operations or
  308. // entry-level locking for complete safety.
  309. bucketDir := s3a.option.BucketsPath + "/" + bucket
  310. return s3a.mkFile(bucketDir, entryPath, entry.Chunks, func(updatedEntry *filer_pb.Entry) {
  311. updatedEntry.Extended = entry.Extended
  312. updatedEntry.WormEnforcedAtTsNs = entry.WormEnforcedAtTsNs
  313. })
  314. }
  315. // ====================================================================
  316. // LEGAL HOLD OPERATIONS
  317. // ====================================================================
  318. // getObjectLegalHold retrieves object legal hold configuration
  319. func (s3a *S3ApiServer) getObjectLegalHold(bucket, object, versionId string) (*ObjectLegalHold, error) {
  320. entry, err := s3a.getObjectEntry(bucket, object, versionId)
  321. if err != nil {
  322. return nil, err
  323. }
  324. if entry.Extended == nil {
  325. return nil, ErrNoLegalHoldConfiguration
  326. }
  327. legalHold := &ObjectLegalHold{}
  328. if statusBytes, exists := entry.Extended[s3_constants.ExtLegalHoldKey]; exists {
  329. legalHold.Status = string(statusBytes)
  330. } else {
  331. return nil, ErrNoLegalHoldConfiguration
  332. }
  333. return legalHold, nil
  334. }
  335. // setObjectLegalHold sets object legal hold configuration
  336. func (s3a *S3ApiServer) setObjectLegalHold(bucket, object, versionId string, legalHold *ObjectLegalHold) error {
  337. var entry *filer_pb.Entry
  338. var err error
  339. var entryPath string
  340. if versionId != "" {
  341. entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
  342. if err != nil {
  343. return fmt.Errorf("failed to get version %s for object %s/%s: %w", versionId, bucket, object, ErrVersionNotFound)
  344. }
  345. entryPath = object + ".versions/" + s3a.getVersionFileName(versionId)
  346. } else {
  347. // Check if versioning is enabled
  348. versioningEnabled, vErr := s3a.isVersioningEnabled(bucket)
  349. if vErr != nil {
  350. return fmt.Errorf("error checking versioning: %w", vErr)
  351. }
  352. if versioningEnabled {
  353. entry, err = s3a.getLatestObjectVersion(bucket, object)
  354. if err != nil {
  355. return fmt.Errorf("failed to get latest version for object %s/%s: %w", bucket, object, ErrLatestVersionNotFound)
  356. }
  357. // Extract version ID from entry metadata
  358. if entry.Extended != nil {
  359. if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists {
  360. versionId = string(versionIdBytes)
  361. entryPath = object + ".versions/" + s3a.getVersionFileName(versionId)
  362. }
  363. }
  364. } else {
  365. bucketDir := s3a.option.BucketsPath + "/" + bucket
  366. entry, err = s3a.getEntry(bucketDir, object)
  367. if err != nil {
  368. return fmt.Errorf("failed to get object %s/%s: %w", bucket, object, ErrObjectNotFound)
  369. }
  370. entryPath = object
  371. }
  372. }
  373. // Update legal hold metadata
  374. if entry.Extended == nil {
  375. entry.Extended = make(map[string][]byte)
  376. }
  377. entry.Extended[s3_constants.ExtLegalHoldKey] = []byte(legalHold.Status)
  378. // Update the entry
  379. // NOTE: Potential race condition exists if concurrent calls to PutObjectRetention
  380. // and PutObjectLegalHold update the same object simultaneously, as they might
  381. // overwrite each other's Extended map changes. This is mitigated by the fact
  382. // that mkFile operations are typically serialized at the filer level, but
  383. // future implementations might consider using atomic update operations or
  384. // entry-level locking for complete safety.
  385. bucketDir := s3a.option.BucketsPath + "/" + bucket
  386. return s3a.mkFile(bucketDir, entryPath, entry.Chunks, func(updatedEntry *filer_pb.Entry) {
  387. updatedEntry.Extended = entry.Extended
  388. })
  389. }
  390. // ====================================================================
  391. // PROTECTION ENFORCEMENT
  392. // ====================================================================
  393. // isObjectRetentionActive checks if object has active retention
  394. func (s3a *S3ApiServer) isObjectRetentionActive(bucket, object, versionId string) (bool, error) {
  395. retention, err := s3a.getObjectRetention(bucket, object, versionId)
  396. if err != nil {
  397. // If no retention found, object is not under retention
  398. if errors.Is(err, ErrNoRetentionConfiguration) {
  399. return false, nil
  400. }
  401. return false, err
  402. }
  403. if retention.RetainUntilDate != nil && retention.RetainUntilDate.After(time.Now()) {
  404. return true, nil
  405. }
  406. return false, nil
  407. }
  408. // getRetentionFromEntry extracts retention configuration from filer entry
  409. func (s3a *S3ApiServer) getRetentionFromEntry(entry *filer_pb.Entry) (*ObjectRetention, bool, error) {
  410. if entry.Extended == nil {
  411. return nil, false, nil
  412. }
  413. retention := &ObjectRetention{}
  414. if modeBytes, exists := entry.Extended[s3_constants.ExtObjectLockModeKey]; exists {
  415. retention.Mode = string(modeBytes)
  416. }
  417. if dateBytes, exists := entry.Extended[s3_constants.ExtRetentionUntilDateKey]; exists {
  418. if timestamp, err := strconv.ParseInt(string(dateBytes), 10, 64); err == nil {
  419. t := time.Unix(timestamp, 0)
  420. retention.RetainUntilDate = &t
  421. } else {
  422. return nil, false, fmt.Errorf("failed to parse retention timestamp: corrupted timestamp data")
  423. }
  424. }
  425. if retention.Mode == "" || retention.RetainUntilDate == nil {
  426. return nil, false, nil
  427. }
  428. // Check if retention is currently active
  429. isActive := retention.RetainUntilDate.After(time.Now())
  430. return retention, isActive, nil
  431. }
  432. // getLegalHoldFromEntry extracts legal hold configuration from filer entry
  433. func (s3a *S3ApiServer) getLegalHoldFromEntry(entry *filer_pb.Entry) (*ObjectLegalHold, bool, error) {
  434. if entry.Extended == nil {
  435. return nil, false, nil
  436. }
  437. legalHold := &ObjectLegalHold{}
  438. if statusBytes, exists := entry.Extended[s3_constants.ExtLegalHoldKey]; exists {
  439. legalHold.Status = string(statusBytes)
  440. } else {
  441. return nil, false, nil
  442. }
  443. isActive := legalHold.Status == s3_constants.LegalHoldOn
  444. return legalHold, isActive, nil
  445. }
  446. // ====================================================================
  447. // GOVERNANCE BYPASS
  448. // ====================================================================
  449. // checkGovernanceBypassPermission checks if user has permission to bypass governance retention
  450. func (s3a *S3ApiServer) checkGovernanceBypassPermission(request *http.Request, bucket, object string) bool {
  451. // Use the existing IAM auth system to check the specific permission
  452. // Create the governance bypass action with proper bucket/object concatenation
  453. // Note: path.Join would drop bucket if object has leading slash, so use explicit formatting
  454. resource := fmt.Sprintf("%s/%s", bucket, strings.TrimPrefix(object, "/"))
  455. action := Action(fmt.Sprintf("%s:%s", s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION, resource))
  456. // Use the IAM system to authenticate and authorize this specific action
  457. identity, errCode := s3a.iam.authRequest(request, action)
  458. if errCode != s3err.ErrNone {
  459. glog.V(3).Infof("IAM auth failed for governance bypass: %v", errCode)
  460. return false
  461. }
  462. // Verify that the authenticated identity can perform this action
  463. if identity != nil && identity.canDo(action, bucket, object) {
  464. return true
  465. }
  466. // Additional check: allow users with Admin action to bypass governance retention
  467. // Use the proper S3 Admin action constant instead of generic isAdmin() method
  468. adminAction := Action(fmt.Sprintf("%s:%s", s3_constants.ACTION_ADMIN, resource))
  469. if identity != nil && identity.canDo(adminAction, bucket, object) {
  470. glog.V(2).Infof("Admin user %s granted governance bypass permission for %s/%s", identity.Name, bucket, object)
  471. return true
  472. }
  473. return false
  474. }
  475. // evaluateGovernanceBypassRequest evaluates if governance bypass is requested and permitted
  476. func (s3a *S3ApiServer) evaluateGovernanceBypassRequest(r *http.Request, bucket, object string) bool {
  477. // Step 1: Check if governance bypass was requested via header
  478. bypassRequested := r.Header.Get("x-amz-bypass-governance-retention") == "true"
  479. if !bypassRequested {
  480. // No bypass requested - normal retention enforcement applies
  481. return false
  482. }
  483. // Step 2: Validate user has permission to bypass governance retention
  484. hasPermission := s3a.checkGovernanceBypassPermission(r, bucket, object)
  485. if !hasPermission {
  486. glog.V(2).Infof("Governance bypass denied for %s/%s: user lacks s3:BypassGovernanceRetention permission", bucket, object)
  487. return false
  488. }
  489. glog.V(2).Infof("Governance bypass granted for %s/%s: header present and user has permission", bucket, object)
  490. return true
  491. }
  492. // enforceObjectLockProtections enforces object lock protections for operations
  493. func (s3a *S3ApiServer) enforceObjectLockProtections(request *http.Request, bucket, object, versionId string, governanceBypassAllowed bool) error {
  494. // Get the object entry to check both retention and legal hold
  495. // For delete operations without versionId, we need to check the latest version
  496. var entry *filer_pb.Entry
  497. var err error
  498. if versionId != "" {
  499. // Check specific version
  500. entry, err = s3a.getObjectEntry(bucket, object, versionId)
  501. } else {
  502. // Check latest version for delete marker creation
  503. entry, err = s3a.getObjectEntry(bucket, object, "")
  504. }
  505. if err != nil {
  506. // If object doesn't exist, it's not under retention or legal hold - this is expected during delete operations
  507. if errors.Is(err, filer_pb.ErrNotFound) || errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) || errors.Is(err, ErrLatestVersionNotFound) {
  508. // Object doesn't exist, so it can't be under retention or legal hold - this is normal
  509. glog.V(4).Infof("Object %s/%s (versionId: %s) not found during object lock check (expected during delete operations)", bucket, object, versionId)
  510. return nil
  511. }
  512. glog.Warningf("Error retrieving object %s/%s (versionId: %s) for lock check: %v", bucket, object, versionId, err)
  513. return err
  514. }
  515. // Extract retention information from the entry
  516. retention, retentionActive, err := s3a.getRetentionFromEntry(entry)
  517. if err != nil {
  518. glog.Warningf("Error parsing retention for %s/%s (versionId: %s): %v", bucket, object, versionId, err)
  519. // Continue with legal hold check even if retention parsing fails
  520. }
  521. // Extract legal hold information from the entry
  522. _, legalHoldActive, err := s3a.getLegalHoldFromEntry(entry)
  523. if err != nil {
  524. glog.Warningf("Error parsing legal hold for %s/%s (versionId: %s): %v", bucket, object, versionId, err)
  525. // Continue with retention check even if legal hold parsing fails
  526. }
  527. // If object is under legal hold, it cannot be deleted or modified (including delete marker creation)
  528. if legalHoldActive {
  529. return ErrObjectUnderLegalHold
  530. }
  531. // If object is under retention, check the mode
  532. if retentionActive && retention != nil {
  533. if retention.Mode == s3_constants.RetentionModeCompliance {
  534. return ErrComplianceModeActive
  535. }
  536. if retention.Mode == s3_constants.RetentionModeGovernance {
  537. if !governanceBypassAllowed {
  538. return ErrGovernanceModeActive
  539. }
  540. // Note: governanceBypassAllowed parameter is already validated by evaluateGovernanceBypassRequest()
  541. // which checks both header presence and IAM permissions, so we trust it here
  542. }
  543. }
  544. return nil
  545. }
  546. // ====================================================================
  547. // AVAILABILITY CHECKS
  548. // ====================================================================
  549. // isObjectLockAvailable checks if object lock is available for the bucket
  550. func (s3a *S3ApiServer) isObjectLockAvailable(bucket string) error {
  551. versioningEnabled, err := s3a.isVersioningEnabled(bucket)
  552. if err != nil {
  553. if errors.Is(err, filer_pb.ErrNotFound) {
  554. return ErrBucketNotFound
  555. }
  556. return fmt.Errorf("error checking versioning status: %w", err)
  557. }
  558. if !versioningEnabled {
  559. return fmt.Errorf("object lock requires versioning to be enabled")
  560. }
  561. return nil
  562. }
  563. // handleObjectLockAvailabilityCheck handles object lock availability checks for API endpoints
  564. func (s3a *S3ApiServer) handleObjectLockAvailabilityCheck(w http.ResponseWriter, request *http.Request, bucket, handlerName string) bool {
  565. if err := s3a.isObjectLockAvailable(bucket); err != nil {
  566. glog.Errorf("%s: object lock not available for bucket %s: %v", handlerName, bucket, err)
  567. if errors.Is(err, ErrBucketNotFound) {
  568. s3err.WriteErrorResponse(w, request, s3err.ErrNoSuchBucket)
  569. } else {
  570. // Return InvalidRequest for object lock operations on buckets without object lock enabled
  571. // This matches AWS S3 behavior and s3-tests expectations (400 Bad Request)
  572. s3err.WriteErrorResponse(w, request, s3err.ErrInvalidRequest)
  573. }
  574. return false
  575. }
  576. return true
  577. }