diff --git a/lib/api/bucketGet.js b/lib/api/bucketGet.js index d4e7e1b81b..4651f89f0e 100644 --- a/lib/api/bucketGet.js +++ b/lib/api/bucketGet.js @@ -8,14 +8,9 @@ const escapeForXml = s3middleware.escapeForXml; const { pushMetric } = require('../utapi/utilities'); const versionIdUtils = versioning.VersionID; const monitoring = require('../utilities/monitoringHandler'); -const { generateToken, decryptToken } - = require('../api/apiUtils/object/continueToken'); +const { generateToken, decryptToken } = require('../api/apiUtils/object/continueToken'); -// do not url encode the continuation tokens -const skipUrlEncoding = new Set([ - 'ContinuationToken', - 'NextContinuationToken', -]); +const xmlParamsToSkipUrlEncoding = new Set(['ContinuationToken', 'NextContinuationToken',]); /* Sample XML response for GET bucket objects V2: @@ -122,17 +117,16 @@ function processVersions(bucketName, listParams, list) { { tag: 'IsTruncated', value: isTruncated }, ]; - const escapeXmlFn = listParams.encoding === 'url' ? - querystring.escape : escapeForXml; + const escapeXmlFn = listParams.encoding === 'url' ? querystring.escape : escapeForXml; xmlParams.forEach(p => { if (p.value) { const val = p.tag !== 'NextVersionIdMarker' || p.value === 'null' ? - p.value : versionIdUtils.encode(p.value); + p.value : + versionIdUtils.encode(p.value); xml.push(`<${p.tag}>${escapeXmlFn(val)}`); } }); - let lastKey = listParams.keyMarker ? - escapeXmlFn(listParams.keyMarker) : undefined; + let lastKey = listParams.keyMarker ? escapeXmlFn(listParams.keyMarker) : undefined; list.Versions.forEach(item => { const v = item.value; const objectKey = escapeXmlFn(item.key); @@ -143,7 +137,8 @@ function processVersions(bucketName, listParams, list) { `${objectKey}`, '', (v.IsNull || v.VersionId === undefined) ? - 'null' : versionIdUtils.encode(v.VersionId), + 'null' + : versionIdUtils.encode(v.VersionId), '', `${isLatest}`, `${v.LastModified}`, @@ -182,31 +177,19 @@ function processMasterVersions(bucketName, listParams, list) { ]; if (listParams.v2) { - xmlParams.push( - { tag: 'StartAfter', value: listParams.startAfter || '' }); - xmlParams.push( - { tag: 'FetchOwner', value: `${listParams.fetchOwner}` }); - xmlParams.push({ - tag: 'ContinuationToken', - value: generateToken(listParams.continuationToken) || '', - }); - xmlParams.push({ - tag: 'NextContinuationToken', - value: generateToken(list.NextContinuationToken), - }); - xmlParams.push({ - tag: 'KeyCount', - value: list.Contents ? list.Contents.length : 0, - }); + xmlParams.push({ tag: 'StartAfter', value: listParams.startAfter || '' }); + xmlParams.push({ tag: 'FetchOwner', value: `${listParams.fetchOwner}` }); + xmlParams.push({ tag: 'ContinuationToken', value: generateToken(listParams.continuationToken) || '', }); + xmlParams.push({ tag: 'NextContinuationToken', value: generateToken(list.NextContinuationToken), }); + xmlParams.push({ tag: 'KeyCount', value: list.Contents ? list.Contents.length : 0, }); } else { xmlParams.push({ tag: 'Marker', value: listParams.marker || '' }); xmlParams.push({ tag: 'NextMarker', value: list.NextMarker }); } - const escapeXmlFn = listParams.encoding === 'url' ? - querystring.escape : escapeForXml; + const escapeXmlFn = listParams.encoding === 'url' ? querystring.escape : escapeForXml; xmlParams.forEach(p => { - if (p.value && skipUrlEncoding.has(p.tag)) { + if (p.value && xmlParamsToSkipUrlEncoding.has(p.tag)) { xml.push(`<${p.tag}>${p.value}`); } else if (p.value || p.tag === 'KeyCount' || p.tag === 'MaxKeys') { xml.push(`<${p.tag}>${escapeXmlFn(p.value)}`); @@ -246,15 +229,14 @@ function processMasterVersions(bucketName, listParams, list) { ); }); list.CommonPrefixes.forEach(item => { - const val = escapeXmlFn(item); - xml.push(`${val}`); + xml.push(`${escapeXmlFn(item)}`); }); xml.push(''); + return xml.join(''); } -function handleResult(listParams, requestMaxKeys, encoding, authInfo, - bucketName, list, corsHeaders, log, callback) { +function handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, list, log) { // eslint-disable-next-line no-param-reassign listParams.maxKeys = requestMaxKeys; // eslint-disable-next-line no-param-reassign @@ -267,9 +249,24 @@ function handleResult(listParams, requestMaxKeys, encoding, authInfo, } pushMetric('listBucket', log, { authInfo, bucket: bucketName }); monitoring.promMetrics('GET', bucketName, '200', 'listBucket'); - return callback(null, res, corsHeaders); + return res; } +const validateBucket = (params, denials, log) => new Promise(resolve => { + standardMetadataValidateBucket(params, denials, log, (error, bucket) => { + resolve({ error, bucket }); + }); +}); + +const getObjectListing = (bucketName, listParams, log) => new Promise((resolve, reject) => { + services.getObjectListing(bucketName, listParams, log, (err, list) => { + if (err) { + return reject(err); + } + return resolve(list); + }); +}); + /** * bucketGet - Return list of objects in bucket, supports v1 & v2 * @param {AuthInfo} authInfo - Instance of AuthInfo class with @@ -280,92 +277,86 @@ function handleResult(listParams, requestMaxKeys, encoding, authInfo, * with either error code or xml response body * @return {undefined} */ -function bucketGet(authInfo, request, log, callback) { - const params = request.query; - const bucketName = request.bucketName; - const v2 = params['list-type']; - if (v2 !== undefined && Number.parseInt(v2, 10) !== 2) { - return callback(errorInstances.InvalidArgument.customizeDescription('Invalid ' + - 'List Type specified in Request')); - } - if (v2) { - log.addDefaultFields({ - action: 'ListObjectsV2', - }); - if (request.serverAccessLog) { - // eslint-disable-next-line no-param-reassign - request.serverAccessLog.analyticsAction = 'ListObjectsV2'; +async function bucketGet(authInfo, request, log, callback) { + try { + const params = request.query; + const bucketName = request.bucketName; + const v2 = params['list-type']; + + if (v2 !== undefined && Number.parseInt(v2, 10) !== 2) { + return callback(errorInstances.InvalidArgument.customizeDescription( + 'Invalid List Type specified in Request' + )); } - } else if (params.versions !== undefined) { - log.addDefaultFields({ - action: 'ListObjectVersions', - }); - if (request.serverAccessLog) { - // eslint-disable-next-line no-param-reassign - request.serverAccessLog.analyticsAction = 'ListObjectVersions'; + + if (v2) { + log.addDefaultFields({ action: 'ListObjectsV2', }); + if (request.serverAccessLog) { + // eslint-disable-next-line no-param-reassign + request.serverAccessLog.analyticsAction = 'ListObjectsV2'; + } + } else if (params.versions !== undefined) { + log.addDefaultFields({ action: 'ListObjectVersions', }); + if (request.serverAccessLog) { + // eslint-disable-next-line no-param-reassign + request.serverAccessLog.analyticsAction = 'ListObjectVersions'; + } } - } - log.debug('processing request', { method: 'bucketGet' }); - const encoding = params['encoding-type']; - if (encoding !== undefined && encoding !== 'url') { - monitoring.promMetrics( - 'GET', bucketName, 400, 'listBucket'); - return callback(errorInstances.InvalidArgument.customizeDescription('Invalid ' + - 'Encoding Method specified in Request')); - } - const requestMaxKeys = params['max-keys'] ? - Number.parseInt(params['max-keys'], 10) : 1000; - if (Number.isNaN(requestMaxKeys) || requestMaxKeys < 0) { - monitoring.promMetrics( - 'GET', bucketName, 400, 'listBucket'); - return callback(errors.InvalidArgument); - } - // AWS only returns 1000 keys even if max keys are greater. - // Max keys stated in response xml can be greater than actual - // keys returned. - const actualMaxKeys = Math.min(constants.listingHardLimit, requestMaxKeys); + log.debug('processing request', { method: 'bucketGet' }); + const encoding = params['encoding-type']; + if (encoding !== undefined && encoding !== 'url') { + monitoring.promMetrics('GET', bucketName, 400, 'listBucket'); + return callback(errorInstances.InvalidArgument.customizeDescription( + 'Invalid Encoding Method specified in Request' + )); + } + const requestMaxKeys = params['max-keys'] ? Number.parseInt(params['max-keys'], 10) : 1000; + if (Number.isNaN(requestMaxKeys) || requestMaxKeys < 0) { + monitoring.promMetrics('GET', bucketName, 400, 'listBucket'); + return callback(errors.InvalidArgument); + } + const actualMaxKeys = Math.min(constants.listingHardLimit, requestMaxKeys); - const metadataValParams = { - authInfo, - bucketName, - requestType: request.apiMethods || 'bucketGet', - request, - }; - const listParams = { - listingType: 'DelimiterMaster', - maxKeys: actualMaxKeys, - prefix: params.prefix, - }; + const metadataValParams = { + authInfo, + bucketName, + requestType: request.apiMethods || 'bucketGet', + request, + }; + const listParams = { + listingType: 'DelimiterMaster', + maxKeys: actualMaxKeys, + prefix: params.prefix, + }; - if (params.delimiter) { - listParams.delimiter = params.delimiter; - } + if (params.delimiter) { + listParams.delimiter = params.delimiter; + } - if (v2) { - listParams.v2 = true; - listParams.startAfter = params['start-after']; - listParams.continuationToken = - decryptToken(params['continuation-token']); - listParams.fetchOwner = params['fetch-owner'] === 'true'; - } else { - listParams.marker = params.marker; - } + if (v2) { + listParams.v2 = true; + listParams.startAfter = params['start-after']; + listParams.continuationToken = decryptToken(params['continuation-token']); + listParams.fetchOwner = params['fetch-owner'] === 'true'; + } else { + listParams.marker = params.marker; + } - standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - if (err) { - log.debug('error processing request', { error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'listBucket'); - return callback(err, null, corsHeaders); + const { error, bucket } = await validateBucket(metadataValParams, request.actionImplicitDenies, log); + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); + + if (error) { + log.debug('error processing request', { error }); + monitoring.promMetrics('GET', bucketName, error.code, 'listBucket'); + return callback(error, null, corsHeaders); } if (params.versions !== undefined) { listParams.listingType = 'DelimiterVersions'; delete listParams.marker; listParams.keyMarker = params['key-marker']; listParams.versionIdMarker = params['version-id-marker'] ? - versionIdUtils.decode(params['version-id-marker']) : undefined; + versionIdUtils.decode(params['version-id-marker']) : + undefined; } if (!requestMaxKeys) { const emptyList = { @@ -374,22 +365,25 @@ function bucketGet(authInfo, request, log, callback) { Versions: [], IsTruncated: false, }; - return handleResult(listParams, requestMaxKeys, encoding, authInfo, - bucketName, emptyList, corsHeaders, log, callback); + const res = handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, emptyList, log); + return callback(null, res, corsHeaders); } - return services.getObjectListing(bucketName, listParams, log, - (err, list) => { - if (err) { - log.debug('error processing request', { error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'listBucket'); - return callback(err, null, corsHeaders); - } - return handleResult(listParams, requestMaxKeys, encoding, authInfo, - bucketName, list, corsHeaders, log, callback); - }); - }); - return undefined; + + let list; + try { + list = await getObjectListing(bucketName, listParams, log); + } catch (err) { + log.debug('error processing request', { error: err }); + monitoring.promMetrics('GET', bucketName, err.code, 'listBucket'); + return callback(err, null, corsHeaders); + } + + const res = handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, list, log); + return callback(null, res, corsHeaders); + } catch (err) { + log.error('unhandled error in bucketGet', { error: err }); + return callback(errors.InternalError); + } } module.exports = { @@ -397,3 +391,4 @@ module.exports = { processMasterVersions, bucketGet, }; + diff --git a/lib/api/objectGetLegalHold.js b/lib/api/objectGetLegalHold.js index 2f165748f3..f8f02a079f 100644 --- a/lib/api/objectGetLegalHold.js +++ b/lib/api/objectGetLegalHold.js @@ -1,4 +1,3 @@ -const async = require('async'); const { errors, errorInstances, s3middleware } = require('arsenal'); const { decodeVersionId, getVersionIdResHeader } @@ -15,26 +14,21 @@ const { convertToXml } = s3middleware.objectLegalHold; * @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} + * @return {Promise} - object containing xml and additionalResHeaders */ -function objectGetLegalHold(authInfo, request, log, callback) { +async function objectGetLegalHold(authInfo, request, log, callback) { log.debug('processing request', { method: 'objectGetLegalHold' }); const { bucketName, objectKey, query } = request; const decodedVidResult = decodeVersionId(query); if (decodedVidResult instanceof Error) { - log.trace('invalid versionId query', { - versionId: query.versionId, - error: decodedVidResult, - }); + log.trace('invalid versionId query', { versionId: query.versionId, error: decodedVidResult }); return process.nextTick(() => callback(decodedVidResult)); } const versionId = decodedVidResult; - // FIXME pass 'getDeleteMarker: true' option to set - // 'x-amz-delete-marker' header (see S3C-7592) + // FIXME pass 'getDeleteMarker: true' option to set 'x-amz-delete-marker' header (see S3C-7592) const metadataValParams = { authInfo, bucketName, @@ -44,71 +38,69 @@ function objectGetLegalHold(authInfo, request, log, callback) { request, }; - return async.waterfall([ - next => standardMetadataValidateBucketAndObj(metadataValParams, request.actionImplicitDenies, log, - (err, bucket, objectMD) => { - if (err) { - log.trace('request authorization failed', - { method: 'objectGetLegalHold', error: err }); - return next(err); - } - if (!objectMD) { - const err = versionId ? errors.NoSuchVersion : - errors.NoSuchKey; - log.trace('error no object metadata found', - { method: 'objectGetLegalHold', error: err }); - return next(err, bucket); - } - if (objectMD.isDeleteMarker) { - if (versionId) { - log.trace('requested version is delete marker', - { method: 'objectGetLegalHold' }); - // FIXME we should return a `x-amz-delete-marker: true` header, - // see S3C-7592 - return next(errors.MethodNotAllowed); + let bucket; + let objectMD; + + try { + ({ bucket, objectMD } = await new Promise((resolve, reject) => { + standardMetadataValidateBucketAndObj( + metadataValParams, request.actionImplicitDenies, log, + (err, bucket, objectMD) => { + if (err) { + log.trace('request authorization failed', { method: 'objectGetLegalHold', error: err }); + return reject(err); } - log.trace('most recent version is delete marker', - { method: 'objectGetLegalHold' }); - // FIXME we should return a `x-amz-delete-marker: true` header, - // see S3C-7592 - return next(errors.NoSuchKey); - } - if (!bucket.isObjectLockEnabled()) { - log.trace('object lock not enabled on bucket', - { method: 'objectGetRetention' }); - return next(errorInstances.InvalidRequest.customizeDescription( - 'Bucket is missing Object Lock Configuration')); - } - return next(null, bucket, objectMD); - }), - (bucket, objectMD, next) => { - const { legalHold } = objectMD; - const xml = convertToXml(legalHold); - if (xml === '') { - return next(errors.NoSuchObjectLockConfiguration); + return resolve({ bucket, objectMD }); + }); + })); + + if (!objectMD) { + const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey; + log.trace('error no object metadata found', { method: 'objectGetLegalHold', error: err }); + throw err; + } + + if (objectMD.isDeleteMarker) { + if (versionId) { + log.trace('requested version is delete marker', { method: 'objectGetLegalHold' }); + // FIXME we should return a `x-amz-delete-marker: true` header, see S3C-7592 + throw errors.MethodNotAllowed; } - return next(null, bucket, xml, objectMD); - }, - ], (err, bucket, xml, objectMD) => { - const additionalResHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - if (err) { - log.trace('error processing request', { error: err, - method: 'objectGetLegalHold' }); - } else { - pushMetric('getObjectLegalHold', log, { - authInfo, - bucket: bucketName, - keys: [objectKey], - versionId: objectMD ? objectMD.versionId : undefined, - location: objectMD ? objectMD.dataStoreName : undefined, - }); - const verCfg = bucket.getVersioningConfiguration(); - additionalResHeaders['x-amz-version-id'] = - getVersionIdResHeader(verCfg, objectMD); + + log.trace('most recent version is delete marker', { method: 'objectGetLegalHold' }); + // FIXME we should return a `x-amz-delete-marker: true` header, see S3C-7592 + throw errors.NoSuchKey; + } + + if (!bucket.isObjectLockEnabled()) { + log.trace('object lock not enabled on bucket', { method: 'objectGetRetention' }); + throw errorInstances.InvalidRequest.customizeDescription('Bucket is missing Object Lock Configuration'); } - return callback(err, xml, additionalResHeaders); - }); + + const { legalHold } = objectMD; + const xml = convertToXml(legalHold); + if (xml === '') { + throw errors.NoSuchObjectLockConfiguration; + } + + const additionalResHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); + + pushMetric('getObjectLegalHold', log, { + authInfo, + bucket: bucketName, + keys: [objectKey], + versionId: objectMD ? objectMD.versionId : undefined, + location: objectMD ? objectMD.dataStoreName : undefined, + }); + const verCfg = bucket.getVersioningConfiguration(); + additionalResHeaders['x-amz-version-id'] = getVersionIdResHeader(verCfg, objectMD); + + return callback(null, xml, additionalResHeaders); + } catch (err) { + const additionalResHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); + err.additionalResHeaders = additionalResHeaders; + return callback(err); + } } module.exports = objectGetLegalHold;