Compare commits

...

5 Commits

Author SHA1 Message Date
Ilke be948a9bf4 ft:S3C-2791_putObjectSupportsObjectLock 2020-06-02 10:28:38 -07:00
Dora Korpar 2abcdb8cc4 update Arsenal get object retention 2020-05-28 13:22:57 -07:00
Dora Korpar 986e79ed11 ft: S3C-2788-get-object-retention 2020-05-28 13:18:14 -07:00
Dora Korpar 26abc2148b update Arsenal-put object retention 2020-05-28 12:08:24 -07:00
Dora Korpar a3738fef29 ft: S3C-2787 put object retention 2020-05-28 12:08:24 -07:00
16 changed files with 615 additions and 48 deletions

View File

@ -97,7 +97,6 @@ const constants = {
'publicAccessBlock', 'publicAccessBlock',
'requestPayment', 'requestPayment',
'restore', 'restore',
'retention',
'torrent', 'torrent',
], ],

View File

@ -45,6 +45,7 @@ const objectPutACL = require('./objectPutACL');
const objectPutTagging = require('./objectPutTagging'); const objectPutTagging = require('./objectPutTagging');
const objectPutPart = require('./objectPutPart'); const objectPutPart = require('./objectPutPart');
const objectPutCopyPart = require('./objectPutCopyPart'); const objectPutCopyPart = require('./objectPutCopyPart');
const objectPutRetention = require('./objectPutRetention');
const prepareRequestContexts const prepareRequestContexts
= require('./apiUtils/authorization/prepareRequestContexts'); = require('./apiUtils/authorization/prepareRequestContexts');
const serviceGet = require('./serviceGet'); const serviceGet = require('./serviceGet');
@ -210,6 +211,7 @@ const api = {
objectPutTagging, objectPutTagging,
objectPutPart, objectPutPart,
objectPutCopyPart, objectPutCopyPart,
objectPutRetention,
serviceGet, serviceGet,
websiteGet, websiteGet,
websiteHead, websiteHead,

View File

@ -1,12 +1,18 @@
const { policies } = require('arsenal'); const { policies } = require('arsenal');
const { config } = require('../../../Config'); const { config } = require('../../../Config');
const RequestContext = policies.RequestContext; const { RequestContext, requestUtils } = policies;
const requestUtils = policies.requestUtils;
let apiMethodAfterVersionCheck; let apiMethodAfterVersionCheck;
const apiMethodWithVersion = { objectGetACL: true, objectPutACL: true, const apiMethodWithVersion = {
objectGet: true, objectDelete: true, objectPutTagging: true, objectGetACL: true,
objectGetTagging: true, objectDeleteTagging: true }; objectPutACL: true,
objectGet: true,
objectDelete: true,
objectPutTagging: true,
objectGetTagging: true,
objectDeleteTagging: true,
objectPutRetention: true,
};
function isHeaderAcl(headers) { function isHeaderAcl(headers) {
return headers['x-amz-grant-read'] || headers['x-amz-grant-read-acp'] || return headers['x-amz-grant-read'] || headers['x-amz-grant-read-acp'] ||

View File

@ -11,6 +11,7 @@ const locationConstraintCheck = require('./locationConstraintCheck');
const { versioningPreprocessing } = require('./versioning'); const { versioningPreprocessing } = require('./versioning');
const removeAWSChunked = require('./removeAWSChunked'); const removeAWSChunked = require('./removeAWSChunked');
const getReplicationInfo = require('./getReplicationInfo'); const getReplicationInfo = require('./getReplicationInfo');
const getObjectLockInfo = require('./getObjectLockInfo');
const { config } = require('../../../Config'); const { config } = require('../../../Config');
const validateWebsiteHeader = require('./websiteServing') const validateWebsiteHeader = require('./websiteServing')
.validateWebsiteHeader; .validateWebsiteHeader;
@ -194,6 +195,7 @@ function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
logger.newRequestLoggerFromSerializedUids(log.getSerializedUids()); logger.newRequestLoggerFromSerializedUids(log.getSerializedUids());
return async.waterfall([ return async.waterfall([
function storeData(next) { function storeData(next) {
console.log(`\n####\n[lib/api/apiUtils/object/createAndStoreObject.js] waterfall ! L197!`);
if (size === 0 && !dontSkipBackend[locationType]) { if (size === 0 && !dontSkipBackend[locationType]) {
metadataStoreParams.contentMD5 = constants.emptyFileMd5; metadataStoreParams.contentMD5 = constants.emptyFileMd5;
return next(null, null, null); return next(null, null, null);

View File

@ -36,6 +36,7 @@ function checkHashMatchMD5(stream, hashedStream, dataRetrievalInfo, log, cb) {
return cb(errors.BadDigest); return cb(errors.BadDigest);
}); });
} }
console.log(`\n^^^^^\n[lib/api/apiUtils/object/storeObject.js] dataRetrievalInfo:${JSON.stringify(dataRetrievalInfo, null,2)}!\n`);
return cb(null, dataRetrievalInfo, completedHash); return cb(null, dataRetrievalInfo, completedHash);
} }

View File

@ -0,0 +1,104 @@
const async = require('async');
const { errors, s3middleware } = require('arsenal');
const { decodeVersionId, getVersionIdResHeader }
= require('./apiUtils/object/versioning');
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
const { pushMetric } = require('../utapi/utilities');
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
const { convertToXml } = s3middleware.retention;
/**
* Object Get Retention - Return retention info for object
* @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}
*/
function objectGetRetention(authInfo, request, log, callback) {
log.debug('processing request', { method: 'objectGetRetention' });
const { bucketName, objectKey } = request;
const decodedVidResult = decodeVersionId(request.query);
if (decodedVidResult instanceof Error) {
log.trace('invalid versionId query', {
versionId: request.query.versionId,
error: decodedVidResult,
});
return process.nextTick(() => callback(decodedVidResult));
}
const reqVersionId = decodedVidResult;
const metadataValParams = {
authInfo,
bucketName,
objectKey,
requestType: 'objectGetRetention',
versionId: reqVersionId,
};
return async.waterfall([
next => metadataValidateBucketAndObj(metadataValParams, log,
(err, bucket, objectMD) => {
if (err) {
log.trace('request authorization failed',
{ method: 'objectGetRetention', error: err });
return next(err);
}
if (!objectMD) {
const err = reqVersionId ? errors.NoSuchVersion :
errors.NoSuchKey;
log.trace('error no object metadata found',
{ method: 'objectGetRetention', error: err });
return next(err, bucket);
}
if (objectMD.isDeleteMarker) {
if (reqVersionId) {
log.trace('requested version is delete marker',
{ method: 'objectGetRetention' });
return next(errors.MethodNotAllowed);
}
log.trace('most recent version is delete marker',
{ method: 'objectGetRetention' });
return next(errors.NoSuchKey);
}
if (!bucket.isObjectLockEnabled()) {
log.trace('object lock not enabled on bucket',
{ method: 'objectGetRetention' });
return next(errors.InvalidRequest.customizeDescription(
'Bucket is missing Object Lock Configuration'));
}
return next(null, bucket, objectMD);
}),
(bucket, objectMD, next) => {
const { retentionInfo } = objectMD;
const xml = convertToXml(retentionInfo);
if (xml === '') {
return next(errors.NoSuchObjectLockConfiguration);
}
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: 'objectGetRetention' });
} else {
pushMetric('getObjectRetention', log, {
authInfo,
bucket: bucketName,
});
const verCfg = bucket.getVersioningConfiguration();
additionalResHeaders['x-amz-version-id'] =
getVersionIdResHeader(verCfg, objectMD);
}
return callback(err, xml, additionalResHeaders);
});
}
module.exports = objectGetRetention;

View File

@ -4,6 +4,7 @@ const { errors, versioning } = require('arsenal');
const aclUtils = require('../utilities/aclUtils'); const aclUtils = require('../utilities/aclUtils');
const { cleanUpBucket } = require('./apiUtils/bucket/bucketCreation'); const { cleanUpBucket } = require('./apiUtils/bucket/bucketCreation');
const collectCorsHeaders = require('../utilities/collectCorsHeaders'); const collectCorsHeaders = require('../utilities/collectCorsHeaders');
const getObjectLockInfo = require('./apiUtils/object/getObjectLockInfo');
const createAndStoreObject = require('./apiUtils/object/createAndStoreObject'); const createAndStoreObject = require('./apiUtils/object/createAndStoreObject');
const { checkQueryVersionId } = require('./apiUtils/object/versioning'); const { checkQueryVersionId } = require('./apiUtils/object/versioning');
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils'); const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
@ -53,6 +54,7 @@ function objectPut(authInfo, request, streamingV4Params, log, callback) {
return metadataValidateBucketAndObj(valParams, log, return metadataValidateBucketAndObj(valParams, log,
(err, bucket, objMD) => { (err, bucket, objMD) => {
console.log(`\n------\n[lib/api/objectPut.js] request.headers:${JSON.stringify(request.headers, null, 2)}\n`);
const responseHeaders = collectCorsHeaders(request.headers.origin, const responseHeaders = collectCorsHeaders(request.headers.origin,
request.method, bucket); request.method, bucket);
if (err) { if (err) {
@ -67,6 +69,11 @@ function objectPut(authInfo, request, streamingV4Params, log, callback) {
'from non-owner account'); 'from non-owner account');
return callback(errors.NoSuchBucket); return callback(errors.NoSuchBucket);
} }
// const retentionHeaders = collectRetentionHeaders(headers, bucket);
const objectLockEnabledForBucket = bucket.isObjectLockEnabled();
// console.log(`\n---\n[lib/api/objectPut.js] retentionHeaders: ${retentionHeaders}`);
console.log(`\n+++++++\n[lib/api/objectPut.js] objectLockEnabledForBucket: ${objectLockEnabledForBucket}`);
return async.waterfall([ return async.waterfall([
function handleTransientOrDeleteBuckets(next) { function handleTransientOrDeleteBuckets(next) {
if (bucket.hasTransientFlag() || bucket.hasDeletedFlag()) { if (bucket.hasTransientFlag() || bucket.hasDeletedFlag()) {
@ -86,7 +93,23 @@ function objectPut(authInfo, request, streamingV4Params, log, callback) {
} }
return next(null, null); return next(null, null);
}, },
// function validateObjectLockConditions(next) {
// const objectHasRetention = request.headers['x-amz-object-lock-mode']
// && request.headers['x-amz-object-lock-retain-until-date'];
// const objectHasLegalHold
// = request.headers['x-amz-object-lock-legal-hold'];
// // If retention headers and/or legal hold header present but
// // object lock is not enabled on the bucket
// if ((objectHasRetention || objectHasLegalHold)
// && !bucket.isObjectLockEnabled()) {
// return errors.InvalidRequest.customizeDescription(
// 'Bucket is missing ObjectLockConfiguration'
// );
// }
// return next();
// },
function objectCreateAndStore(cipherBundle, next) { function objectCreateAndStore(cipherBundle, next) {
console.log(`\n+++++++\n[lib/api/objectPut.js] bucket.isObjectLockEnabled(): ${bucket.isObjectLockEnabled()}`);
return createAndStoreObject(bucketName, return createAndStoreObject(bucketName,
bucket, objectKey, objMD, authInfo, canonicalID, cipherBundle, bucket, objectKey, objMD, authInfo, canonicalID, cipherBundle,
request, false, streamingV4Params, log, next); request, false, streamingV4Params, log, next);

View File

@ -0,0 +1,125 @@
const async = require('async');
const { errors, s3middleware } = require('arsenal');
const { decodeVersionId, getVersionIdResHeader } =
require('./apiUtils/object/versioning');
const { metadataValidateBucketAndObj } = 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 { config } = require('../Config');
const multipleBackendGateway = require('../data/multipleBackendGateway');
const { parseRetentionXml } = s3middleware.retention;
const REPLICATION_ACTION = 'PUT_RETENTION';
/**
* Object Put Retention - Adds retention information to object
* @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}
*/
function objectPutRetention(authInfo, request, log, callback) {
log.debug('processing request', { method: 'objectPutRetention' });
const { bucketName, objectKey } = request;
const decodedVidResult = decodeVersionId(request.query);
if (decodedVidResult instanceof Error) {
log.trace('invalid versionId query', {
versionId: request.query.versionId,
error: decodedVidResult,
});
return process.nextTick(() => callback(decodedVidResult));
}
const reqVersionId = decodedVidResult;
const metadataValParams = {
authInfo,
bucketName,
objectKey,
requestType: 'objectPutRetention',
versionId: reqVersionId,
};
return async.waterfall([
next => metadataValidateBucketAndObj(metadataValParams, log,
(err, bucket, objectMD) => {
if (err) {
log.trace('request authorization failed',
{ method: 'objectPutRetention', error: err });
return next(err);
}
if (!objectMD) {
const err = reqVersionId ? errors.NoSuchVersion :
errors.NoSuchKey;
log.trace('error no object metadata found',
{ method: 'objectPutRetention', error: err });
return next(err, bucket);
}
if (objectMD.isDeleteMarker) {
log.trace('version is a delete marker',
{ method: 'objectPutRetention' });
return next(errors.MethodNotAllowed, bucket);
}
if (!bucket.isObjectLockEnabled()) {
log.trace('object lock not enabled on bucket',
{ method: 'objectPutRetention' });
return next(errors.InvalidRequest.customizeDescription(
'Bucket is missing Object Lock Configuration'
), bucket);
}
return next(null, bucket, objectMD);
}),
(bucket, objectMD, next) => {
log.trace('parsing retention information');
parseRetentionXml(request.post, log,
(err, retentionInfo) => next(err, bucket, retentionInfo, objectMD));
},
(bucket, retentionInfo, objectMD, next) => {
// eslint-disable-next-line no-param-reassign
objectMD.retentionInfo = retentionInfo;
const params = objectMD.versionId ?
{ versionId: objectMD.versionId } : {};
const replicationInfo = getReplicationInfo(objectKey, bucket, true,
0, REPLICATION_ACTION, objectMD);
if (replicationInfo) {
// eslint-disable-next-line no-param-reassign
objectMD.replicationInfo = Object.assign({},
objectMD.replicationInfo, replicationInfo);
}
metadata.putObjectMD(bucket.getName(), objectKey, objectMD, params,
log, err => next(err, bucket, objectMD));
},
(bucket, objectMD, next) => {
if (config.backends.data === 'multiple') {
return multipleBackendGateway.objectRetention('Put', objectKey,
bucket, objectMD, log, err => next(err, bucket, objectMD));
}
return next(null, bucket, objectMD);
},
], (err, bucket, objectMD) => {
const additionalResHeaders = collectCorsHeaders(request.headers.origin,
request.method, bucket);
if (err) {
log.trace('error processing request',
{ error: err, method: 'objectPutRetention' });
} else {
pushMetric('putObjectRetention', log, {
authInfo,
bucket: bucketName,
keys: [objectKey],
});
const verCfg = bucket.getVersioningConfiguration();
additionalResHeaders['x-amz-version-id'] =
getVersionIdResHeader(verCfg, objectMD);
}
return callback(err, additionalResHeaders);
});
}
module.exports = objectPutRetention;

View File

@ -94,6 +94,7 @@ const metadata = {
objVal.getValue() : objVal; objVal.getValue() : objVal;
client.putObject(bucketName, objName, value, params, log, client.putObject(bucketName, objName, value, params, log,
(err, data) => { (err, data) => {
console.log(`\n[lib/metadata/wrapper.js] L97 putObjectMD: data: ${data} `)
if (err) { if (err) {
log.debug('error from metadata', { implName, error: err }); log.debug('error from metadata', { implName, error: err });
return cb(err); return cb(err);

View File

@ -95,7 +95,7 @@ const services = {
contentType, cacheControl, contentDisposition, contentEncoding, contentType, cacheControl, contentDisposition, contentEncoding,
expires, multipart, headers, overrideMetadata, log, expires, multipart, headers, overrideMetadata, log,
lastModifiedDate, versioning, versionId, tagging, taggingCopy, lastModifiedDate, versioning, versionId, tagging, taggingCopy,
replicationInfo, dataStoreName } = params; replicationInfo, objectLockInfo, dataStoreName } = params;
log.trace('storing object in metadata'); log.trace('storing object in metadata');
assert.strictEqual(typeof bucketName, 'string'); assert.strictEqual(typeof bucketName, 'string');
const md = new ObjectMD(); const md = new ObjectMD();
@ -128,6 +128,24 @@ const services = {
if (headers && headers['x-amz-website-redirect-location']) { if (headers && headers['x-amz-website-redirect-location']) {
md.setRedirectLocation(headers['x-amz-website-redirect-location']); md.setRedirectLocation(headers['x-amz-website-redirect-location']);
} }
if (headers && headers['x-amz-object-lock-retain-until-date']
&& headers['x-amz-object-lock-mode']) {
console.log(`\n++++++\n[lib/services.js] HERE! (retentionInfo)\n ~~~~~~\n`);
const retention = {
mode: headers['x-amz-object-lock-mode'],
retainUntilDate: headers['x-amz-object-lock-retain-until-date'],
}
md.setRetentionInfo(retention);
}
console.log(`\n!!!!\n[lib/services.js] L140 headers['x-amz-object-lock-legal-hold']: ${headers['x-amz-object-lock-legal-hold']}`);
if (headers && headers['x-amz-object-lock-legal-hold']) {
const legalHold = headers['x-amz-object-lock-legal-hold']
=== 'ON' ? true : false;
console.log(`\n++++++\n[lib/services.js] HERE! legalHold: ${legalHold}\n ~~~~~~\n`);
// md.setLegalHold(legalHold);
}
console.log(`\n!!!!\n[lib/services.js] L147 md.getRetentionInfo: ${JSON.stringify(md.getRetentionInfo())}`);
// console.log(`\n!!!!\n[lib/services.js] L141 md: ${JSON.stringify(md, null, 2)}`);
if (replicationInfo) { if (replicationInfo) {
md.setReplicationInfo(replicationInfo); md.setReplicationInfo(replicationInfo);
} }

View File

@ -75,6 +75,19 @@ function collectResponseHeaders(objectMD, corsHeaders, versioningCfg,
responseMetaHeaders['x-amz-tagging-count'] = responseMetaHeaders['x-amz-tagging-count'] =
Object.keys(objectMD.tags).length; Object.keys(objectMD.tags).length;
} }
if (objectMD.retentionInfo && objectMD.retentionInfo.retainUntilDate
&& objectMD.retentionInfo.mode) {
responseMetaHeaders['x-amz-object-lock-retain-until-date']
= objectMD.retentionInfo.retainUntilDate;
responseMetaHeaders['x-amz-object-lock-mode']
= objectMD.retentionInfo.mode;
console.log(`\n[lib/utilities/collectResponseHeaders.js]\n Y e HU UUUU uuu UU !!! @@@ \n\n`);
}
if (objectMD.legalHold !== undefined) {
console.log(`\n[lib/utilities/collectResponseHeaders.js]\n legal hold !!!\n`);
responseMetaHeaders['x-amz-replication-status'] =
objectMD.legalHold ? 'ON' : 'OFF';
}
if (objectMD.replicationInfo && objectMD.replicationInfo.status) { if (objectMD.replicationInfo && objectMD.replicationInfo.status) {
responseMetaHeaders['x-amz-replication-status'] = responseMetaHeaders['x-amz-replication-status'] =
objectMD.replicationInfo.status; objectMD.replicationInfo.status;

View File

@ -19,7 +19,7 @@
}, },
"homepage": "https://github.com/scality/S3#readme", "homepage": "https://github.com/scality/S3#readme",
"dependencies": { "dependencies": {
"arsenal": "github:scality/Arsenal#0d49eff", "arsenal": "github:scality/Arsenal#bdd8368",
"async": "~2.5.0", "async": "~2.5.0",
"aws-sdk": "2.363.0", "aws-sdk": "2.363.0",
"azure-storage": "^2.1.0", "azure-storage": "^2.1.0",

View File

@ -0,0 +1,105 @@
const assert = require('assert');
const { bucketPut } = require('../../../lib/api/bucketPut');
const objectPut = require('../../../lib/api/objectPut');
const objectPutRetention = require('../../../lib/api/objectPutRetention');
const objectGetRetention = require('../../../lib/api/objectGetRetention');
const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers');
const DummyRequest = require('../DummyRequest');
const log = new DummyRequestLogger();
const authInfo = makeAuthInfo('accessKey1');
const namespace = 'default';
const bucketName = 'bucketname';
const objectName = 'objectName';
const postBody = Buffer.from('I am a body', 'utf8');
const date = new Date();
date.setDate(date.getDate() + 1);
const bucketPutRequest = {
bucketName,
headers: { host: `${bucketName}.s3.amazonaws.com` },
url: '/',
};
const putObjectRequest = new DummyRequest({
bucketName,
namespace,
objectKey: objectName,
headers: {},
url: `/${bucketName}/${objectName}`,
}, postBody);
const objectRetentionXml = '<ObjectRetention ' +
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' +
'<Mode>GOVERNANCE</Mode>' +
`<RetainUntilDate>${date.toISOString()}</RetainUntilDate>` +
'</ObjectRetention>';
const putObjRetRequest = {
bucketName,
objectKey: objectName,
headers: { host: `${bucketName}.s3.amazonaws.com` },
post: objectRetentionXml,
};
const getObjRetRequest = {
bucketName,
objectKey: objectName,
headers: { host: `${bucketName}.s3.amazonaws.com` },
};
describe('getObjectRetention API', () => {
before(() => cleanup());
describe('without Object Lock enabled on bucket', () => {
beforeEach(done => {
bucketPut(authInfo, bucketPutRequest, log, err => {
assert.ifError(err);
objectPut(authInfo, putObjectRequest, undefined, log, done);
});
});
afterEach(() => cleanup());
it('should return InvalidRequest error', done => {
objectGetRetention(authInfo, getObjRetRequest, log, err => {
assert.strictEqual(err.InvalidRequest, true);
done();
});
});
});
describe('with Object Lock enabled on bucket', () => {
const bucketObjLockRequest = Object.assign({}, bucketPutRequest,
{ headers: { 'x-amz-bucket-object-lock-enabled': true } });
beforeEach(done => {
bucketPut(authInfo, bucketObjLockRequest, log, err => {
assert.ifError(err);
objectPut(authInfo, putObjectRequest, undefined, log, done);
});
});
afterEach(() => cleanup());
it('should return NoSuchObjectLockConfiguration if no retention set',
done => {
objectGetRetention(authInfo, getObjRetRequest, log, err => {
assert.strictEqual(err.NoSuchObjectLockConfiguration, true);
done();
});
});
it('should get an object\'s retention info', done => {
objectPutRetention(authInfo, putObjRetRequest, log, err => {
assert.ifError(err);
objectGetRetention(authInfo, getObjRetRequest, log,
(err, xml) => {
assert.ifError(err);
assert.strictEqual(xml, objectRetentionXml);
done();
});
});
});
});
});

View File

@ -162,6 +162,75 @@ describe('objectPut API', () => {
}); });
}); });
const generatePutObjectReq = (date, mode) => new DummyRequest({
bucketName,
namespace,
objectKey: objectName,
headers: {
'x-amz-object-lock-retain-until-date': date,
'x-amz-object-lock-mode': mode,
},
url: `/${bucketName}/${objectName}`,
calculatedHash: 'vnR+tLdVF79rPPfF+7YvOg==',
}, postBody);
const retentionInfo = (date, mode) => ({
retainUntilDate: date,
mode: mode,
});
it('should successfully put an object with valid retention date and COMPLIANCE mode', done => {
bucketPut(authInfo, testPutBucketRequest, log, () => {
const testDate = new Date(2022, 6, 3);
const testModeComp = 'COMPLIANCE';
const request = generatePutObjectReq(testDate, testModeComp);
const expectedRetention = retentionInfo(testDate, testModeComp);
objectPut(authInfo, request, undefined, log, (err, resHeaders) => {
assert.ifError(err);
assert.strictEqual(resHeaders.ETag, `"${correctMD5}"`);
metadata.getObjectMD(bucketName, objectName, {}, log,
(err, objectMD) => {
assert(objectMD);
assert.ifError(err);
assert.deepStrictEqual(objectMD.retentionInfo,
expectedRetention);
assert.strictEqual(objectMD.retentionInfo.mode,
expectedRetention.mode);
assert.strictEqual(
objectMD.retentionInfo.retainUntilDate,
expectedRetention.retainUntilDate);
done();
});
});
});
});
it('should successfully put an object with valid retention date and GOVERNANCE mode', done => {
bucketPut(authInfo, testPutBucketRequest, log, () => {
const testDate = new Date(2022, 6, 3);
const testModeComp = 'GOVERNANCE';
const request = generatePutObjectReq(testDate, testModeComp);
const expectedRetention = retentionInfo(testDate, testModeComp);
objectPut(authInfo, request, undefined, log, (err, resHeaders) => {
assert.ifError(err);
assert.strictEqual(resHeaders.ETag, `"${correctMD5}"`);
metadata.getObjectMD(bucketName, objectName, {}, log,
(err, objectMD) => {
assert(objectMD);
assert.ifError(err);
assert.deepStrictEqual(objectMD.retentionInfo,
expectedRetention);
assert.strictEqual(objectMD.retentionInfo.mode,
expectedRetention.mode);
assert.strictEqual(
objectMD.retentionInfo.retainUntilDate,
expectedRetention.retainUntilDate);
done();
});
});
});
});
it('should successfully put an object with user metadata', done => { it('should successfully put an object with user metadata', done => {
const testPutObjectRequest = new DummyRequest({ const testPutObjectRequest = new DummyRequest({
bucketName, bucketName,

View File

@ -0,0 +1,99 @@
const assert = require('assert');
const { bucketPut } = require('../../../lib/api/bucketPut');
const objectPut = require('../../../lib/api/objectPut');
const objectPutRetention = require('../../../lib/api/objectPutRetention');
const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers');
const metadata = require('../../../lib/metadata/wrapper');
const DummyRequest = require('../DummyRequest');
const log = new DummyRequestLogger();
const authInfo = makeAuthInfo('accessKey1');
const namespace = 'default';
const bucketName = 'bucketname';
const objectName = 'objectName';
const postBody = Buffer.from('I am a body', 'utf8');
const date = new Date();
date.setDate(date.getDate() + 1);
const bucketPutRequest = {
bucketName,
headers: { host: `${bucketName}.s3.amazonaws.com` },
url: '/',
};
const putObjectRequest = new DummyRequest({
bucketName,
namespace,
objectKey: objectName,
headers: {},
url: `/${bucketName}/${objectName}`,
}, postBody);
const objectRetentionXml = '<ObjectRetention ' +
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' +
'<Mode>GOVERNANCE</Mode>' +
`<RetainUntilDate>${date.toISOString()}</RetainUntilDate>` +
'</ObjectRetention>';
const putObjRetRequest = {
bucketName,
objectKey: objectName,
headers: { host: `${bucketName}.s3.amazonaws.com` },
post: objectRetentionXml,
};
const expectedRetInfo = {
retention: {
mode: 'GOVERNANCE',
retainUntilDate: date.toISOString(),
},
};
describe('putObjectRetention API', () => {
before(() => cleanup());
describe('without Object Lock enabled on bucket', () => {
beforeEach(done => {
bucketPut(authInfo, bucketPutRequest, log, err => {
assert.ifError(err);
objectPut(authInfo, putObjectRequest, undefined, log, done);
});
});
afterEach(() => cleanup());
it('should return InvalidRequest error', done => {
objectPutRetention(authInfo, putObjRetRequest, log, err => {
assert.strictEqual(err.InvalidRequest, true);
done();
});
});
});
describe('with Object Lock enabled on bucket', () => {
const bucketObjLockRequest = Object.assign({}, bucketPutRequest,
{ headers: { 'x-amz-bucket-object-lock-enabled': true } });
beforeEach(done => {
bucketPut(authInfo, bucketObjLockRequest, log, err => {
assert.ifError(err);
objectPut(authInfo, putObjectRequest, undefined, log, done);
});
});
afterEach(() => cleanup());
it('should update an object\'s metadata with retention info', done => {
objectPutRetention(authInfo, putObjRetRequest, log, err => {
assert.ifError(err);
return metadata.getObjectMD(bucketName, objectName, {}, log,
(err, objMD) => {
assert.ifError(err);
assert.deepStrictEqual(objMD.retentionInfo,
expectedRetInfo);
return done();
});
});
});
});
});

View File

@ -210,9 +210,9 @@ arraybuffer.slice@0.0.6:
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca"
integrity sha1-8zshWfBTKj8xB6JywMz70a0peco= integrity sha1-8zshWfBTKj8xB6JywMz70a0peco=
"arsenal@github:scality/Arsenal#0d49eff": "arsenal@github:scality/Arsenal#bdd8368":
version "7.5.0" version "7.5.0"
resolved "https://codeload.github.com/scality/Arsenal/tar.gz/0d49eff7e4da07eb27c516820e258221d543d082" resolved "https://codeload.github.com/scality/Arsenal/tar.gz/bdd8368c39097d9f367cc0901dd643f1b97ea532"
dependencies: dependencies:
"@hapi/joi" "^15.1.0" "@hapi/joi" "^15.1.0"
JSONStream "^1.0.0" JSONStream "^1.0.0"
@ -236,7 +236,7 @@ arraybuffer.slice@0.0.6:
ioctl "2.0.0" ioctl "2.0.0"
arsenal@scality/Arsenal#32c895b: arsenal@scality/Arsenal#32c895b:
version "7.5.0" version "7.4.3"
resolved "https://codeload.github.com/scality/Arsenal/tar.gz/32c895b21a31eb67dacc6e76d7f58b8142bf3ad1" resolved "https://codeload.github.com/scality/Arsenal/tar.gz/32c895b21a31eb67dacc6e76d7f58b8142bf3ad1"
dependencies: dependencies:
"@hapi/joi" "^15.1.0" "@hapi/joi" "^15.1.0"
@ -379,9 +379,9 @@ aws-sdk@2.363.0:
xml2js "0.4.19" xml2js "0.4.19"
aws-sdk@^2.2.23: aws-sdk@^2.2.23:
version "2.678.0" version "2.686.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.678.0.tgz#b16230f4894d40ead50f9e23805c874f4ca62549" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.686.0.tgz#c58c3874eff4c76c763d137df617e87971321eb7"
integrity sha512-i8t7+1/C6maQzUYUFRQXPAsUPT0YdpNsf/oHZKmmZrsOX+epnn2jmAGIBTZgUakY8jRrZxCJka+QokUIadUVQg== integrity sha512-QhYhJ5y8tUG5SlmY3CSf9RBaa3EFbta28oarOyiwceHKmY80cMCafRI1YypT6CVDx/q91dbnSNQfWhs0cZPbBQ==
dependencies: dependencies:
buffer "4.9.1" buffer "4.9.1"
events "1.1.1" events "1.1.1"
@ -399,9 +399,9 @@ aws-sign2@~0.7.0:
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
aws4@^1.8.0: aws4@^1.8.0:
version "1.9.1" version "1.10.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2"
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==
axios@^0.18.0: axios@^0.18.0:
version "0.18.1" version "0.18.1"
@ -814,12 +814,12 @@ cron-parser@1.1.0:
integrity sha1-B1uExFnBVejEgqtNVq/5na5YNS4= integrity sha1-B1uExFnBVejEgqtNVq/5na5YNS4=
cron-parser@^2.11.0: cron-parser@^2.11.0:
version "2.13.0" version "2.15.0"
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.13.0.tgz#6f930bb6f2931790d2a9eec83b3ec276e27a6725" resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.15.0.tgz#04803cd51d8efcfcc6f83ac08e60f3f8c40c7ec5"
integrity sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ== integrity sha512-rMFkrQw8+oG5OuwjiXesup4KeIlEG/IU82YtG4xyAHbO5jhKmYaHPp/ZNhq9+7TjSJ65E3zV3kQPUbmXSff2/g==
dependencies: dependencies:
is-nan "^1.2.1" is-nan "^1.3.0"
moment-timezone "^0.5.25" moment-timezone "^0.5.31"
cross-spawn@^6.0.5: cross-spawn@^6.0.5:
version "6.0.5" version "6.0.5"
@ -1627,9 +1627,9 @@ hosted-git-info@^2.1.4:
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
http-proxy@^1.17.0: http-proxy@^1.17.0:
version "1.18.0" version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
dependencies: dependencies:
eventemitter3 "^4.0.0" eventemitter3 "^4.0.0"
follow-redirects "^1.0.0" follow-redirects "^1.0.0"
@ -1745,9 +1745,9 @@ ioredis@4.9.5:
standard-as-callback "^2.0.1" standard-as-callback "^2.0.1"
ioredis@^4.9.5: ioredis@^4.9.5:
version "4.16.3" version "4.17.1"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.16.3.tgz#6a6b85830206fd98353b7ff8536521f17943be53" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.1.tgz#06ef3d3b2cb96b7e6bc90a7b8839a33e743843ad"
integrity sha512-Ejvcs2yW19Vq8AipvbtfcX3Ig8XG9EAyFOvGbhI/Q1QoVOK9ZdgY092kdOyOWIYBnPHjfjMJhU9qhsnp0i0K1w== integrity sha512-kfxkN/YO1dnyaoAGyNdH3my4A1eoGDy4QOfqn6o86fo4dTboxyxYVW0S0v/d3MkwCWlvSWhlwq6IJMY9BlWs6w==
dependencies: dependencies:
cluster-key-slot "^1.1.0" cluster-key-slot "^1.1.0"
debug "^4.1.1" debug "^4.1.1"
@ -1822,7 +1822,7 @@ is-my-json-valid@^2.10.0:
jsonpointer "^4.0.0" jsonpointer "^4.0.0"
xtend "^4.0.0" xtend "^4.0.0"
is-nan@^1.2.1: is-nan@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03"
integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ== integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==
@ -2028,9 +2028,9 @@ js-tokens@^3.0.2:
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0: js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.13.1" version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
dependencies: dependencies:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
@ -2425,9 +2425,9 @@ mime@^1.3.4:
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.2.0: mime@^2.2.0:
version "2.4.5" version "2.4.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.5.tgz#d8de2ecb92982dedbb6541c9b6841d7f218ea009" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
integrity sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w== integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
minimatch@0.3: minimatch@0.3:
version "0.3.0" version "0.3.0"
@ -2523,17 +2523,17 @@ mocha@^2.3.3, mocha@^2.3.4:
supports-color "1.2.0" supports-color "1.2.0"
to-iso-string "0.0.2" to-iso-string "0.0.2"
moment-timezone@^0.5.25: moment-timezone@^0.5.31:
version "0.5.28" version "0.5.31"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05"
integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw== integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==
dependencies: dependencies:
moment ">= 2.9.0" moment ">= 2.9.0"
"moment@>= 2.9.0": "moment@>= 2.9.0":
version "2.25.3" version "2.26.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.25.3.tgz#252ff41319cf41e47761a1a88cab30edfe9808c0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg== integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
ms@0.7.1: ms@0.7.1:
version "0.7.1" version "0.7.1"
@ -3287,9 +3287,9 @@ source-map@^0.6.1:
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
spdx-correct@^3.0.0: spdx-correct@^3.0.0:
version "3.1.0" version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
dependencies: dependencies:
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
spdx-license-ids "^3.0.0" spdx-license-ids "^3.0.0"
@ -3640,9 +3640,9 @@ uc.micro@^1.0.0, uc.micro@^1.0.1:
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
uglify-js@^3.1.4: uglify-js@^3.1.4:
version "3.9.3" version "3.9.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.9.3.tgz#4a285d1658b8a2ebaef9e51366b3a0f7acd79ec2" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.9.4.tgz#867402377e043c1fc7b102253a22b64e5862401b"
integrity sha512-r5ImcL6QyzQGVimQoov3aL2ZScywrOgBXGndbWrdehKoSvGe/RmiE5Jpw/v+GvxODt6l2tpBXwA7n+qZVlHBMA== integrity sha512-8RZBJq5smLOa7KslsNsVcSH+KOXf1uDU8yqLeNuVKwmT0T3FA0ZoXlinQfRad7SDcbZZRZE4ov+2v71EnxNyCA==
dependencies: dependencies:
commander "~2.20.3" commander "~2.20.3"