diff --git a/config.json b/config.json index 185d752d87..4ef04f7f6f 100644 --- a/config.json +++ b/config.json @@ -142,5 +142,8 @@ }, "kmip": { "providerName": "thales" + }, + "integrityChecks": { + "objectPutRetention": true } } diff --git a/lib/Config.js b/lib/Config.js index 23b02f6a8d..5fe7d79462 100644 --- a/lib/Config.js +++ b/lib/Config.js @@ -533,6 +533,37 @@ function bucketNotifAssert(bucketNotifConfig) { return bucketNotifConfig; } +function parseIntegrityChecks(config) { + const integrityChecks = { + 'bucketPutACL': true, + 'bucketPutCors': true, + 'bucketPutEncryption': true, + 'bucketPutLifecycle': true, + 'bucketPutNotification': true, + 'bucketPutObjectLock': true, + 'bucketPutPolicy': true, + 'bucketPutReplication': true, + 'bucketPutVersioning': true, + 'bucketPutWebsite': true, + 'multiObjectDelete': true, + 'objectPutACL': true, + 'objectPutLegalHold': true, + 'objectPutTagging': true, + 'objectPutRetention': true, + }; + + if (config && config.integrityChecks) { + for (const method in integrityChecks) { + if (method in config.integrityChecks) { + assert(typeof config.integrityChecks[method] == 'boolean', `bad config: ${method} not boolean`); + integrityChecks[method] = config.integrityChecks[method]; + } + } + } + + return integrityChecks; +} + /** * Reads from a config file and returns the content as a config object */ @@ -1743,6 +1774,8 @@ class Config extends EventEmitter { this.supportedLifecycleRules = parseSupportedLifecycleRules(config.supportedLifecycleRules); + this.integrityChecks = parseIntegrityChecks(config); + /** * S3C-10336: PutObject max size of 5GB is new in 9.5.1 * Provides a way to bypass the new validation if it breaks customer workflows @@ -2081,4 +2114,5 @@ module.exports = { azureGetStorageAccountName, azureGetLocationCredentials, parseSupportedLifecycleRules, + parseIntegrityChecks, }; diff --git a/lib/api/api.js b/lib/api/api.js index bba3fe2e67..6b12d3cfde 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -74,6 +74,7 @@ const parseCopySource = require('./apiUtils/object/parseCopySource'); const { tagConditionKeyAuth } = require('./apiUtils/authorization/tagConditionKeys'); const { isRequesterASessionUser } = require('./apiUtils/authorization/permissionChecks'); const checkHttpHeadersSize = require('./apiUtils/object/checkHttpHeadersSize'); +const { validateMethodChecksumNoChunking } = require('./apiUtils/integrity/validateChecksums'); const monitoringMap = policies.actionMaps.actionMonitoringMapS3; @@ -242,8 +243,16 @@ const api = { { postLength }); return next(errors.InvalidRequest); } + + const buff = Buffer.concat(post, postLength); + + const err = validateMethodChecksumNoChunking(request, buff, log); + if (err) { + return next(err); + } + // Convert array of post buffers into one string - request.post = Buffer.concat(post, postLength).toString(); + request.post = buff.toString(); return next(null, userInfo, authorizationResults, streamingV4Params, infos); }); return undefined; diff --git a/lib/api/apiUtils/integrity/validateChecksums.js b/lib/api/apiUtils/integrity/validateChecksums.js new file mode 100644 index 0000000000..36864604cf --- /dev/null +++ b/lib/api/apiUtils/integrity/validateChecksums.js @@ -0,0 +1,82 @@ +const crypto = require('crypto'); +const { errors: ArsenalErrors } = require('arsenal'); +const { config } = require('../../../Config'); + +const ChecksumError = Object.freeze({ + MD5Mismatch: 'MD5Mismatch', + MissingChecksum: 'MissingChecksum', +}); + +/** + * validateChecksumsNoChunking - Validate the checksums of a request. + * @param {object} headers - http headers + * @param {Buffer} body - http request body + * @return {object} - error + */ +function validateChecksumsNoChunking(headers, body) { + if (headers && 'content-md5' in headers) { + const md5 = crypto.createHash('md5').update(body).digest('base64'); + if (md5 !== headers['content-md5']) { + return { error: ChecksumError.MD5Mismatch, details: { calculated: md5, expected: headers['content-md5'] } }; + } + + return null; + } + + return { error: ChecksumError.MissingChecksum, details: null }; +} + +function defaultValidationFunc(request, body, log) { + const err = validateChecksumsNoChunking(request.headers, body); + if (err && err.error !== ChecksumError.MissingChecksum) { + log.debug('failed checksum validation', { method: request.apiMethod }, err); + return ArsenalErrors.BadDigest; + } + + return null; +} + +const methodValidationFunc = Object.freeze({ + 'bucketPutACL': defaultValidationFunc, + 'bucketPutCors': defaultValidationFunc, + 'bucketPutEncryption': defaultValidationFunc, + 'bucketPutLifecycle': defaultValidationFunc, + 'bucketPutNotification': defaultValidationFunc, + 'bucketPutObjectLock': defaultValidationFunc, + 'bucketPutPolicy': defaultValidationFunc, + 'bucketPutReplication': defaultValidationFunc, + 'bucketPutVersioning': defaultValidationFunc, + 'bucketPutWebsite': defaultValidationFunc, + // TODO: DeleteObjects requires a checksum. Should return an error if ChecksumError.MissingChecksum. + 'multiObjectDelete': defaultValidationFunc, + 'objectPutACL': defaultValidationFunc, + 'objectPutLegalHold': defaultValidationFunc, + 'objectPutTagging': defaultValidationFunc, + 'objectPutRetention': defaultValidationFunc, +}); + +/** + * validateMethodChecksumsNoChunking - Validate the checksums of a request. + * @param {object} request - http request + * @param {Buffer} body - http request body + * @param {object} log - logger + * @return {object} - error + */ +function validateMethodChecksumNoChunking(request, body, log) { + if (config.integrityChecks[request.apiMethod]) { + const validationFunc = methodValidationFunc[request.apiMethod]; + if (!validationFunc) { + return null; + } + + return validationFunc(request, body, log); + } + + return null; +} + +module.exports = { + ChecksumError, + validateChecksumsNoChunking, + validateMethodChecksumNoChunking, +}; diff --git a/lib/api/bucketPutCors.js b/lib/api/bucketPutCors.js index 256af6257b..03a34430de 100644 --- a/lib/api/bucketPutCors.js +++ b/lib/api/bucketPutCors.js @@ -1,4 +1,3 @@ -const crypto = require('crypto'); const async = require('async'); const { errors, errorInstances } = require('arsenal'); @@ -33,16 +32,6 @@ function bucketPutCors(authInfo, request, log, callback) { return callback(errors.MissingRequestBodyError); } - if (request.headers['content-md5']) { - const md5 = crypto.createHash('md5') - .update(request.post, 'utf8').digest('base64'); - if (md5 !== request.headers['content-md5']) { - log.debug('bad md5 digest', { error: errors.BadDigest }); - monitoring.promMetrics('PUT', bucketName, 400, 'putBucketCors'); - return callback(errors.BadDigest); - } - } - if (parseInt(request.headers['content-length'], 10) > 65536) { const errMsg = 'The CORS XML document is limited to 64 KB in size.'; log.debug(errMsg, { error: errors.MalformedXML }); diff --git a/lib/api/bucketPutReplication.js b/lib/api/bucketPutReplication.js index 5fca68aaee..c940e660b4 100644 --- a/lib/api/bucketPutReplication.js +++ b/lib/api/bucketPutReplication.js @@ -33,6 +33,7 @@ function bucketPutReplication(authInfo, request, log, callback) { requestType: request.apiMethods || 'bucketPutReplication', request, }; + return waterfall([ // Validate the request XML and return the replication configuration. next => getReplicationConfiguration(post, log, next), diff --git a/lib/api/multiObjectDelete.js b/lib/api/multiObjectDelete.js index b2fdff61f4..63b7306465 100644 --- a/lib/api/multiObjectDelete.js +++ b/lib/api/multiObjectDelete.js @@ -1,5 +1,3 @@ -const crypto = require('crypto'); - const async = require('async'); const { parseString } = require('xml2js'); const { auth, errors, versioning, s3middleware, policies } = require('arsenal'); @@ -475,16 +473,6 @@ function multiObjectDelete(authInfo, request, log, callback) { return callback(errors.MissingRequestBodyError); } - if (request.headers['content-md5']) { - const md5 = crypto.createHash('md5') - .update(request.post, 'utf8').digest('base64'); - if (md5 !== request.headers['content-md5']) { - monitoring.promMetrics('DELETE', request.bucketName, 400, - 'multiObjectDelete'); - return callback(errors.BadDigest); - } - } - const inPlayInternal = []; const bucketName = request.bucketName; const canonicalID = authInfo.getCanonicalID(); diff --git a/lib/api/objectPutTagging.js b/lib/api/objectPutTagging.js index fbb1bba96d..ef23dcf64d 100644 --- a/lib/api/objectPutTagging.js +++ b/lib/api/objectPutTagging.js @@ -27,7 +27,6 @@ function objectPutTagging(authInfo, request, log, callback) { log.debug('processing request', { method: 'objectPutTagging' }); const { bucketName, objectKey } = request; - const decodedVidResult = decodeVersionId(request.query); if (decodedVidResult instanceof Error) { log.trace('invalid versionId query', { diff --git a/package.json b/package.json index 8b0b0759cf..362688e221 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.0.26", + "version": "9.0.27", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": { diff --git a/tests/unit/Config.js b/tests/unit/Config.js index e7d7d6e42a..d9374df3af 100644 --- a/tests/unit/Config.js +++ b/tests/unit/Config.js @@ -7,6 +7,7 @@ const { azureGetLocationCredentials, locationConstraintAssert, parseSupportedLifecycleRules, + parseIntegrityChecks, ConfigObject, } = require('../../lib/Config'); @@ -885,4 +886,40 @@ describe('Config', () => { assert.strictEqual(config.instanceId.length, 6); }); }); + + describe('parse integrity checks', () => { + it('should replace default values with new values', () => { + const newConfig = { + integrityChecks: { + 'bucketPutACL': false, + 'bucketPutCors': false, + 'bucketPutEncryption': false, + 'bucketPutLifecycle': false, + 'bucketPutNotification': false, + 'bucketPutObjectLock': false, + 'bucketPutPolicy': false, + 'bucketPutReplication': false, + 'bucketPutVersioning': false, + 'bucketPutWebsite': false, + 'multiObjectDelete': false, + 'objectPutACL': false, + 'objectPutLegalHold': false, + 'objectPutTagging': false, + 'objectPutRetention': false, + }, + }; + + const result = parseIntegrityChecks(newConfig); + for (const method in result) { + assert(result[method] == false, method); + } + }); + + it('default method value is true', () => { + const result = parseIntegrityChecks(null); + for (const method in result) { + assert(result[method] == true, method); + } + }); + }); }); diff --git a/tests/unit/api/api.js b/tests/unit/api/api.js index b9d860ef0d..78626bd651 100644 --- a/tests/unit/api/api.js +++ b/tests/unit/api/api.js @@ -4,6 +4,7 @@ const api = require('../../../lib/api/api'); const DummyRequest = require('../DummyRequest'); const { default: AuthInfo } = require('arsenal/build/lib/auth/AuthInfo'); const assert = require('assert'); +const crypto = require('crypto'); describe('api.callApiMethod', () => { let sandbox; @@ -49,6 +50,7 @@ describe('api.callApiMethod', () => { sandbox.restore(); }); + it('should attach apiMethod to request', done => { const testMethod = 'bucketGet'; api.callApiMethod(testMethod, request, response, log, () => { @@ -112,4 +114,101 @@ describe('api.callApiMethod', () => { (userInfo, _request, streamingV4Params, log, cb) => cb); api.callApiMethod('multipartDelete', request, response, log); }); + + describe('MD5 checksum validation', () => { + const methodsWithChecksumValidation = [ + 'bucketPutACL', + 'bucketPutCors', + 'bucketPutEncryption', + 'bucketPutLifecycle', + 'bucketPutNotification', + 'bucketPutObjectLock', + 'bucketPutPolicy', + 'bucketPutReplication', + 'bucketPutVersioning', + 'bucketPutWebsite', + 'multiObjectDelete', + 'objectPutACL', + 'objectPutLegalHold', + 'objectPutTagging', + 'objectPutRetention' + ]; + + methodsWithChecksumValidation.forEach(method => { + it(`should return BadDigest for ${method} when bad MD5 checksum is provided`, done => { + const body = ''; + const headers = { + 'content-md5': 'badchecksum123=', // Invalid MD5 + 'content-length': body.length.toString() + }; + + const requestWithBody = new DummyRequest({ + headers, + query: {}, + socket: { remoteAddress: '127.0.0.1', destroy: sandbox.stub() } + }, body); + + sandbox.stub(api, method).callsFake(() => { + done(new Error(`${method} was called despite bad checksum`)); + }); + + api.callApiMethod(method, requestWithBody, response, log, err => { + assert(err, `Expected error for ${method} with bad checksum`); + assert(err.is.BadDigest, `Expected BadDigest error for ${method}, got: ${err.code}`); + done(); + }); + }); + }); + + methodsWithChecksumValidation.forEach(method => { + it(`should succeed for ${method} when correct MD5 checksum is provided`, done => { + const body = ''; + const correctMd5 = crypto.createHash('md5').update(body).digest('base64'); + const headers = { + 'content-md5': correctMd5, + 'content-length': body.length.toString() + }; + + const requestWithBody = new DummyRequest({ + headers, + query: {}, + socket: { remoteAddress: '127.0.0.1', destroy: sandbox.stub() } + }, body); + + sandbox.stub(api, method).callsFake((userInfo, _request, log, cb) => { + cb(); + }); + + api.callApiMethod(method, requestWithBody, response, log, err => { + assert.ifError(err, `Unexpected error for ${method} with good checksum: ${err}`); + done(); + }); + }); + }); + + methodsWithChecksumValidation.forEach(method => { + it(`should fail for ${method} when empty MD5 checksum is provided`, done => { + const body = ''; + const headers = { + 'content-md5': '', + 'content-length': body.length.toString() + }; + + const requestWithBody = new DummyRequest({ + headers, + query: {}, + socket: { remoteAddress: '127.0.0.1', destroy: sandbox.stub() } + }, body); + + sandbox.stub(api, method).callsFake((userInfo, _request, log, cb) => { + cb(); + }); + + api.callApiMethod(method, requestWithBody, response, log, err => { + assert(err, `expected error for ${method} with no checksum`); + done(); + }); + }); + }); + }); }); diff --git a/tests/unit/api/apiUtils/integrity/validateChecksums.js b/tests/unit/api/apiUtils/integrity/validateChecksums.js new file mode 100644 index 0000000000..b3f2d89b99 --- /dev/null +++ b/tests/unit/api/apiUtils/integrity/validateChecksums.js @@ -0,0 +1,276 @@ +const assert = require('assert'); +const crypto = require('crypto'); +const sinon = require('sinon'); + +const { validateChecksumsNoChunking, ChecksumError, validateMethodChecksumNoChunking } = + require('../../../../../lib/api/apiUtils/integrity/validateChecksums'); +const { errors: ArsenalErrors } = require('arsenal'); +const { config } = require('../../../../../lib/Config'); + +describe('validateChecksumsNoChunking', () => { + describe('with valid Content-MD5 header', () => { + it('should return null when MD5 matches the body content', () => { + const body = 'Hello, World!'; + const expectedMd5 = crypto.createHash('md5').update(body, 'utf8').digest('base64'); + const headers = { + 'content-md5': expectedMd5 + }; + + const result = validateChecksumsNoChunking(headers, body); + assert.strictEqual(result, null); + }); + }); + + describe('with MD5 mismatch', () => { + it('should return MD5Mismatch error when checksums do not match', () => { + const body = 'Hello, World!'; + const wrongMd5 = 'wrongchecksum123='; + const expectedMd5 = crypto.createHash('md5').update(body, 'utf8').digest('base64'); + const headers = { + 'content-md5': wrongMd5 + }; + + const result = validateChecksumsNoChunking(headers, body); + assert.strictEqual(result.error, ChecksumError.MD5Mismatch); + assert.strictEqual(result.details.calculated, expectedMd5); + assert.strictEqual(result.details.expected, wrongMd5); + }); + }); + + describe('without Content-MD5 header', () => { + it('should return MissingChecksum error when no content-md5 header is present', () => { + const body = 'Hello, World!'; + const headers = {}; + + const result = validateChecksumsNoChunking(headers, body); + assert.strictEqual(result.error, ChecksumError.MissingChecksum); + assert.strictEqual(result.details, null); + }); + + it('should return MissingChecksum error when headers object is undefined', () => { + const body = 'Hello, World!'; + const headers = undefined; + + const result = validateChecksumsNoChunking(headers, body); + assert.strictEqual(result.error, ChecksumError.MissingChecksum); + assert.strictEqual(result.details, null); + }); + + it('should return MD5Mismatch error when content-md5 header is undefined', () => { + const body = 'Hello, World!'; + const headers = { + 'content-type': 'application/json', + 'content-md5': undefined + }; + const calculatedMD5 = crypto.createHash('md5').update(body, 'utf8').digest('base64'); + + const result = validateChecksumsNoChunking(headers, body); + assert.strictEqual(result.error, ChecksumError.MD5Mismatch); + assert.strictEqual(result.details.calculated, calculatedMD5); + assert.strictEqual(result.details.expected, undefined); + }); + + it('should return MD5Mismatch error when content-md5 header is null', () => { + const body = 'Hello, World!'; + const headers = { + 'content-type': 'application/json', + 'content-md5': null + }; + const calculatedMD5 = crypto.createHash('md5').update(body, 'utf8').digest('base64'); + + const result = validateChecksumsNoChunking(headers, body); + assert.strictEqual(result.error, ChecksumError.MD5Mismatch); + assert.strictEqual(result.details.calculated, calculatedMD5); + assert.strictEqual(result.details.expected, null); + }); + + it('should return MD5Mismatch error when content-md5 header is empty string', () => { + const body = 'Hello, World!'; + const headers = { + 'content-type': 'application/json', + 'content-md5': '' + }; + const calculatedMD5 = crypto.createHash('md5').update(body, 'utf8').digest('base64'); + + const result = validateChecksumsNoChunking(headers, body); + assert.strictEqual(result.error, ChecksumError.MD5Mismatch); + assert.strictEqual(result.details.calculated, calculatedMD5); + assert.strictEqual(result.details.expected, ''); + }); + }); +}); + +describe('validateMethodChecksumNoChunking', () => { + let sandbox; + let originalIntegrityChecks; + + const supportedMethods = [ + 'bucketPutACL', + 'bucketPutCors', + 'bucketPutEncryption', + 'bucketPutLifecycle', + 'bucketPutNotification', + 'bucketPutObjectLock', + 'bucketPutPolicy', + 'bucketPutReplication', + 'bucketPutVersioning', + 'bucketPutWebsite', + 'multiObjectDelete', + 'objectPutACL', + 'objectPutLegalHold', + 'objectPutTagging', + 'objectPutRetention' + ]; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + originalIntegrityChecks = { ...config.integrityChecks }; + }); + + afterEach(() => { + sandbox.restore(); + config.integrityChecks = originalIntegrityChecks; + }); + + describe('when checksum mismatches', () => { + supportedMethods.forEach(method => { + it(`should return BadDigest error for ${method} when checksum mismatch`, () => { + config.integrityChecks[method] = true; + + const body = 'Hello, World!'; + const wrongMd5 = 'wrongchecksum123='; + const request = { + apiMethod: method, + headers: { + 'content-md5': wrongMd5 + } + }; + const log = { debug: sandbox.stub() }; + + const result = validateMethodChecksumNoChunking(request, body, log); + + assert.deepStrictEqual(result, ArsenalErrors.BadDigest, 'Expected BadDigest error'); + assert(log.debug.calledOnce); + }); + }); + }); + + describe('when no checksum is provided', () => { + supportedMethods.forEach(method => { + it(`should return null for ${method} when no checksum is provided`, () => { + config.integrityChecks[method] = true; + + const body = 'Hello, World!'; + const request = { + apiMethod: method, + headers: {} + }; + const log = { debug: sandbox.stub() }; + + const result = validateMethodChecksumNoChunking(request, body, log); + + assert.strictEqual(result, null); + assert(log.debug.notCalled); + }); + }); + }); + + describe('when checksum matches', () => { + supportedMethods.forEach(method => { + it(`should return null for ${method} when checksum matches`, () => { + config.integrityChecks[method] = true; + + const body = 'Hello, World!'; + const correctMd5 = crypto.createHash('md5').update(body, 'utf8').digest('base64'); + const request = { + apiMethod: method, + headers: { + 'content-md5': correctMd5 + } + }; + const log = { debug: sandbox.stub() }; + + const result = validateMethodChecksumNoChunking(request, body, log); + + assert.strictEqual(result, null); + assert(log.debug.notCalled); + }); + }); + }); + + describe('when method is disabled in config', () => { + supportedMethods.forEach(method => { + it(`should return null for ${method} when disabled, even with checksum mismatch`, () => { + config.integrityChecks[method] = false; + + const body = 'Hello, World!'; + const wrongMd5 = 'wrongchecksum123='; + const request = { + apiMethod: method, + headers: { + 'content-md5': wrongMd5 + } + }; + const log = { debug: sandbox.stub() }; + + const result = validateMethodChecksumNoChunking(request, body, log); + + assert.strictEqual(result, null); + assert(log.debug.notCalled); + }); + }); + }); + + describe('when method is not in validation function mapping', () => { + it('should return null for unsupported method even when enabled in config', () => { + const unsupportedMethod = 'someUnsupportedMethod'; + config.integrityChecks[unsupportedMethod] = true; + + const body = 'Hello, World!'; + const wrongMd5 = 'wrongchecksum123='; + const request = { + apiMethod: unsupportedMethod, + headers: { + 'content-md5': wrongMd5 + } + }; + const log = { debug: sandbox.stub() }; + + const result = validateMethodChecksumNoChunking(request, body, log); + + assert.strictEqual(result, null); + assert(log.debug.notCalled); + }); + }); + + describe('edge cases', () => { + it('should return null when request has no apiMethod', () => { + const body = 'Hello, World!'; + const request = { + headers: { + 'content-md5': 'wrongchecksum123=' + } + }; + const log = { debug: sandbox.stub() }; + + const result = validateMethodChecksumNoChunking(request, body, log); + + assert.strictEqual(result, null); + }); + + it('should return null when request apiMethod is undefined in config', () => { + const body = 'Hello, World!'; + const request = { + apiMethod: 'nonExistentMethod', + headers: { + 'content-md5': 'wrongchecksum123=' + } + }; + const log = { debug: sandbox.stub() }; + + const result = validateMethodChecksumNoChunking(request, body, log); + + assert.strictEqual(result, null); + }); + }); +}); diff --git a/tests/unit/api/bucketPutACL.js b/tests/unit/api/bucketPutACL.js index dfc507c2bd..7c08b760ca 100644 --- a/tests/unit/api/bucketPutACL.js +++ b/tests/unit/api/bucketPutACL.js @@ -1,5 +1,4 @@ const assert = require('assert'); - const aclUtils = require('../../../lib/utilities/aclUtils'); const { bucketPut } = require('../../../lib/api/bucketPut'); const bucketPutACL = require('../../../lib/api/bucketPutACL'); diff --git a/tests/unit/api/bucketPutCors.js b/tests/unit/api/bucketPutCors.js index 074532b609..fc9e7bdcf2 100644 --- a/tests/unit/api/bucketPutCors.js +++ b/tests/unit/api/bucketPutCors.js @@ -70,37 +70,6 @@ describe('putBucketCORS API', () => { }); }); - it('should accept request if md5 is omitted', done => { - const corsUtil = new CorsConfigTester(); - const testBucketPutCorsRequest = corsUtil - .createBucketCorsRequest('PUT', bucketName); - testBucketPutCorsRequest.headers['content-md5'] = undefined; - bucketPutCors(authInfo, testBucketPutCorsRequest, log, err => { - if (err) { - process.stdout.write(`Err putting bucket cors ${err}`); - return done(err); - } - return metadata.getBucket(bucketName, log, (err, bucket) => { - if (err) { - process.stdout.write(`Err retrieving bucket MD ${err}`); - return done(err); - } - const uploadedCors = bucket.getCors(); - assert.deepStrictEqual(uploadedCors, corsUtil.getCors()); - return done(); - }); - }); - }); - - it('should reject request if md5 is mismatch', done => { - const corsUtil = new CorsConfigTester(); - const testBucketPutCorsRequest = corsUtil - .createBucketCorsRequest('PUT', bucketName); - testBucketPutCorsRequest.headers['content-md5'] = 'wrong md5'; - _testPutBucketCors(authInfo, testBucketPutCorsRequest, - log, 'BadDigest', done); - }); - it('should return MalformedXML if body greater than 64KB', done => { const corsUtil = new CorsConfigTester(); const body = Buffer.alloc(65537); // 64 * 1024 = 65536 bytes diff --git a/tests/unit/api/multiObjectDelete.js b/tests/unit/api/multiObjectDelete.js index 58de5c05fa..9652b03b4f 100644 --- a/tests/unit/api/multiObjectDelete.js +++ b/tests/unit/api/multiObjectDelete.js @@ -412,85 +412,6 @@ describe('multiObjectDelete function', () => { done(); }); }); - - it('should accept request when content-md5 header is missing', done => { - const post = 'objectname'; - const testObjectKey = 'objectname'; - const testBucketName = 'test-bucket'; - const request = new DummyRequest({ - bucketName: testBucketName, - objectKey: testObjectKey, - parsedHost: 'localhost', - headers: { - // No content-md5 header - }, - post, - socket: { - remoteAddress: '127.0.0.1', - }, - url: `/${testBucketName}`, - }); - - // Use the same canonicalID for both authInfo and bucket owner to avoid AccessDenied - const testAuthInfo = makeAuthInfo(canonicalID); - - // Create bucket with proper ownership - const testBucketRequest = new DummyRequest({ - bucketName: testBucketName, - namespace, - headers: {}, - url: `/${testBucketName}`, - }); - - // Create object to delete - const testObjectRequest = new DummyRequest({ - bucketName: testBucketName, - namespace, - objectKey: testObjectKey, - headers: {}, - url: `/${testBucketName}/${testObjectKey}`, - }, postBody); - - bucketPut(testAuthInfo, testBucketRequest, log, () => { - objectPut(testAuthInfo, testObjectRequest, undefined, log, () => { - multiObjectDelete.multiObjectDelete(testAuthInfo, request, log, (err, res) => { - // Request should succeed even without content-md5 header - assert.strictEqual(err, null); - assert.strictEqual(typeof res, 'string'); - // Should contain successful deletion response - assert.strictEqual(res.includes('objectname'), true); - done(); - }); - }); - }); - }); - - it('should reject request with BadDigest error when content-md5 header mismatches', done => { - const post = 'objectname'; - const incorrectMd5 = 'incorrectMd5Hash'; - - const request = new DummyRequest({ - bucketName: 'bucketname', - objectKey: 'objectname', - parsedHost: 'localhost', - headers: { - 'content-md5': incorrectMd5, - }, - post, - socket: { - remoteAddress: '127.0.0.1', - }, - url: '/bucketname', - }); - const authInfo = makeAuthInfo('123456'); - - multiObjectDelete.multiObjectDelete(authInfo, request, log, (err, res) => { - // Should return BadDigest error for mismatched content-md5 - assert.strictEqual(err.is.BadDigest, true); - assert.strictEqual(res, undefined); - done(); - }); - }); }); describe('multiObjectDelete function', () => { @@ -534,80 +455,4 @@ describe('multiObjectDelete function', () => { done(); }); }); - - it('should accept request when content-md5 header is missing', done => { - const post = 'objectname'; - const testObjectKey = 'objectname'; - const testBucketName = 'test-bucket'; - const request = new DummyRequest({ - bucketName: testBucketName, - objectKey: testObjectKey, - parsedHost: 'localhost', - headers: { - // No content-md5 header - }, - post, - socket: { - remoteAddress: '127.0.0.1', - }, - url: `/${testBucketName}`, - }); - // Use the same canonicalID for both authInfo and bucket owner to avoid AccessDenied - const testAuthInfo = makeAuthInfo(canonicalID); - - // Create bucket with proper ownership - const testBucketRequest = new DummyRequest({ - bucketName: testBucketName, - namespace, - headers: {}, - url: `/${testBucketName}`, - }); - // Create object to delete - const testObjectRequest = new DummyRequest({ - bucketName: testBucketName, - namespace, - objectKey: testObjectKey, - headers: {}, - url: `/${testBucketName}/${testObjectKey}`, - }, postBody); - - bucketPut(testAuthInfo, testBucketRequest, log, () => { - objectPut(testAuthInfo, testObjectRequest, undefined, log, () => { - multiObjectDelete.multiObjectDelete(testAuthInfo, request, log, (err, res) => { - // Request should succeed even without content-md5 header - assert.strictEqual(err, null); - assert.strictEqual(typeof res, 'string'); - // Should contain successful deletion response - assert.strictEqual(res.includes('objectname'), true); - done(); - }); - }); - }); - }); - - it('should reject request with BadDigest error when content-md5 header mismatches', done => { - const post = 'objectname'; - const incorrectMd5 = 'incorrectMd5Hash'; - const request = new DummyRequest({ - bucketName: 'bucketname', - objectKey: 'objectname', - parsedHost: 'localhost', - headers: { - 'content-md5': incorrectMd5, - }, - post, - socket: { - remoteAddress: '127.0.0.1', - }, - url: '/bucketname', - }); - const authInfo = makeAuthInfo('123456'); - - multiObjectDelete.multiObjectDelete(authInfo, request, log, (err, res) => { - // Should return BadDigest error for mismatched content-md5 - assert.strictEqual(err.is.BadDigest, true); - assert.strictEqual(res, undefined); - done(); - }); - }); }); diff --git a/tests/unit/api/objectPutACL.js b/tests/unit/api/objectPutACL.js index a88cc93bc2..5ba89d8908 100644 --- a/tests/unit/api/objectPutACL.js +++ b/tests/unit/api/objectPutACL.js @@ -1,6 +1,5 @@ const assert = require('assert'); const async = require('async'); - const { errorInstances } = require('arsenal'); const AuthInfo = require('arsenal').auth.AuthInfo; diff --git a/tests/unit/api/objectPutRetention.js b/tests/unit/api/objectPutRetention.js index 4bf24cbcea..e0f8b19ae3 100644 --- a/tests/unit/api/objectPutRetention.js +++ b/tests/unit/api/objectPutRetention.js @@ -1,6 +1,5 @@ const assert = require('assert'); const moment = require('moment'); - const { bucketPut } = require('../../../lib/api/bucketPut'); const objectPut = require('../../../lib/api/objectPut'); const objectPutRetention = require('../../../lib/api/objectPutRetention');