-
Notifications
You must be signed in to change notification settings - Fork 102
allow removing GOVERNANCE retention with empty Retention body #9918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,26 +10,31 @@ const s3_utils = require('../s3_utils'); | |
| async function put_object_retention(req) { | ||
| // TODO: may require at the future Content-MD5 support | ||
| if (!req.body.Retention) throw new S3Error(S3Error.MalformedXML); | ||
| const mode = req.body.Retention.Mode[0]; | ||
| let retain_until_date = req.body.Retention.RetainUntilDate[0]; | ||
| if (!mode && !retain_until_date) throw new S3Error(S3Error.AccessDenied); | ||
| if (!mode || !retain_until_date) throw new S3Error(S3Error.MalformedXML); | ||
| retain_until_date = new Date(req.body.Retention.RetainUntilDate[0]); | ||
|
|
||
| const bypass_governance = req.headers['x-amz-bypass-governance-retention'] && req.headers['x-amz-bypass-governance-retention'].toUpperCase() === 'TRUE'; | ||
| const bypass_governance = req.headers['x-amz-bypass-governance-retention'] && | ||
| req.headers['x-amz-bypass-governance-retention'].toUpperCase() === 'TRUE'; | ||
|
|
||
| if (s3_utils._is_valid_retention(mode, retain_until_date)) { | ||
| await req.object_sdk.put_object_retention({ | ||
| bucket: req.params.bucket, | ||
| key: req.params.key, | ||
| version_id: s3_utils.parse_version_id(req.query.versionId), | ||
| bypass_governance, | ||
| retention: { | ||
| mode, | ||
| retain_until_date, | ||
| } | ||
| }); | ||
| const mode = req.body.Retention.Mode && req.body.Retention.Mode[0]; | ||
| const retain_until_date_str = req.body.Retention.RetainUntilDate && req.body.Retention.RetainUntilDate[0]; | ||
|
|
||
| let retention; | ||
| if (!mode && !retain_until_date_str) { | ||
|
coderabbitai[bot] marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just for readability. I think it would be better to do it like: WDYT?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion, restructured to if / else if / else. |
||
| retention = {}; | ||
| } else if (!mode || !retain_until_date_str) { | ||
| throw new S3Error(S3Error.MalformedXML); | ||
| } else { | ||
| const retain_until_date = new Date(retain_until_date_str); | ||
| if (!s3_utils._is_valid_retention(mode, retain_until_date)) return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that on purpose? To return if the retention is not valid?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is pre-existing behavior (not from this PR). _is_valid_retention returns false if mode is invalid or retain_until_date is in the past — the early return silently no-ops, matching the original code. |
||
| retention = { mode, retain_until_date }; | ||
| } | ||
|
|
||
| await req.object_sdk.put_object_retention({ | ||
| bucket: req.params.bucket, | ||
| key: req.params.key, | ||
| version_id: s3_utils.parse_version_id(req.query.versionId), | ||
| bypass_governance, | ||
| retention, | ||
| }); | ||
| } | ||
|
|
||
| module.exports = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,8 +84,10 @@ const XATTR_DIR_CONTENT = XATTR_NOOBAA_INTERNAL_PREFIX + 'dir_content'; | |
| const XATTR_NON_CURRENT_TIMESTASMP = XATTR_NOOBAA_INTERNAL_PREFIX + 'non_current_timestamp'; | ||
| const XATTR_TAG = XATTR_NOOBAA_INTERNAL_PREFIX + 'tag.'; | ||
| const XATTR_LEGAL_HOLD = XATTR_NOOBAA_INTERNAL_PREFIX + 'legal_hold'; | ||
| const XATTR_RETENTION_MODE = XATTR_NOOBAA_INTERNAL_PREFIX + 'retention_mode'; | ||
| const XATTR_RETENTION_DATE = XATTR_NOOBAA_INTERNAL_PREFIX + 'retention_date'; | ||
| // prefix used by set_fs_xattr_op to clear all retention xattrs by prefix | ||
| const XATTR_RETENTION_PREFIX = XATTR_NOOBAA_INTERNAL_PREFIX + 'retention_'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why change this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. XATTR_RETENTION_PREFIX is needed by the clear path (line 2539) — set_fs_xattr_op uses it to strip both retention_mode and retention_date xattrs by prefix. Without it declared, that path crashes with ReferenceError. Deriving MODE/DATE from PREFIX ensures consistency and avoids a separate string that could drift. The final xattr values are unchanged (user.noobaa.retention_mode, user.noobaa.retention_date).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still don't understand why the prefix is needed. they are still called by XATTR_RETENTION_MODE and XATTR_RETENTION_DATE. the prefix is never used. why would separate string drift. and even if they do. why does it matters
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ahh its so you can clear them by prefix. can you add a comment explaining this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
| const XATTR_RETENTION_MODE = XATTR_RETENTION_PREFIX + 'mode'; | ||
| const XATTR_RETENTION_DATE = XATTR_RETENTION_PREFIX + 'date'; | ||
| const HIDDEN_VERSIONS_PATH = '.versions'; | ||
| const NULL_VERSION_ID = 'null'; | ||
| const NULL_VERSION_SUFFIX = '_' + NULL_VERSION_ID; | ||
|
|
@@ -2399,24 +2401,29 @@ class NamespaceFS { | |
| /** | ||
| * check if the object retention lock settings can be updated to the new retention settings. if not, will throw AccessDenied error | ||
| * the rules are: | ||
| * 1. if the new retention is longer than the current retention, it can be updated (can increase retention time) | ||
| * 2. if the new retention is shorter than the current retention, it cannot be updated and will throw error, unless the user has bypass_governance permission and the current retention mode is GOVERNANCE | ||
| * 1. if new_retention is omitted/empty, retention is being cleared — same bypass rules as delete protection | ||
| * 2. if the new retention is longer than the current retention, it can be updated (can increase retention time) | ||
| * 3. if the new retention is shorter than the current retention, it cannot be updated and will throw error, unless the user has bypass_governance permission and the current retention mode is GOVERNANCE | ||
| * @param {Object} current_retention - current object retention lock settings | ||
| * @param {Object} new_retention - new object retention lock settings | ||
| * @param {Object} [new_retention] - new object retention lock settings; omit/empty to clear retention | ||
| * @param {boolean} bypass_governance - if true, and user has permission to use this flag, will allow to bypass governance mode retention lock. compliance mode retention lock cannot be bypassed. | ||
| * @throws {S3Error.AccessDenied} if the object is protected by object lock and the user does not have permission to bypass the lock | ||
| */ | ||
| _compare_object_retention(fs_context, current_retention, new_retention, bypass_governance) { | ||
| if (current_retention) { | ||
| const retain_until_date = new Date(current_retention.retain_until_date); | ||
| const new_date = new Date(new_retention.retain_until_date); | ||
| //can always increase retention time when mode is unchanged | ||
| if (new_date >= retain_until_date && new_retention.mode === current_retention.mode) return; | ||
| bypass_governance = bypass_governance && this._has_bypass_governance_permission(fs_context); | ||
| if (current_retention.mode === 'COMPLIANCE' || | ||
| (current_retention.mode === 'GOVERNANCE' && !bypass_governance)) { | ||
| throw new S3Error(S3Error.AccessDeniedObjectLocked); | ||
| } | ||
| if (!current_retention) return; | ||
| // empty retention object means clear — enforce bypass rules before removing | ||
| if (!new_retention.mode && !new_retention.retain_until_date) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a comment. that this is the empty object scenario
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
| this._check_object_retention(fs_context, current_retention, bypass_governance); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you add this call to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _check_object_retention enforces bypass-governance rules — it checks whether retention is still active (now < retain_until_date) and whether GOVERNANCE can be bypassed or COMPLIANCE which can never be bypassed. When clearing retention (empty object), we still need to verify the caller has permission to override the existing lock before removing it. |
||
| return; | ||
| } | ||
| const retain_until_date = new Date(current_retention.retain_until_date); | ||
| const new_date = new Date(new_retention.retain_until_date); | ||
| //can always increase retention time when mode is unchanged | ||
| if (new_date >= retain_until_date && new_retention.mode === current_retention.mode) return; | ||
| bypass_governance = bypass_governance && this._has_bypass_governance_permission(fs_context); | ||
| if (current_retention.mode === 'COMPLIANCE' || | ||
| (current_retention.mode === 'GOVERNANCE' && !bypass_governance)) { | ||
| throw new S3Error(S3Error.AccessDeniedObjectLocked); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -2518,14 +2525,19 @@ class NamespaceFS { | |
| const fs_context = this.prepare_fs_context(object_sdk); | ||
| const file_path = await this._find_version_path(fs_context, params, true); | ||
| await this._check_path_in_bucket_boundaries(fs_context, file_path); | ||
| const is_clear = !params.retention.mode && !params.retention.retain_until_date; | ||
| try { | ||
| const stat = await nb_native().fs.stat(fs_context, file_path); | ||
| const current_retention = this._get_retention_mode_from_xattr(stat.xattr); | ||
| this._compare_object_retention(fs_context, current_retention, params.retention, params.bypass_governance); | ||
|
Comment on lines
+2528
to
2532
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline src/api/object_api.js --match 'put_object_retention' --view expanded || true
ast-grep outline src/server/object_services/object_server.js --match 'put_object_retention' --view expanded || true
ast-grep outline src/sdk/namespace_fs.js --match 'put_object_retention' --view expanded || true
rg -n -C 10 '\bput_object_retention\s*\(|retention\s*[:?]|retention\s*\?\?' \
src/api/object_api.js \
src/server/object_services/object_server.js \
src/sdk/namespace_fs.jsRepository: noobaa/noobaa-core Length of output: 25904 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- all put_object_retention references ---'
rg -n -C 6 '\bput_object_retention\b' --glob '!node_modules/**' --glob '!dist/**' .
printf '%s\n' '--- RPC schema block ---'
sed -n '1308,1342p' src/api/object_api.js
printf '%s\n' '--- server handler block ---'
sed -n '336,383p' src/server/object_services/object_server.js
printf '%s\n' '--- NamespaceFS handler block ---'
sed -n '2521,2545p' src/sdk/namespace_fs.js
printf '%s\n' '--- retention normalization patterns ---'
rg -n -C 4 'rpc_params\.(retention|lock_settings)|new_retention|retention\s*=\s*req\.rpc_params' \
src/api src/server src/sdk --glob '*.js'Repository: noobaa/noobaa-core Length of output: 42805 Normalize omitted 🤖 Prompt for AI Agents |
||
| const fs_xattr = {}; | ||
| fs_xattr[XATTR_RETENTION_MODE] = params.retention.mode; | ||
| fs_xattr[XATTR_RETENTION_DATE] = params.retention.retain_until_date.toISOString(); | ||
| await this.set_fs_xattr_op(fs_context, file_path, fs_xattr, undefined); | ||
| if (is_clear) { | ||
| await this.set_fs_xattr_op(fs_context, file_path, undefined, XATTR_RETENTION_PREFIX); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+2528
to
+2534
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift Serialize retention validation and xattr mutation. The Use an object-scoped lock or an atomic compare-and-update operation for the read, authorization check, and xattr update. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } else { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If one of them empty it is fine? did you intentionally set the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, intentional. set_fs_xattr_op(ctx, path, undefined, XATTR_RETENTION_PREFIX) — first arg undefined means "nothing to set", second arg (prefix) means "remove all xattrs matching this prefix". This removes both retention_mode and retention_date without touching legal_hold. |
||
| const fs_xattr = {}; | ||
| fs_xattr[XATTR_RETENTION_MODE] = params.retention.mode; | ||
| fs_xattr[XATTR_RETENTION_DATE] = params.retention.retain_until_date.toISOString(); | ||
| await this.set_fs_xattr_op(fs_context, file_path, fs_xattr, undefined); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } catch (err) { | ||
| dbg.error(`NamespaceFS.put_object_retention: failed for file ${file_path} with error: `, err); | ||
| throw native_fs_utils.translate_error_codes(err, native_fs_utils.entity_enum.OBJECT); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -193,7 +193,6 @@ async function get_object_tagging(req) { | |
| }; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * | ||
| * delete_object_tagging | ||
|
|
@@ -346,17 +345,30 @@ async function put_object_retention(req) { | |
| if (!req.bucket.object_lock_configuration || req.bucket.object_lock_configuration.object_lock_enabled !== 'Enabled') { | ||
| throw new RpcError('INVALID_REQUEST'); | ||
| } | ||
| const current_retention = info.lock_settings?.retention; | ||
| const new_retention = req.rpc_params.retention; | ||
| const is_clear = !new_retention.mode && !new_retention.retain_until_date; | ||
| const current_retention = info.lock_settings?.retention; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| _throw_if_retention_update_forbidden({ | ||
| key: obj.key, | ||
| obj_id: info.obj_id, | ||
| current_retention, | ||
| new_retention, | ||
| new_retention: is_clear ? undefined : new_retention, | ||
| bypass_governance: Boolean(req.rpc_params.bypass_governance), | ||
| }); | ||
| const legal_hold_status = info.lock_settings?.legal_hold?.status; | ||
| const legal_hold = legal_hold_status ? { status: legal_hold_status } : undefined; | ||
| if (is_clear) { | ||
| if (legal_hold) { | ||
| await MDStore.instance().update_object_by_id( | ||
| obj._id, { lock_settings: { legal_hold } }, undefined, undefined | ||
| ); | ||
| } else { | ||
| await MDStore.instance().update_object_by_id( | ||
| obj._id, undefined, { lock_settings: 1 }, undefined | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about the last argument? Do you want to explicitly pass
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, intentional. update_object_by_id takes 4 positional args (id, set, unset, inc). The trailing undefineds are needed to pass the right value to the right position. Same pattern used for tagging operations above in the same file. |
||
| ); | ||
| } | ||
| return; | ||
| } | ||
| await MDStore.instance().update_object_by_id( | ||
| obj._id, { | ||
| lock_settings: { | ||
|
|
@@ -496,7 +508,6 @@ async function _complete_multipart_upload(req) { | |
| set_updates.last_modified_time = new Date(req.rpc_params.last_modified_time); | ||
| } | ||
|
|
||
|
|
||
| await _put_object_handle_latest_with_retries({ req, put_obj: obj, set_updates, unset_updates }); | ||
|
|
||
| const took_ms = set_updates.create_time.getTime() - obj._id.getTimestamp().getTime(); | ||
|
|
@@ -646,7 +657,6 @@ async function _complete_simple_upload(req) { | |
| }; | ||
| } | ||
|
|
||
|
|
||
| async function update_bucket_counters({ system, bucket_name, content_type, read_count, write_count }) { | ||
| const bucket = system.buckets_by_name[bucket_name.unwrap()]; | ||
| if (!bucket || bucket.deleting) return; | ||
|
|
@@ -659,8 +669,6 @@ async function update_bucket_counters({ system, bucket_name, content_type, read_ | |
| }); | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /** | ||
| * | ||
| * abort_object_upload | ||
|
|
@@ -839,7 +847,6 @@ async function get_mapping(req) { | |
| return { chunks: res_chunks.map(chunk => chunk.to_api()) }; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * | ||
| * PUT_MAPPING | ||
|
|
@@ -929,7 +936,6 @@ async function read_object_mapping(req) { | |
| }; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * | ||
| * read_object_mapping_admin | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -566,6 +566,68 @@ mocha.describe('s3 worm', function() { | |
| }); | ||
| }); | ||
|
|
||
| mocha.describe('clear GOVERNANCE retention with empty Retention', function() { | ||
| const CLEAR_RET_KEY = 'clear-retention-test'; | ||
| let clear_ret_version_id; | ||
|
|
||
| mocha.it('should create object with GOVERNANCE retention', async function() { | ||
| const shortDate = new Date(); | ||
| shortDate.setSeconds(shortDate.getSeconds() + 60); | ||
| const res = await s3_owner.putObject({ | ||
| Bucket: BKT1, | ||
| Key: CLEAR_RET_KEY, | ||
| Body: file_body, | ||
| ContentType: 'text/plain', | ||
| ObjectLockMode: 'GOVERNANCE', | ||
| ObjectLockRetainUntilDate: shortDate | ||
| }); | ||
| clear_ret_version_id = res.VersionId; | ||
| assert.ok(res.VersionId); | ||
| }); | ||
|
|
||
| mocha.it('should fail to clear retention without bypass flag', async function() { | ||
| await assert_throws_async(s3_owner.putObjectRetention({ | ||
| Bucket: BKT1, | ||
| Key: CLEAR_RET_KEY, | ||
| VersionId: clear_ret_version_id, | ||
| Retention: {}, | ||
| }), 'AccessDenied', 'Access Denied because object protected by object lock.'); | ||
| }); | ||
|
|
||
| mocha.it('should clear GOVERNANCE retention with bypass flag', async function() { | ||
| const res = await s3_owner.putObjectRetention({ | ||
| Bucket: BKT1, | ||
| Key: CLEAR_RET_KEY, | ||
| VersionId: clear_ret_version_id, | ||
| Retention: {}, | ||
| BypassGovernanceRetention: true, | ||
| }); | ||
| delete res.$metadata; | ||
| assert.deepEqual(res, {}); | ||
| }); | ||
|
|
||
| mocha.it('should confirm retention is cleared', async function() { | ||
| await assert_throws_async(s3_owner.getObjectRetention({ | ||
| Bucket: BKT1, | ||
| Key: CLEAR_RET_KEY, | ||
| VersionId: clear_ret_version_id, | ||
| }), 'NoSuchObjectLockConfiguration', 'The specified object does not have a ObjectLock configuration'); | ||
| }); | ||
|
|
||
| mocha.it('should be able to set retention again after clearing', async function() { | ||
| const newDate = new Date(); | ||
| newDate.setDate(newDate.getDate() + 1); | ||
| const res = await s3_owner.putObjectRetention({ | ||
| Bucket: BKT1, | ||
| Key: CLEAR_RET_KEY, | ||
| VersionId: clear_ret_version_id, | ||
| Retention: { Mode: 'GOVERNANCE', RetainUntilDate: newDate }, | ||
| }); | ||
| delete res.$metadata; | ||
| assert.deepEqual(res, {}); | ||
| }); | ||
| }); | ||
|
Comment on lines
+569
to
+629
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find established assertion patterns for retention and legal-hold responses.
rg -n -C 8 '\b(putObjectRetention|getObjectRetention|putObjectLegalHold|getObjectLegalHold)\s*\(' \
src/test/integration_tests/api/s3/test_s3_worm.jsRepository: noobaa/noobaa-core Length of output: 24310 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the surrounding WORM suites to determine whether the requested
# retention persistence, COMPLIANCE, and legal-hold coverage already exists.
sed -n '500,910p' src/test/integration_tests/api/s3/test_s3_worm.jsRepository: noobaa/noobaa-core Length of output: 16335 Assert the remaining Object Lock invariants. After reapplying retention at lines 620–627, call 🤖 Prompt for AI AgentsSource: Path instructions
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestions. Will add COMPLIANCE rejection and legal_hold preservation coverage in a follow-up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The follow-up should also verify the Would you like me to create a GitHub issue for the follow-up work? 🐇 You are interacting with an AI system. |
||
|
|
||
| mocha.describe('legal hold toggle (on/off)', function() { | ||
| const LEGAL_HOLD_TOGGLE_KEY = 'legal-hold-toggle-test'; | ||
| let legal_hold_toggle_version_id; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is removing this comment on purpose? why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed it — Content-MD5 is not required for PutObjectRetention per the S3 spec. It's an optional integrity header that SDKs calculate automatically, but AWS does not reject requests without it. The TODO was misleading since it implied it might become required, when it's actually just an optional body checksum validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the comment is still removed. I though we already talked about keeping it. has something changed?