Compare commits

...

4 Commits

Author SHA1 Message Date
Ilke 997a5d5e6e [squash] add error 2020-05-14 11:17:35 -07:00
Ilke 8e45e4c4f7 [squash] remove error 2020-05-14 09:31:47 -07:00
Ilke 9279a64b36 ft: S3C-2790 get object lock configuration 2020-05-12 13:56:21 -07:00
Dora Korpar c5e21e0719 ft: S3C-2789 object lock configuration 2020-05-05 13:15:14 -07:00
6 changed files with 326 additions and 7 deletions

View File

@ -284,6 +284,10 @@
"code": 404, "code": 404,
"description": "The specified bucket does not have a bucket policy." "description": "The specified bucket does not have a bucket policy."
}, },
"ObjectLockConfigurationNotFoundError": {
"code": 404,
"description": "Object Lock configuration does not exist for this bucket."
},
"OperationAborted": { "OperationAborted": {
"code": 409, "code": 409,
"description": "A conflicting conditional operation is currently in progress against this resource. Try again." "description": "A conflicting conditional operation is currently in progress against this resource. Try again."

View File

@ -112,6 +112,8 @@ module.exports = {
LifecycleConfiguration: LifecycleConfiguration:
require('./lib/models/LifecycleConfiguration'), require('./lib/models/LifecycleConfiguration'),
BucketPolicy: require('./lib/models/BucketPolicy'), BucketPolicy: require('./lib/models/BucketPolicy'),
ObjectLockConfiguration:
require('./lib/models/ObjectLockConfiguration'),
}, },
metrics: { metrics: {
StatsClient: require('./lib/metrics/StatsClient'), StatsClient: require('./lib/metrics/StatsClient'),

View File

@ -1,5 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const errors = require('../errors');
/** /**
* Format of xml request: * Format of xml request:
* *
@ -36,6 +38,128 @@ class ObjectLockConfiguration {
this._config = {}; this._config = {};
} }
/**
* Get the object lock configuration
* @return {object} - contains error if parsing failed
*/
getValidatedObjectLockConfiguration() {
const validConfig = this._parseObjectLockConfig();
if (validConfig.error) {
this._config.error = validConfig.error;
}
return this._config;
}
/**
* Check that mode is valid
* @param {array} mode - array containing mode value
* @return {object} - contains error if parsing failed
*/
_parseMode(mode) {
const validMode = {};
const expectedModes = ['GOVERNANCE', 'COMPLIANCE'];
if (!mode || !mode[0] || mode[0] === '') {
validMode.error = errors.MalformedXML.customizeDescription(
'request xml does not contain Mode');
return validMode;
}
if (!expectedModes.includes(mode[0])) {
validMode.error = errors.MalformedXML.customizeDescription(
'Mode request xml must be one of "GOVERNANCE", "COMPLIANCE"');
return validMode;
}
validMode.mode = mode[0];
return validMode;
}
/**
* Check that time limit is valid
* @param {object} dr - DefaultRetention object containing days or years
* @return {object} - contains error if parsing failed
*/
_parseTime(dr) {
const validTime = {};
if (dr.Days && dr.Years) {
validTime.error = errors.MalformedXML.customizeDescription(
'request xml contains both Days and Years');
return validTime;
}
const timeType = dr.Days ? 'Days' : 'Years';
if (!dr[timeType] || !dr[timeType][0] || dr[timeType][0] === '') {
validTime.error = errors.MalformedXML.customizeDescription(
'request xml does not contain Days or Years');
return validTime;
}
const timeValue = Number.parseInt(dr[timeType][0], 10);
if (Number.isNaN(timeValue)) {
validTime.error = errors.MalformedXML.customizeDescription(
'request xml does not contain valid retention period');
return validTime;
}
validTime.timeType = timeType.toLowerCase();
validTime.timeValue = timeValue;
return validTime;
}
/**
* Check that object lock configuration is valid
* @return {object} - contains error if parsing failed
*/
_parseObjectLockConfig() {
const validConfig = {};
if (!this._parsedXml || this._parsedXml === '') {
validConfig.error = errors.MalformedXML.customizeDescription(
'request xml is undefined or empty');
return validConfig;
}
const objectLockConfig = this._parsedXml.ObjectLockConfiguration;
if (!objectLockConfig && objectLockConfig !== '') {
validConfig.error = errors.MalformedXML.customizeDescription(
'request xml does not include ObjectLockConfiguration');
return validConfig;
}
const objectLockEnabled = objectLockConfig.ObjectLockEnabled;
if (!objectLockEnabled || objectLockEnabled[0] !== 'Enabled') {
validConfig.error = errors.MalformedXML.customizeDescription(
'request xml does not include valid ObjectLockEnabled');
return validConfig;
}
const ruleArray = objectLockConfig.Rule;
if (ruleArray.length > 1) {
validConfig.error = errors.MalformedXML.customizeDescription(
'request xml contains more than one rule');
return validConfig;
}
const drArray = ruleArray[0].DefaultRetention;
if (!drArray || !drArray[0] || drArray[0] === '') {
validConfig.error = errors.MalformedXML.customizeDescription(
'Rule request xml does not contain DefaultRetention');
return validConfig;
}
if (!drArray[0].Mode || (!drArray[0].Days && !drArray[0].Years)) {
validConfig.error = errors.MalformedXML.customizeDescription(
'DefaultRetention request xml does not contain Mode or ' +
'retention period (Days or Years)');
return validConfig;
}
const validMode = this._parseMode(drArray[0].Mode);
if (validMode.error) {
validConfig.error = validMode.error;
return validConfig;
}
const validTime = this._parseTime(drArray[0]);
if (validTime.error) {
validConfig.error = validTime.error;
return validConfig;
}
this._config.rule = {};
this._config.rule.mode = validMode.mode;
this._config.rule[validTime.timeType] = validTime.timeValue;
return validConfig;
}
/** /**
* Validate the bucket metadata lifecycle configuration structure and * Validate the bucket metadata lifecycle configuration structure and
* value types * value types
@ -53,6 +177,31 @@ class ObjectLockConfiguration {
assert.strictEqual(typeof rule.years, 'number'); assert.strictEqual(typeof rule.years, 'number');
} }
} }
/**
* Get the XML representation of the configuration object
* @param {object} config - The bucket object lock configuration
* @return {string} - The XML representation of the configuration
*/
static getConfigXML(config) {
const { days, years, mode } = config.rule;
const retentionDays = days !== undefined ? `<Days>${days}</Days>` : '';
const retentionYears = years !== undefined ?
`<Years>${years}</Years>` : '';
const retentionModeXml = `<Mode>${mode}</Mode>`;
const retentionTimeXml = retentionDays || retentionYears;
return '<?xml version="1.0" encoding="UTF-8"?>' +
'<ObjectLockConfiguration' +
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' +
'<ObjectLockEnabled>Enabled</ObjectLockEnabled>' +
'<Rule>' +
'<DefaultRetention>' +
`${retentionModeXml}` +
`${retentionTimeXml}` +
'</DefaultRetention>' +
'</Rule>' +
'</ObjectLockConfiguration>';
}
} }
module.exports = ObjectLockConfiguration; module.exports = ObjectLockConfiguration;

View File

@ -51,11 +51,11 @@ function routerGET(request, response, api, log, statsClient, dataRetrievalFn) {
}); });
} else if (request.query.lifecycle !== undefined) { } else if (request.query.lifecycle !== undefined) {
api.callApiMethod('bucketGetLifecycle', request, response, log, api.callApiMethod('bucketGetLifecycle', request, response, log,
(err, xml, corsHeaders) => { (err, xml, corsHeaders) => {
routesUtils.statsReport500(err, statsClient); routesUtils.statsReport500(err, statsClient);
routesUtils.responseXMLBody(err, xml, response, log, routesUtils.responseXMLBody(err, xml, response, log,
corsHeaders); corsHeaders);
}); });
} else if (request.query.uploads !== undefined) { } else if (request.query.uploads !== undefined) {
// List MultipartUploads // List MultipartUploads
api.callApiMethod('listMultipartUploads', request, response, log, api.callApiMethod('listMultipartUploads', request, response, log,
@ -78,6 +78,13 @@ function routerGET(request, response, api, log, statsClient, dataRetrievalFn) {
return routesUtils.responseXMLBody(err, xml, response, return routesUtils.responseXMLBody(err, xml, response,
log, corsHeaders); log, corsHeaders);
}); });
} else if (request.query['object-lock'] !== undefined) {
api.callApiMethod('bucketGetObjectLock', request, response, log,
(err, xml, corsHeaders) => {
routesUtils.statsReport500(err, statsClient);
return routesUtils.responseXMLBody(err, xml, response,
log, corsHeaders);
});
} else { } else {
// GET bucket // GET bucket
api.callApiMethod('bucketGet', request, response, log, api.callApiMethod('bucketGet', request, response, log,
@ -88,7 +95,6 @@ function routerGET(request, response, api, log, statsClient, dataRetrievalFn) {
}); });
} }
} else { } else {
/* eslint-disable no-lonely-if */
if (request.query.acl !== undefined) { if (request.query.acl !== undefined) {
// GET object ACL // GET object ACL
api.callApiMethod('objectGetACL', request, response, log, api.callApiMethod('objectGetACL', request, response, log,
@ -128,7 +134,6 @@ function routerGET(request, response, api, log, statsClient, dataRetrievalFn) {
range, log); range, log);
}); });
} }
/* eslint-enable */
} }
} }

View File

@ -67,6 +67,13 @@ function routePUT(request, response, api, log, statsClient) {
routesUtils.responseNoBody(err, corsHeaders, response, 200, routesUtils.responseNoBody(err, corsHeaders, response, 200,
log); log);
}); });
} else if (request.query['object-lock'] !== undefined) {
api.callApiMethod('bucketPutObjectLock', request, response, log,
(err, corsHeaders) => {
routesUtils.statsReport500(err, statsClient);
routesUtils.responseNoBody(err, corsHeaders, response, 200,
log);
});
} else { } else {
// PUT bucket // PUT bucket
return api.callApiMethod('bucketPut', request, response, log, return api.callApiMethod('bucketPut', request, response, log,

View File

@ -0,0 +1,152 @@
const assert = require('assert');
const { parseString } = require('xml2js');
const ObjectLockConfiguration =
require('../../../lib/models/ObjectLockConfiguration.js');
function checkError(parsedXml, errMessage, cb) {
const config = new ObjectLockConfiguration(parsedXml).
getValidatedObjectLockConfiguration();
assert.strictEqual(config.error.MalformedXML, true);
assert.strictEqual(config.error.description, errMessage);
cb();
}
function generateRule(testParams) {
if (testParams.key === 'Rule') {
return `<Rule>${testParams.value}</Rule>`;
}
if (testParams.key === 'DefaultRetention') {
return `<Rule><DefaultRetention>${testParams.value} ` +
'</DefaultRetention></Rule>';
}
const mode = testParams.key === 'Mode' ?
`<Mode>${testParams.value}</Mode>` : '<Mode>GOVERNANCE</Mode>';
let time = '<Days>1</Days>';
if (testParams.key === 'Days') {
time = `<Days>${testParams.value}</Days>`;
}
if (testParams.key === 'Years') {
time = `<Years>${testParams.value}</Years>`;
}
return `<Rule><DefaultRetention>${mode}${time}</DefaultRetention></Rule>`;
}
function generateXml(testParams) {
const Enabled = testParams.key === 'ObjectLockEnabled' ?
`<ObjectLockEnabled>${testParams.value}</ObjectLockEnabled>` :
'<ObjectLockEnabled>Enabled</ObjectLockEnabled>';
const Rule = generateRule(testParams);
const ObjectLock = testParams.key === 'ObjectLockConfiguration' ? '' :
`<ObjectLockConfiguration>${Enabled}${Rule}` +
'</ObjectLockConfiguration>';
return ObjectLock;
}
function generateParsedXml(testParams, cb) {
const xml = generateXml(testParams);
parseString(xml, (err, parsedXml) => {
assert.equal(err, null, 'Error parsing xml');
cb(parsedXml);
});
}
const failTests = [
{
name: 'fail with empty configuration',
params: { key: 'ObjectLockConfiguration' },
errorMessage: 'request xml is undefined or empty',
},
{
name: 'fail with empty ObjectLockEnabled',
params: { key: 'ObjectLockEnabled', value: '' },
errorMessage: 'request xml does not include valid ObjectLockEnabled',
},
{
name: 'fail with invalid value for ObjectLockEnabled',
params: { key: 'ObjectLockEnabled', value: 'Disabled' },
errorMessage: 'request xml does not include valid ObjectLockEnabled',
},
{
name: 'fail with empty rule',
params: { key: 'Rule', value: '' },
errorMessage: 'Rule request xml does not contain DefaultRetention',
},
{
name: 'fail with empty DefaultRetention',
params: { key: 'DefaultRetention', value: '' },
errorMessage: 'DefaultRetention request xml does not contain Mode or ' +
'retention period (Days or Years)',
},
{
name: 'fail with empty mode',
params: { key: 'Mode', value: '' },
errorMessage: 'request xml does not contain Mode',
},
{
name: 'fail with invalid mode',
params: { key: 'Mode', value: 'COMPLOVERNANCE' },
errorMessage: 'Mode request xml must be one of "GOVERNANCE", ' +
'"COMPLIANCE"',
},
{
name: 'fail with lowercase mode',
params: { key: 'Mode', value: 'governance' },
errorMessage: 'Mode request xml must be one of "GOVERNANCE", ' +
'"COMPLIANCE"',
},
{
name: 'fail with empty retention period',
params: { key: 'Days', value: '' },
errorMessage: 'request xml does not contain Days or Years',
},
{
name: 'fail with NaN retention period',
params: { key: 'Days', value: 'one' },
errorMessage: 'request xml does not contain valid retention period',
},
];
const passTests = [
{
name: 'pass with GOVERNANCE retention mode and valid Days ' +
'retention period',
params: {},
},
{
name: 'pass with COMPLIANCE retention mode',
params: { key: 'Mode', value: 'COMPLIANCE' },
},
{
name: 'pass with valid Years retention period',
params: { key: 'Years', value: 1 },
},
];
describe('ObjectLockConfiguration class getValidatedObjectLockConfiguration',
() => {
it('should return MalformedXML error if request xml is empty', done => {
const errMessage = 'request xml is undefined or empty';
checkError('', errMessage, done);
});
failTests.forEach(test => {
it(`should ${test.name}`, done => {
generateParsedXml(test.params, xml => {
checkError(xml, test.errorMessage, done);
});
});
});
passTests.forEach(test => {
it(`should ${test.name}`, done => {
generateParsedXml(test.params, xml => {
const config = new ObjectLockConfiguration(xml).
getValidatedObjectLockConfiguration();
assert.ifError(config.error);
done();
});
});
});
});