diff --git a/lib/api/apiUtils/object/getReplicationBackendDataLocator.js b/lib/api/apiUtils/object/getReplicationBackendDataLocator.js new file mode 100644 index 0000000000..986fe3012d --- /dev/null +++ b/lib/api/apiUtils/object/getReplicationBackendDataLocator.js @@ -0,0 +1,45 @@ +const { errors } = require('arsenal'); + +/** + * getReplicationBackendDataLocator - compares given location constraint to + * replication backends + * @param {object} locationObj - object containing location information + * @param {string} locationObj.location - name of location constraint + * @param {string} locationObj.key - keyname of object in location constraint + * @param {string} locationObj.locationType - type of location constraint + * @param {object} replicationInfo - information about object replication + * @param {array} replicationInfo.backends - array containing information about + * each replication location + * @param {string} replicationInfo.backends[].site - name of replication + * location + * @param {string} replicationInfo.backends[].status - status of replication + * @param {string} replicationInfo.backends[].dataStoreVersionId - version id + * of object at replication location + * @return {object} contains error if no replication backend matches or + * dataLocator object + */ +function getReplicationBackendDataLocator(locationObj, replicationInfo) { + const repBackendResult = {}; + const locMatch = replicationInfo.backends.find( + backend => backend.site === locationObj.location); + if (!locMatch) { + repBackendResult.error = errors.InvalidLocationConstraint. + customizeDescription('Object is not replicated to location ' + + 'passed in location header'); + return repBackendResult; + } + if (['PENDING', 'FAILED'].includes(locMatch.status)) { + repBackendResult.error = errors.NoSuchKey.customizeDescription( + `Object replication to specified backend is ${locMatch.status}`); + repBackendResult.status = locMatch.status; + return repBackendResult; + } + repBackendResult.dataLocator = [{ + key: locationObj.key, + dataStoreName: locationObj.location, + dataStoreType: locationObj.locationType, + dataStoreVersionId: locMatch.dataStoreVersionId }]; + return repBackendResult; +} + +module.exports = getReplicationBackendDataLocator; diff --git a/lib/api/apiUtils/object/locationHeaderCheck.js b/lib/api/apiUtils/object/locationHeaderCheck.js new file mode 100644 index 0000000000..c5312d2a28 --- /dev/null +++ b/lib/api/apiUtils/object/locationHeaderCheck.js @@ -0,0 +1,37 @@ +const { errors } = require('arsenal'); + +const { config } = require('../../../Config'); + +/** + * locationHeaderCheck - compares 'x-amz-location-constraint' header + * to location constraints in config + * @param {object} headers - request headers + * @param {string} objectKey - key name of object + * @param {string} bucketName - name of bucket + * @return {undefined|Object} returns error, object, or undefined + * @return {string} return.location - name of location constraint + * @return {string} return.key - name of object at location constraint + * @return {string} - return.locationType - type of location constraint + */ +function locationHeaderCheck(headers, objectKey, bucketName) { + const location = headers['x-amz-location-constraint']; + if (location) { + const validLocation = config.locationConstraints[location]; + if (!validLocation) { + return errors.InvalidLocationConstraint.customizeDescription( + 'Invalid location constraint specified in header'); + } + const bucketMatch = validLocation.details.bucketMatch; + const backendKey = bucketMatch ? objectKey : + `${bucketName}/${objectKey}`; + return { + location, + key: backendKey, + locationType: validLocation.type, + }; + } + // no location header was passed + return undefined; +} + +module.exports = locationHeaderCheck; diff --git a/lib/api/objectGet.js b/lib/api/objectGet.js index 780af3e844..283b3a3cea 100644 --- a/lib/api/objectGet.js +++ b/lib/api/objectGet.js @@ -9,6 +9,10 @@ const collectResponseHeaders = require('../utilities/collectResponseHeaders'); const { pushMetric } = require('../utapi/utilities'); const { getVersionIdResHeader } = require('./apiUtils/object/versioning'); const setPartRanges = require('./apiUtils/object/setPartRanges'); +const locationHeaderCheck = + require('./apiUtils/object/locationHeaderCheck'); +const getReplicationBackendDataLocator = + require('./apiUtils/object/getReplicationBackendDataLocator'); const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils'); const validateHeaders = s3middleware.validateConditionalHeaders; @@ -27,6 +31,17 @@ function objectGet(authInfo, request, returnTagCount, log, callback) { const bucketName = request.bucketName; const objectKey = request.objectKey; + // returns name of location to get from and key if successful + const locCheckResult = + locationHeaderCheck(request.headers, objectKey, bucketName); + if (locCheckResult instanceof Error) { + log.trace('invalid location constraint to get from', { + location: request.headers['x-amz-location-constraint'], + error: locCheckResult, + }); + return callback(locCheckResult); + } + const decodedVidResult = decodeVersionId(request.query); if (decodedVidResult instanceof Error) { log.trace('invalid versionId query', { @@ -113,6 +128,19 @@ function objectGet(authInfo, request, returnTagCount, log, callback) { // objMD.location is just a string dataLocator = Array.isArray(objMD.location) ? objMD.location : [{ key: objMD.location }]; + + if (locCheckResult) { + const repBackendResult = getReplicationBackendDataLocator( + locCheckResult, objMD.replicationInfo); + if (repBackendResult.error) { + log.error('Error with location constraint header', { + error: repBackendResult.error, + status: repBackendResult.status, + }); + return callback(repBackendResult.error, null, corsHeaders); + } + dataLocator = repBackendResult.dataLocator; + } // if the data backend is azure, there will only ever be at // most one item in the dataLocator array if (dataLocator[0] && dataLocator[0].dataStoreType === 'azure') { diff --git a/tests/unit/multipleBackend/getReplicationBackendDataLocator.js b/tests/unit/multipleBackend/getReplicationBackendDataLocator.js new file mode 100644 index 0000000000..0ff16e2e58 --- /dev/null +++ b/tests/unit/multipleBackend/getReplicationBackendDataLocator.js @@ -0,0 +1,53 @@ +const assert = require('assert'); + +const getReplicationBackendDataLocator = require( + '../../../lib/api/apiUtils/object/getReplicationBackendDataLocator'); + +const locCheckResult = { + location: 'spoofbackend', + key: 'spoofkey', + locationType: 'spoof', +}; +const repNoMatch = { backends: [{ site: 'nomatch' }] }; +const repMatchPending = { backends: + [{ site: 'spoofbackend', status: 'PENDING', dataVersionId: '' }] }; +const repMatchFailed = { backends: + [{ site: 'spoofbackend', status: 'FAILED', dataVersionId: '' }] }; +const repMatch = { backends: [{ + site: 'spoofbackend', + status: 'COMPLETE', + dataStoreVersionId: 'spoofid' }], +}; +const expDataLocator = [{ + key: locCheckResult.key, + dataStoreName: locCheckResult.location, + dataStoreType: locCheckResult.locationType, + dataStoreVersionId: repMatch.backends[0].dataStoreVersionId, +}]; + + +describe('Replication Backend Compare', () => { + it('should return error if no match in replication backends', () => { + const repBackendResult = + getReplicationBackendDataLocator(locCheckResult, repNoMatch); + assert(repBackendResult.error.InvalidLocationConstraint); + }); + it('should return error if backend status is PENDING', () => { + const repBackendResult = + getReplicationBackendDataLocator(locCheckResult, repMatchPending); + assert(repBackendResult.error.NoSuchKey); + assert.strictEqual(repBackendResult.status, 'PENDING'); + }); + it('should return error if backend status is FAILED', () => { + const repBackendResult = + getReplicationBackendDataLocator(locCheckResult, repMatchFailed); + assert(repBackendResult.error.NoSuchKey); + assert.strictEqual(repBackendResult.status, 'FAILED'); + }); + it('should return dataLocator obj if backend matches and rep is complete', + () => { + const repBackendResult = + getReplicationBackendDataLocator(locCheckResult, repMatch); + assert.deepStrictEqual(repBackendResult.dataLocator, expDataLocator); + }); +}); diff --git a/tests/unit/multipleBackend/locationConstraintCheck.js b/tests/unit/multipleBackend/locationConstraintCheck.js index d249b38769..34c98f9a30 100644 --- a/tests/unit/multipleBackend/locationConstraintCheck.js +++ b/tests/unit/multipleBackend/locationConstraintCheck.js @@ -1,4 +1,5 @@ const assert = require('assert'); + const { BackendInfo } = require('../../../lib/api/apiUtils/object/BackendInfo'); const BucketInfo = require('arsenal').models.BucketInfo; const DummyRequest = require('../DummyRequest'); diff --git a/tests/unit/multipleBackend/locationHeaderCheck.js b/tests/unit/multipleBackend/locationHeaderCheck.js new file mode 100644 index 0000000000..6db3f02908 --- /dev/null +++ b/tests/unit/multipleBackend/locationHeaderCheck.js @@ -0,0 +1,45 @@ +const assert = require('assert'); +const { errors } = require('arsenal'); + +const locationHeaderCheck = + require('../../../lib/api/apiUtils/object/locationHeaderCheck'); + +const objectKey = 'locationHeaderCheckObject'; +const bucketName = 'locationHeaderCheckBucket'; + +const testCases = [ + { + location: 'doesnotexist', + expRes: errors.InvalidLocationConstraint.customizeDescription( + 'Invalid location constraint specified in header'), + }, { + location: '', + expRes: undefined, + }, { + location: 'awsbackend', + expRes: { + location: 'awsbackend', + key: objectKey, + locationType: 'aws_s3', + }, + }, { + location: 'awsbackendmismatch', + expRes: { + location: 'awsbackendmismatch', + key: `${bucketName}/${objectKey}`, + locationType: 'aws_s3', + }, + }, +]; + +describe('Location Header Check', () => { + testCases.forEach(test => { + it('should return expected result with location constraint header ' + + `set to ${test.location}`, () => { + const headers = { 'x-amz-location-constraint': `${test.location}` }; + const checkRes = + locationHeaderCheck(headers, objectKey, bucketName); + assert.deepStrictEqual(checkRes, test.expRes); + }); + }); +}); diff --git a/tests/unit/utils/proxyCompareURL.js b/tests/unit/utils/proxyCompareURL.js index 88ac13824b..9500e9a934 100644 --- a/tests/unit/utils/proxyCompareURL.js +++ b/tests/unit/utils/proxyCompareURL.js @@ -42,11 +42,10 @@ const testCases = [ describe('proxyCompareURL util function', () => { testCases.forEach(test => { - it(`should return ${test.expRes} if ${test.desc}`, done => { + it(`should return ${test.expRes} if ${test.desc}`, () => { process.env.NO_PROXY = test.noProxy; const proxyMatch = proxyCompareUrl(test.endpoint); assert.strictEqual(test.expRes, proxyMatch); - done(); }); });