diff --git a/lib/api/apiUtils/authorization/prepareRequestContexts.js b/lib/api/apiUtils/authorization/prepareRequestContexts.js index abfe09adce..9098c346fc 100644 --- a/lib/api/apiUtils/authorization/prepareRequestContexts.js +++ b/lib/api/apiUtils/authorization/prepareRequestContexts.js @@ -1,5 +1,6 @@ const { policies } = require('arsenal'); const { config } = require('../../../Config'); +const { hasGovernanceBypassHeader } = require('../object/objectLockHelpers'); const { RequestContext, requestUtils } = policies; let apiMethodAfterVersionCheck; @@ -193,6 +194,11 @@ function prepareRequestContexts(apiMethod, request, sourceBucket, const putObjectLockRequestContext = generateRequestContext('objectPutRetention'); requestContexts.push(putObjectLockRequestContext); + if (hasGovernanceBypassHeader(request.headers)) { + const checkUserGovernanceBypassRequestContext = + generateRequestContext('bypassGovernanceRetention'); + requestContexts.push(checkUserGovernanceBypassRequestContext); + } } if (request.headers['x-amz-version-id']) { const putObjectVersionRequestContext = @@ -200,6 +206,16 @@ function prepareRequestContexts(apiMethod, request, sourceBucket, requestContexts.push(putObjectVersionRequestContext); } } + } else if (apiMethodAfterVersionCheck === 'objectPutRetention' || + apiMethodAfterVersionCheck === 'objectPutRetentionVersion') { + const putRetentionRequestContext = + generateRequestContext(apiMethodAfterVersionCheck); + requestContexts.push(putRetentionRequestContext); + if (hasGovernanceBypassHeader(request.headers)) { + const checkUserGovernanceBypassRequestContext = + generateRequestContext('bypassGovernanceRetention'); + requestContexts.push(checkUserGovernanceBypassRequestContext); + } } else if (apiMethodAfterVersionCheck === 'initiateMultipartUpload' || apiMethodAfterVersionCheck === 'objectPutPart' || apiMethodAfterVersionCheck === 'completeMultipartUpload' @@ -232,11 +248,23 @@ function prepareRequestContexts(apiMethod, request, sourceBucket, generateRequestContext('objectPutTaggingVersion'); requestContexts.push(putObjectVersionRequestContext); } + // AWS only returns an object lock error if a version id + // is specified, else continue to create a delete marker + } else if (sourceVersionId && apiMethodAfterVersionCheck === 'objectDeleteVersion') { + const deleteRequestContext = + generateRequestContext(apiMethodAfterVersionCheck); + requestContexts.push(deleteRequestContext); + if (hasGovernanceBypassHeader(request.headers)) { + const checkUserGovernanceBypassRequestContext = + generateRequestContext('bypassGovernanceRetention'); + requestContexts.push(checkUserGovernanceBypassRequestContext); + } } else { const requestContext = generateRequestContext(apiMethodAfterVersionCheck); requestContexts.push(requestContext); } + return requestContexts; } diff --git a/lib/api/objectDelete.js b/lib/api/objectDelete.js index c1b538752e..d2045821bd 100644 --- a/lib/api/objectDelete.js +++ b/lib/api/objectDelete.js @@ -10,9 +10,8 @@ const { decodeVersionId, preprocessingVersioningDelete } = require('./apiUtils/object/versioning'); const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils'); const monitoring = require('../utilities/monitoringHandler'); -const { hasGovernanceBypassHeader, checkUserGovernanceBypass, ObjectLockInfo } +const { hasGovernanceBypassHeader, ObjectLockInfo } = require('./apiUtils/object/objectLockHelpers'); -const { isRequesterNonAccountUser } = require('./apiUtils/authorization/permissionChecks'); const { config } = require('../Config'); const { _bucketRequiresOplogUpdate } = require('./apiUtils/object/deleteObject'); @@ -50,6 +49,7 @@ function objectDeleteInternal(authInfo, request, log, isExpiration, cb) { return cb(decodedVidResult); } const reqVersionId = decodedVidResult; + const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers); const valParams = { authInfo, @@ -101,25 +101,7 @@ function objectDeleteInternal(authInfo, request, log, isExpiration, cb) { return next(null, bucketMD, objMD); }); }, - function checkGovernanceBypassHeader(bucketMD, objectMD, next) { - // AWS only returns an object lock error if a version id - // is specified, else continue to create a delete marker - if (!reqVersionId) { - return next(null, null, bucketMD, objectMD); - } - const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers); - if (hasGovernanceBypass && isRequesterNonAccountUser(authInfo)) { - return checkUserGovernanceBypass(request, authInfo, bucketMD, objectKey, log, err => { - if (err) { - log.debug('user does not have BypassGovernanceRetention and object is locked'); - return next(err, bucketMD); - } - return next(null, hasGovernanceBypass, bucketMD, objectMD); - }); - } - return next(null, hasGovernanceBypass, bucketMD, objectMD); - }, - function evaluateObjectLockPolicy(hasGovernanceBypass, bucketMD, objectMD, next) { + function evaluateObjectLockPolicy(bucketMD, objectMD, next) { // AWS only returns an object lock error if a version id // is specified, else continue to create a delete marker if (!reqVersionId) { diff --git a/lib/api/objectPutRetention.js b/lib/api/objectPutRetention.js index efdb3376da..793965511e 100644 --- a/lib/api/objectPutRetention.js +++ b/lib/api/objectPutRetention.js @@ -3,14 +3,13 @@ const { errors, s3middleware } = require('arsenal'); const { decodeVersionId, getVersionIdResHeader, getVersionSpecificMetadataOptions } = require('./apiUtils/object/versioning'); -const { ObjectLockInfo, checkUserGovernanceBypass, hasGovernanceBypassHeader } = +const { ObjectLockInfo, hasGovernanceBypassHeader } = require('./apiUtils/object/objectLockHelpers'); const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils'); const { pushMetric } = require('../utapi/utilities'); const getReplicationInfo = require('./apiUtils/object/getReplicationInfo'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); const metadata = require('../metadata/wrapper'); -const { isRequesterNonAccountUser } = require('./apiUtils/authorization/permissionChecks'); const { config } = require('../Config'); const { parseRetentionXml } = s3middleware.retention; @@ -49,6 +48,8 @@ function objectPutRetention(authInfo, request, log, callback) { request, }; + const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers); + return async.waterfall([ next => { log.trace('parsing retention information'); @@ -94,21 +95,6 @@ function objectPutRetention(authInfo, request, log, callback) { return next(null, bucket, retentionInfo, objectMD); }), (bucket, retentionInfo, objectMD, next) => { - const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers); - if (hasGovernanceBypass && isRequesterNonAccountUser(authInfo)) { - return checkUserGovernanceBypass(request, authInfo, bucket, objectKey, log, err => { - if (err) { - if (err.is.AccessDenied) { - log.debug('user does not have BypassGovernanceRetention and object is locked'); - } - return next(err, bucket); - } - return next(null, bucket, retentionInfo, hasGovernanceBypass, objectMD); - }); - } - return next(null, bucket, retentionInfo, hasGovernanceBypass, objectMD); - }, - (bucket, retentionInfo, hasGovernanceBypass, objectMD, next) => { const objLockInfo = new ObjectLockInfo({ mode: objectMD.retentionMode, date: objectMD.retentionDate, diff --git a/package.json b/package.json index 128149104c..173ab962b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "8.8.36", + "version": "8.8.37", "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/api/apiUtils/authorization/prepareRequestContexts.js b/tests/unit/api/apiUtils/authorization/prepareRequestContexts.js index 9a870be6bb..6cf2e3c591 100644 --- a/tests/unit/api/apiUtils/authorization/prepareRequestContexts.js +++ b/tests/unit/api/apiUtils/authorization/prepareRequestContexts.js @@ -150,6 +150,167 @@ describe('prepareRequestContexts', () => { assert.strictEqual(results[2].getAction(), 'scality:GetObjectArchiveInfo'); }); + it('should return s3:PutObjectRetention with header x-amz-object-lock-mode', () => { + const apiMethod = 'objectPut'; + const request = makeRequest({ + 'x-amz-object-lock-mode': 'GOVERNANCE', + 'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 2); + const expectedAction1 = 's3:PutObject'; + const expectedAction2 = 's3:PutObjectRetention'; + assert.strictEqual(results[0].getAction(), expectedAction1); + assert.strictEqual(results[1].getAction(), expectedAction2); + }); + + it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPut ' + + 'with header x-amz-bypass-governance-retention', () => { + const apiMethod = 'objectPut'; + const request = makeRequest({ + 'x-amz-object-lock-mode': 'GOVERNANCE', + 'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z', + 'x-amz-bypass-governance-retention': 'true', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 3); + const expectedAction1 = 's3:PutObject'; + const expectedAction2 = 's3:PutObjectRetention'; + const expectedAction3 = 's3:BypassGovernanceRetention'; + assert.strictEqual(results[0].getAction(), expectedAction1); + assert.strictEqual(results[1].getAction(), expectedAction2); + assert.strictEqual(results[2].getAction(), expectedAction3); + }); + + it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPut ' + + 'with header x-amz-bypass-governance-retention with version id specified', () => { + const apiMethod = 'objectPut'; + const request = makeRequest({ + 'x-amz-object-lock-mode': 'GOVERNANCE', + 'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z', + 'x-amz-bypass-governance-retention': 'true', + }, { + versionId: 'vid1', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 3); + const expectedAction1 = 's3:PutObject'; + const expectedAction2 = 's3:PutObjectRetention'; + const expectedAction3 = 's3:BypassGovernanceRetention'; + assert.strictEqual(results[0].getAction(), expectedAction1); + assert.strictEqual(results[1].getAction(), expectedAction2); + assert.strictEqual(results[2].getAction(), expectedAction3); + }); + + it('should return s3:PutObjectRetention with header x-amz-object-lock-mode for objectPutRetention action', () => { + const apiMethod = 'objectPutRetention'; + const request = makeRequest({ + 'x-amz-object-lock-mode': 'GOVERNANCE', + 'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 1); + const expectedAction = 's3:PutObjectRetention'; + assert.strictEqual(results[0].getAction(), expectedAction); + }); + + it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPutRetention ' + + 'with header x-amz-bypass-governance-retention', () => { + const apiMethod = 'objectPutRetention'; + const request = makeRequest({ + 'x-amz-object-lock-mode': 'GOVERNANCE', + 'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z', + 'x-amz-bypass-governance-retention': 'true', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 2); + const expectedAction1 = 's3:PutObjectRetention'; + const expectedAction2 = 's3:BypassGovernanceRetention'; + assert.strictEqual(results[0].getAction(), expectedAction1); + assert.strictEqual(results[1].getAction(), expectedAction2); + }); + + it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPutRetention ' + + 'with header x-amz-bypass-governance-retention with version id specified', () => { + const apiMethod = 'objectPutRetention'; + const request = makeRequest({ + 'x-amz-object-lock-mode': 'GOVERNANCE', + 'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z', + 'x-amz-bypass-governance-retention': 'true', + }, { + versionId: 'vid1', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 2); + const expectedAction1 = 's3:PutObjectRetention'; + const expectedAction2 = 's3:BypassGovernanceRetention'; + assert.strictEqual(results[0].getAction(), expectedAction1); + assert.strictEqual(results[1].getAction(), expectedAction2); + }); + + it('should return s3:DeleteObject for objectDelete method', () => { + const apiMethod = 'objectDelete'; + const request = makeRequest(); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, + sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].getAction(), 's3:DeleteObject'); + }); + + it('should return s3:DeleteObjectVersion for objectDelete method with version id specified', () => { + const apiMethod = 'objectDelete'; + const request = makeRequest({}, { + versionId: 'vid1', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, + sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].getAction(), 's3:DeleteObjectVersion'); + }); + + // Now it shuld include the bypass header if set + it('should return s3:DeleteObjectVersion and s3:BypassGovernanceRetention for objectDelete method ' + + 'with version id specified and x-amz-bypass-governance-retention header', () => { + const apiMethod = 'objectDelete'; + const request = makeRequest({ + 'x-amz-bypass-governance-retention': 'true', + }, { + versionId: 'vid1', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, + sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 2); + const expectedAction1 = 's3:DeleteObjectVersion'; + const expectedAction2 = 's3:BypassGovernanceRetention'; + assert.strictEqual(results[0].getAction(), expectedAction1); + assert.strictEqual(results[1].getAction(), expectedAction2); + }); + + // When there is no version ID, AWS does not return any error if the object + // is locked, but creates a delete marker + it('should only return s3:DeleteObject for objectDelete method ' + + 'with x-amz-bypass-governance-retention header and no version id', () => { + const apiMethod = 'objectDelete'; + const request = makeRequest({ + 'x-amz-bypass-governance-retention': 'true', + }); + const results = prepareRequestContexts(apiMethod, request, sourceBucket, + sourceObject, sourceVersionId); + + assert.strictEqual(results.length, 1); + const expectedAction = 's3:DeleteObject'; + assert.strictEqual(results[0].getAction(), expectedAction); + }); + ['initiateMultipartUpload', 'objectPutPart', 'completeMultipartUpload'].forEach(apiMethod => { it(`should return s3:PutObjectVersion request context action for ${apiMethod} method ` + 'with x-scal-s3-version-id header', () => {