fix(s3): enforce Object Lock on PutObjectRetention
Fixes #2717
PutObjectRetention overwrote a version's retention unconditionally. It now checks the existing retention first, as S3 does:
- Under an unexpired COMPLIANCE retention, shortening the retain-until date, changing the mode, or clearing the retention returns 403 AccessDenied, with or without
x-amz-bypass-governance-retention. - Under an unexpired GOVERNANCE retention, the same changes return 403 AccessDenied unless
x-amz-bypass-governance-retention: trueis sent. - Extending the date with the same mode is always allowed, and expired retentions can be replaced.
The bypass-header parsing is shared with the existing overwrite lock check.
A second commit fixes the minor point from the issue: a bucket default retention in Years now adds calendar years instead of years * 365 days.
Test plan
- New unit tests in
fakecloud-s3covering GOVERNANCE with and without bypass, COMPLIANCE with bypass, mode change, clearing, extending, expired retention, the version-id repro from the issue, and the leap-dayYearscase. The four regression tests fail onmainand pass with this change. cargo test -p fakecloud-s3,cargo clippy -p fakecloud-s3 --all-targets -- -D warnings,cargo fmt --all --check
Summary by cubic
Fixes #2717. PutObjectRetention was overwriting a version's retention unconditionally; it now enforces Object Lock rules like S3. Under an unexpired COMPLIANCE retention, shortening the retain-until date, changing the mode, or clearing the retention returns 403 AccessDenied, with or without the bypass header. Under GOVERNANCE the same changes return 403 unless x-amz-bypass-governance-retention: true is sent. Extending the date with the same mode is always allowed, and expired retentions can be replaced. The bypass-header parse is shared with the existing overwrite lock check.
Sending the bypass header now also requires s3:BypassGovernanceRetention on the object for PutObjectRetention and DeleteObject under IAM enforcement; DeleteObjects is excluded because AWS authorizes it per key and reports a denial as an <Error> entry in a 200 response.
Bucket default retentions specified in Years now add calendar years instead of years * 365 days, so leap days count; Days and Years values are trimmed before parsing, matching the stored-config validator.