Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
IAM policy on lambda execution role contains incorrect resource prefixes for writing logs when manually specifying function name
<!--
1. If you have a question and not a bug report please ask first at http://forum.serverless.com
2. Please check if an issue already exists. This bug may have already been documented
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Bug Report

## Description

The policies for `logs:CreateLogStream` and `logs:PutLogEvents` actions have resources with an incorrect prefix compared to the created log groups when additionally specifying the function name manually.
As of 1.45.0 these statements are generated with `arn:aws:logs:region:account:log-group:/aws/lambda/{service}-{stage}-{function}:*`, while in 1.44.1 it used to be `arn:aws:logs:region:account:log-group:/aws/lambda/{functionObject.name}:*`.

Due to this bug, no logs show up in CloudWatch for such functions as the created log groups do not match the IAM statements.

Sample config:
```
service: ${self:custom.service.name}

custom:
  service:
    name: log-group-issue-lambda

provider:
  name: aws
  runtime: nodejs10.x
  stage: ${opt:stage, 'dev'}
  region: eu-west-1
  stackName: ${self:custom.service.name}
  apiKeys:
    - ${self:custom.service.name}-key

functions:
  hello:
    name: ${self:custom.service.name}-hello
    handler: handler.hello
```

## Additional Data

* ***Serverless Framework Version you're using***: 1.45.0
* ***Operating System***: Linux 4.15.0-50-generic
* ***Stack Trace***: n/a
* ***Provider Error messages***: none



Discussion:
@weeniearms thanks for report. 

Are you sure it's the case? Technically what changed is that following:
```
arn:aws:logs:region:account:log-group:/aws/lambda/service-stage-function:*
```
was replaced with:
```
arn:aws:logs:region:account:log-group:/aws/lambda/service-stage*:*
```

And as we tested it didn't affect configuration of CloudWatch logs.

Maybe it's some plugin that doesn't work well with a change?
@medikoo Just updated the bug description with a sample config that can be used to reproduce this issue. Apparently this has to do with specifying the function name manually as it affects the name of the created log group.

So it's either an issue with IAM policy generation or with how the log groups are created.
@weeniearms indeed, thanks for clarification, I'm looking into it now.

Regression introduced with https://github.com/serverless/serverless/pull/6212

Resolving patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f38b8c8710..7bf612b1638 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+# 1.45.1 (2019-06-12)
+
+- [Fix IAM policies setup for functions with custom name](https://github.com/serverless/serverless/pull/6240)
+- [Fix Travis CI deploy config](https://github.com/serverless/serverless/pull/6234)
+
+## Meta
+- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.45.0...v1.45.1)
+
+
 # 1.45.0 (2019-06-12)
 
 - [Add `--config` option](https://github.com/serverless/serverless/pull/6216)
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js
index 2698be83fc6..030751af7e7 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js
@@ -83,33 +83,58 @@ module.exports = {
       }
     );
 
+    const canonicalFunctionNamePrefix =
+      `${this.provider.serverless.service.service}-${this.provider.getStage()}`;
     const logGroupsPrefix = this.provider.naming
-      .getLogGroupName(`${this.provider.serverless.service.service}-${this.provider.getStage()}`);
+      .getLogGroupName(canonicalFunctionNamePrefix);
 
-    this.serverless.service.provider.compiledCloudFormationTemplate
+    const policyDocumentStatements = this.serverless.service.provider.compiledCloudFormationTemplate
       .Resources[this.provider.naming.getRoleLogicalId()]
       .Properties
       .Policies[0]
       .PolicyDocument
-      .Statement[0]
+      .Statement;
+
+    // Ensure general polices for functions with default name resolution
+    policyDocumentStatements[0]
       .Resource
       .push({
         'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
           `:log-group:${logGroupsPrefix}*:*`,
       });
 
-    this.serverless.service.provider.compiledCloudFormationTemplate
-      .Resources[this.provider.naming.getRoleLogicalId()]
-      .Properties
-      .Policies[0]
-      .PolicyDocument
-      .Statement[1]
+    policyDocumentStatements[1]
       .Resource
       .push({
         'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
           `:log-group:${logGroupsPrefix}*:*:*`,
       });
 
+    // Ensure policies for functions with custom name resolution
+    this.serverless.service.getAllFunctions().forEach((functionName) => {
+      const { name: resolvedFunctionName } = this.serverless.service.getFunction(functionName);
+      if (!resolvedFunctionName || resolvedFunctionName.startsWith(canonicalFunctionNamePrefix)) {
+        return;
+      }
+
+      const customFunctionNamelogGroupsPrefix =
+        this.provider.naming.getLogGroupName(resolvedFunctionName);
+
+      policyDocumentStatements[0]
+        .Resource
+        .push({
+          'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
+            `:log-group:${customFunctionNamelogGroupsPrefix}:*`,
+        });
+
+      policyDocumentStatements[1]
+        .Resource
+        .push({
+          'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
+            `:log-group:${customFunctionNamelogGroupsPrefix}:*:*`,
+        });
+    });
+
     if (this.serverless.service.provider.iamRoleStatements) {
       // add custom iam role statements
       this.serverless.service.provider.compiledCloudFormationTemplate
@@ -117,12 +142,8 @@ module.exports = {
         .Properties
         .Policies[0]
         .PolicyDocument
-        .Statement = this.serverless.service.provider.compiledCloudFormationTemplate
-          .Resources[this.provider.naming.getRoleLogicalId()]
-          .Properties
-          .Policies[0]
-          .PolicyDocument
-          .Statement.concat(this.serverless.service.provider.iamRoleStatements);
+        .Statement = policyDocumentStatements
+          .concat(this.serverless.service.provider.iamRoleStatements);
     }
 
     if (this.serverless.service.provider.iamManagedPolicies) {
diff --git a/package-lock.json b/package-lock.json
index cd27db0179f..0e746271247 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless",
-  "version": "1.45.0",
+  "version": "1.45.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -7521,9 +7521,9 @@
       }
     },
     "resolve": {
-      "version": "1.11.1",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz",
-      "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==",
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
+      "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==",
       "dev": true,
       "requires": {
         "path-parse": "^1.0.6"
diff --git a/package.json b/package.json
index 7e682f05b96..05497a91fc7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless",
-  "version": "1.45.0",
+  "version": "1.45.1",
   "engines": {
     "node": ">=6.0"
   },


Test patch:
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
index 59a41473cba..c659ab00fe7 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
@@ -9,12 +9,15 @@ const AwsPackage = require('../index');
 describe('#mergeIamTemplates()', () => {
   let awsPackage;
   let serverless;
+  const serviceName = 'new-service';
   const functionName = 'test';
+  const stage = 'dev';
+  const resolvedFunctionName = `${serviceName}-${stage}-${functionName}`;
 
   beforeEach(() => {
     serverless = new Serverless();
     const options = {
-      stage: 'dev',
+      stage,
       region: 'us-east-1',
     };
     serverless.setProvider('aws', new AwsProvider(serverless, options));
@@ -23,14 +26,14 @@ describe('#mergeIamTemplates()', () => {
     awsPackage.serverless.service.provider.compiledCloudFormationTemplate = {
       Resources: {},
     };
-    awsPackage.serverless.service.service = 'new-service';
+    awsPackage.serverless.service.service = serviceName;
     awsPackage.serverless.service.functions = {
       [functionName]: {
-        name: 'test',
         artifact: 'test.zip',
         handler: 'handler.hello',
       },
     };
+    serverless.service.setFunctionNames(); // Ensure to resolve function names
   });
 
   it('should not merge if there are no functions', () => {
@@ -136,6 +139,114 @@ describe('#mergeIamTemplates()', () => {
       })
   );
 
+
+  it('should ensure IAM policies for custom named functions', () => {
+    const customFunctionName = 'foo-bar';
+    awsPackage.serverless.service.functions = {
+      [functionName]: {
+        name: customFunctionName,
+        artifact: 'test.zip',
+        handler: 'handler.hello',
+      },
+    };
+    serverless.service.setFunctionNames(); // Ensure to resolve function names
+
+    return awsPackage.mergeIamTemplates()
+      .then(() => {
+        const canonicalFunctionsPrefix =
+          `${awsPackage.serverless.service.service}-${awsPackage.provider.getStage()}`;
+
+        expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources[awsPackage.provider.naming.getRoleLogicalId()]
+        ).to.deep.equal({
+          Type: 'AWS::IAM::Role',
+          Properties: {
+            AssumeRolePolicyDocument: {
+              Version: '2012-10-17',
+              Statement: [
+                {
+                  Effect: 'Allow',
+                  Principal: {
+                    Service: [
+                      'lambda.amazonaws.com',
+                    ],
+                  },
+                  Action: [
+                    'sts:AssumeRole',
+                  ],
+                },
+              ],
+            },
+            Path: '/',
+            Policies: [
+              {
+                PolicyName: {
+                  'Fn::Join': [
+                    '-',
+                    [
+                      awsPackage.provider.getStage(),
+                      awsPackage.serverless.service.service,
+                      'lambda',
+                    ],
+                  ],
+                },
+                PolicyDocument: {
+                  Version: '2012-10-17',
+                  Statement: [
+                    {
+                      Effect: 'Allow',
+                      Action: [
+                        'logs:CreateLogStream',
+                      ],
+                      Resource: [
+                        {
+                          'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+                            + `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*`,
+                        },
+                        {
+                          'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+                            + `log-group:/aws/lambda/${customFunctionName}:*`,
+                        },
+                      ],
+                    },
+                    {
+                      Effect: 'Allow',
+                      Action: [
+                        'logs:PutLogEvents',
+                      ],
+                      Resource: [
+                        {
+                          'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+                            + `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*:*`,
+                        },
+                        {
+                          'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+                            + `log-group:/aws/lambda/${customFunctionName}:*:*`,
+                        },
+                      ],
+                    },
+                  ],
+                },
+              },
+            ],
+            RoleName: {
+              'Fn::Join': [
+                '-',
+                [
+                  awsPackage.serverless.service.service,
+                  awsPackage.provider.getStage(),
+                  {
+                    Ref: 'AWS::Region',
+                  },
+                  'lambdaRole',
+                ],
+              ],
+            },
+          },
+        });
+      });
+  });
+
   it('should add custom IAM policy statements', () => {
     awsPackage.serverless.service.provider.iamRoleStatements = [
       {
@@ -291,7 +402,7 @@ describe('#mergeIamTemplates()', () => {
         {
           Type: 'AWS::Logs::LogGroup',
           Properties: {
-            LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName),
+            LogGroupName: awsPackage.provider.naming.getLogGroupName(resolvedFunctionName),
           },
         }
         );
@@ -310,7 +421,7 @@ describe('#mergeIamTemplates()', () => {
             {
               Type: 'AWS::Logs::LogGroup',
               Properties: {
-                LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName),
+                LogGroupName: awsPackage.provider.naming.getLogGroupName(resolvedFunctionName),
                 RetentionInDays: 5,
               },
             }