From 0be0aba545b0b84e209dfea7c74f711356caed38 Mon Sep 17 00:00:00 2001 From: Jeremy Thomerson Date: Mon, 27 Feb 2017 20:12:09 -0500 Subject: [PATCH] Add event unwrapper: s3-in-sns A common pattern in Lambda functions is to be subscribed to an SNS topic that is sending out S3 notification events. Getting down to the meat of the S3 event(s) that triggered the Lambda function requires quite a bit of parsing through the event structure(s), so this unwrapper makes that into a reusable function. --- event-unwrappers/s3-in-sns.js | 56 ++++++++++ tests/event-unwrappers/s3-in-sns.test.js | 126 +++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 event-unwrappers/s3-in-sns.js create mode 100644 tests/event-unwrappers/s3-in-sns.test.js diff --git a/event-unwrappers/s3-in-sns.js b/event-unwrappers/s3-in-sns.js new file mode 100644 index 0000000..33911b9 --- /dev/null +++ b/event-unwrappers/s3-in-sns.js @@ -0,0 +1,56 @@ +'use strict'; + +var _ = require('underscore'); + +/** + * Converts an SNS event that contains S3 events as its message into a list of + * objects representing each S3 item included in the message(s). + * + * Format of each S3 event object returned by this function: + * { + * region: 'us-east-1', + * time: '2017-02-27T23:16:36.367Z', + * name: 'ObjectCreated:Put', + * bucket: { // whatever is passed in the raw S3 message bucket object + * name: 'some-bucket-name', + * arn: 'arn:....:some-bucket-name', + * ownerIdentity: { principalId: 'ABCDEFGHIJ' }, + * }, + * object: { // whatever is passed in the raw S3 message object object + * key: 'the-s3-object-key.foo', + * size: 1234, // file size + * eTag: '2g4d34e9e7g513geg542958891434e1e', + * versionId: '6CSRvM11Ku9dnTY4TgHdOP1tG1XYo1jj', + * sequencer: '1158G4B16A1FC5422D' + * }, + * } + * + * @param object evt the event that your Lambda function was invoked with + * @return array of simplified events, one event for each S3 event in the message(s) + */ +module.exports = function(evt) { + return _.chain(evt.Records) + .map(function(snsRecord) { + if (snsRecord.EventSource !== 'aws:sns') { + throw new Error('The event must come from SNS, but appears to come from ' + snsRecord.EventSource); + } + + return JSON.parse(snsRecord.Sns.Message); + }) + .pluck('Records') + .flatten() + .map(function(s3Record) { + if (s3Record.eventSource !== 'aws:s3') { + throw new Error('The messages in the event must come from S3, but appear to come from ' + s3Record.eventSource); + } + + return { + region: s3Record.awsRegion, + time: s3Record.eventTime, + name: s3Record.eventName, + bucket: s3Record.s3.bucket, + object: s3Record.s3.object, + }; + }) + .value(); +}; diff --git a/tests/event-unwrappers/s3-in-sns.test.js b/tests/event-unwrappers/s3-in-sns.test.js new file mode 100644 index 0000000..c06bad1 --- /dev/null +++ b/tests/event-unwrappers/s3-in-sns.test.js @@ -0,0 +1,126 @@ +'use strict'; + +var expect = require('expect.js'), + unwrapper = require('../../event-unwrappers/s3-in-sns'), + SAMPLE_EVENT; + +SAMPLE_EVENT = { + Records: [ + { + EventSource: 'aws:sns', + EventVersion: '1.0', + EventSubscriptionArn: 'arn:aws:sns:us-east-1:1234567890:some-topic-name:08d701fa-2d0d-432e-9a21-ea3986e31223', + Sns: { + Type: 'Notification', + MessageId: 'bc37cd54-0279-4e97-8727-2facae3817d3', + TopicArn: 'arn:aws:sns:us-east-1:1234567890:some-topic-name', + Subject: 'Amazon S3 Notification', + // eslint-disable-next-line max-len + Message: '{"Records":[{"eventVersion":"2.0","eventSource":"aws:s3","awsRegion":"us-east-1","eventTime":"2017-02-27T23:19:31.970Z","eventName":"ObjectCreated:Put","userIdentity":{"principalId":"AWS:ABCDEFGHIJKLMNOPQRSTUV:cli"},"requestParameters":{"sourceIPAddress":"8.8.8.8"},"responseElements":{"x-amz-request-id":"ABCDEFGHIJK123","x-amz-id-2":"ABCDEFGHJKLMNOPQRSTUV"},"s3":{"s3SchemaVersion":"1.0","configurationId":"8c9d1f41-bdd8-42be-a2ab-6a0a98b29b21","bucket":{"name":"some-bucket-name","ownerIdentity":{"principalId":"ABCDEFGHIJKL"},"arn":"arn:aws:s3:::some-bucket-name"},"object":{"key":"some-object-key-1.foo","size":1539,"eTag":"f51f20b3552d4293ba39d836a31b49a1","versionId":"3019dda287184f33944042866943875a","sequencer":"0e042b98180d"}}}]}', + Timestamp: '2017-02-27T23:19:32.053Z', + SignatureVersion: '1', + Signature: 'Bt/ABCDEFGH==', + SigningCertUrl: 'https://sns.us-east-1.amazonaws.com/SimpleNotificationService-874eba81258f454ba8bf74bf476a6f4f.pem', + UnsubscribeUrl: 'https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:1234567890:some-topic-name:08d701fa-2d0d-432e-9a21-ea3986e31223', + MessageAttributes: {} + } + }, + { + EventSource: 'aws:sns', + EventVersion: '1.0', + EventSubscriptionArn: 'arn:aws:sns:us-east-1:1234567890:some-topic-name:08d701fa-2d0d-432e-9a21-ea3986e31223', + Sns: { + Type: 'Notification', + MessageId: 'bc37cd54-0279-4e97-8727-2facae3817d3', + TopicArn: 'arn:aws:sns:us-east-1:1234567890:some-topic-name', + Subject: 'Amazon S3 Notification', + // eslint-disable-next-line max-len + Message: '{"Records":[{"eventVersion":"2.0","eventSource":"aws:s3","awsRegion":"us-east-1","eventTime":"2017-02-27T23:19:31.970Z","eventName":"ObjectCreated:Put","userIdentity":{"principalId":"AWS:ABCDEFGHIJKLMNOPQRSTUV:cli"},"requestParameters":{"sourceIPAddress":"8.8.8.8"},"responseElements":{"x-amz-request-id":"ABCDEFGHIJK123","x-amz-id-2":"ABCDEFGHJKLMNOPQRSTUV"},"s3":{"s3SchemaVersion":"1.0","configurationId":"8c9d1f41-bdd8-42be-a2ab-6a0a98b29b21","bucket":{"name":"some-bucket-name","ownerIdentity":{"principalId":"ABCDEFGHIJKL"},"arn":"arn:aws:s3:::some-bucket-name"},"object":{"key":"some-object-key-2.foo","size":1539,"eTag":"f51f20b3552d4293ba39d836a31b49a1","versionId":"3019dda287184f33944042866943875a","sequencer":"0e042b98180d"}}},{"eventVersion":"2.0","eventSource":"aws:s3","awsRegion":"us-east-1","eventTime":"2017-02-27T23:19:31.970Z","eventName":"ObjectCreated:Put","userIdentity":{"principalId":"AWS:ABCDEFGHIJKLMNOPQRSTUV:cli"},"requestParameters":{"sourceIPAddress":"8.8.8.8"},"responseElements":{"x-amz-request-id":"ABCDEFGHIJK123","x-amz-id-2":"ABCDEFGHJKLMNOPQRSTUV"},"s3":{"s3SchemaVersion":"1.0","configurationId":"8c9d1f41-bdd8-42be-a2ab-6a0a98b29b21","bucket":{"name":"some-bucket-name","ownerIdentity":{"principalId":"ABCDEFGHIJKL"},"arn":"arn:aws:s3:::some-bucket-name"},"object":{"key":"some-object-key-3.foo","size":1539,"eTag":"f51f20b3552d4293ba39d836a31b49a1","versionId":"3019dda287184f33944042866943875a","sequencer":"0e042b98180d"}}}]}', + Timestamp: '2017-02-27T23:19:32.053Z', + SignatureVersion: '1', + Signature: 'Bt/ABCDEFGH==', + SigningCertUrl: 'https://sns.us-east-1.amazonaws.com/SimpleNotificationService-874eba81258f454ba8bf74bf476a6f4f.pem', + UnsubscribeUrl: 'https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:1234567890:some-topic-name:08d701fa-2d0d-432e-9a21-ea3986e31223', + MessageAttributes: {} + } + }, + ], +}; + +describe('event-unwrappers/s3-in-sns', function() { + + it('unwraps events as expected', function() { + var resp = unwrapper(SAMPLE_EVENT); + + expect(resp).to.eql([ + { + region: 'us-east-1', + time: '2017-02-27T23:19:31.970Z', + name: 'ObjectCreated:Put', + bucket: { + name: 'some-bucket-name', + ownerIdentity: { principalId: 'ABCDEFGHIJKL' }, + arn: 'arn:aws:s3:::some-bucket-name', + }, + object: { + key: 'some-object-key-1.foo', + size: 1539, + eTag: 'f51f20b3552d4293ba39d836a31b49a1', + versionId: '3019dda287184f33944042866943875a', + sequencer: '0e042b98180d', + }, + }, + { + region: 'us-east-1', + time: '2017-02-27T23:19:31.970Z', + name: 'ObjectCreated:Put', + bucket: { + name: 'some-bucket-name', + ownerIdentity: { principalId: 'ABCDEFGHIJKL' }, + arn: 'arn:aws:s3:::some-bucket-name', + }, + object: { + key: 'some-object-key-2.foo', + size: 1539, + eTag: 'f51f20b3552d4293ba39d836a31b49a1', + versionId: '3019dda287184f33944042866943875a', + sequencer: '0e042b98180d', + }, + }, + { + region: 'us-east-1', + time: '2017-02-27T23:19:31.970Z', + name: 'ObjectCreated:Put', + bucket: { + name: 'some-bucket-name', + ownerIdentity: { principalId: 'ABCDEFGHIJKL' }, + arn: 'arn:aws:s3:::some-bucket-name', + }, + object: { + key: 'some-object-key-3.foo', + size: 1539, + eTag: 'f51f20b3552d4293ba39d836a31b49a1', + versionId: '3019dda287184f33944042866943875a', + sequencer: '0e042b98180d', + }, + }, + ]); + }); + + it('throws an error when the top-level event is not from SNS', function() { + var sample = JSON.parse(JSON.stringify(SAMPLE_EVENT)); + + sample.Records[1].EventSource = 'not-sns'; + + expect(unwrapper.bind(null, sample)).to.throwError(); + }); + + it('throws an error when the embedded S3 events are not from S3', function() { + var sample = JSON.parse(JSON.stringify(SAMPLE_EVENT)); + + sample.Records[1].Sns.Message = sample.Records[1].Sns.Message.replace('"eventSource":"aws:s3"', '"eventSource":"aws:not-s3"'); + + expect(unwrapper.bind(null, sample)).to.throwError(); + }); + +});