diff --git a/lib/api/apiUtils/authorization/permissionChecks.js b/lib/api/apiUtils/authorization/permissionChecks.js index d82a1bc392..ab7c888c9c 100644 --- a/lib/api/apiUtils/authorization/permissionChecks.js +++ b/lib/api/apiUtils/authorization/permissionChecks.js @@ -445,7 +445,8 @@ function _checkPrincipals(canonicalID, arn, principal, bucketOwnerCanonicalID) { // └───────┼──────────────────┘ │ // └──────────────────────────────────────────┘ // -function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, log, request, actionImplicitDenies) { +function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, log, request, actionImplicitDenies, + policyTarget) { let permission = checkBucketPolicyResult.DEFAULT_DENY; // if requester is user within bucket owner account, actions should be // allowed unless explicitly denied (assumes allowed by IAM policy) @@ -454,10 +455,20 @@ function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, l } let copiedStatement = JSON.parse(JSON.stringify(policy.Statement)); + // The policy target, when set, is the resource being authorized when it + // differs from the one the request targets (e.g. the source of a copy) + let bucketName = request?.bucketName; + let objectKey = request?.objectKey; + let query = request?.query; + if (policyTarget) { + bucketName = policyTarget.bucketName; + objectKey = policyTarget.objectKey; + query = Object.assign({}, query, { versionId: policyTarget.versionId }); + } const ip = request ? requestUtils.getClientIp(request, config) : undefined; const isSecure = request ? requestUtils.getHttpProtocolSecurity(request, config) : undefined; - const requestContext = request ? new RequestContext(request.headers, request.query, - request.bucketName, request.objectKey, ip, + const requestContext = request ? new RequestContext(request.headers, query, + bucketName, objectKey, ip, isSecure, request.resourceType, 's3', null, null, null, null, null, null, null, null, null, null, null, request.objectLockRetentionDays) : undefined; @@ -506,7 +517,7 @@ function checkBucketPolicy(policy, requestType, canonicalID, arn, bucketOwner, l } function processBucketPolicy(requestType, bucket, canonicalID, arn, bucketOwner, log, - request, aclPermission, results, actionImplicitDenies) { + request, aclPermission, results, actionImplicitDenies, policyTarget) { const bucketPolicy = bucket.getBucketPolicy(); let processedResult = results[requestType]; let aclRequired = false; @@ -515,7 +526,7 @@ function processBucketPolicy(requestType, bucket, canonicalID, arn, bucketOwner, aclRequired = true; } else { const bucketPolicyPermission = checkBucketPolicy(bucketPolicy, requestType, canonicalID, arn, - bucketOwner, log, request, actionImplicitDenies); + bucketOwner, log, request, actionImplicitDenies, policyTarget); if (bucketPolicyPermission === checkBucketPolicyResult.EXPLICIT_DENY) { processedResult = false; @@ -534,7 +545,7 @@ function processBucketPolicy(requestType, bucket, canonicalID, arn, bucketOwner, } function isBucketAuthorized(bucket, requestTypesInput, canonicalID, authInfo, log, request, - actionImplicitDeniesInput = {}, isWebsite = false) { + actionImplicitDeniesInput = {}, isWebsite = false, policyTarget = undefined) { const requestTypes = Array.isArray(requestTypesInput) ? requestTypesInput : [requestTypesInput]; const actionImplicitDenies = !actionImplicitDeniesInput ? {} : actionImplicitDeniesInput; const mainApiCall = requestTypes[0]; @@ -568,7 +579,7 @@ function isBucketAuthorized(bucket, requestTypesInput, canonicalID, authInfo, lo actionImplicitDenies.objectGet = actionImplicitDenies.objectGet || false; } const { allowed, aclRequired } = processBucketPolicy(_requestType, bucket, canonicalID, arn, - bucket.getOwner(), log, request, aclPermission, results, actionImplicitDenies); + bucket.getOwner(), log, request, aclPermission, results, actionImplicitDenies, policyTarget); if (aclRequired && request?.serverAccessLog) { // eslint-disable-next-line no-param-reassign request.serverAccessLog.aclRequired = 'Yes'; @@ -597,7 +608,7 @@ function evaluateBucketPolicyWithIAM(bucket, requestTypesInput, canonicalID, aut } function isObjAuthorized(bucket, objectMD, requestTypesInput, canonicalID, authInfo, log, request, - actionImplicitDeniesInput = {}, isWebsite = false) { + actionImplicitDeniesInput = {}, isWebsite = false, policyTarget = undefined) { const requestTypes = Array.isArray(requestTypesInput) ? requestTypesInput : [requestTypesInput]; const actionImplicitDenies = !actionImplicitDeniesInput ? {} : actionImplicitDeniesInput; const results = {}; @@ -617,7 +628,7 @@ function isObjAuthorized(bucket, objectMD, requestTypesInput, canonicalID, authI permission = 'objectPut'; } results[_requestType] = isBucketAuthorized(bucket, permission, canonicalID, authInfo, log, request, - actionImplicitDenies, isWebsite); + actionImplicitDenies, isWebsite, policyTarget); // User is already authorized on the bucket for FULL_CONTROL or WRITE or // bucket has canned ACL public-read-write if ((parsedMethodName === 'objectPut' || parsedMethodName === 'objectDelete') @@ -651,7 +662,7 @@ function isObjAuthorized(bucket, objectMD, requestTypesInput, canonicalID, authI const aclPermission = checkObjectAcls(bucket, objectMD, parsedMethodName, canonicalID, requesterIsNotUser, isUserUnauthenticated, mainApiCall); const { allowed, aclRequired } = processBucketPolicy(_requestType, bucket, canonicalID, arn, bucketOwner, - log, request, aclPermission, results, actionImplicitDenies); + log, request, aclPermission, results, actionImplicitDenies, policyTarget); if (aclRequired && request?.serverAccessLog) { // eslint-disable-next-line no-param-reassign request.serverAccessLog.aclRequired = 'Yes'; diff --git a/lib/api/objectCopy.js b/lib/api/objectCopy.js index 6073678bc3..2832974c6e 100644 --- a/lib/api/objectCopy.js +++ b/lib/api/objectCopy.js @@ -242,6 +242,9 @@ function objectCopy(authInfo, request, sourceBucket, sourceObject, sourceVersion */ checkQuota: true, request, + // The request targets the destination, the bucket policy must be + // evaluated against the source + policyTarget: { bucketName: sourceBucket, objectKey: sourceObject, versionId: sourceVersionId }, }; const valPutParams = { authInfo, diff --git a/lib/api/objectPutCopyPart.js b/lib/api/objectPutCopyPart.js index 3f3999b212..ae8fe640de 100644 --- a/lib/api/objectPutCopyPart.js +++ b/lib/api/objectPutCopyPart.js @@ -57,6 +57,9 @@ function objectPutCopyPart(authInfo, request, sourceBucket, */ checkQuota: true, request, + // The request targets the destination, the bucket policy must be + // evaluated against the source + policyTarget: { bucketName: sourceBucket, objectKey: sourceObject, versionId: reqVersionId }, }; const partNumber = Number.parseInt(request.query.partNumber, 10); diff --git a/lib/metadata/metadataUtils.js b/lib/metadata/metadataUtils.js index 35c6e01de6..d21f0a2ff9 100644 --- a/lib/metadata/metadataUtils.js +++ b/lib/metadata/metadataUtils.js @@ -202,6 +202,8 @@ function metadataGetObjects(bucketName, objectsKeys, log, cb) { * @param {string} params.requestType - type of request * @param {string} [params.preciseRequestType] - precise type of request * @param {object} params.request - http request object + * @param {object} [params.policyTarget] - resource to evaluate the bucket + * policy against when it differs from the request target * @param {RequestLogger} log - request logger * @param {object} actionImplicitDenies - identity authorization results * @return {ArsenalError|null} returns a validation error, or null if validation OK @@ -212,7 +214,7 @@ function metadataGetObjects(bucketName, objectsKeys, log, cb) { * - AccessDenied: bucket is not authorized */ function validateBucket(bucket, params, log, actionImplicitDenies = {}) { - const { authInfo, preciseRequestType, request } = params; + const { authInfo, preciseRequestType, request, policyTarget } = params; let requestType = params.requestType; if (bucketShield(bucket, requestType)) { log.debug('bucket is shielded from request', { @@ -249,6 +251,8 @@ function validateBucket(bucket, params, log, actionImplicitDenies = {}) { log, request, actionImplicitDenies, + false, + policyTarget, ) ) { log.debug('access denied for user on bucket', { requestType }); @@ -349,13 +353,19 @@ function checkRateLimitIfNeeded(request, authInfo, bucketMD, log, callback) { * @param {object} [params.serverAccessLogOptions] - options for server access logging * @param {boolean} [params.serverAccessLogOptions.copySource] - if true, stores source bucket * information in request.sourceServerAccessLog for copy operations + * @param {object} [params.policyTarget] - resource to evaluate the bucket policy against + * when it differs from the request target, e.g. the source of a copy + * @param {string} params.policyTarget.bucketName - name of bucket + * @param {string} params.policyTarget.objectKey - name of object + * @param {string} [params.policyTarget.versionId] - version id * @param {boolean} actionImplicitDenies - identity authorization results * @param {RequestLogger} log - request logger * @param {function} callback - callback * @return {undefined} - and call callback with params err, bucket md */ function standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, callback) { - const { authInfo, bucketName, objectKey, versionId, getDeleteMarker, request, withVersionId } = params; + const { authInfo, bucketName, objectKey, versionId, getDeleteMarker, request, withVersionId, policyTarget } = + params; let requestType = params.requestType; if (!Array.isArray(requestType)) { requestType = [requestType]; @@ -433,6 +443,8 @@ function standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, log, request, actionImplicitDenies, + false, + policyTarget, ) ) { log.debug('access denied for user on object', { requestType }); diff --git a/tests/functional/aws-node-sdk/test/object/copySourceBucketPolicy.js b/tests/functional/aws-node-sdk/test/object/copySourceBucketPolicy.js new file mode 100644 index 0000000000..5992bf9ea3 --- /dev/null +++ b/tests/functional/aws-node-sdk/test/object/copySourceBucketPolicy.js @@ -0,0 +1,203 @@ +const assert = require('assert'); +const { + S3Client, + PutObjectCommand, + GetObjectCommand, + CopyObjectCommand, + CreateMultipartUploadCommand, + UploadPartCopyCommand, + AbortMultipartUploadCommand, + PutBucketPolicyCommand, + DeleteBucketPolicyCommand, +} = require('@aws-sdk/client-s3'); +const { + IAMClient, + CreateUserCommand, + CreateAccessKeyCommand, + DeleteAccessKeyCommand, + DeleteUserCommand, +} = require('@aws-sdk/client-iam'); +const { v4: uuid } = require('uuid'); + +const getConfig = require('../support/config'); +const BucketUtility = require('../../lib/utility/bucket-util'); +const { config } = require('../../../../../lib/Config'); + +const bucketName = `copy-source-bp-${uuid()}`; +const userName = `copy-source-bp-user-${uuid()}`; +const forbiddenKey = 'forbidden/myfile'; +const allowedKey = 'allowed/myfile'; +const mpuKey = 'allowed/mpu-copy'; +const allowedContent = 'allowed content'; + +// this test needs a real vault to create an IAM user +const isVaultScality = config.backends.auth !== 'mem'; +const vaultHost = config.vaultd?.host || 'localhost'; + +const describeWithVault = isVaultScality ? describe : describe.skip; + +describeWithVault('Copy source with a bucket policy scoped to key prefixes', () => { + const bucketUtilAccount = new BucketUtility('default'); + const s3ClientAccount = bucketUtilAccount.s3; + + const iamConfig = getConfig('default', { region: 'us-east-1' }); + iamConfig.endpoint = `http://${vaultHost}:8600`; // define outside of getConfig for Integration + const iamClient = new IAMClient(iamConfig); + + let userS3Client; + let accessKeyId; + let uploadId; + + async function assertAccessDenied(promise) { + await assert.rejects(promise, { name: 'AccessDenied' }); + } + + before(async () => { + await bucketUtilAccount.createOne(bucketName); + await s3ClientAccount.send( + new PutObjectCommand({ + Bucket: bucketName, + Key: forbiddenKey, + Body: 'forbidden content', + }), + ); + await s3ClientAccount.send( + new PutObjectCommand({ + Bucket: bucketName, + Key: allowedKey, + Body: allowedContent, + }), + ); + await s3ClientAccount.send( + new PutBucketPolicyCommand({ + Bucket: bucketName, + Policy: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Sid: 'AllowReadWriteOnAllowedPrefix', + Effect: 'Allow', + Principal: '*', + Action: ['s3:GetObject', 's3:PutObject'], + Resource: `arn:aws:s3:::${bucketName}/allowed/*`, + }, + { + Sid: 'DenyReadOnForbiddenPrefix', + Effect: 'Deny', + Principal: '*', + Action: 's3:GetObject', + Resource: `arn:aws:s3:::${bucketName}/forbidden/*`, + }, + ], + }), + }), + ); + + // IAM user without any identity policy + await iamClient.send(new CreateUserCommand({ UserName: userName })); + const accessKeyResponse = await iamClient.send(new CreateAccessKeyCommand({ UserName: userName })); + accessKeyId = accessKeyResponse.AccessKey.AccessKeyId; + userS3Client = new S3Client( + getConfig('default', { + credentials: { + accessKeyId, + secretAccessKey: accessKeyResponse.AccessKey.SecretAccessKey, + }, + }), + ); + + const mpuResponse = await userS3Client.send( + new CreateMultipartUploadCommand({ + Bucket: bucketName, + Key: mpuKey, + }), + ); + uploadId = mpuResponse.UploadId; + }); + + after(async () => { + if (uploadId) { + await s3ClientAccount.send( + new AbortMultipartUploadCommand({ + Bucket: bucketName, + Key: mpuKey, + UploadId: uploadId, + }), + ); + } + await s3ClientAccount.send(new DeleteBucketPolicyCommand({ Bucket: bucketName })); + await bucketUtilAccount.empty(bucketName); + await bucketUtilAccount.deleteOne(bucketName); + if (accessKeyId) { + await iamClient.send(new DeleteAccessKeyCommand({ UserName: userName, AccessKeyId: accessKeyId })); + } + await iamClient.send(new DeleteUserCommand({ UserName: userName })); + }); + + it('should deny GetObject on a source denied by the bucket policy', async () => { + await assertAccessDenied( + userS3Client.send( + new GetObjectCommand({ + Bucket: bucketName, + Key: forbiddenKey, + }), + ), + ); + }); + + it('should deny CopyObject from a source denied by the bucket policy', async () => { + await assertAccessDenied( + userS3Client.send( + new CopyObjectCommand({ + Bucket: bucketName, + Key: 'allowed/myfile-copy', + CopySource: `${bucketName}/${forbiddenKey}`, + }), + ), + ); + }); + + it('should deny UploadPartCopy from a source denied by the bucket policy', async () => { + await assertAccessDenied( + userS3Client.send( + new UploadPartCopyCommand({ + Bucket: bucketName, + Key: mpuKey, + UploadId: uploadId, + PartNumber: 1, + CopySource: `${bucketName}/${forbiddenKey}`, + }), + ), + ); + }); + + it('should allow CopyObject from a source allowed by the bucket policy', async () => { + await userS3Client.send( + new CopyObjectCommand({ + Bucket: bucketName, + Key: 'allowed/allowed-copy', + CopySource: `${bucketName}/${allowedKey}`, + }), + ); + const getResponse = await userS3Client.send( + new GetObjectCommand({ + Bucket: bucketName, + Key: 'allowed/allowed-copy', + }), + ); + assert.strictEqual(await getResponse.Body.transformToString(), allowedContent); + }); + + it('should allow UploadPartCopy from a source allowed by the bucket policy', async () => { + const res = await userS3Client.send( + new UploadPartCopyCommand({ + Bucket: bucketName, + Key: mpuKey, + UploadId: uploadId, + PartNumber: 2, + CopySource: `${bucketName}/${allowedKey}`, + }), + ); + assert(res.CopyPartResult.ETag, 'Expected a part ETag'); + }); +}); diff --git a/tests/unit/api/bucketPolicyAuth.js b/tests/unit/api/bucketPolicyAuth.js index 618f7b8908..16334bdaf0 100644 --- a/tests/unit/api/bucketPolicyAuth.js +++ b/tests/unit/api/bucketPolicyAuth.js @@ -558,6 +558,104 @@ describe('bucket policy authorization', () => { }); }); + describe('bucket policy evaluated against a policy target', () => { + const userAuthInfo = makeAuthInfo(accessKey, 'user'); + const bucketArn = `arn:aws:s3:::${bucket.getName()}`; + const prefixPolicy = { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: '*', + Action: ['s3:GetObject', 's3:PutObject'], + Resource: `${bucketArn}/allowed/*`, + }, + { + Effect: 'Deny', + Principal: '*', + Action: 's3:GetObject', + Resource: `${bucketArn}/forbidden/*`, + }, + ], + }; + const versionIdPolicy = { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: '*', + Action: 's3:GetObject', + Resource: `${bucketArn}/*`, + }, + { + Effect: 'Deny', + Principal: '*', + Action: 's3:GetObject', + Resource: `${bucketArn}/*`, + Condition: { + StringEquals: { 's3:VersionId': 'protected-version' }, + }, + }, + ], + }; + + // Like the source check of a copy: the request targets the + // destination key, the authorization is for another key + function makeCopyRequest() { + return new DummyRequest({ + bucketName: bucket.getName(), + objectKey: 'allowed/myfile-copy', + headers: {}, + query: {}, + socket: {}, + }); + } + + function setPolicy(policyObj) { + bucket.setBucketPolicy(new BucketPolicy(JSON.stringify(policyObj)).getBucketPolicy()); + } + + it('should deny objectGet when the target key matches a Deny statement', () => { + setPolicy(prefixPolicy); + const allowed = isObjAuthorized(bucket, object, 'objectGet', bucketOwnerCanonicalId, + userAuthInfo, log, makeCopyRequest(), { objectGet: true }, false, + { bucketName: bucket.getName(), objectKey: 'forbidden/myfile' }); + assert.strictEqual(allowed, false); + }); + + it('should allow objectGet when the target key matches an Allow statement', () => { + setPolicy(prefixPolicy); + const allowed = isObjAuthorized(bucket, object, 'objectGet', bucketOwnerCanonicalId, + userAuthInfo, log, makeCopyRequest(), { objectGet: true }, false, + { bucketName: bucket.getName(), objectKey: 'allowed/myfile' }); + assert.strictEqual(allowed, true); + }); + + it('should deny bucket-level objectGet when the target key matches a Deny statement', () => { + setPolicy(prefixPolicy); + const allowed = isBucketAuthorized(bucket, 'objectGet', bucketOwnerCanonicalId, + userAuthInfo, log, makeCopyRequest(), { objectGet: true }, false, + { bucketName: bucket.getName(), objectKey: 'forbidden/myfile' }); + assert.strictEqual(allowed, false); + }); + + it('should evaluate s3:VersionId against the target version', () => { + setPolicy(versionIdPolicy); + const allowed = isObjAuthorized(bucket, object, 'objectGet', bucketOwnerCanonicalId, + userAuthInfo, log, makeCopyRequest(), { objectGet: true }, false, + { bucketName: bucket.getName(), objectKey: 'allowed/myfile', versionId: 'protected-version' }); + assert.strictEqual(allowed, false); + }); + + it('should allow objectGet when the target version does not match a Deny condition', () => { + setPolicy(versionIdPolicy); + const allowed = isObjAuthorized(bucket, object, 'objectGet', bucketOwnerCanonicalId, + userAuthInfo, log, makeCopyRequest(), { objectGet: true }, false, + { bucketName: bucket.getName(), objectKey: 'allowed/myfile', versionId: 'other-version' }); + assert.strictEqual(allowed, true); + }); + }); + describe('validate policy resource', () => { resourceTests.forEach(t => { it(`should return ${t.name}`, done => { diff --git a/tests/unit/api/objectCopy.js b/tests/unit/api/objectCopy.js index 526bef3fb8..f29402f237 100644 --- a/tests/unit/api/objectCopy.js +++ b/tests/unit/api/objectCopy.js @@ -152,10 +152,7 @@ describe('objectCopy with versioning', () => { }); }); - // TODO: S3C-9965 - // Skipped because the policy is not checked correctly - // When source bucket policy is checked destination arn is used - it.skip('should set bucketOwnerId if requesting account differs from dest bucket owner', done => { + it('should set bucketOwnerId if requesting account differs from dest bucket owner', done => { const authInfo2 = makeAuthInfo('accessKey2'); const testObjectCopyRequest = _createObjectCopyRequest(destBucketName); const testPutSrcPolicyRequest = new DummyRequest({ @@ -790,3 +787,95 @@ describe('objectCopy source size limit', () => { }); }); }); + +describe('objectCopy with a bucket policy scoped to key prefixes', () => { + const bucketName = 'prefixpolicybucket'; + const userAuthInfo = makeAuthInfo(canonicalID, 'user'); + const putPolicyRequest = new DummyRequest({ + bucketName, + namespace, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: '/', + socket: {}, + post: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: '*', + Action: ['s3:GetObject', 's3:PutObject'], + Resource: `arn:aws:s3:::${bucketName}/allowed/*`, + }, + { + Effect: 'Deny', + Principal: '*', + Action: 's3:GetObject', + Resource: `arn:aws:s3:::${bucketName}/forbidden/*`, + }, + ], + }), + }); + + // An IAM user without identity policy: Vault returns an implicit deny + // for both the source read and the destination write + function _createUserCopyRequest(destKey) { + const request = new DummyRequest({ + bucketName, + namespace, + objectKey: destKey, + headers: {}, + url: `/${bucketName}/${destKey}`, + socket: {}, + }); + request.actionImplicitDenies = { objectGet: true, objectPut: true }; + return request; + } + + before(done => { + cleanup(); + async.series( + [ + callback => bucketPut(authInfo, _createBucketPutRequest(bucketName), log, callback), + callback => bucketPutPolicy(authInfo, putPolicyRequest, log, callback), + callback => + objectPut( + authInfo, + versioningTestUtils.createPutObjectRequest(bucketName, 'forbidden/myfile', objData[0]), + undefined, + log, + callback, + ), + callback => + objectPut( + authInfo, + versioningTestUtils.createPutObjectRequest(bucketName, 'allowed/myfile', objData[1]), + undefined, + log, + callback, + ), + ], + done, + ); + }); + + after(() => { + cleanup(); + }); + + it('should deny copying a source denied by the bucket policy to an allowed destination', done => { + const request = _createUserCopyRequest('allowed/myfile-copy'); + objectCopy(userAuthInfo, request, bucketName, 'forbidden/myfile', undefined, log, err => { + assert(err, 'expected the copy to be denied'); + assert.strictEqual(err.is.AccessDenied, true); + done(); + }); + }); + + it('should allow copying a source allowed by the bucket policy to an allowed destination', done => { + const request = _createUserCopyRequest('allowed/myfile-copy'); + objectCopy(userAuthInfo, request, bucketName, 'allowed/myfile', undefined, log, err => { + assert.ifError(err); + done(); + }); + }); +}); diff --git a/tests/unit/api/objectCopyPart.js b/tests/unit/api/objectCopyPart.js index 46c95a0452..6613d3c147 100644 --- a/tests/unit/api/objectCopyPart.js +++ b/tests/unit/api/objectCopyPart.js @@ -4,6 +4,7 @@ const sinon = require('sinon'); const { parseString } = require('xml2js'); const { storage } = require('arsenal'); const { bucketPut } = require('../../../lib/api/bucketPut'); +const bucketPutPolicy = require('../../../lib/api/bucketPutPolicy'); const objectPut = require('../../../lib/api/objectPut'); const objectPutCopyPart = require('../../../lib/api/objectPutCopyPart'); const initiateMultipartUpload @@ -155,3 +156,108 @@ describe('objectCopyPart', () => { }); }); }); + +describe('objectCopyPart with a bucket policy scoped to key prefixes', () => { + const bucketName = 'prefixpolicybucket'; + const destKey = 'allowed/mpu-copy'; + const userAuthInfo = makeAuthInfo(canonicalID, 'user'); + const putPolicyRequest = new DummyRequest({ + bucketName, + namespace, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: '/', + socket: {}, + post: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: '*', + Action: ['s3:GetObject', 's3:PutObject'], + Resource: `arn:aws:s3:::${bucketName}/allowed/*`, + }, + { + Effect: 'Deny', + Principal: '*', + Action: 's3:GetObject', + Resource: `arn:aws:s3:::${bucketName}/forbidden/*`, + }, + ], + }), + }); + const initiateUserRequest = new DummyRequest({ + bucketName, + namespace, + objectKey: destKey, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: `/${destKey}?uploads`, + socket: {}, + }); + let uploadId; + + // An IAM user without identity policy: Vault returns an implicit deny + // for both the source read and the destination write + function _createUserCopyPartRequest() { + const request = new DummyRequest({ + bucketName, + namespace, + objectKey: destKey, + headers: {}, + url: `/${bucketName}/${destKey}?partNumber=1`, + query: { + partNumber: 1, + uploadId, + }, + socket: {}, + }); + request.actionImplicitDenies = { objectGet: true, objectPut: true }; + return request; + } + + before(done => { + cleanup(); + async.waterfall([ + callback => bucketPut(authInfo, _createBucketPutRequest(bucketName), log, + err => callback(err)), + callback => bucketPutPolicy(authInfo, putPolicyRequest, log, + err => callback(err)), + callback => objectPut(authInfo, + versioningTestUtils.createPutObjectRequest(bucketName, 'forbidden/myfile', Buffer.from('foo', 'utf8')), + undefined, log, err => callback(err)), + callback => objectPut(authInfo, + versioningTestUtils.createPutObjectRequest(bucketName, 'allowed/myfile', Buffer.from('bar', 'utf8')), + undefined, log, err => callback(err)), + callback => initiateMultipartUpload(userAuthInfo, initiateUserRequest, + log, (err, res) => callback(err, res)), + ], (err, res) => { + if (err) { + return done(err); + } + return parseString(res, (err, json) => { + uploadId = json.InitiateMultipartUploadResult.UploadId[0]; + return done(err); + }); + }); + }); + + after(() => { + cleanup(); + }); + + it('should deny copying a part from a source denied by the bucket policy', done => { + objectPutCopyPart(userAuthInfo, _createUserCopyPartRequest(), bucketName, 'forbidden/myfile', + undefined, log, err => { + assert(err, 'expected the part copy to be denied'); + assert.strictEqual(err.is.AccessDenied, true); + done(); + }); + }); + + it('should allow copying a part from a source allowed by the bucket policy', done => { + objectPutCopyPart(userAuthInfo, _createUserCopyPartRequest(), bucketName, 'allowed/myfile', + undefined, log, err => { + assert.ifError(err); + done(); + }); + }); +});