From f795a8e22379fe1d5fe1015ab55929322d527a3b Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Thu, 18 Dec 2025 14:08:39 +0100 Subject: [PATCH 1/6] Override an unversioned object originOp when moving it to a version This prevents triggering a possible s3:ObjectCreated:Put bucket notification when moving this object to a version, when deleting it (when creating a deletion marker). Issue: CLDSRV-816 Signed-off-by: Thomas Flament --- lib/api/apiUtils/object/versioning.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/api/apiUtils/object/versioning.js b/lib/api/apiUtils/object/versioning.js index 9747f7f015..42ef91ba38 100644 --- a/lib/api/apiUtils/object/versioning.js +++ b/lib/api/apiUtils/object/versioning.js @@ -98,6 +98,7 @@ function _storeNullVersionMD(bucketName, objKey, nullVersionId, objMD, log, cb) isNull2: true, }); } + nullVersionMD.originOp = 's3:ObjectNullVersionStoredAsNewVersion'; metadata.putObjectMD(bucketName, objKey, nullVersionMD, { versionId }, log, err => { if (err) { log.debug('error from metadata storing null version as new version', From 82854023757e57f02339b51d13b5f75a59008e1d Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Fri, 19 Dec 2025 08:49:34 +0100 Subject: [PATCH 2/6] fixup! Override an unversioned object originOp when moving it to a version --- lib/api/apiUtils/object/versioning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/apiUtils/object/versioning.js b/lib/api/apiUtils/object/versioning.js index 42ef91ba38..4249e867cc 100644 --- a/lib/api/apiUtils/object/versioning.js +++ b/lib/api/apiUtils/object/versioning.js @@ -98,7 +98,7 @@ function _storeNullVersionMD(bucketName, objKey, nullVersionId, objMD, log, cb) isNull2: true, }); } - nullVersionMD.originOp = 's3:ObjectNullVersionStoredAsNewVersion'; + nullVersionMD.originOp = 's3:StoreNullVersion'; metadata.putObjectMD(bucketName, objKey, nullVersionMD, { versionId }, log, err => { if (err) { log.debug('error from metadata storing null version as new version', From b011b3b356b0db599be221ec48e2d413d05c8500 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Fri, 19 Dec 2025 12:04:13 +0100 Subject: [PATCH 3/6] fixup! Override an unversioned object originOp when moving it to a version --- tests/unit/api/objectDelete.js | 59 +++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/unit/api/objectDelete.js b/tests/unit/api/objectDelete.js index b46687ebaf..d8a1293ba8 100644 --- a/tests/unit/api/objectDelete.js +++ b/tests/unit/api/objectDelete.js @@ -7,7 +7,7 @@ const services = require('../../../lib/services'); const { bucketPut } = require('../../../lib/api/bucketPut'); const bucketPutACL = require('../../../lib/api/bucketPutACL'); const constants = require('../../../constants'); -const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers'); +const { cleanup, DummyRequestLogger, makeAuthInfo, versioningTestUtils} = require('../helpers'); const objectPut = require('../../../lib/api/objectPut'); const { objectDelete, objectDeleteInternal } = require('../../../lib/api/objectDelete'); const objectGet = require('../../../lib/api/objectGet'); @@ -16,6 +16,7 @@ const mpuUtils = require('../utils/mpuUtils'); const metadataswitch = require('../metadataswitch'); const { fakeMetadataArchive } = require('../../functional/aws-node-sdk/test/utils/init'); const bucketPutNotification = require('../../../lib/api/bucketPutNotification'); +const bucketPutVersioning = require('../../../lib/api/bucketPutVersioning'); const any = sinon.match.any; const originalDeleteObject = services.deleteObject; @@ -32,6 +33,8 @@ const lateDate = new Date(); earlyDate.setMinutes(earlyDate.getMinutes() - 30); lateDate.setMinutes(lateDate.getMinutes() + 30); +const enableVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Enabled'); + function testAuth(bucketOwner, authUser, bucketPutReq, objPutReq, objDelReq, log, cb) { bucketPut(bucketOwner, bucketPutReq, log, () => { @@ -371,3 +374,57 @@ describe('objectDelete API', () => { }); }); }); + + +describe('objectDelete API with versioning', () => { + let testPutObjectRequest; + + beforeEach(() => { + cleanup(); + testPutObjectRequest = new DummyRequest({ + bucketName, + namespace, + objectKey, + headers: {}, + url: `/${bucketName}/${objectKey}`, + }, postBody); + + sinon.stub(services, 'deleteObject').callsFake(originalDeleteObject); + sinon.spy(metadataswitch, 'putObjectMD'); + sinon.spy(metadataswitch, 'deleteObjectMD'); + }); + + afterEach(() => { + sinon.restore(); + }); + + const testBucketPutRequest = new DummyRequest({ + bucketName, + namespace, + headers: {}, + url: `/${bucketName}`, + }); + const testDeleteRequest = new DummyRequest({ + bucketName, + namespace, + objectKey, + headers: {}, + url: `/${bucketName}/${objectKey}`, + }); + + it('should store modified originOp when moving object null version', done => { + async.series([ + next => bucketPut(authInfo, testBucketPutRequest, log, next), + next => objectPut(authInfo, testPutObjectRequest, undefined, log, next), + next => bucketPutVersioning(authInfo, enableVersioningRequest, log, next), + next => objectDelete(authInfo, testDeleteRequest, log, next), + async () => { + const calls = metadataswitch.putObjectMD.getCalls(); + sinon.assert.calledWith(calls[calls.length - 2], + bucketName, objectKey, sinon.match({ + originOp: 's3:StoreNullVersion', + }), any, any, any); + }, + ], done); + }); +}); From 478fdade76dba16929573efcabf72c4dc736c9ea Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Fri, 19 Dec 2025 12:07:05 +0100 Subject: [PATCH 4/6] fixup! Override an unversioned object originOp when moving it to a version --- tests/unit/api/objectDelete.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/api/objectDelete.js b/tests/unit/api/objectDelete.js index d8a1293ba8..e204b2f34b 100644 --- a/tests/unit/api/objectDelete.js +++ b/tests/unit/api/objectDelete.js @@ -422,6 +422,8 @@ describe('objectDelete API with versioning', () => { const calls = metadataswitch.putObjectMD.getCalls(); sinon.assert.calledWith(calls[calls.length - 2], bucketName, objectKey, sinon.match({ + versionId: sinon.match.truthy, + isNull: true, originOp: 's3:StoreNullVersion', }), any, any, any); }, From d9049e6ca1158741bdc42045b834485702608c71 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Mon, 22 Dec 2025 11:56:32 +0100 Subject: [PATCH 5/6] fixup! Override an unversioned object originOp when moving it to a version --- tests/unit/api/multipartUpload.js | 2 ++ tests/unit/api/objectCopy.js | 17 +++++++++++++++-- tests/unit/api/objectDelete.js | 1 + tests/unit/api/objectPut.js | 24 ++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/unit/api/multipartUpload.js b/tests/unit/api/multipartUpload.js index e5d8bd54a4..0cc2d5ef6a 100644 --- a/tests/unit/api/multipartUpload.js +++ b/tests/unit/api/multipartUpload.js @@ -2396,6 +2396,7 @@ describe('complete mpu with versioning', () => { assert.strictEqual(objVal.completeInProgress, true); } else { assert.strictEqual(params.replayId, testUploadId); + assert.strictEqual(objVal.originOp, 's3:ObjectCreated:CompleteMultipartUpload'); metadataBackend.putObject = origPutObject; } origPutObject( @@ -2413,6 +2414,7 @@ describe('complete mpu with versioning', () => { metadataBackend.putObject = (putBucketName, objName, objVal, params, log, cb) => { assert.strictEqual(params.oldReplayId, testUploadId); + assert.strictEqual(objVal.originOp, 's3:ObjectCreated:Put'); metadataBackend.putObject = origPutObject; origPutObject( putBucketName, objName, objVal, params, log, cb); diff --git a/tests/unit/api/objectCopy.js b/tests/unit/api/objectCopy.js index dc5981d373..b3d39614c0 100644 --- a/tests/unit/api/objectCopy.js +++ b/tests/unit/api/objectCopy.js @@ -113,6 +113,15 @@ describe('objectCopy with versioning', () => { objectCopy(authInfo, testObjectCopyRequest, sourceBucketName, objectKey, undefined, log, err => { assert.ifError(err, `Unexpected err: ${err}`); + sinon.assert.calledWith( + metadata.putObjectMD.lastCall, + destBucketName, + objectKey, + sinon.match({ _data: { originOp: 's3:ObjectCreated:Copy' } }), + sinon.match.any, + sinon.match.any, + sinon.match.any + ); setImmediate(() => { versioningTestUtils .assertDataStoreValues(ds, expectedValues); @@ -276,7 +285,9 @@ describe('non-versioned objectCopy', () => { undefined, log, next), async () => { sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, any, sinon.match({ + destBucketName, objectKey, sinon.match({ + _data: { originOp: 's3:ObjectCreated:Copy' }, + }), sinon.match({ needOplogUpdate: undefined, originOp: undefined, }), any, any); @@ -291,7 +302,9 @@ describe('non-versioned objectCopy', () => { undefined, log, next), async () => { sinon.assert.calledWith(metadata.putObjectMD.lastCall, - destBucketName, objectKey, any, sinon.match({ + destBucketName, objectKey, sinon.match({ + _data: { originOp: 's3:ObjectCreated:Copy' }, + }), sinon.match({ needOplogUpdate: undefined, originOp: undefined, }), any, any); diff --git a/tests/unit/api/objectDelete.js b/tests/unit/api/objectDelete.js index e204b2f34b..8b7aed24d0 100644 --- a/tests/unit/api/objectDelete.js +++ b/tests/unit/api/objectDelete.js @@ -34,6 +34,7 @@ earlyDate.setMinutes(earlyDate.getMinutes() - 30); lateDate.setMinutes(lateDate.getMinutes() + 30); const enableVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Enabled'); +const suspendVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Suspended'); function testAuth(bucketOwner, authUser, bucketPutReq, objPutReq, objDelReq, log, cb) { diff --git a/tests/unit/api/objectPut.js b/tests/unit/api/objectPut.js index 5825d1838b..7f172e8ffc 100644 --- a/tests/unit/api/objectPut.js +++ b/tests/unit/api/objectPut.js @@ -980,6 +980,30 @@ describe('objectPut API with versioning', () => { }); }); + it('should set originOp when moving null version', done => { + async.series([ + next => bucketPut(authInfo, testPutBucketRequest, log, next), + next => objectPut(authInfo, testPutObjectRequest, undefined, log, next), + next => bucketPutVersioning(authInfo, enableVersioningRequest, log, next), + next => objectPut(authInfo, testPutObjectRequest, undefined, log, next), + async () => { + // M was moved to V, with originOp overridden to prevent bucket notifications. + const calls = metadata.putObjectMD.getCalls(); + sinon.assert.calledWith(calls[calls.length - 2], + bucketName, objectName, sinon.match({ + originOp: 's3:StoreNullVersion', + }), any, any, any); + }, + async () => { + // New V was created with the right originOp. + sinon.assert.calledWith(metadata.putObjectMD.lastCall, + bucketName, objectName, sinon.match({ + _data: { originOp: 's3:ObjectCreated:Put' }, + }), any, any, any); + }, + ], done); + }); + it('should not pass needOplogUpdate when writing new object', done => { async.series([ next => bucketPut(authInfo, testPutBucketRequest, log, next), From 7b6feeb4b2a6dc91885783a3fc44da883a900ff1 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Mon, 22 Dec 2025 12:03:02 +0100 Subject: [PATCH 6/6] fixup! Override an unversioned object originOp when moving it to a version --- tests/unit/api/objectDelete.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/api/objectDelete.js b/tests/unit/api/objectDelete.js index 8b7aed24d0..e204b2f34b 100644 --- a/tests/unit/api/objectDelete.js +++ b/tests/unit/api/objectDelete.js @@ -34,7 +34,6 @@ earlyDate.setMinutes(earlyDate.getMinutes() - 30); lateDate.setMinutes(lateDate.getMinutes() + 30); const enableVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Enabled'); -const suspendVersioningRequest = versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Suspended'); function testAuth(bucketOwner, authUser, bucketPutReq, objPutReq, objDelReq, log, cb) {