Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Wider `logs:CreateLogStream`/`logs:PutLogEvents` permissions in policy for Lambda functions with manual names (v1.45.1)
<!--
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

* What went wrong?

   When a service contains only custom named functions, the policies for `logs:CreateLogStream` and `logs:PutLogEvents` created using v1.45.1 have a wider set of permissions than they previously did with v1.44.1.

   ### Resource access allowed when using v1.44.1

   * `logs:CreateLogStream`:
      * `arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-group-policy-custom-named-fn:*`
   * `logs:PutLogEvents`:
      * `arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-group-policy-custom-named-fn:*:*`


   ### Resource access allowed when using v1.45.1

   * `logs:CreateLogStream`:
      * `arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-group-policy-dev*:*`
      * `arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-group-policy-custom-named-fn:*`
   * `logs:PutLogEvents`:
      * `arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-group-policy-dev*:*:*`
      * `arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-group-policy-custom-named-fn:*:*`

* What did you expect should have happened?

   Based on the principle of least privilege, the additional permissions for `/aws/lambda/log-group-policy-dev*` should not have been added to the IAM policy.

* What was the config you used?

   Full source: [log-group-policy.zip](https://github.com/serverless/serverless/files/3296986/log-group-policy.zip)

   ```yaml
   service: log-group-policy

   provider:
      name: aws
      runtime: nodejs8.10

   functions:
      testFunction:
         name: ${self:service}-custom-named-fn
         handler: index.handler
   ```

   #### To compare output between v1.44.1 and v1.45.1

   ```
   unzip log-group-policy.zip
   cd log-group-policy
   echo '{}' > package.json
   npm i serverless@1.44.1
   ./node_modules/.bin/sls package
   cp .serverless/cloudformation-template-update-stack.json cf-update-stack-v1.44.1.json
   npm i serverless@1.45.1
   ./node_modules/.bin/sls package
   cp .serverless/cloudformation-template-update-stack.json cf-update-stack-v1.45.1.json
   diff cf-update-stack-v1.44.1.json cf-update-stack-v1.45.1.json
   ```

Similar or dependent issues:
* #6236 (PR #6240)
* #6212

## Additional Data

* ***Serverless Framework Version you're using***: 1.45.1
* ***Operating System***: macOS 10.14
* ***Stack Trace***: n/a
* ***Provider Error messages***: none



Discussion:
This was done to avoid a hard AWS limit of the size of a single role.

We can make the least-privileged permission as an option if some users find the wider troublesome. 

Another option is to have each function to have its own role, but the statements added via the `iamRoleStatements` would need to be duplicated on each of them and this could potentially break some 3rd party plugins.
@rdsedmundo Yep, that's what I gathered from the original PR (#6212). The concept behind those changes makes sense. My main concern is serverless now allows access to `/aws/lambda/log-group-policy-dev*` when it is not actually needed (i.e. when the service does not contain any functions that are using the default serverless naming scheme).

Looking at the changes in #6240, is seems like it might be fairly straight forward to add a check that only adds the new "merged/wildcard" permission when it is actually needed (i.e. when there is at least one function using the default naming pattern). I'm happy to submit a PR to change this. However before I did so, wanted to get your all's stance on the matter.
Oh ok, you're talking only about the functions with custom names, that's fair enough. I agree with that.

[Here](https://github.com/serverless/serverless/pull/6240/files#diff-bb02cdd69514f709dc6a204a5d0dc376R114) we're already looping in all the functions and adding an entry for each of the custom names, so I agree that the wildcards are not necessary in this case. 

I wouldn't even bother of trying to detect when there's more than one fn starting with the same pattern and trying to use a wildcard for these cases. In my opinion, we can leverage the wildcard just for the default Serverless naming pattern, and if a user chooses to use custom names he has to bear in mind that this is going to grow his/her the role size. For hitting the limit the user would need to have like 40+ custom functions.

Resolving patch:
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js
index 945312b6987..e96534ec1eb 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js
@@ -90,23 +90,13 @@ module.exports = {
       .Resources[this.provider.naming.getRoleLogicalId()].Properties.Policies[0].PolicyDocument
       .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}*:*`,
-    });
-
-    policyDocumentStatements[1].Resource.push({
-      'Fn::Sub':
-        'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
-        `:log-group:${logGroupsPrefix}*:*:*`,
-    });
+    let hasOneOrMoreCanonicallyNamedFunctions = false;
 
     // 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)) {
+        hasOneOrMoreCanonicallyNamedFunctions = true;
         return;
       }
 
@@ -127,6 +117,21 @@ module.exports = {
       });
     });
 
+    if (hasOneOrMoreCanonicallyNamedFunctions) {
+      // Ensure general policies for functions with default name resolution
+      policyDocumentStatements[0].Resource.push({
+        'Fn::Sub':
+          'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
+          `:log-group:${logGroupsPrefix}*:*`,
+      });
+
+      policyDocumentStatements[1].Resource.push({
+        'Fn::Sub':
+          'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
+          `:log-group:${logGroupsPrefix}*:*:*`,
+      });
+    }
+
     if (this.serverless.service.provider.iamRoleStatements) {
       // add custom iam role statements
       this.serverless.service.provider.compiledCloudFormationTemplate.Resources[


Test patch:
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
index 09447f64157..b7da9693183 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
@@ -127,7 +127,7 @@ describe('#mergeIamTemplates()', () => {
       });
     }));
 
-  it('should ensure IAM policies for custom named functions', () => {
+  it('should ensure IAM policies when service contains only custom named functions', () => {
     const customFunctionName = 'foo-bar';
     awsPackage.serverless.service.functions = {
       [functionName]: {
@@ -138,6 +138,91 @@ describe('#mergeIamTemplates()', () => {
     };
     serverless.service.setFunctionNames(); // Ensure to resolve function names
 
+    return awsPackage.mergeIamTemplates().then(() => {
+      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/${customFunctionName}:*`,
+                      },
+                    ],
+                  },
+                  {
+                    Effect: 'Allow',
+                    Action: ['logs:PutLogEvents'],
+                    Resource: [
+                      {
+                        '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 ensure IAM policies when service contains only canonically named functions', () => {
+    awsPackage.serverless.service.functions = {
+      [functionName]: {
+        artifact: 'test.zip',
+        handler: 'handler.hello',
+      },
+    };
+    serverless.service.setFunctionNames(); // Ensure to resolve function names
+
     return awsPackage.mergeIamTemplates().then(() => {
       const canonicalFunctionsPrefix = `${
         awsPackage.serverless.service.service
@@ -183,11 +268,106 @@ describe('#mergeIamTemplates()', () => {
                           'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' +
                           `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*`,
                       },
+                    ],
+                  },
+                  {
+                    Effect: 'Allow',
+                    Action: ['logs:PutLogEvents'],
+                    Resource: [
+                      {
+                        'Fn::Sub':
+                          'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' +
+                          `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*:*`,
+                      },
+                    ],
+                  },
+                ],
+              },
+            },
+          ],
+          RoleName: {
+            'Fn::Join': [
+              '-',
+              [
+                awsPackage.serverless.service.service,
+                awsPackage.provider.getStage(),
+                {
+                  Ref: 'AWS::Region',
+                },
+                'lambdaRole',
+              ],
+            ],
+          },
+        },
+      });
+    });
+  });
+
+  it('should ensure IAM policies for custom and canonically named functions', () => {
+    const customFunctionName = 'foo-bar';
+    awsPackage.serverless.service.functions = {
+      [functionName]: {
+        name: customFunctionName,
+        artifact: 'test.zip',
+        handler: 'handler.hello',
+      },
+      test2: {
+        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/${customFunctionName}:*`,
                       },
+                      {
+                        'Fn::Sub':
+                          'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' +
+                          `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*`,
+                      },
                     ],
                   },
                   {
@@ -197,12 +377,12 @@ describe('#mergeIamTemplates()', () => {
                       {
                         'Fn::Sub':
                           'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' +
-                          `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*:*`,
+                          `log-group:/aws/lambda/${customFunctionName}:*:*`,
                       },
                       {
                         'Fn::Sub':
                           'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' +
-                          `log-group:/aws/lambda/${customFunctionName}:*:*`,
+                          `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*:*`,
                       },
                     ],
                   },