From beb86e91db71e69b515eda192a3bb2b4b270dc90 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Thu, 18 Jun 2026 13:50:31 +0200 Subject: [PATCH 1/5] Apply prettier to putBucketLifecycle handler and test Pure formatting pass (no logic change) on the functional test file and the bucketPutLifecycle API handler touched by the Days=0 work, so the prettier CI check (which runs on whole files modified by the PR) passes. Isolating the reformat keeps the functional commits reviewable. Issue: CLDSRV-928 --- lib/api/bucketPutLifecycle.js | 89 ++- .../test/bucket/putBucketLifecycle.js | 513 ++++++++++-------- 2 files changed, 338 insertions(+), 264 deletions(-) diff --git a/lib/api/bucketPutLifecycle.js b/lib/api/bucketPutLifecycle.js index b1a30e27bf..9e0ad00aad 100644 --- a/lib/api/bucketPutLifecycle.js +++ b/lib/api/bucketPutLifecycle.js @@ -1,7 +1,6 @@ const { waterfall } = require('async'); const uuid = require('uuid').v4; -const LifecycleConfiguration = - require('arsenal').models.LifecycleConfiguration; +const LifecycleConfiguration = require('arsenal').models.LifecycleConfiguration; const config = require('../Config').config; const parseXML = require('../utilities/parseXML'); @@ -30,53 +29,51 @@ function bucketPutLifecycle(authInfo, request, log, callback) { requestType: request.apiMethods || 'bucketPutLifecycle', request, }; - return waterfall([ - next => parseXML(request.post, log, next), - (parsedXml, next) => { - const lcConfigClass = - new LifecycleConfiguration(parsedXml, config); - // if there was an error getting lifecycle configuration, - // returned configObj will contain 'error' key - process.nextTick(() => { - const configObj = lcConfigClass.getLifecycleConfiguration(); - if (configObj.error) { - return next(configObj.error); + return waterfall( + [ + next => parseXML(request.post, log, next), + (parsedXml, next) => { + const lcConfigClass = new LifecycleConfiguration(parsedXml, config); + // if there was an error getting lifecycle configuration, + // returned configObj will contain 'error' key + process.nextTick(() => { + const configObj = lcConfigClass.getLifecycleConfiguration(); + if (configObj.error) { + return next(configObj.error); + } + return next(null, configObj); + }); + }, + (lcConfig, next) => + standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { + if (err) { + return next(err, bucket); + } + return next(null, bucket, lcConfig); + }), + (bucket, lcConfig, next) => { + if (!bucket.getUid()) { + bucket.setUid(uuid()); } - return next(null, configObj); - }); - }, - (lcConfig, next) => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, - (err, bucket) => { - if (err) { - return next(err, bucket); - } - return next(null, bucket, lcConfig); - }), - (bucket, lcConfig, next) => { - if (!bucket.getUid()) { - bucket.setUid(uuid()); + bucket.setLifecycleConfiguration(lcConfig); + metadata.updateBucket(bucket.getName(), bucket, log, err => next(err, bucket)); + }, + ], + (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); + if (err) { + log.trace('error processing request', { error: err, method: 'bucketPutLifecycle' }); + monitoring.promMetrics('PUT', bucketName, err.code, 'putBucketLifecycle'); + return callback(err, corsHeaders); } - bucket.setLifecycleConfiguration(lcConfig); - metadata.updateBucket(bucket.getName(), bucket, log, err => - next(err, bucket)); + pushMetric('putBucketLifecycle', log, { + authInfo, + bucket: bucketName, + }); + monitoring.promMetrics('PUT', bucketName, '200', 'putBucketLifecycle'); + return callback(null, corsHeaders); }, - ], (err, bucket) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - if (err) { - log.trace('error processing request', { error: err, - method: 'bucketPutLifecycle' }); - monitoring.promMetrics( - 'PUT', bucketName, err.code, 'putBucketLifecycle'); - return callback(err, corsHeaders); - } - pushMetric('putBucketLifecycle', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics('PUT', bucketName, '200', 'putBucketLifecycle'); - return callback(null, corsHeaders); - }); + ); } module.exports = bucketPutLifecycle; diff --git a/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js b/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js index ecec515963..19941bb585 100644 --- a/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js +++ b/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js @@ -1,9 +1,11 @@ const assert = require('assert'); const { errors } = require('arsenal'); -const { S3Client, +const { + S3Client, CreateBucketCommand, DeleteBucketCommand, - PutBucketLifecycleConfigurationCommand } = require('@aws-sdk/client-s3'); + PutBucketLifecycleConfigurationCommand, +} = require('@aws-sdk/client-s3'); const getConfig = require('../support/config'); const BucketUtility = require('../../lib/utility/bucket-util'); @@ -26,11 +28,17 @@ function assertError(err, expectedErr) { if (expectedErr === null) { assert.strictEqual(err, null, `expected no error but got '${err}'`); } else { - assert.strictEqual(err.name, expectedErr, 'incorrect error response ' + - `code: should be '${expectedErr}' but got '${err.name}'`); - assert.strictEqual(err.$metadata.httpStatusCode, errors[expectedErr].code, + assert.strictEqual( + err.name, + expectedErr, + 'incorrect error response ' + `code: should be '${expectedErr}' but got '${err.name}'`, + ); + assert.strictEqual( + err.$metadata.httpStatusCode, + errors[expectedErr].code, 'incorrect error status code: should be ' + - `${errors[expectedErr].code}, but got '${err.$metadata.httpStatusCode}'`); + `${errors[expectedErr].code}, but got '${err.$metadata.httpStatusCode}'`, + ); } } @@ -89,40 +97,42 @@ describe('aws-sdk test put bucket lifecycle', () => { const params = getLifecycleParams(); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); - - it('should not allow lifecycle configuration with duplicated rule id ' + - 'and with Origin header set', async () => { - const origin = 'http://www.allowedwebsite.com'; - const lifecycleConfig = { - Rules: [expirationRule, expirationRule], - }; - const params = { - Bucket: bucket, - LifecycleConfiguration: lifecycleConfig, - }; - const clientConfig = getConfig('default', { signatureVersion: 'v4' }); - const clientWithOrigin = new S3Client({ - ...clientConfig, - requestHandler: { - handle: async request => { - if (!request.headers) { + it( + 'should not allow lifecycle configuration with duplicated rule id ' + 'and with Origin header set', + async () => { + const origin = 'http://www.allowedwebsite.com'; + const lifecycleConfig = { + Rules: [expirationRule, expirationRule], + }; + const params = { + Bucket: bucket, + LifecycleConfiguration: lifecycleConfig, + }; + + const clientConfig = getConfig('default', { signatureVersion: 'v4' }); + const clientWithOrigin = new S3Client({ + ...clientConfig, + requestHandler: { + handle: async request => { + if (!request.headers) { + // eslint-disable-next-line no-param-reassign + request.headers = {}; + } // eslint-disable-next-line no-param-reassign - request.headers = {}; - } - // eslint-disable-next-line no-param-reassign - request.headers.origin = origin; - return clientConfig.requestHandler.handle(request); - } + request.headers.origin = origin; + return clientConfig.requestHandler.handle(request); + }, + }, + }); + try { + await clientWithOrigin.send(new PutBucketLifecycleConfigurationCommand(params)); + throw new Error('Expected InvalidRequest error'); + } catch (err) { + assertError(err, 'InvalidRequest'); } - }); - try { - await clientWithOrigin.send(new PutBucketLifecycleConfigurationCommand(params)); - throw new Error('Expected InvalidRequest error'); - } catch (err) { - assertError(err, 'InvalidRequest'); - } - }); + }, + ); it('should not allow lifecycle config with no Status', async () => { const params = getLifecycleParams({ key: 'Status', value: '' }); @@ -155,8 +165,7 @@ describe('aws-sdk test put bucket lifecycle', () => { }); it('should not allow lifecycle config with ID longer than 255 char', async () => { - const params = - getLifecycleParams({ key: 'ID', value: 'a'.repeat(256) }); + const params = getLifecycleParams({ key: 'ID', value: 'a'.repeat(256) }); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected InvalidArgument error'); @@ -166,20 +175,17 @@ describe('aws-sdk test put bucket lifecycle', () => { }); it('should allow lifecycle config with Prefix length < 1024', async () => { - const params = - getLifecycleParams({ key: 'Prefix', value: 'a'.repeat(1023) }); + const params = getLifecycleParams({ key: 'Prefix', value: 'a'.repeat(1023) }); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); it('should allow lifecycle config with Prefix length === 1024', async () => { - const params = - getLifecycleParams({ key: 'Prefix', value: 'a'.repeat(1024) }); + const params = getLifecycleParams({ key: 'Prefix', value: 'a'.repeat(1024) }); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); it('should not allow lifecycle config with Prefix length > 1024', async () => { - const params = - getLifecycleParams({ key: 'Prefix', value: 'a'.repeat(1025) }); + const params = getLifecycleParams({ key: 'Prefix', value: 'a'.repeat(1025) }); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected InvalidRequest error'); @@ -202,8 +208,7 @@ describe('aws-sdk test put bucket lifecycle', () => { } }); - it('should not allow lifecycle config with Filter.And.Prefix length ' + - '> 1024', async () => { + it('should not allow lifecycle config with Filter.And.Prefix length ' + '> 1024', async () => { const params = getLifecycleParams({ key: 'Filter', value: { @@ -287,8 +292,7 @@ describe('aws-sdk test put bucket lifecycle', () => { }); it('should not allow lifecycle config with Prefix and Filter', async () => { - const params = getLifecycleParams( - { key: 'Filter', value: { Prefix: 'foo' } }); + const params = getLifecycleParams({ key: 'Filter', value: { Prefix: 'foo' } }); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected MalformedXML error'); @@ -310,7 +314,6 @@ describe('aws-sdk test put bucket lifecycle', () => { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); - describe('with Rule.Filter not Rule.Prefix', () => { before(done => { expirationRule.Prefix = null; @@ -323,8 +326,7 @@ describe('aws-sdk test put bucket lifecycle', () => { }); it('should not allow config with And & Prefix', async () => { - const params = getLifecycleParams( - { key: 'Filter', value: { Prefix: 'foo', And: {} } }); + const params = getLifecycleParams({ key: 'Filter', value: { Prefix: 'foo', And: {} } }); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected MalformedXML error'); @@ -360,8 +362,7 @@ describe('aws-sdk test put bucket lifecycle', () => { }); it('should allow config with only Prefix', async () => { - const params = getLifecycleParams( - { key: 'Filter', value: { Prefix: 'foo' } }); + const params = getLifecycleParams({ key: 'Filter', value: { Prefix: 'foo' } }); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); @@ -374,8 +375,7 @@ describe('aws-sdk test put bucket lifecycle', () => { }); it('should not allow config with And.Prefix & no And.Tags', async () => { - const params = getLifecycleParams( - { key: 'Filter', value: { And: { Prefix: 'foo' } } }); + const params = getLifecycleParams({ key: 'Filter', value: { And: { Prefix: 'foo' } } }); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected MalformedXML error'); @@ -400,9 +400,14 @@ describe('aws-sdk test put bucket lifecycle', () => { it('should allow config with And.Tags & no And.Prefix', async () => { const params = getLifecycleParams({ key: 'Filter', - value: { And: { Tags: - [{ Key: 'foo', Value: 'bar' }, - { Key: 'foo2', Value: 'bar2' }] } }, + value: { + And: { + Tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'foo2', Value: 'bar2' }, + ], + }, + }, }); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); @@ -410,9 +415,15 @@ describe('aws-sdk test put bucket lifecycle', () => { it('should allow config with And.Tags & And.Prefix', async () => { const params = getLifecycleParams({ key: 'Filter', - value: { And: { Prefix: 'foo', Tags: - [{ Key: 'foo', Value: 'bar' }, - { Key: 'foo2', Value: 'bar2' }] } }, + value: { + And: { + Prefix: 'foo', + Tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'foo2', Value: 'bar2' }, + ], + }, + }, }); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); @@ -423,12 +434,14 @@ describe('aws-sdk test put bucket lifecycle', () => { return { Bucket: bucket, LifecycleConfiguration: { - Rules: [{ - ID: 'test', - Status: 'Enabled', - Prefix: '', - noncurrentVersionTransition, - }], + Rules: [ + { + ID: 'test', + Status: 'Enabled', + Prefix: '', + noncurrentVersionTransition, + }, + ], }, }; } @@ -464,9 +477,10 @@ describe('aws-sdk test put bucket lifecycle', () => { throw new Error('Expected InvalidArgument error'); } catch (err) { assert.strictEqual(err.name, 'InvalidArgument'); - assert.strictEqual(err.message, - "'NoncurrentDays' in NoncurrentVersionExpiration " + - 'action must be nonnegative'); + assert.strictEqual( + err.message, + "'NoncurrentDays' in NoncurrentVersionExpiration " + 'action must be nonnegative', + ); } }); @@ -487,21 +501,25 @@ describe('aws-sdk test put bucket lifecycle', () => { return { Bucket: bucket, LifecycleConfiguration: { - Rules: [{ - ID: 'test', - Status: 'Enabled', - Prefix: '', - NoncurrentVersionTransitions: noncurrentVersionTransitions, - }], + Rules: [ + { + ID: 'test', + Status: 'Enabled', + Prefix: '', + NoncurrentVersionTransitions: noncurrentVersionTransitions, + }, + ], }, }; } it('should allow config', async () => { - const noncurrentVersionTransitions = [{ - NoncurrentDays: 1, - StorageClass: 'us-east-2', - }]; + const noncurrentVersionTransitions = [ + { + NoncurrentDays: 1, + StorageClass: 'us-east-2', + }, + ]; const params = getParams(noncurrentVersionTransitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); @@ -514,13 +532,16 @@ describe('aws-sdk test put bucket lifecycle', () => { }); it.skip('should not allow duplicate StorageClass', async () => { - const noncurrentVersionTransitions = [{ - NoncurrentDays: 1, - StorageClass: 'us-east-2', - }, { - NoncurrentDays: 2, - StorageClass: 'us-east-2', - }]; + const noncurrentVersionTransitions = [ + { + NoncurrentDays: 1, + StorageClass: 'us-east-2', + }, + { + NoncurrentDays: 2, + StorageClass: 'us-east-2', + }, + ]; const params = getParams(noncurrentVersionTransitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); @@ -532,88 +553,111 @@ describe('aws-sdk test put bucket lifecycle', () => { return; } assert.strictEqual(err.name, 'InvalidRequest'); - assert.strictEqual(err.message, + assert.strictEqual( + err.message, "'StorageClass' must be different for " + - "'NoncurrentVersionTransition' actions in same " + - "'Rule' with prefix ''"); + "'NoncurrentVersionTransition' actions in same " + + "'Rule' with prefix ''", + ); } }); it('should not allow unknown StorageClass', async () => { - const noncurrentVersionTransitions = [{ - NoncurrentDays: 1, - StorageClass: 'unknown', - }]; + const noncurrentVersionTransitions = [ + { + NoncurrentDays: 1, + StorageClass: 'unknown', + }, + ]; const params = getParams(noncurrentVersionTransitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected MalformedXML error'); } catch (err) { - assert(err.name === 'MalformedXML' || err.name === 'NotImplemented', - `Expected MalformedXML or NotImplemented, got ${err.name}`); + assert( + err.name === 'MalformedXML' || err.name === 'NotImplemented', + `Expected MalformedXML or NotImplemented, got ${err.name}`, + ); } }); it(`should not allow NoncurrentDays value exceeding ${MAX_DAYS}`, async () => { - const noncurrentVersionTransitions = [{ - NoncurrentDays: MAX_DAYS + 1, - StorageClass: 'us-east-2', - }]; + const noncurrentVersionTransitions = [ + { + NoncurrentDays: MAX_DAYS + 1, + StorageClass: 'us-east-2', + }, + ]; const params = getParams(noncurrentVersionTransitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected MalformedXML error'); } catch (err) { - assert(err.name === 'MalformedXML' || err.name === 'NotImplemented', - `Expected MalformedXML or NotImplemented, got ${err.name}`); + assert( + err.name === 'MalformedXML' || err.name === 'NotImplemented', + `Expected MalformedXML or NotImplemented, got ${err.name}`, + ); } }); it('should not allow negative NoncurrentDays', async () => { - const noncurrentVersionTransitions = [{ - NoncurrentDays: -1, - StorageClass: 'us-east-2', - }]; + const noncurrentVersionTransitions = [ + { + NoncurrentDays: -1, + StorageClass: 'us-east-2', + }, + ]; const params = getParams(noncurrentVersionTransitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected error'); } catch (err) { - assert(err.name === 'InvalidArgument' || err.name === 'NotImplemented', - `Expected InvalidArgument or NotImplemented, got ${err.name}`); + assert( + err.name === 'InvalidArgument' || err.name === 'NotImplemented', + `Expected InvalidArgument or NotImplemented, got ${err.name}`, + ); if (err.name === 'InvalidArgument') { - assert.strictEqual(err.message, - "'NoncurrentDays' in NoncurrentVersionTransition " + - 'action must be nonnegative'); + assert.strictEqual( + err.message, + "'NoncurrentDays' in NoncurrentVersionTransition " + 'action must be nonnegative', + ); } } }); it('should not allow config missing NoncurrentDays', async () => { - const noncurrentVersionTransitions = [{ - StorageClass: 'us-east-2', - }]; + const noncurrentVersionTransitions = [ + { + StorageClass: 'us-east-2', + }, + ]; const params = getParams(noncurrentVersionTransitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected error'); } catch (err) { - assert(err.name === 'MalformedXML' || err.name === 'NotImplemented', - `Expected MalformedXML or NotImplemented, got ${err.name}`); + assert( + err.name === 'MalformedXML' || err.name === 'NotImplemented', + `Expected MalformedXML or NotImplemented, got ${err.name}`, + ); } }); it('should not allow config missing StorageClass', async () => { - const noncurrentVersionTransitions = [{ - NoncurrentDays: 1, - }]; + const noncurrentVersionTransitions = [ + { + NoncurrentDays: 1, + }, + ]; const params = getParams(noncurrentVersionTransitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected error'); } catch (err) { - assert(err.name === 'MalformedXML' || err.name === 'NotImplemented', - `Expected MalformedXML or NotImplemented, got ${err.name}`); + assert( + err.name === 'MalformedXML' || err.name === 'NotImplemented', + `Expected MalformedXML or NotImplemented, got ${err.name}`, + ); } }); }); @@ -626,15 +670,19 @@ describe('aws-sdk test put bucket lifecycle', () => { const params = { Bucket: bucket, LifecycleConfiguration: { - Rules: [{ - ID: 'test', - Status: 'Enabled', - Prefix: '', - Transitions: [{ - Days: 2, - StorageClass: 'us-east-2', - }], - }], + Rules: [ + { + ID: 'test', + Status: 'Enabled', + Prefix: '', + Transitions: [ + { + Days: 2, + StorageClass: 'us-east-2', + }, + ], + }, + ], }, }; try { @@ -652,60 +700,72 @@ describe('aws-sdk test put bucket lifecycle', () => { return { Bucket: bucket, LifecycleConfiguration: { - Rules: [{ - ID: 'test', - Status: 'Enabled', - Prefix: '', - Transitions: transitions, - }], + Rules: [ + { + ID: 'test', + Status: 'Enabled', + Prefix: '', + Transitions: transitions, + }, + ], }, }; } it('should allow config', async () => { - const transitions = [{ - Days: 1, - StorageClass: 'us-east-2', - }]; + const transitions = [ + { + Days: 1, + StorageClass: 'us-east-2', + }, + ]; const params = getParams(transitions); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); it('should not allow duplicate StorageClass', async () => { - const transitions = [{ - Days: 1, - StorageClass: 'us-east-2', - }, { - Days: 2, - StorageClass: 'us-east-2', - }]; + const transitions = [ + { + Days: 1, + StorageClass: 'us-east-2', + }, + { + Days: 2, + StorageClass: 'us-east-2', + }, + ]; const params = getParams(transitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); throw new Error('Expected InvalidRequest error'); } catch (err) { assert.strictEqual(err.name, 'InvalidRequest'); - assert.strictEqual(err.message, - "'StorageClass' must be different for 'Transition' " + - "actions in same 'Rule' with prefix ''"); + assert.strictEqual( + err.message, + "'StorageClass' must be different for 'Transition' " + "actions in same 'Rule' with prefix ''", + ); } }); it('should allow Date', async () => { - const transitions = [{ - Date: new Date('2016-01-01T00:00:00.000Z'), - StorageClass: 'us-east-2', - }]; + const transitions = [ + { + Date: new Date('2016-01-01T00:00:00.000Z'), + StorageClass: 'us-east-2', + }, + ]; const params = getParams(transitions); await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); it('should not allow speficying both Days and Date value', async () => { - const transitions = [{ - Date: new Date('2016-01-01T00:00:00.000Z'), - Days: 1, - StorageClass: 'us-east-2', - }]; + const transitions = [ + { + Date: new Date('2016-01-01T00:00:00.000Z'), + Days: 1, + StorageClass: 'us-east-2', + }, + ]; const params = getParams(transitions); try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); @@ -716,87 +776,104 @@ describe('aws-sdk test put bucket lifecycle', () => { }); // TODO: Upgrade to aws-sdk >= 2.60.0 for correct Date field support - it.skip('should not allow speficying both Days and Date value ' + - 'across transitions', done => { - const transitions = [{ - Date: '2016-01-01T00:00:00.000Z', - StorageClass: 'us-east-2', - }, { - Days: 1, - StorageClass: 'zenko', - }]; + it.skip('should not allow speficying both Days and Date value ' + 'across transitions', done => { + const transitions = [ + { + Date: '2016-01-01T00:00:00.000Z', + StorageClass: 'us-east-2', + }, + { + Days: 1, + StorageClass: 'zenko', + }, + ]; const params = getParams(transitions); s3.putBucketLifecycleConfiguration(params, err => { assert.strictEqual(err.code, 'InvalidRequest'); - assert.strictEqual(err.message, - "Found mixed 'Date' and 'Days' based Transition " + - "actions in lifecycle rule for prefix ''"); + assert.strictEqual( + err.message, + "Found mixed 'Date' and 'Days' based Transition " + "actions in lifecycle rule for prefix ''", + ); done(); }); }); - it('should not allow speficying both Days and Date value ' + - 'across transitions and expiration', async () => { - const transitions = [{ - Days: 1, - StorageClass: 'us-east-2', - }]; - const params = getParams(transitions); - params.LifecycleConfiguration.Rules[0].Expiration = { - Date: new Date('2016-01-01T00:00:00.000Z') // Use proper Date object - }; - try { - await s3.send(new PutBucketLifecycleConfigurationCommand(params)); - throw new Error('Expected InvalidRequest error'); - } catch (err) { - assert.strictEqual(err.name, 'InvalidRequest'); - assert.strictEqual(err.message, - "Found mixed 'Date' and 'Days' based Expiration and " + - "Transition actions in lifecycle rule for prefix ''"); - } - }); + it( + 'should not allow speficying both Days and Date value ' + 'across transitions and expiration', + async () => { + const transitions = [ + { + Days: 1, + StorageClass: 'us-east-2', + }, + ]; + const params = getParams(transitions); + params.LifecycleConfiguration.Rules[0].Expiration = { + Date: new Date('2016-01-01T00:00:00.000Z'), // Use proper Date object + }; + try { + await s3.send(new PutBucketLifecycleConfigurationCommand(params)); + throw new Error('Expected InvalidRequest error'); + } catch (err) { + assert.strictEqual(err.name, 'InvalidRequest'); + assert.strictEqual( + err.message, + "Found mixed 'Date' and 'Days' based Expiration and " + + "Transition actions in lifecycle rule for prefix ''", + ); + } + }, + ); }); // NoncurrentVersionTransitions not implemented - describe.skip('with NoncurrentVersionTransitions and Transitions', - () => { + describe.skip('with NoncurrentVersionTransitions and Transitions', () => { it('should allow config', async () => { const params = { Bucket: bucket, LifecycleConfiguration: { - Rules: [{ - ID: 'test', - Status: 'Enabled', - Prefix: '', - NoncurrentVersionTransitions: [{ - NoncurrentDays: 1, - StorageClass: 'us-east-2', - }], - Transitions: [{ - Days: 1, - StorageClass: 'us-east-2', - }], - }], + Rules: [ + { + ID: 'test', + Status: 'Enabled', + Prefix: '', + NoncurrentVersionTransitions: [ + { + NoncurrentDays: 1, + StorageClass: 'us-east-2', + }, + ], + Transitions: [ + { + Days: 1, + StorageClass: 'us-east-2', + }, + ], + }, + ], }, }; await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); }); - it.skip('should not allow config when specifying ' + - 'NoncurrentVersionTransitions', async () => { + it.skip('should not allow config when specifying ' + 'NoncurrentVersionTransitions', async () => { const params = { Bucket: bucket, LifecycleConfiguration: { - Rules: [{ - ID: 'test', - Status: 'Enabled', - Prefix: '', - NoncurrentVersionTransitions: [{ - NoncurrentDays: 1, - StorageClass: 'us-east-2', - }], - }], + Rules: [ + { + ID: 'test', + Status: 'Enabled', + Prefix: '', + NoncurrentVersionTransitions: [ + { + NoncurrentDays: 1, + StorageClass: 'us-east-2', + }, + ], + }, + ], }, }; try { From 91b222de73e4d2ea323c909bf806cf477c738bbf Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Mon, 22 Jun 2026 12:27:16 +0200 Subject: [PATCH 2/5] Pass werelogs logger to LifecycleConfiguration The arsenal LifecycleConfiguration constructor now takes a werelogs logger as its second argument and logs when a rule is configured with a 0-day action time. Pass the request logger so that audit line is correlated to the request. Issue: CLDSRV-928 --- lib/api/bucketPutLifecycle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/bucketPutLifecycle.js b/lib/api/bucketPutLifecycle.js index 9e0ad00aad..8d217b6d3e 100644 --- a/lib/api/bucketPutLifecycle.js +++ b/lib/api/bucketPutLifecycle.js @@ -33,7 +33,7 @@ function bucketPutLifecycle(authInfo, request, log, callback) { [ next => parseXML(request.post, log, next), (parsedXml, next) => { - const lcConfigClass = new LifecycleConfiguration(parsedXml, config); + const lcConfigClass = new LifecycleConfiguration(parsedXml, log, config); // if there was an error getting lifecycle configuration, // returned configObj will contain 'error' key process.nextTick(() => { From 94e42ad30787c40cb1ce2cfa7479a06e8b848e8e Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Thu, 18 Jun 2026 13:50:42 +0200 Subject: [PATCH 3/5] Add functional tests for Days=0 lifecycle expiration Cover the new Days=0 ("empty this bucket") capability in PutBucketLifecycle: - Expiration Days=0 accepted and round-trips via GetBucketLifecycle - NoncurrentVersionExpiration NoncurrentDays=0 accepted - AbortIncompleteMultipartUpload DaysAfterInitiation=0 accepted - negative Expiration Days rejected (must be nonnegative) - Expiration Days exceeding MAX_DAYS rejected (MalformedXML) These pass once the arsenal dependency is bumped to the version including ARSN-597. Issue: CLDSRV-928 --- .../test/bucket/putBucketLifecycle.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js b/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js index 19941bb585..e8f9fa9306 100644 --- a/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js +++ b/tests/functional/aws-node-sdk/test/bucket/putBucketLifecycle.js @@ -5,6 +5,7 @@ const { CreateBucketCommand, DeleteBucketCommand, PutBucketLifecycleConfigurationCommand, + GetBucketLifecycleConfigurationCommand, } = require('@aws-sdk/client-s3'); const getConfig = require('../support/config'); @@ -98,6 +99,75 @@ describe('aws-sdk test put bucket lifecycle', () => { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); }); + it('should allow Expiration Days=0 (explicit bucket-emptying intent) ' + 'and round-trip it', async () => { + const params = getLifecycleParams({ key: 'Expiration', value: { Days: 0 } }); + await s3.send(new PutBucketLifecycleConfigurationCommand(params)); + const got = await s3.send(new GetBucketLifecycleConfigurationCommand({ Bucket: bucket })); + assert.strictEqual(got.Rules[0].Expiration.Days, 0); + }); + + it('should not allow negative Expiration Days', async () => { + const params = getLifecycleParams({ key: 'Expiration', value: { Days: -1 } }); + try { + await s3.send(new PutBucketLifecycleConfigurationCommand(params)); + throw new Error('Expected InvalidArgument error'); + } catch (err) { + assert.strictEqual(err.name, 'InvalidArgument'); + assert.strictEqual(err.message, "'Days' in Expiration action must be nonnegative"); + } + }); + + it(`should not allow Expiration Days exceeding ${MAX_DAYS}`, async () => { + const params = getLifecycleParams({ + key: 'Expiration', + value: { Days: MAX_DAYS + 1 }, + }); + try { + await s3.send(new PutBucketLifecycleConfigurationCommand(params)); + throw new Error('Expected MalformedXML error'); + } catch (err) { + assert.strictEqual(err.name, 'MalformedXML'); + } + }); + + it('should allow NoncurrentVersionExpiration NoncurrentDays=0', async () => { + const params = { + Bucket: bucket, + LifecycleConfiguration: { + Rules: [ + { + ID: 'test-id', + Status: 'Enabled', + Prefix: '', + NoncurrentVersionExpiration: { NoncurrentDays: 0 }, + }, + ], + }, + }; + await s3.send(new PutBucketLifecycleConfigurationCommand(params)); + const got = await s3.send(new GetBucketLifecycleConfigurationCommand({ Bucket: bucket })); + assert.strictEqual(got.Rules[0].NoncurrentVersionExpiration.NoncurrentDays, 0); + }); + + it('should allow AbortIncompleteMultipartUpload DaysAfterInitiation=0', async () => { + const params = { + Bucket: bucket, + LifecycleConfiguration: { + Rules: [ + { + ID: 'test-id', + Status: 'Enabled', + Prefix: '', + AbortIncompleteMultipartUpload: { DaysAfterInitiation: 0 }, + }, + ], + }, + }; + await s3.send(new PutBucketLifecycleConfigurationCommand(params)); + const got = await s3.send(new GetBucketLifecycleConfigurationCommand({ Bucket: bucket })); + assert.strictEqual(got.Rules[0].AbortIncompleteMultipartUpload.DaysAfterInitiation, 0); + }); + it( 'should not allow lifecycle configuration with duplicated rule id ' + 'and with Origin header set', async () => { From 69d6c0394396742bbd67618cc939535af2098009 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Thu, 18 Jun 2026 13:50:52 +0200 Subject: [PATCH 4/5] Bump arsenal to allow Days=0 in lifecycle rules Point arsenal at the ARSN-597 commit (8b8d0ae0983d28e6bbfca12c8512add839fcd996) rather than a released tag, since the version is not cut yet. ARSN-597 targets the 8.5 line (dev/8.5, 8.5.3), which 9.4 already tracks, so this delivers Days=0 support (the explicit "empty this bucket" lifecycle signal) plus the 0-day audit logging the previous commit wires up, and makes the functional tests added earlier pass. To amend once ARSN-597 is released: replace the commit pin with the released arsenal version (>= 8.5.4) and re-run yarn install. Issue: CLDSRV-928 --- package.json | 2 +- yarn.lock | 34 ++++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 3dd6f905b0..07b62eea86 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@opentelemetry/instrumentation-ioredis": "~0.64.0", "@opentelemetry/instrumentation-mongodb": "~0.69.0", "@smithy/node-http-handler": "^3.0.0", - "arsenal": "git+https://github.com/scality/arsenal#8.5.2", + "arsenal": "git+https://github.com/scality/arsenal#8b8d0ae0983d28e6bbfca12c8512add839fcd996", "async": "2.6.4", "bucketclient": "scality/bucketclient#8.2.7", "bufferutil": "^4.0.8", diff --git a/yarn.lock b/yarn.lock index cf10359a70..b8f4d08ede 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6595,9 +6595,9 @@ arraybuffer.prototype.slice@^1.0.4: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/arsenal#8.5.2": - version "8.5.2" - resolved "git+https://github.com/scality/arsenal#cd9ddfca043b522c9199a0edbf23a73dbc7973fd" +"arsenal@git+https://github.com/scality/arsenal#8b8d0ae0983d28e6bbfca12c8512add839fcd996": + version "8.5.3" + resolved "git+https://github.com/scality/arsenal#8b8d0ae0983d28e6bbfca12c8512add839fcd996" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" @@ -6607,6 +6607,10 @@ arraybuffer.prototype.slice@^1.0.4: "@azure/storage-blob" "^12.31.0" "@js-sdsl/ordered-set" "^4.4.2" "@opentelemetry/api" "^1.9.1" + "@opentelemetry/exporter-trace-otlp-http" "^0.219.0" + "@opentelemetry/resources" "^2.8.0" + "@opentelemetry/sdk-node" "^0.219.0" + "@opentelemetry/sdk-trace-base" "^2.8.0" "@scality/hdclient" "^1.3.2" "@smithy/node-http-handler" "^4.3.0" "@smithy/protocol-http" "^5.3.5" @@ -6632,16 +6636,12 @@ arraybuffer.prototype.slice@^1.0.4: simple-glob "^0.2.0" socket.io "^4.8.0" socket.io-client "^4.8.0" - sproxydclient "github:scality/sproxydclient#8.1.0" + sproxydclient "github:scality/sproxydclient#8.2.1" utf8 "^3.0.0" uuid "^10.0.0" - werelogs scality/werelogs#8.2.2 + werelogs scality/werelogs#8.2.4 xml2js "^0.6.2" optionalDependencies: - "@opentelemetry/exporter-trace-otlp-http" "^0.219.0" - "@opentelemetry/resources" "^2.8.0" - "@opentelemetry/sdk-node" "^0.219.0" - "@opentelemetry/sdk-trace-base" "^2.8.0" ioctl "^2.0.2" asn1@~0.2.3: @@ -12085,6 +12085,14 @@ sprintf-js@~1.0.2: httpagent "github:scality/httpagent#1.1.0" werelogs scality/werelogs#8.2.0 +"sproxydclient@github:scality/sproxydclient#8.2.1": + version "8.2.1" + resolved "https://codeload.github.com/scality/sproxydclient/tar.gz/501829f5521787e7e946de6792f5806ffa6ec437" + dependencies: + async "^3.2.6" + httpagent "github:scality/httpagent#1.1.0" + werelogs scality/werelogs#8.2.0 + sql-where-parser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/sql-where-parser/-/sql-where-parser-2.2.1.tgz#d9af68c20ebfdffe9a115a119f65abc4fbb7c2e5" @@ -12844,7 +12852,6 @@ webidl-conversions@^7.0.0: "werelogs@github:scality/werelogs#8.2.2", werelogs@scality/werelogs#8.2.2: version "8.2.2" - uid e53bef5145697bf8af940dcbe59408988d64854f resolved "https://codeload.github.com/scality/werelogs/tar.gz/e53bef5145697bf8af940dcbe59408988d64854f" dependencies: fast-safe-stringify "^2.1.1" @@ -12857,6 +12864,13 @@ werelogs@scality/werelogs#8.2.0: fast-safe-stringify "^2.1.1" safe-json-stringify "^1.2.0" +werelogs@scality/werelogs#8.2.4: + version "8.2.4" + resolved "https://codeload.github.com/scality/werelogs/tar.gz/a7bbb5917a08b035d3763b24b070d517483d6982" + dependencies: + fast-safe-stringify "^2.1.1" + safe-json-stringify "^1.2.0" + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" From 2ff1690a0b1eed13e1d5af11a26f55784b283333 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Mon, 22 Jun 2026 12:45:42 +0200 Subject: [PATCH 5/5] Refactor bucketPutLifecycle to async/await CodeQL's "Callback-style function (async migration)" query flags the waterfall step callbacks in this handler once it is modified by the PR. Convert the handler to async/await, promisifying the callback-based helpers (parseXML, standardMetadataValidateBucket, metadata.updateBucket), following the established pattern in bucketGet / objectGetLegalHold: a thin callback wrapper delegates to the async implementation and forwards CORS headers via err.additionalResHeaders on error. Issue: CLDSRV-928 --- lib/api/bucketPutLifecycle.js | 86 +++++++++++++++-------------------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/lib/api/bucketPutLifecycle.js b/lib/api/bucketPutLifecycle.js index 8d217b6d3e..7c20cec519 100644 --- a/lib/api/bucketPutLifecycle.js +++ b/lib/api/bucketPutLifecycle.js @@ -1,4 +1,4 @@ -const { waterfall } = require('async'); +const { promisify } = require('util'); const uuid = require('uuid').v4; const LifecycleConfiguration = require('arsenal').models.LifecycleConfiguration; @@ -15,11 +15,16 @@ const monitoring = require('../utilities/monitoringHandler'); * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info * @param {object} request - http request object * @param {object} log - Werelogs logger - * @param {function} callback - callback to server - * @return {undefined} + * @param {function} [callback] - callback to server + * @return {Promise} - resolves with the CORS response headers */ +async function bucketPutLifecycle(authInfo, request, log, callback) { + if (callback) { + return bucketPutLifecycle(authInfo, request, log) + .then(corsHeaders => callback(null, corsHeaders)) + .catch(err => callback(err, err.additionalResHeaders)); + } -function bucketPutLifecycle(authInfo, request, log, callback) { log.debug('processing request', { method: 'bucketPutLifecycle' }); const { bucketName } = request; @@ -29,51 +34,34 @@ function bucketPutLifecycle(authInfo, request, log, callback) { requestType: request.apiMethods || 'bucketPutLifecycle', request, }; - return waterfall( - [ - next => parseXML(request.post, log, next), - (parsedXml, next) => { - const lcConfigClass = new LifecycleConfiguration(parsedXml, log, config); - // if there was an error getting lifecycle configuration, - // returned configObj will contain 'error' key - process.nextTick(() => { - const configObj = lcConfigClass.getLifecycleConfiguration(); - if (configObj.error) { - return next(configObj.error); - } - return next(null, configObj); - }); - }, - (lcConfig, next) => - standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { - if (err) { - return next(err, bucket); - } - return next(null, bucket, lcConfig); - }), - (bucket, lcConfig, next) => { - if (!bucket.getUid()) { - bucket.setUid(uuid()); - } - bucket.setLifecycleConfiguration(lcConfig); - metadata.updateBucket(bucket.getName(), bucket, log, err => next(err, bucket)); - }, - ], - (err, bucket) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); - if (err) { - log.trace('error processing request', { error: err, method: 'bucketPutLifecycle' }); - monitoring.promMetrics('PUT', bucketName, err.code, 'putBucketLifecycle'); - return callback(err, corsHeaders); - } - pushMetric('putBucketLifecycle', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics('PUT', bucketName, '200', 'putBucketLifecycle'); - return callback(null, corsHeaders); - }, - ); + + let bucket; + try { + const parsedXml = await promisify(parseXML)(request.post, log); + const lcConfig = new LifecycleConfiguration(parsedXml, log, config).getLifecycleConfiguration(); + if (lcConfig.error) { + throw lcConfig.error; + } + bucket = await promisify(standardMetadataValidateBucket)(metadataValParams, request.actionImplicitDenies, log); + if (!bucket.getUid()) { + bucket.setUid(uuid()); + } + bucket.setLifecycleConfiguration(lcConfig); + await promisify(metadata.updateBucket).call(metadata, bucket.getName(), bucket, log); + } catch (err) { + log.trace('error processing request', { error: err, method: 'bucketPutLifecycle' }); + monitoring.promMetrics('PUT', bucketName, err.code, 'putBucketLifecycle'); + err.additionalResHeaders = + err.additionalResHeaders || collectCorsHeaders(request.headers.origin, request.method, bucket); + throw err; + } + + pushMetric('putBucketLifecycle', log, { + authInfo, + bucket: bucketName, + }); + monitoring.promMetrics('PUT', bucketName, '200', 'putBucketLifecycle'); + return collectCorsHeaders(request.headers.origin, request.method, bucket); } module.exports = bucketPutLifecycle;