Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/api/apiUtils/quotas/quotaUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const async = require('async');
const { errors } = require('arsenal');
const monitoring = require('../../../utilities/monitoringHandler');
const {
actionNeedQuotaCheckCopy,
actionNeedQuotaCheck,
actionWithDataDeletion,
} = require('arsenal').policies;
Expand Down Expand Up @@ -253,8 +252,11 @@ function validateQuotas(request, bucket, account, apiNames, apiMethod, inflight,
// meaning the source object, containing the current bytes,
// is checked second. This logic handles these APIs calls by
// ensuring the bytes are positives (i.e., not an object
// replacement).
if (actionNeedQuotaCheckCopy(apiName, apiMethod)) {
// replacement). The source is checked with objectGetVersion
// instead of objectGet when the copy specifies its version.
const isCopySource = (apiName === 'objectGet' || apiName === 'objectGetVersion')
&& (apiMethod === 'objectCopy' || apiMethod === 'objectPutCopyPart');
if (isCopySource) {
// eslint-disable-next-line no-param-reassign
inflight = Math.abs(inflight);
} else if (!actionNeedQuotaCheck[apiName] && !actionWithDataDeletion[apiName]) {
Expand Down
3 changes: 2 additions & 1 deletion lib/api/objectCopy.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ function objectCopy(authInfo, request, sourceBucket, sourceObject, sourceVersion
objectKey: sourceObject,
versionId: sourceVersionId,
getDeleteMarker: true,
requestType: 'objectGet',
// Same action as the one authorized by Vault for the source
requestType: sourceVersionId ? 'objectGetVersion' : 'objectGet',
/**
* Authorization will first check the target object, with an objectPut
* action. But in this context, the source object metadata is still
Expand Down
3 changes: 2 additions & 1 deletion lib/api/objectPutCopyPart.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function objectPutCopyPart(authInfo, request, sourceBucket,
objectKey: sourceObject,
versionId: reqVersionId,
getDeleteMarker: true,
requestType: 'objectGet',
// Same action as the one authorized by Vault for the source
requestType: reqVersionId ? 'objectGetVersion' : 'objectGet',
/**
* Authorization will first check the target object, with an objectPut
* action. But in this context, the source object metadata is still
Expand Down
61 changes: 61 additions & 0 deletions tests/functional/aws-node-sdk/lib/utility/iam-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { S3Client } = require('@aws-sdk/client-s3');
const {
IAMClient,
CreateUserCommand,
CreateAccessKeyCommand,
DeleteAccessKeyCommand,
DeleteUserCommand,
CreatePolicyCommand,
AttachUserPolicyCommand,
DetachUserPolicyCommand,
DeletePolicyCommand,
} = require('@aws-sdk/client-iam');
const { v4: uuid } = require('uuid');

const getConfig = require('../../test/support/config');
const { config } = require('../../../../../lib/Config');

const vaultHost = config.vaultd?.host || 'localhost';

function makeIamClient(profile) {
const iamConfig = getConfig(profile, { region: 'us-east-1' });
iamConfig.endpoint = `http://${vaultHost}:8600`; // define outside of getConfig for Integration
return new IAMClient(iamConfig);
}

// Creates an IAM user with one managed policy, returns its S3 client and a
// function removing the user
async function createUserWithPolicy(iamClient, statements) {
const name = `test-user-${uuid()}`;
const policyResponse = await iamClient.send(
new CreatePolicyCommand({
PolicyName: name,
PolicyDocument: JSON.stringify({ Version: '2012-10-17', Statement: statements }),
}),
);
const policyArn = policyResponse.Policy.Arn;
await iamClient.send(new CreateUserCommand({ UserName: name }));
await iamClient.send(new AttachUserPolicyCommand({ UserName: name, PolicyArn: policyArn }));
const accessKeyResponse = await iamClient.send(new CreateAccessKeyCommand({ UserName: name }));
const { AccessKeyId, SecretAccessKey } = accessKeyResponse.AccessKey;
const s3Client = new S3Client(
getConfig('default', {
credentials: {
accessKeyId: AccessKeyId,
secretAccessKey: SecretAccessKey,
},
}),
);
async function deleteUser() {
await iamClient.send(new DeleteAccessKeyCommand({ UserName: name, AccessKeyId }));
await iamClient.send(new DetachUserPolicyCommand({ UserName: name, PolicyArn: policyArn }));
await iamClient.send(new DeletePolicyCommand({ PolicyArn: policyArn }));
await iamClient.send(new DeleteUserCommand({ UserName: name }));
}
return { s3Client, deleteUser };
}

module.exports = {
makeIamClient,
createUserWithPolicy,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
const assert = require('assert');
const {
PutObjectCommand,
GetObjectCommand,
CopyObjectCommand,
CreateMultipartUploadCommand,
UploadPartCopyCommand,
AbortMultipartUploadCommand,
PutBucketPolicyCommand,
DeleteBucketPolicyCommand,
PutBucketVersioningCommand,
} = require('@aws-sdk/client-s3');
const { v4: uuid } = require('uuid');

const BucketUtility = require('../../lib/utility/bucket-util');
const { makeIamClient, createUserWithPolicy } = require('../../lib/utility/iam-util');
const { config } = require('../../../../../lib/Config');

// this test needs a real vault to create IAM users
const isVaultScality = config.backends.auth !== 'mem';

const describeWithVault = isVaultScality ? describe : describe.skip;

describeWithVault('Copy of a specific source version', () => {
const secretKey = 'secret';

async function assertAccessDenied(promise) {
await assert.rejects(promise, { name: 'AccessDenied' });
}

describe('in the same account, without IAM permission to read the source', () => {
const ownBucketName = `copy-source-own-${uuid()}`;
const bucketUtil = new BucketUtility('default');
const iamClient = makeIamClient('default');
const mpuKey = 'mine/mpu-copy';
let user;
let uploadId;

before(async () => {
await bucketUtil.createOne(ownBucketName);
await bucketUtil.s3.send(
new PutObjectCommand({
Bucket: ownBucketName,
Key: secretKey,
Body: 'secret content',
}),
);
user = await createUserWithPolicy(iamClient, [
{
Effect: 'Allow',
Action: ['s3:GetObject', 's3:PutObject'],
Resource: `arn:aws:s3:::${ownBucketName}/mine/*`,
},
]);
const mpuResponse = await user.s3Client.send(
new CreateMultipartUploadCommand({
Bucket: ownBucketName,
Key: mpuKey,
}),
);
uploadId = mpuResponse.UploadId;
});

after(async () => {
if (uploadId) {
await bucketUtil.s3.send(
new AbortMultipartUploadCommand({
Bucket: ownBucketName,
Key: mpuKey,
UploadId: uploadId,
}),
);
}
await bucketUtil.empty(ownBucketName);
await bucketUtil.deleteOne(ownBucketName);
if (user) {
await user.deleteUser();
}
});

it('should deny CopyObject from the latest version', async () => {
await assertAccessDenied(
user.s3Client.send(
new CopyObjectCommand({
Bucket: ownBucketName,
Key: 'mine/copy',
CopySource: `${ownBucketName}/${secretKey}`,
}),
),
);
});

it('should deny CopyObject from the null version', async () => {
await assertAccessDenied(
user.s3Client.send(
new CopyObjectCommand({
Bucket: ownBucketName,
Key: 'mine/copy',
CopySource: `${ownBucketName}/${secretKey}?versionId=null`,
}),
),
);
});

it('should deny UploadPartCopy from the null version', async () => {
await assertAccessDenied(
user.s3Client.send(
new UploadPartCopyCommand({
Bucket: ownBucketName,
Key: mpuKey,
UploadId: uploadId,
PartNumber: 1,
CopySource: `${ownBucketName}/${secretKey}?versionId=null`,
}),
),
);
});
});

describe('with a bucket policy denying s3:GetObjectVersion', () => {
const policyBucketName = `copy-source-policy-${uuid()}`;
const bucketUtil = new BucketUtility('default');
const iamClient = makeIamClient('default');
const latestContent = 'latest content';
let user;
let oldVersionId;

before(async () => {
await bucketUtil.createOne(policyBucketName);
await bucketUtil.s3.send(
new PutBucketVersioningCommand({
Bucket: policyBucketName,
VersioningConfiguration: { Status: 'Enabled' },
}),
);
const oldPut = await bucketUtil.s3.send(
new PutObjectCommand({
Bucket: policyBucketName,
Key: secretKey,
Body: 'old content',
}),
);
oldVersionId = oldPut.VersionId;
await bucketUtil.s3.send(
new PutObjectCommand({
Bucket: policyBucketName,
Key: secretKey,
Body: latestContent,
}),
);
await bucketUtil.s3.send(
new PutBucketPolicyCommand({
Bucket: policyBucketName,
Policy: JSON.stringify({
Version: '2012-10-17',
Statement: [
{
Effect: 'Deny',
Principal: '*',
Action: 's3:GetObjectVersion',
Resource: `arn:aws:s3:::${policyBucketName}/*`,
},
],
}),
}),
);
user = await createUserWithPolicy(iamClient, [
{
Effect: 'Allow',
Action: ['s3:GetObject', 's3:GetObjectVersion', 's3:PutObject'],
Resource: `arn:aws:s3:::${policyBucketName}/*`,
},
]);
});

after(async () => {
await bucketUtil.s3.send(new DeleteBucketPolicyCommand({ Bucket: policyBucketName }));
await bucketUtil.empty(policyBucketName);
await bucketUtil.deleteOne(policyBucketName);
if (user) {
await user.deleteUser();
}
});

it('should deny GetObject of an older version', async () => {
await assertAccessDenied(
user.s3Client.send(
new GetObjectCommand({
Bucket: policyBucketName,
Key: secretKey,
VersionId: oldVersionId,
}),
),
);
});

it('should deny CopyObject from an older version', async () => {
await assertAccessDenied(
user.s3Client.send(
new CopyObjectCommand({
Bucket: policyBucketName,
Key: 'leak',
CopySource: `${policyBucketName}/${secretKey}?versionId=${oldVersionId}`,
}),
),
);
});

it('should allow CopyObject from the latest version', async () => {
await user.s3Client.send(
new CopyObjectCommand({
Bucket: policyBucketName,
Key: 'latest-copy',
CopySource: `${policyBucketName}/${secretKey}`,
}),
);
const getResponse = await user.s3Client.send(
new GetObjectCommand({
Bucket: policyBucketName,
Key: 'latest-copy',
}),
);
assert.strictEqual(await getResponse.Body.transformToString(), latestContent);
});
});
});
13 changes: 13 additions & 0 deletions tests/unit/api/apiUtils/quotas/quotaUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ describe('validateQuotas (buckets)', () => {
});
});

['objectCopy', 'objectPutCopyPart'].forEach(apiMethod => {
it(`should return errors.QuotaExceeded if quota is exceeded by a ${apiMethod} of a source version`, done => {
QuotaService._getLatestMetricsCallback.yields(null, { bytesTotal: BigInt(150) });

validateQuotas(request, mockBucket, {}, ['objectGetVersion'], apiMethod, 1, false, mockLog, err => {
assert(err, 'expected the quota to be evaluated');
assert.strictEqual(err.is.QuotaExceeded, true);
assert.strictEqual(QuotaService._getLatestMetricsCallback.callCount, 1);
done();
});
});
});

it('should not return QuotaExceeded if quotas are exceeded but operation is creating a delete marker', done => {
const result1 = {
bytesTotal: BigInt(150),
Expand Down
Loading
Loading