Compare commits
1 Commits
developmen
...
feature/S3
Author | SHA1 | Date |
---|---|---|
Dora Korpar | 5744565347 |
|
@ -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;
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue