Repository: serverless/serverless (JavaScript)
Date: 2019-10-15 22:59:22+00:00
Task type: Feature

Issue:
Support NotAction and NotResource
# Feature Proposal

## Description

<!-- Please use https://forum.serverless.com, StackOverflow or other forums for Q&A -->
<!-- Please answer ALL the question below. Otherwise we probably have to close the issue due to missing information -->

Currently Serverless refuses to build if there are iamRoleStatements that contain [`NotAction`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notaction.html) or [`NotResource`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notresource.html).

These should be supported, as they are valid IAM role statements.

e.g. to allow a lambda to send text messages, but not publish to any SNS topics
```yaml
      - Effect: 'Allow'
        Action: 'sns:Publish'
        NotResource: 'arn:aws:sns:*:*:*'
```


Issue discussion:

Solution patch:
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js
index 2d97671ad03..c302e496f42 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js
@@ -192,12 +192,14 @@ module.exports = {
       violationsFound = 'it is not an array';
     } else {
       const descriptions = statements.map((statement, i) => {
-        const missing = ['Effect', 'Action', 'Resource'].filter(
-          prop => statement[prop] === undefined
+        const missing = [['Effect'], ['Action', 'NotAction'], ['Resource', 'NotResource']].filter(
+          props => props.every(prop => statement[prop] === undefined)
         );
         return missing.length === 0
           ? null
-          : `statement ${i} is missing the following properties: ${missing.join(', ')}`;
+          : `statement ${i} is missing the following properties: ${missing
+              .map(m => m.join(' / '))
+              .join(', ')}`;
       });
       const flawed = descriptions.filter(curr => curr);
       if (flawed.length) {
@@ -208,7 +210,7 @@ module.exports = {
     if (violationsFound) {
       const errorMessage = [
         'iamRoleStatements should be an array of objects,',
-        ' where each object has Effect, Action, Resource fields.',
+        ' where each object has Effect, Action / NotAction, Resource / NotResource fields.',
         ` Specifically, ${violationsFound}`,
       ].join('');
       throw new this.serverless.classes.Error(errorMessage);


Test patch:
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
index b7da9693183..7e7d898b585 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
@@ -513,10 +513,28 @@ describe('#mergeIamTemplates()', () => {
     ];
 
     expect(() => awsPackage.mergeIamTemplates()).to.throw(
-      'missing the following properties: Action'
+      'missing the following properties: Action / NotAction'
     );
   });
 
+  it('should not throw error if a custom IAM policy statement has a NotAction field', () => {
+    awsPackage.serverless.service.provider.iamRoleStatements = [
+      {
+        Effect: 'Allow',
+        Resource: '*',
+        NotAction: 'iam:DeleteUser',
+      },
+    ];
+
+    return awsPackage.mergeIamTemplates().then(() => {
+      expect(
+        awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[
+          awsPackage.provider.naming.getRoleLogicalId()
+        ].Properties.Policies[0].PolicyDocument.Statement[2]
+      ).to.deep.equal(awsPackage.serverless.service.provider.iamRoleStatements[0]);
+    });
+  });
+
   it('should throw error if a custom IAM policy statement does not have a Resource field', () => {
     awsPackage.serverless.service.provider.iamRoleStatements = [
       {
@@ -526,10 +544,28 @@ describe('#mergeIamTemplates()', () => {
     ];
 
     expect(() => awsPackage.mergeIamTemplates()).to.throw(
-      'missing the following properties: Resource'
+      'missing the following properties: Resource / NotResource'
     );
   });
 
+  it('should not throw error if a custom IAM policy statement has a NotResource field', () => {
+    awsPackage.serverless.service.provider.iamRoleStatements = [
+      {
+        Action: ['something:SomethingElse'],
+        Effect: 'Allow',
+        NotResource: 'arn:aws:sns:*:*:*',
+      },
+    ];
+
+    return awsPackage.mergeIamTemplates().then(() => {
+      expect(
+        awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[
+          awsPackage.provider.naming.getRoleLogicalId()
+        ].Properties.Policies[0].PolicyDocument.Statement[2]
+      ).to.deep.equal(awsPackage.serverless.service.provider.iamRoleStatements[0]);
+    });
+  });
+
   it('should throw an error describing all problematics custom IAM policy statements', () => {
     awsPackage.serverless.service.provider.iamRoleStatements = [
       {
@@ -547,7 +583,7 @@ describe('#mergeIamTemplates()', () => {
     ];
 
     expect(() => awsPackage.mergeIamTemplates()).to.throw(
-      /statement 0 is missing.*Resource; statement 2 is missing.*Effect, Action/
+      /statement 0 is missing.*Resource \/ NotResource; statement 2 is missing.*Effect, Action \/ NotAction/
     );
   });