Compare commits

...

1 Commits

Author SHA1 Message Date
Dora Korpar 5744565347 ft: S3C-1788 bucket notification model 2020-08-07 21:29:47 -07:00
2 changed files with 463 additions and 0 deletions

View File

@ -0,0 +1,247 @@
const UUID = require('uuid');
const errors = require('../errors');
/**
* Format of xml request:
*
* <ONotificationConfiguration>
* <QueueConfiguration>
* <Event>array</Event>
* <Filter>
* <S3Key>
* <FilterRule>
* <Name>string</Name>
* <Value>string</Value>
* </FilterRule>
* </S3Key>
* </Filter>
* <Id>string</Id>
* <Queue>string</Queue>
* </QueueConfiguration>
* </NotificationConfiguration>
*/
/**
* Format of config:
*
* config = {
* queueConfig: [
* {
* events: array,
* queueArn: string,
* filterRules: [
* {
* name: string,
* value: string
* },
* {
* name: string,
* value:string
* },
* ],
* id: string
* }
* ]
* }
*/
class NotificationConfiguration {
/**
* Create a Notification Configuration instance
* @param {string} xml - parsed configuration xml
* @return {object} - NotificationConfiguration instance
*/
constructor(xml) {
this._parsedXml = xml;
this._config = {};
this._ids = [];
}
/**
* Get notification configuration
* @return {object} - contains error if parsing failed
*/
getValidatedNotificationConfiguration() {
const validationError = this._parseNotificationConfig();
if (validationError) {
this._config.error = validationError;
}
return this._config;
}
/**
* Check that notification configuration is valid
* @return {error | undefined} - error if parsing failed, else undefined
*/
_parseNotificationConfig() {
if (!this._parsedXml || this._parsedXml === '') {
return errors.MalformedXML.customizeDescription(
'request xml is undefined or empty');
}
const notificationConfig = this._parsedXml.NotificationConfiguration;
if (!notificationConfig || notificationConfig === '') {
return errors.MalformedXML.customizeDescription(
'request xml does not include NotificationConfiguration');
}
const queueConfig = notificationConfig.QueueConfiguration;
if (!queueConfig || !queueConfig[0] || queueConfig[0] === '') {
return errors.MalformedXML.customizeDescription(
'request xml does not include QueueConfiguration');
}
this._config.queueConfig = [];
let parseError;
queueConfig.forEach(c => {
const eventObj = this._parseEvents(c.Event);
const filterObj = this._parseFilter(c.Filter);
const idObj = this._parseId(c.Id);
const arnObj = this._parseArn(c.QueueArn);
if (eventObj.error) {
parseError = eventObj.error;
} else if (filterObj.error) {
parseError = filterObj.error;
} else if (idObj.error) {
parseError = idObj.error;
} else if (arnObj.error) {
parseError = arnObj.error;
} else {
this._config.queueConfig.push({
events: eventObj.events,
queueArn: arnObj.arn,
id: idObj.id,
filterRules: filterObj.filterRules,
});
}
});
return parseError;
}
/**
* Check that events array is valid
* @param {array} events - event array
* @return {object} - contains error if parsing failed or events array
*/
_parseEvents(events) {
const eventsObj = {
events: [],
};
if (!events || !events[0]) {
eventsObj.error = errors.MalformedXML.customizeDescription(
'each queue configuration must contain an event');
return eventsObj;
}
const supportedEvents = {
ObjectCreated: ['*', 'Put', 'Post', 'Copy', 'CompleteMultipartUpload'],
ObjectRemoved: ['*', 'Delete', 'DeleteMarkerCreated'],
};
events.forEach(e => {
const eventSplit = e.split(':');
if (!(eventSplit[0] === 's3') ||
!supportedEvents[eventSplit[1]] ||
!supportedEvents[eventSplit[1]].includes(eventSplit[2])) {
eventsObj.error = errors.MalformedXML.customizeDescription(
'event array contains invalid or unsupported event');
}
eventsObj.events.push(e);
});
return eventsObj;
}
/**
* Check that filter array is valid
* @param {array} filter - filter array
* @return {object} - contains error if parsing failed or filter array
*/
_parseFilter(filter) {
const filterObj = {
filterRules: [],
};
if (filter && filter[0]) {
if (!filter[0].S3Key || !filter[0].S3Key[0]) {
filterObj.error = errors.MalformedXML.customizeDescription(
'if included, queue configuration filter must contain S3Key');
return filterObj;
}
const filterRules = filter[0].S3Key[0];
if (!filterRules.FilterRule || !filterRules.FilterRule[0]) {
filterObj.error = errors.MalformedXML.customizeDescription(
'if included, queue configuration filter must contain a rule');
return filterObj;
}
const ruleArray = filterRules.FilterRule;
ruleArray.forEach(r => {
if (!r.Name || !r.Name[0] || !r.Value || !r.Value[0]) {
filterObj.error = errors.MalformedXML.customizeDescription(
'each included filter must contain a name and value');
} else if (!['Prefix', 'Suffix'].includes(r.Name[0])) {
filterObj.error = errors.MalformedXML.customizeDescription(
'filter Name must be one of Prefix or Suffix');
} else {
filterObj.filterRules.push({
name: r.Name[0],
value: r.Value[0],
});
}
});
}
return filterObj;
}
/**
* Check that id string is valid
* @param {string} id - id string (optional)
* @return {object} - contains error if parsing failed or id
*/
_parseId(id) {
const idObj = {};
idObj.propName = 'ruleID';
if (id && id[0].length > 255) {
idObj.error = errors.InvalidArgument.customizeDescription(
'queue configuration ID is greater than 255 characters long');
return idObj;
}
if (!id || !id[0] || id[0] === '') {
// id is optional property, so create one if not provided or is ''
// We generate 48-character alphanumeric, unique id for rule
idObj.id = Buffer.from(UUID.v4()).toString('base64');
} else {
idObj.id = id[0];
}
// Each ID in a list of rules must be unique.
if (this._ids.includes(idObj.id)) {
idObj.error = errors.InvalidRequest.customizeDescription(
'queue configuration ID must be unique');
return idObj;
}
this._ids.push(idObj.id);
return idObj;
}
/**
* Check that arn string is valid
* @param {string} arn - queue arn
* @return {object} - contains error if parsing failed or queue arn
*/
_parseArn(arn) {
const arnObj = {};
if (!arn || !arn[0]) {
arnObj.error = errors.MalformedXML.customizeDescription(
'each queue configuration must contain a queue arn');
return arnObj;
}
const splitArn = arn[0].split(':');
if (splitArn.length !== 6 ||
!(splitArn[0] === 'arn') ||
!(splitArn[1] === 'scality') ||
!(splitArn[2] === 'bucketnotif')) {
arnObj.error = errors.MalformedXML.customizeDescription(
'queue arn is invalid');
}
// remaining 3 parts of arn are evaluated in cloudserver
arnObj.arn = arn[0];
return arnObj;
}
}
module.exports = NotificationConfiguration;

View File

@ -0,0 +1,216 @@
const assert = require('assert');
const { parseString } = require('xml2js');
const NotificationConfiguration =
require('../../../lib/models/NotificationConfiguration.js');
function checkError(parsedXml, err, errMessage, cb) {
const config = new NotificationConfiguration(parsedXml).
getValidatedNotificationConfiguration();
assert.strictEqual(config.error[err], true);
assert.strictEqual(config.error.description, errMessage);
cb();
}
function generateEvent(testParams) {
const event = [];
if (testParams.key === 'Event') {
if (Array.isArray(testParams.value)) {
testParams.value.forEach(v => {
event.push(`${event}<Event>${v}</Event>`);
});
} else {
event.push(`<Event>${testParams.value}</Event>`);
}
} else {
event.push('<Event>s3:ObjectCreated:*</Event>');
}
return event.join('');
}
function generateFilter(testParams) {
let filter = '';
if (testParams.key === 'Filter') {
filter = `<Filter>${testParams.value}</Filter>`;
}
if (testParams.key === 'S3Key') {
filter = `<Filter><S3Key>${testParams.value}</S3Key></Filter>`;
}
if (testParams.key === 'FilterRule') {
if (Array.isArray(testParams.value)) {
testParams.value.forEach(v => {
filter = `${filter}<Filter><S3Key><FilterRule>${v}` +
'</FilterRule></S3Key></Filter>';
});
} else {
filter = `<Filter><S3Key><FilterRule>${testParams.value}` +
'</FilterRule></S3Key></Filter>';
}
}
return filter;
}
function generateXml(testParams) {
const id = testParams.key === 'Id' ? `<Id>${testParams.value}</Id>` : '<Id>queue-id</Id>';
const arn = testParams.key === 'QueueArn' ?
`<QueueArn>${testParams.value}</QueueArn>` :
'<QueueArn>arn:scality:bucketnotif:::target</QueueArn>';
const event = generateEvent(testParams);
const filter = generateFilter(testParams);
let queueConfig = `<QueueConfiguration>${id}${arn}${event}${filter}` +
'</QueueConfiguration>';
if (testParams.key === 'QueueConfiguration') {
if (testParams.value === 'double') {
queueConfig = `${queueConfig}${queueConfig}`;
} else {
queueConfig = testParams.value;
}
}
const notification = testParams.key === 'NotificationConfiguration' ? '' :
`<NotificationConfiguration>${queueConfig}</NotificationConfiguration>`;
return notification;
}
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: 'NotificationConfiguration' },
error: 'MalformedXML',
errorMessage: 'request xml is undefined or empty',
},
{
name: 'fail with empty QueueConfiguration',
params: { key: 'QueueConfiguration', value: '<QueueArn>arn:scality:bucketnotif:::target</QueueArn>' },
error: 'MalformedXML',
errorMessage: 'request xml does not include QueueConfiguration',
},
{
name: 'fail with invalid id',
params: { key: 'Id', value: 'a'.repeat(256) },
error: 'InvalidArgument',
errorMessage: 'queue configuration ID is greater than 255 characters long',
},
{
name: 'fail with repeated id',
params: { key: 'QueueConfiguration', value: 'double' },
error: 'InvalidRequest',
errorMessage: 'queue configuration ID must be unique',
},
{
name: 'fail with empty QueueArn',
params: { key: 'QueueArn', value: '' },
error: 'MalformedXML',
errorMessage: 'each queue configuration must contain a queue arn',
},
{
name: 'fail with invalid QueueArn',
params: { key: 'QueueArn', value: 'arn:scality:bucketnotif:target' },
error: 'MalformedXML',
errorMessage: 'queue arn is invalid',
},
{
name: 'fail with invalid QueueArn partition',
params: { key: 'QueueArn', value: 'arn:aws:bucketnotif:::target' },
error: 'MalformedXML',
errorMessage: 'queue arn is invalid',
},
{
name: 'fail with empty event',
params: { key: 'Event', value: '' },
error: 'MalformedXML',
errorMessage: 'each queue configuration must contain an event',
},
{
name: 'fail with invalid event',
params: { key: 'Event', value: 's3:BucketCreated:Put' },
error: 'MalformedXML',
errorMessage: 'event array contains invalid or unsupported event',
},
{
name: 'fail with unsupported event',
params: { key: 'Event', value: 's3:Replication:OperationNotTracked' },
error: 'MalformedXML',
errorMessage: 'event array contains invalid or unsupported event',
},
{
name: 'fail with filter that does not contain S3Key',
params: { key: 'Filter', value: '<FilterRule><Name>Prefix</Name><Value>logs/</Value></FilterRule>' },
error: 'MalformedXML',
errorMessage: 'if included, queue configuration filter must contain S3Key',
},
{
name: 'fail with filter that does not contain a rule',
params: { key: 'S3Key', value: '<Name>Prefix</Name><Value>logs/</Value>' },
error: 'MalformedXML',
errorMessage: 'if included, queue configuration filter must contain a rule',
},
{
name: 'fail with filter rule that does not contain name and value',
params: { key: 'FilterRule', value: '<Value>noname</Value>' },
error: 'MalformedXML',
errorMessage: 'each included filter must contain a name and value',
},
{
name: 'fail with invalid name in filter rule',
params: { key: 'FilterRule', value: '<Name>Invalid</Name><Value>logs/</Value>' },
error: 'MalformedXML',
errorMessage: 'filter Name must be one of Prefix or Suffix',
},
];
const passTests = [
{
name: 'pass with multiple events in one queue configuration',
params: {
key: 'Event', value: ['s3:ObjectCreated:Put', 's3:ObjectCreated:Copy'],
},
},
{
name: 'pass with multiple filter rules',
params: {
key: 'FilterRule',
value: ['<Name>Prefix</Name><Value>logs/</Value>', '<Name>Suffix</Name><Value>.pdf</Value>'] },
},
{
name: 'pass with no id',
params: { key: 'Id', value: '' },
},
{
name: 'pass with basic config', params: {},
},
];
describe.only('NotificationConfiguration class getValidatedNotificationConfiguration',
() => {
it('should return MalformedXML error if request xml is empty', done => {
const errMessage = 'request xml is undefined or empty';
checkError('', 'MalformedXML', errMessage, done);
});
failTests.forEach(test => {
it(`should ${test.name}`, done => {
generateParsedXml(test.params, xml => {
checkError(xml, test.error, test.errorMessage, done);
});
});
});
passTests.forEach(test => {
it(`should ${test.name}`, done => {
generateParsedXml(test.params, xml => {
const config = new NotificationConfiguration(xml).
getValidatedNotificationConfiguration();
assert.ifError(config.error);
done();
});
});
});
});