Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Warn when incorrect format for additional IAM Role Statements causes them not to be included
# Bug Report
## Description

When using a `$ref` to include additional IAM Role Statements for a service, if the `$ref`'d file is not in the correct format (i.e. an array including more role statements), the role statements you are attempting to include are silently not included.

This may also happen when not using `$ref` and simply including incorrectly formatted statements in the `yml` directly.
- What went wrong?

Incorrectly formatted role statements were silently not included in the final composed CloudFormation template.
- What did you expect should have happened?

For the CLI to throw an error or warning that the role statements would not be included.
- What was the config you used?

Example `serverless.yml`

``` yml
service: myfancyservice
runtime: nodejs4.3
provider:
  name: aws
  iamRoleStatements:
    $ref: ./roleStatements.json
```

Example incorrect `roleStatements.json`:

``` json
{
  "IamPolicyLambdaInvocationAndDynamoDBStream": {
    "Type": "AWS::IAM::Policy",
    "Properties": {
      "PolicyName": "iam-policy-lambda-dynamo-${opt:stage}",
      "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": [
              "lambda:InvokeFunction"
            ],
            "Resource": "*"
          },
          {
            "Effect": "Allow",
            "Action": [
              "dynamodb:*"
            ],
            "Resource": "*"
          }
        ]
      },
      "Roles": [
        {
          "Ref": "IamRoleLambda"
        }
      ]
    }
  }
}
```

Example correct `roleStatements.json`:

``` json
[
  {
    "Effect": "Allow",
    "Action": [
      "lambda:InvokeFunction"
    ],
    "Resource": "*"
  },
  {
    "Effect": "Allow",
    "Action": [
      "dynamodb:*"
    ],
    "Resource": "*"
  }
]
```
- What stacktrace or error message from your provider did you see?

None by default.

Issue presents itself when the lambda is executed. If you add an event mapping which depends on these policies then CloudFormation will throw an error when creating the stack, if you do not have that, it will not throw an error.
## Additional Data
- **_Serverless Framework Version you're using**_: 1.0.0-rc1
- **_Operating System**_: OS X 10.11.6
- **_Stack Trace**_: N/A
- **_Provider Error messages**_: none



Discussion:
Thanks for reporting, sorry for the delay on answering, this should definitely be fixed through some validation.


Resolving patch:
diff --git a/lib/plugins/aws/deploy/lib/mergeIamTemplates.js b/lib/plugins/aws/deploy/lib/mergeIamTemplates.js
index a85c8518ba9..ff4844bed3c 100644
--- a/lib/plugins/aws/deploy/lib/mergeIamTemplates.js
+++ b/lib/plugins/aws/deploy/lib/mergeIamTemplates.js
@@ -6,6 +6,11 @@ const path = require('path');
 
 module.exports = {
   mergeIamTemplates() {
+    this.validateStatements(this.serverless.service.provider.iamRoleStatements);
+    return this.merge();
+  },
+
+  merge() {
     if (!this.serverless.service.getAllFunctions().length) {
       return BbPromise.resolve();
     }
@@ -119,9 +124,8 @@ module.exports = {
         });
       }
 
-      // add custom iam role statements
-      if (this.serverless.service.provider.iamRoleStatements &&
-        this.serverless.service.provider.iamRoleStatements instanceof Array) {
+      if (this.serverless.service.provider.iamRoleStatements) {
+        // add custom iam role statements
         this.serverless.service.provider.compiledCloudFormationTemplate
           .Resources[this.provider.naming.getPolicyLogicalId()]
           .Properties
@@ -137,4 +141,36 @@ module.exports = {
     return BbPromise.resolve();
   },
 
+  validateStatements(statements) {
+    // Verify that iamRoleStatements (if present) is an array of { Effect: ...,
+    // Action: ..., Resource: ... } objects.
+    if (!statements) {
+      return;
+    }
+    let violationsFound;
+    if (!(statements instanceof Array)) {
+      violationsFound = 'it is not an array';
+    } else {
+      const descriptions = statements.map((statement, i) => {
+        const missing = ['Effect', 'Action', 'Resource'].filter(
+            prop => statement[prop] === undefined);
+        return missing.length === 0 ? null :
+          `statement ${i} is missing the following properties: ${missing.join(', ')}`;
+      });
+      const flawed = descriptions.filter(curr => curr);
+      if (flawed.length) {
+        violationsFound = flawed.join('; ');
+      }
+    }
+
+    if (violationsFound) {
+      const errorMessage = [
+        'iamRoleStatements should be an array of objects,',
+        ' where each object has Effect, Action, Resource fields.',
+        ` Specifically, ${violationsFound}`,
+      ].join('');
+      throw new this.serverless.classes.Error(errorMessage);
+    }
+  },
 };
+


Test patch:
diff --git a/lib/plugins/aws/deploy/lib/mergeIamTemplates.test.js b/lib/plugins/aws/deploy/lib/mergeIamTemplates.test.js
index 3dadf19a7f2..982380095e7 100644
--- a/lib/plugins/aws/deploy/lib/mergeIamTemplates.test.js
+++ b/lib/plugins/aws/deploy/lib/mergeIamTemplates.test.js
@@ -114,7 +114,6 @@ describe('#mergeIamTemplates()', () => {
       },
     ];
 
-
     return awsDeploy.mergeIamTemplates()
       .then(() => {
         expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
@@ -126,6 +125,73 @@ describe('#mergeIamTemplates()', () => {
       });
   });
 
+  it('should throw error if custom IAM policy statements is not an array', () => {
+    awsDeploy.serverless.service.provider.iamRoleStatements = {
+      policy: 'some_value',
+      statments: [
+        {
+          Effect: 'Allow',
+          Action: [
+            'something:SomethingElse',
+          ],
+          Resource: 'some:aws:arn:xxx:*:*',
+        },
+      ],
+    };
+
+    expect(() => awsDeploy.mergeIamTemplates()).to.throw('not an array');
+  });
+
+  it('should throw error if a custom IAM policy statement does not have an Effect field', () => {
+    awsDeploy.serverless.service.provider.iamRoleStatements = [{
+      Action: ['something:SomethingElse'],
+      Resource: '*',
+    }];
+
+    expect(() => awsDeploy.mergeIamTemplates()).to.throw(
+        'missing the following properties: Effect');
+  });
+
+  it('should throw error if a custom IAM policy statement does not have an Action field', () => {
+    awsDeploy.serverless.service.provider.iamRoleStatements = [{
+      Effect: 'Allow',
+      Resource: '*',
+    }];
+
+    expect(() => awsDeploy.mergeIamTemplates()).to.throw(
+        'missing the following properties: Action');
+  });
+
+  it('should throw error if a custom IAM policy statement does not have a Resource field', () => {
+    awsDeploy.serverless.service.provider.iamRoleStatements = [{
+      Action: ['something:SomethingElse'],
+      Effect: 'Allow',
+    }];
+
+    expect(() => awsDeploy.mergeIamTemplates()).to.throw(
+        'missing the following properties: Resource');
+  });
+
+  it('should throw an error describing all problematics custom IAM policy statements', () => {
+    awsDeploy.serverless.service.provider.iamRoleStatements = [
+      {
+        Action: ['something:SomethingElse'],
+        Effect: 'Allow',
+      },
+      {
+        Action: ['something:SomethingElse'],
+        Resource: '*',
+        Effect: 'Allow',
+      },
+      {
+        Resource: '*',
+      },
+    ];
+
+    expect(() => awsDeploy.mergeIamTemplates())
+      .to.throw(/statement 0 is missing.*Resource; statement 2 is missing.*Effect, Action/);
+  });
+
   it('should add a CloudWatch LogGroup resource', () => {
     awsDeploy.serverless.service.provider.cfLogs = true;
     const normalizedName = awsDeploy.provider.naming.getLogGroupLogicalId(functionName);