From 23ab79d85ef071cbe6423796a47619cffd2d2db5 Mon Sep 17 00:00:00 2001 From: Peter Horvath Date: Wed, 28 Feb 2024 15:06:54 +0100 Subject: [PATCH] Collect SQS resources for the IAM role separately and add them in one step to the template --- .../serverless-sns-sqs-lambda.test.ts.snap | 657 +++++++++++++++++- lib/serverless-sns-sqs-lambda.test.ts | 140 +++- lib/serverless-sns-sqs-lambda.ts | 119 ++-- 3 files changed, 829 insertions(+), 87 deletions(-) diff --git a/lib/__snapshots__/serverless-sns-sqs-lambda.test.ts.snap b/lib/__snapshots__/serverless-sns-sqs-lambda.test.ts.snap index 7902a4be..7abcda6e 100644 --- a/lib/__snapshots__/serverless-sns-sqs-lambda.test.ts.snap +++ b/lib/__snapshots__/serverless-sns-sqs-lambda.test.ts.snap @@ -600,7 +600,9 @@ Object { "kms:Decrypt", ], "Effect": "Allow", - "Resource": "arn:aws:kms:::key/some key", + "Resource": Array [ + "arn:aws:kms:::key/some key", + ], }, ], "Version": "2012-10-17", @@ -2479,6 +2481,659 @@ Object { } `; +exports[`Test Serverless SNS SQS Lambda when the provider is specified via a command line option when there are multiple functions with snsSqs should produce an IAM role with merged resources 1`] = ` +Object { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application", + "Outputs": Object { + "ServerlessDeploymentBucketName": Object { + "Export": Object { + "Name": "sls-test-service-dev-test-ServerlessDeploymentBucketName", + }, + "Value": Object { + "Ref": "ServerlessDeploymentBucket", + }, + }, + "TestDashfunction2LambdaFunctionQualifiedArn": Object { + "Description": "Current Lambda function version", + "Export": Object { + "Name": "sls-test-service-dev-test-TestDashfunction2LambdaFunctionQualifiedArn", + }, + "Value": Object { + "Ref": "TestDashfunction2LambdaVersionpkdbJVGn15K2PFByNWdnzkZM3ZUwNbbAp5oPLsIIdQ", + }, + }, + "TestDashfunction3LambdaFunctionQualifiedArn": Object { + "Description": "Current Lambda function version", + "Export": Object { + "Name": "sls-test-service-dev-test-TestDashfunction3LambdaFunctionQualifiedArn", + }, + "Value": Object { + "Ref": "TestDashfunction3LambdaVersionSTVDvOa0K2Ydms9BOyRe9ijY3Pl3fDllGNEwG9HSw", + }, + }, + "TestDashfunctionLambdaFunctionQualifiedArn": Object { + "Description": "Current Lambda function version", + "Export": Object { + "Name": "sls-test-service-dev-test-TestDashfunctionLambdaFunctionQualifiedArn", + }, + "Value": Object { + "Ref": "TestDashfunctionLambdaVersionA6M23sE6AN9SgN5IQgI9bd1tqh7YgxtybZ9LOhkLY4", + }, + }, + }, + "Resources": Object { + "IamRoleLambdaExecution": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "sts:AssumeRole", + ], + "Effect": "Allow", + "Principal": Object { + "Service": Array [ + "lambda.amazonaws.com", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "Path": "/", + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Sub": "arn:\${AWS::Partition}:logs:\${AWS::Region}:\${AWS::AccountId}:log-group:/aws/lambda/test-service-dev-test*:*", + }, + ], + }, + Object { + "Action": Array [ + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Sub": "arn:\${AWS::Partition}:logs:\${AWS::Region}:\${AWS::AccountId}:log-group:/aws/lambda/test-service-dev-test*:*:*", + }, + ], + }, + Object { + "Action": Array [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "some-nameQueue", + "Arn", + ], + }, + Object { + "Fn::GetAtt": Array [ + "some-nameDeadLetterQueue", + "Arn", + ], + }, + Object { + "Fn::GetAtt": Array [ + "some-name2Queue", + "Arn", + ], + }, + Object { + "Fn::GetAtt": Array [ + "some-name2DeadLetterQueue", + "Arn", + ], + }, + Object { + "Fn::GetAtt": Array [ + "some-name3Queue", + "Arn", + ], + }, + Object { + "Fn::GetAtt": Array [ + "some-name3DeadLetterQueue", + "Arn", + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + ], + "Effect": "Allow", + "Resource": Array [ + "arn:aws:kms:::key/kms1", + "arn:aws:kms:::key/kms2", + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": Object { + "Fn::Join": Array [ + "-", + Array [ + "test-service", + "dev-test", + "lambda", + ], + ], + }, + }, + ], + "RoleName": Object { + "Fn::Join": Array [ + "-", + Array [ + "test-service", + "dev-test", + Object { + "Ref": "AWS::Region", + }, + "lambdaRole", + ], + ], + }, + }, + "Type": "AWS::IAM::Role", + }, + "ServerlessDeploymentBucket": Object { + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + }, + "Type": "AWS::S3::Bucket", + }, + "ServerlessDeploymentBucketPolicy": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "ServerlessDeploymentBucket", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": false, + }, + }, + "Effect": "Deny", + "Principal": "*", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::", + Object { + "Ref": "ServerlessDeploymentBucket", + }, + "/*", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::", + Object { + "Ref": "ServerlessDeploymentBucket", + }, + ], + ], + }, + ], + }, + ], + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "Subscribesome-name2Topic": Object { + "Properties": Object { + "Endpoint": Object { + "Fn::GetAtt": Array [ + "some-name2Queue", + "Arn", + ], + }, + "Protocol": "sqs", + "RawMessageDelivery": false, + "TopicArn": "arn:aws:sns:us-east-2:123456789012:MyTopic2", + }, + "Type": "AWS::SNS::Subscription", + }, + "Subscribesome-name3Topic": Object { + "Properties": Object { + "Endpoint": Object { + "Fn::GetAtt": Array [ + "some-name3Queue", + "Arn", + ], + }, + "Protocol": "sqs", + "RawMessageDelivery": false, + "TopicArn": "arn:aws:sns:us-east-2:123456789012:MyTopic3", + }, + "Type": "AWS::SNS::Subscription", + }, + "Subscribesome-nameTopic": Object { + "Properties": Object { + "Endpoint": Object { + "Fn::GetAtt": Array [ + "some-nameQueue", + "Arn", + ], + }, + "Protocol": "sqs", + "RawMessageDelivery": false, + "TopicArn": "arn:aws:sns:us-east-2:123456789012:MyTopic", + }, + "Type": "AWS::SNS::Subscription", + }, + "Test-function2EventSourceMappingSQSsome-name2Queue": Object { + "Properties": Object { + "BatchSize": 10, + "Enabled": "True", + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "some-name2Queue", + "Arn", + ], + }, + "FunctionName": Object { + "Fn::GetAtt": Array [ + "Test-function2LambdaFunction", + "Arn", + ], + }, + "MaximumBatchingWindowInSeconds": 0, + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "Test-function3EventSourceMappingSQSsome-name3Queue": Object { + "Properties": Object { + "BatchSize": 10, + "Enabled": "True", + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "some-name3Queue", + "Arn", + ], + }, + "FunctionName": Object { + "Fn::GetAtt": Array [ + "Test-function3LambdaFunction", + "Arn", + ], + }, + "MaximumBatchingWindowInSeconds": 0, + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "Test-functionEventSourceMappingSQSsome-nameQueue": Object { + "Properties": Object { + "BatchSize": 10, + "Enabled": "True", + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "some-nameQueue", + "Arn", + ], + }, + "FunctionName": Object { + "Fn::GetAtt": Array [ + "Test-functionLambdaFunction", + "Arn", + ], + }, + "MaximumBatchingWindowInSeconds": 0, + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "TestDashfunction2LambdaFunction": Object { + "DependsOn": Array [ + "TestDashfunction2LogGroup", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "ServerlessDeploymentBucket", + }, + "S3Key": Any, + }, + "FunctionName": "test-service-dev-test-test-function2", + "Handler": "handler2.handler", + "MemorySize": 1024, + "Role": Object { + "Fn::GetAtt": Array [ + "IamRoleLambdaExecution", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 6, + }, + "Type": "AWS::Lambda::Function", + }, + "TestDashfunction2LambdaVersionpkdbJVGn15K2PFByNWdnzkZM3ZUwNbbAp5oPLsIIdQ": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "CodeSha256": "gxQ2/ARVAXYSjz4OF5PnsOiOB+yUlXG8z5y5h6bNs7U=", + "FunctionName": Object { + "Ref": "TestDashfunction2LambdaFunction", + }, + }, + "Type": "AWS::Lambda::Version", + }, + "TestDashfunction2LogGroup": Object { + "Properties": Object { + "LogGroupName": "/aws/lambda/test-service-dev-test-test-function2", + }, + "Type": "AWS::Logs::LogGroup", + }, + "TestDashfunction3LambdaFunction": Object { + "DependsOn": Array [ + "TestDashfunction3LogGroup", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "ServerlessDeploymentBucket", + }, + "S3Key": Any, + }, + "FunctionName": "test-service-dev-test-test-function3", + "Handler": "handler3.handler", + "MemorySize": 1024, + "Role": Object { + "Fn::GetAtt": Array [ + "IamRoleLambdaExecution", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 6, + }, + "Type": "AWS::Lambda::Function", + }, + "TestDashfunction3LambdaVersionSTVDvOa0K2Ydms9BOyRe9ijY3Pl3fDllGNEwG9HSw": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "CodeSha256": "gxQ2/ARVAXYSjz4OF5PnsOiOB+yUlXG8z5y5h6bNs7U=", + "FunctionName": Object { + "Ref": "TestDashfunction3LambdaFunction", + }, + }, + "Type": "AWS::Lambda::Version", + }, + "TestDashfunction3LogGroup": Object { + "Properties": Object { + "LogGroupName": "/aws/lambda/test-service-dev-test-test-function3", + }, + "Type": "AWS::Logs::LogGroup", + }, + "TestDashfunctionLambdaFunction": Object { + "DependsOn": Array [ + "TestDashfunctionLogGroup", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "ServerlessDeploymentBucket", + }, + "S3Key": Any, + }, + "FunctionName": "test-service-dev-test-test-function", + "Handler": "handler.handler", + "MemorySize": 1024, + "Role": Object { + "Fn::GetAtt": Array [ + "IamRoleLambdaExecution", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 6, + }, + "Type": "AWS::Lambda::Function", + }, + "TestDashfunctionLambdaVersionA6M23sE6AN9SgN5IQgI9bd1tqh7YgxtybZ9LOhkLY4": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "CodeSha256": "gxQ2/ARVAXYSjz4OF5PnsOiOB+yUlXG8z5y5h6bNs7U=", + "FunctionName": Object { + "Ref": "TestDashfunctionLambdaFunction", + }, + }, + "Type": "AWS::Lambda::Version", + }, + "TestDashfunctionLogGroup": Object { + "Properties": Object { + "LogGroupName": "/aws/lambda/test-service-dev-test-test-function", + }, + "Type": "AWS::Logs::LogGroup", + }, + "some-name2DeadLetterQueue": Object { + "Properties": Object { + "KmsMasterKeyId": "kms2", + "QueueName": "test-service-dev-test-Test-function2some-name2DeadLetterQueue", + }, + "Type": "AWS::SQS::Queue", + }, + "some-name2Queue": Object { + "Properties": Object { + "KmsMasterKeyId": "kms2", + "QueueName": "test-service-dev-test-Test-function2some-name2Queue", + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "some-name2DeadLetterQueue", + "Arn", + ], + }, + "maxReceiveCount": 5, + }, + }, + "Type": "AWS::SQS::Queue", + }, + "some-name2QueuePolicy": Object { + "Properties": Object { + "PolicyDocument": Object { + "Id": "test-service-dev-test-Test-function2some-name2Queue", + "Statement": Array [ + Object { + "Action": "SQS:SendMessage", + "Condition": Object { + "ArnEquals": Object { + "aws:SourceArn": Array [ + "arn:aws:sns:us-east-2:123456789012:MyTopic2", + ], + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": "sns.amazonaws.com", + }, + "Resource": Object { + "Fn::GetAtt": Array [ + "some-name2Queue", + "Arn", + ], + }, + "Sid": "test-service-dev-test-Test-function2some-name2Sid", + }, + ], + "Version": "2012-10-17", + }, + "Queues": Array [ + Object { + "Ref": "some-name2Queue", + }, + ], + }, + "Type": "AWS::SQS::QueuePolicy", + }, + "some-name3DeadLetterQueue": Object { + "Properties": Object { + "KmsMasterKeyId": "arn:aws:kms:::key/kms2", + "QueueName": "test-service-dev-test-Test-function3some-name3DeadLetterQueue", + }, + "Type": "AWS::SQS::Queue", + }, + "some-name3Queue": Object { + "Properties": Object { + "KmsMasterKeyId": "arn:aws:kms:::key/kms2", + "QueueName": "test-service-dev-test-Test-function3some-name3Queue", + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "some-name3DeadLetterQueue", + "Arn", + ], + }, + "maxReceiveCount": 5, + }, + }, + "Type": "AWS::SQS::Queue", + }, + "some-name3QueuePolicy": Object { + "Properties": Object { + "PolicyDocument": Object { + "Id": "test-service-dev-test-Test-function3some-name3Queue", + "Statement": Array [ + Object { + "Action": "SQS:SendMessage", + "Condition": Object { + "ArnEquals": Object { + "aws:SourceArn": Array [ + "arn:aws:sns:us-east-2:123456789012:MyTopic3", + ], + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": "sns.amazonaws.com", + }, + "Resource": Object { + "Fn::GetAtt": Array [ + "some-name3Queue", + "Arn", + ], + }, + "Sid": "test-service-dev-test-Test-function3some-name3Sid", + }, + ], + "Version": "2012-10-17", + }, + "Queues": Array [ + Object { + "Ref": "some-name3Queue", + }, + ], + }, + "Type": "AWS::SQS::QueuePolicy", + }, + "some-nameDeadLetterQueue": Object { + "Properties": Object { + "KmsMasterKeyId": "kms1", + "QueueName": "test-service-dev-test-Test-functionsome-nameDeadLetterQueue", + }, + "Type": "AWS::SQS::Queue", + }, + "some-nameQueue": Object { + "Properties": Object { + "KmsMasterKeyId": "kms1", + "QueueName": "test-service-dev-test-Test-functionsome-nameQueue", + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "some-nameDeadLetterQueue", + "Arn", + ], + }, + "maxReceiveCount": 5, + }, + }, + "Type": "AWS::SQS::Queue", + }, + "some-nameQueuePolicy": Object { + "Properties": Object { + "PolicyDocument": Object { + "Id": "test-service-dev-test-Test-functionsome-nameQueue", + "Statement": Array [ + Object { + "Action": "SQS:SendMessage", + "Condition": Object { + "ArnEquals": Object { + "aws:SourceArn": Array [ + "arn:aws:sns:us-east-2:123456789012:MyTopic", + ], + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": "sns.amazonaws.com", + }, + "Resource": Object { + "Fn::GetAtt": Array [ + "some-nameQueue", + "Arn", + ], + }, + "Sid": "test-service-dev-test-Test-functionsome-nameSid", + }, + ], + "Version": "2012-10-17", + }, + "Queues": Array [ + Object { + "Ref": "some-nameQueue", + }, + ], + }, + "Type": "AWS::SQS::QueuePolicy", + }, + }, +} +`; + exports[`Test Serverless SNS SQS Lambda when the provider is specified via a config option in serverless.yml when fifo is true should produce valid fifo queues 1`] = ` Object { "Resources": Object { diff --git a/lib/serverless-sns-sqs-lambda.test.ts b/lib/serverless-sns-sqs-lambda.test.ts index 5460f4f2..9a61df4f 100644 --- a/lib/serverless-sns-sqs-lambda.test.ts +++ b/lib/serverless-sns-sqs-lambda.test.ts @@ -464,6 +464,75 @@ describe("Test Serverless SNS SQS Lambda", () => { }); }); }); + + describe("when there are multiple functions with snsSqs", () => { + it("should produce an IAM role with merged resources", async () => { + const { cfTemplate } = await runServerless(serverlessPath, { + command: "package", + config: { + ...baseConfig, + functions: { + ["test-function"]: { + handler: "handler.handler", + events: [ + { + snsSqs: { + name: "some-name", + topicArn: "arn:aws:sns:us-east-2:123456789012:MyTopic", + kmsMasterKeyId: "kms1" + } + } + ] + }, + ["test-function2"]: { + handler: "handler2.handler", + events: [ + { + snsSqs: { + name: "some-name2", + topicArn: "arn:aws:sns:us-east-2:123456789012:MyTopic2", + kmsMasterKeyId: "kms2" + } + } + ] + }, + ["test-function3"]: { + handler: "handler3.handler", + events: [ + { + snsSqs: { + name: "some-name3", + topicArn: "arn:aws:sns:us-east-2:123456789012:MyTopic3", + kmsMasterKeyId: "arn:aws:kms:::key/kms2" + } + } + ] + } + } + } + }); + + expect(cfTemplate).toMatchSnapshot({ + Resources: { + TestDashfunctionLambdaFunction: { + Properties: { + Code: { S3Key: expect.any(String) } + } + }, + TestDashfunction2LambdaFunction: { + Properties: { + Code: { S3Key: expect.any(String) } + } + }, + TestDashfunction3LambdaFunction: { + Properties: { + Code: { S3Key: expect.any(String) } + } + } + } + }); + }); + }); }); describe("when the provider is specified via a config option in serverless.yml", () => { @@ -507,10 +576,9 @@ describe("Test Serverless SNS SQS Lambda", () => { ); serverlessSnsSqsLambda.addEventSourceMapping(template, validatedConfig); serverlessSnsSqsLambda.addTopicSubscription(template, validatedConfig); - serverlessSnsSqsLambda.addLambdaSqsPermissions( - template, + serverlessSnsSqsLambda.addLambdaSqsPermissions(template, [ validatedConfig - ); + ]); expect(template).toMatchSnapshot(); }); @@ -540,10 +608,9 @@ describe("Test Serverless SNS SQS Lambda", () => { ); serverlessSnsSqsLambda.addEventSourceMapping(template, validatedConfig); serverlessSnsSqsLambda.addTopicSubscription(template, validatedConfig); - serverlessSnsSqsLambda.addLambdaSqsPermissions( - template, + serverlessSnsSqsLambda.addLambdaSqsPermissions(template, [ validatedConfig - ); + ]); expect(template).toMatchSnapshot(); }); @@ -590,10 +657,9 @@ describe("Test Serverless SNS SQS Lambda", () => { ); serverlessSnsSqsLambda.addEventSourceMapping(template, validatedConfig); serverlessSnsSqsLambda.addTopicSubscription(template, validatedConfig); - serverlessSnsSqsLambda.addLambdaSqsPermissions( - template, + serverlessSnsSqsLambda.addLambdaSqsPermissions(template, [ validatedConfig - ); + ]); expect(template).toMatchSnapshot(); }); @@ -623,10 +689,9 @@ describe("Test Serverless SNS SQS Lambda", () => { ); serverlessSnsSqsLambda.addEventSourceMapping(template, validatedConfig); serverlessSnsSqsLambda.addTopicSubscription(template, validatedConfig); - serverlessSnsSqsLambda.addLambdaSqsPermissions( - template, + serverlessSnsSqsLambda.addLambdaSqsPermissions(template, [ validatedConfig - ); + ]); expect(template).toMatchSnapshot(); }); @@ -670,10 +735,9 @@ describe("Test Serverless SNS SQS Lambda", () => { ); serverlessSnsSqsLambda.addEventSourceMapping(template, validatedConfig); serverlessSnsSqsLambda.addTopicSubscription(template, validatedConfig); - serverlessSnsSqsLambda.addLambdaSqsPermissions( - template, + serverlessSnsSqsLambda.addLambdaSqsPermissions(template, [ validatedConfig - ); + ]); expect(template).toMatchSnapshot(); }); @@ -703,10 +767,9 @@ describe("Test Serverless SNS SQS Lambda", () => { ); serverlessSnsSqsLambda.addEventSourceMapping(template, validatedConfig); serverlessSnsSqsLambda.addTopicSubscription(template, validatedConfig); - serverlessSnsSqsLambda.addLambdaSqsPermissions( - template, + serverlessSnsSqsLambda.addLambdaSqsPermissions(template, [ validatedConfig - ); + ]); expect(template).toMatchSnapshot(); }); @@ -744,19 +807,20 @@ describe("Test Serverless SNS SQS Lambda", () => { } } as const; + const fn1Config = serverlessSnsSqsLambda.validateConfig( + "Fn1", + "unit-test", + testCase.functions.Fn1.events[0].snsSqs + ); + const fn2Config = serverlessSnsSqsLambda.validateConfig( + "Fn2", + "unit-test", + testCase.functions.Fn2.events[0].snsSqs + ); + const thunk = () => { - serverlessSnsSqsLambda.addSnsSqsResources( - template, - "Fn1", - "unit-test", - testCase.functions.Fn1.events[0].snsSqs - ); - serverlessSnsSqsLambda.addSnsSqsResources( - template, - "Fn2", - "unit-test", - testCase.functions.Fn2.events[0].snsSqs - ); + serverlessSnsSqsLambda.addSnsSqsResources(template, fn1Config); + serverlessSnsSqsLambda.addSnsSqsResources(template, fn2Config); }; expect(thunk).toThrowErrorMatchingInlineSnapshot( @@ -789,13 +853,13 @@ describe("Test Serverless SNS SQS Lambda", () => { } } as const; + const fn1Config = serverlessSnsSqsLambda.validateConfig( + "Fn1", + "unit-test", + testCase.functions.Fn1.events[0].snsSqs + ); const thunk = () => { - serverlessSnsSqsLambda.addSnsSqsResources( - template, - "Fn1", - "unit-test", - testCase.functions.Fn1.events[0].snsSqs - ); + serverlessSnsSqsLambda.addSnsSqsResources(template, fn1Config); }; expect(thunk).toThrowErrorMatchingInlineSnapshot( @@ -828,12 +892,12 @@ describe("Test Serverless SNS SQS Lambda", () => { } } as const; - serverlessSnsSqsLambda.addSnsSqsResources( - template, + const fn1Config = serverlessSnsSqsLambda.validateConfig( "Fn1", "unit-test", testCase.functions.Fn1.events[0].snsSqs ); + serverlessSnsSqsLambda.addSnsSqsResources(template, fn1Config); const regularQueueName = template.Resources["over-80-characters-which-is-no-goodQueue"] diff --git a/lib/serverless-sns-sqs-lambda.ts b/lib/serverless-sns-sqs-lambda.ts index ca35dcef..c865f45e 100644 --- a/lib/serverless-sns-sqs-lambda.ts +++ b/lib/serverless-sns-sqs-lambda.ts @@ -233,46 +233,56 @@ export default class ServerlessSnsSqsLambda { const template = this.serverless.service.provider.compiledCloudFormationTemplate; + const snsSqsEvents = this.getValidSnsSqsEvents(functions); + + snsSqsEvents.forEach(snsSqsEvent => { + if (this.options.verbose) { + console.info( + `Adding snsSqs event handler [${JSON.stringify(snsSqsEvent)}]` + ); + } + this.addSnsSqsResources(template, snsSqsEvent); + }); + + this.addLambdaSqsPermissions(template, snsSqsEvents); + } + + /** + * + * @param functions functions from serverless + * @returns an array of valid snsSqs events + */ + getValidSnsSqsEvents(functions): Config[] { + const snsSqsEvents = []; Object.keys(functions).forEach(funcKey => { const func = functions[funcKey]; if (func.events) { func.events.forEach(event => { if (event.snsSqs) { - if (this.options.verbose) { - console.info( - `Adding snsSqs event handler [${JSON.stringify(event.snsSqs)}]` - ); - } - this.addSnsSqsResources( - template, - funcKey, - this.stage, - event.snsSqs + snsSqsEvents.push( + this.validateConfig(funcKey, this.stage, event.snsSqs) ); } }); } }); + + return snsSqsEvents; } /** * * @param {object} template the template which gets mutated - * @param {string} funcName the name of the function from serverless config - * @param {string} stage the stage name from the serverless config - * @param {object} snsSqsConfig the configuration values from the snsSqs + * @param {object} snsSqsConfig the validated configuration values from the snsSqs * event portion of the serverless function config */ - addSnsSqsResources(template, funcName, stage, snsSqsConfig) { - const config = this.validateConfig(funcName, stage, snsSqsConfig); - + addSnsSqsResources(template, config) { [ this.addEventSourceMapping, this.addEventDeadLetterQueue, this.addEventQueue, this.addEventQueuePolicy, - this.addTopicSubscription, - this.addLambdaSqsPermissions + this.addTopicSubscription ].reduce((template, func) => { func(template, config); return template; @@ -596,49 +606,62 @@ Usage * Add permissions so that the SQS handler can access the queue. * * @param {object} template the template which gets mutated - * @param {{name, prefix}} config the name of the queue the lambda is subscribed to + * @param {Config[]} array of valid snsSqsEvents */ - addLambdaSqsPermissions( - template, - { name, kmsMasterKeyId, deadLetterQueueEnabled } - ) { + addLambdaSqsPermissions(template, snsSqsEvents: Config[]) { if (template.Resources.IamRoleLambdaExecution === undefined) { // The user has set their own custom role ARN so the Serverless generated role is not generated // We can safely skip this step because the owner of the custom role ARN is responsible for setting // this the relevant policy to allow the lambda to access the queue. return; } - const queues = [{ "Fn::GetAtt": [`${name}Queue`, "Arn"] }]; - if (deadLetterQueueEnabled) { - queues.push({ "Fn::GetAtt": [`${name}DeadLetterQueue`, "Arn"] }); - } - template.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push( - { - Effect: "Allow", - Action: [ - "sqs:ReceiveMessage", - "sqs:DeleteMessage", - "sqs:GetQueueAttributes" - ], - Resource: queues + + const newQueueNames: string[] = []; + const kmsMasterKeyIds = new Set(); + + snsSqsEvents.forEach(snsSqsEvent => { + newQueueNames.push(`${snsSqsEvent.name}Queue`); + if (snsSqsEvent.deadLetterQueueEnabled) { + newQueueNames.push(`${snsSqsEvent.name}DeadLetterQueue`); } - ); - if (kmsMasterKeyId !== undefined && kmsMasterKeyId !== null) { - // TODO: Should we rename kmsMasterKeyId to make it clearer that it can accept an ARN? - const resource = - // If the key ID is an object, it is most likely a "Ref" or "GetAtt" so we should pass it straight through so it gets resolved by CloudFormation - // If an ARN is provided, pass it straight through too, because no processing is needed - // Otherwise if it isn't either of those things, it is probably an ID, so we need to - // transform it to an ARN to make the policy valid - typeof kmsMasterKeyId === "object" || isKmsArn(kmsMasterKeyId) - ? kmsMasterKeyId - : `arn:aws:kms:::key/${kmsMasterKeyId}`; + const { kmsMasterKeyId } = snsSqsEvent; + if (kmsMasterKeyId !== undefined && kmsMasterKeyId !== null) { + // TODO: Should we rename kmsMasterKeyId to make it clearer that it can accept an ARN? + const resource = + // If the key ID is an object, it is most likely a "Ref" or "GetAtt" so we should pass it straight through so it gets resolved by CloudFormation + // If an ARN is provided, pass it straight through too, because no processing is needed + // Otherwise if it isn't either of those things, it is probably an ID, so we need to + // transform it to an ARN to make the policy valid + typeof kmsMasterKeyId === "object" || isKmsArn(kmsMasterKeyId) + ? kmsMasterKeyId + : `arn:aws:kms:::key/${kmsMasterKeyId}`; + kmsMasterKeyIds.add(resource); + } + }); + + if (newQueueNames.length) { + template.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push( + { + Effect: "Allow", + Action: [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + Resource: newQueueNames.map(queueName => { + return { "Fn::GetAtt": [queueName, "Arn"] }; + }) + } + ); + } + + if (kmsMasterKeyIds.size) { template.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push( { Effect: "Allow", Action: ["kms:Decrypt"], - Resource: resource + Resource: Array.from(kmsMasterKeyIds) } ); }