Skip to content
Merged
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
5 changes: 5 additions & 0 deletions lib/api/apiUtils/integrity/validateChecksums.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function uint32ToBase64(num) {

const algorithms = Object.freeze({
crc64nvme: {
getObjectAttributesXMLTag: 'ChecksumCRC64NVME',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we export these strings as constants, if used in other projects as well, arsenal would be a good place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are not going to be used in other projects

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://scality.atlassian.net/browse/ARSN-564 I moved the xml creation to cloudserver need to remove it from arsenal

digest: async data => {
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
const crc = new CrtCrc64Nvme();
Expand All @@ -84,6 +85,7 @@ const algorithms = Object.freeze({
createHash: () => new CrtCrc64Nvme()
},
crc32: {
getObjectAttributesXMLTag: 'ChecksumCRC32',
digest: data => {
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
return uint32ToBase64(new Crc32().update(input).digest() >>> 0); // >>> 0 coerce number to uint32
Expand All @@ -96,6 +98,7 @@ const algorithms = Object.freeze({
createHash: () => new Crc32()
},
crc32c: {
getObjectAttributesXMLTag: 'ChecksumCRC32C',
digest: data => {
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
return uint32ToBase64(new Crc32c().update(input).digest() >>> 0); // >>> 0 coerce number to uint32
Expand All @@ -105,6 +108,7 @@ const algorithms = Object.freeze({
createHash: () => new Crc32c()
},
sha1: {
getObjectAttributesXMLTag: 'ChecksumSHA1',
digest: data => {
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
return crypto.createHash('sha1').update(input).digest('base64');
Expand All @@ -114,6 +118,7 @@ const algorithms = Object.freeze({
createHash: () => crypto.createHash('sha1')
},
sha256: {
getObjectAttributesXMLTag: 'ChecksumSHA256',
digest: data => {
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
return crypto.createHash('sha256').update(input).digest('base64');
Expand Down
112 changes: 67 additions & 45 deletions lib/api/apiUtils/object/objectAttributes.js
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This file is using 2-space indentation. Please switch to 4 spaces to match our formatting rules

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { errorInstances } = require('arsenal');
const { getPartCountFromMd5 } = require('./partInfo');
const { algorithms } = require('../integrity/validateChecksums');

/**
* Parse and validate attribute headers from a request.
Expand Down Expand Up @@ -53,59 +54,80 @@ function parseAttributesHeaders(headers, headerName, supportedAttributes) {
* @param {Object.<string, any>} userMetadata - Key-value pairs of user-defined metadata.
* @param {string[]} requestedAttrs - A list of specific attributes to include in the output.
* Supports 'ETag', 'ObjectParts', 'StorageClass', 'ObjectSize',
* 'RestoreStatus', and 'x-amz-meta-*' for all user metadata.
* 'Checksum', 'RestoreStatus', and 'x-amz-meta-*' for all user metadata.
* @param {string[]} xml - The string array acting as the output buffer/collector.
* @param {object} log - Werelogs logger.
* @returns {void} - this function does not return a value, it mutates the `xml` param.
*/
function buildAttributesXml(objectMD, userMetadata, requestedAttrs, xml) {
const customAttributes = new Set();
for (const attribute of requestedAttrs) {
switch (attribute) {
case 'ETag':
xml.push(`<ETag>${objectMD['content-md5']}</ETag>`);
break;
case 'ObjectParts': {
const partCount = getPartCountFromMd5(objectMD);
if (partCount) {
xml.push(
'<ObjectParts>',
`<PartsCount>${partCount}</PartsCount>`,
'</ObjectParts>',
);
}
break;
}
case 'StorageClass':
xml.push(`<StorageClass>${objectMD['x-amz-storage-class']}</StorageClass>`);
break;
case 'ObjectSize':
xml.push(`<ObjectSize>${objectMD['content-length']}</ObjectSize>`);
break;
case 'RestoreStatus':
xml.push('<RestoreStatus>');
xml.push(`<IsRestoreInProgress>${!!objectMD.restoreStatus?.inProgress}</IsRestoreInProgress>`);
function buildAttributesXml(objectMD, userMetadata, requestedAttrs, xml, log) {
const customAttributes = new Set();
for (const attribute of requestedAttrs) {
switch (attribute) {
case 'ETag':
xml.push(`<ETag>${objectMD['content-md5']}</ETag>`);
break;
case 'ObjectParts': {
const partCount = getPartCountFromMd5(objectMD);
if (partCount) {
xml.push(
'<ObjectParts>',
`<PartsCount>${partCount}</PartsCount>`,
'</ObjectParts>',
);
}
break;
}
case 'StorageClass':
xml.push(`<StorageClass>${objectMD['x-amz-storage-class']}</StorageClass>`);
break;
case 'ObjectSize':
xml.push(`<ObjectSize>${objectMD['content-length']}</ObjectSize>`);
break;
case 'Checksum': {
const { checksum } = objectMD;
if (checksum) {
const algo = algorithms[checksum.checksumAlgorithm];
if (!algo) {
log.error('unknown checksum algorithm in object metadata', {
checksumAlgorithm: checksum.checksumAlgorithm,
});
break;
}
const tag = algo.getObjectAttributesXMLTag;
xml.push(
'<Checksum>',
`<${tag}>${checksum.checksumValue}</${tag}>`,
`<ChecksumType>${checksum.checksumType}</ChecksumType>`,
'</Checksum>',
);
}
break;
}
case 'RestoreStatus':
xml.push('<RestoreStatus>');
xml.push(`<IsRestoreInProgress>${!!objectMD.restoreStatus?.inProgress}</IsRestoreInProgress>`);

if (objectMD.restoreStatus?.expiryDate) {
xml.push(`<RestoreExpiryDate>${objectMD.restoreStatus?.expiryDate}</RestoreExpiryDate>`);
}
if (objectMD.restoreStatus?.expiryDate) {
xml.push(`<RestoreExpiryDate>${objectMD.restoreStatus?.expiryDate}</RestoreExpiryDate>`);
}

xml.push('</RestoreStatus>');
break;
case 'x-amz-meta-*':
for (const key of Object.keys(userMetadata)) {
customAttributes.add(key);
}
break;
default:
if (userMetadata[attribute]) {
customAttributes.add(attribute);
xml.push('</RestoreStatus>');
break;
case 'x-amz-meta-*':
for (const key of Object.keys(userMetadata)) {
customAttributes.add(key);
}
break;
default:
if (userMetadata[attribute]) {
customAttributes.add(attribute);
}
}
}
}

for (const key of customAttributes) {
xml.push(`<${key}>${userMetadata[key]}</${key}>`);
}
for (const key of customAttributes) {
xml.push(`<${key}>${userMetadata[key]}</${key}>`);
}
}

module.exports = {
Expand Down
19 changes: 4 additions & 15 deletions lib/api/objectGetAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes';
* buildXmlResponse - Build XML response for GetObjectAttributes
* @param {object} objMD - object metadata
* @param {Set<string>} requestedAttrs - set of requested attribute names
* @param {object} log - Werelogs logger
* @returns {string} XML response
*/
function buildXmlResponse(objMD, requestedAttrs) {
function buildXmlResponse(objMD, requestedAttrs, log) {
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<GetObjectAttributesResponse>',
];

const userMetadata = getUserMetadata(objMD);
buildAttributesXml(objMD, userMetadata, requestedAttrs, xml);
buildAttributesXml(objMD, userMetadata, requestedAttrs, xml, log);

xml.push('</GetObjectAttributesResponse>');
return xml.join('');
Expand Down Expand Up @@ -150,18 +151,6 @@ async function objectGetAttributes(authInfo, request, log, callback) {
true,
);

if (requestedAttrs.has('Checksum')) {
log.debug('Checksum attribute requested but not implemented', {
method: OBJECT_GET_ATTRIBUTES,
bucket: bucketName,
key: objectKey,
versionId,
});
const err = errors.NotImplemented.customizeDescription('Checksum attribute is not implemented');
err.responseHeaders = responseHeaders;
throw err;
}

pushMetric(OBJECT_GET_ATTRIBUTES, log, {
authInfo,
bucket: bucketName,
Expand All @@ -170,7 +159,7 @@ async function objectGetAttributes(authInfo, request, log, callback) {
location: objectMD?.dataStoreName,
});

const xml = buildXmlResponse(objectMD, requestedAttrs);
const xml = buildXmlResponse(objectMD, requestedAttrs, log);
return { xml, responseHeaders };
}

Expand Down
129 changes: 116 additions & 13 deletions tests/functional/aws-node-sdk/test/object/objectGetAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
const { GetObjectAttributesExtendedCommand } = require('@scality/cloudserverclient');
const withV4 = require('../support/withV4');
const BucketUtility = require('../../lib/utility/bucket-util');
const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums');

const bucket = 'testbucket';
const key = 'testobject';
Expand All @@ -30,7 +31,9 @@ describe('objectGetAttributes', () => {

beforeEach(async () => {
await s3.send(new CreateBucketCommand({ Bucket: bucket }));
await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body }));
await s3.send(new PutObjectCommand({
Bucket: bucket, Key: key, Body: body, ChecksumAlgorithm: 'CRC64NVME',
}));
});

afterEach(async () => {
Expand Down Expand Up @@ -119,18 +122,28 @@ describe('objectGetAttributes', () => {
assert.strictEqual(data.ETag, expectedMD5);
});

it('should fail with NotImplemented when Checksum is requested', async () => {
try {
await s3.send(new GetObjectAttributesCommand({
Bucket: bucket,
Key: key,
ObjectAttributes: ['Checksum'],
}));
assert.fail('Expected NotImplemented error');
} catch (err) {
assert.strictEqual(err.name, 'NotImplemented');
assert.strictEqual(err.message, 'Checksum attribute is not implemented');
}
it('should return ChecksumCRC64NVME for object', async () => {
const data = await s3.send(new GetObjectAttributesCommand({
Bucket: bucket,
Key: key,
ObjectAttributes: ['Checksum'],
}));

assert(data.Checksum, 'Checksum should be present');
assert(data.Checksum.ChecksumCRC64NVME, 'ChecksumCRC64NVME should be present');
assert.strictEqual(data.Checksum.ChecksumType, 'FULL_OBJECT');
});

it('should not return Checksum when not requested', async () => {
const data = await s3.send(new GetObjectAttributesCommand({
Bucket: bucket,
Key: key,
ObjectAttributes: ['ETag', 'ObjectSize'],
}));

assert(data.ETag, 'ETag should be present');
assert(data.ObjectSize, 'ObjectSize should be present');
assert.strictEqual(data.Checksum, undefined, 'Checksum should not be present');
});

it("shouldn't return ObjectParts for non-MPU objects", async () => {
Expand Down Expand Up @@ -480,3 +493,93 @@ describe('objectGetAttributes with user metadata', () => {
});
});
});

describe('objectGetAttributes with checksum', () => {
withV4(sigCfg => {
let bucketUtil;
let s3;
const checksumBucket = 'checksum-getattr-test';
const checksumKey = 'checksum-test-object';
const checksumBody = Buffer.from('checksum test body');

const expectedDigests = {};

before(async () => {
bucketUtil = new BucketUtility('default', sigCfg);
s3 = bucketUtil.s3;
await s3.send(new CreateBucketCommand({ Bucket: checksumBucket }));

for (const [name, algo] of Object.entries(algorithms)) {
expectedDigests[name] = await algo.digest(checksumBody);
}
});

after(async () => {
await bucketUtil.empty(checksumBucket);
await s3.send(new DeleteBucketCommand({ Bucket: checksumBucket }));
});

Object.entries(algorithms).forEach(([name, { getObjectAttributesXMLTag }]) => {
const sdkAlgorithm = name.toUpperCase();

it(`should return ${getObjectAttributesXMLTag} when object has ${name} checksum`, async () => {
await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: checksumBody,
ChecksumAlgorithm: sdkAlgorithm,
}));

const data = await s3.send(new GetObjectAttributesCommand({
Bucket: checksumBucket,
Key: checksumKey,
ObjectAttributes: ['Checksum'],
}));

assert(data.Checksum, 'Checksum should be present');
assert.strictEqual(data.Checksum[getObjectAttributesXMLTag], expectedDigests[name]);
assert.strictEqual(data.Checksum.ChecksumType, 'FULL_OBJECT');
});

it(`should return ${getObjectAttributesXMLTag} along with other attributes`, async () => {
await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: checksumBody,
ChecksumAlgorithm: sdkAlgorithm,
}));

const data = await s3.send(new GetObjectAttributesCommand({
Bucket: checksumBucket,
Key: checksumKey,
ObjectAttributes: ['ETag', 'Checksum', 'ObjectSize'],
}));

assert(data.ETag, 'ETag should be present');
assert(data.ObjectSize, 'ObjectSize should be present');
assert(data.Checksum, 'Checksum should be present');
assert.strictEqual(data.Checksum[getObjectAttributesXMLTag], expectedDigests[name]);
assert.strictEqual(data.Checksum.ChecksumType, 'FULL_OBJECT');
});
});

it('should not return Checksum when not requested', async () => {
await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: checksumBody,
ChecksumAlgorithm: 'CRC64NVME',
}));

const data = await s3.send(new GetObjectAttributesCommand({
Bucket: checksumBucket,
Key: checksumKey,
ObjectAttributes: ['ETag', 'ObjectSize'],
}));

assert(data.ETag, 'ETag should be present');
assert(data.ObjectSize, 'ObjectSize should be present');
assert.strictEqual(data.Checksum, undefined, 'Checksum should not be present');
});
});
});
Loading
Loading