Repository: serverless/serverless (JavaScript)
Date: 2018-06-25 11:03:25+00:00
Task type: Feature

Issue:
API Gateway Resource Policy Support Feature
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
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 Feature Proposal

## Description
Recently API Gateway (April 2nd, 2018) started providing resource policy to handle each API. Which includes blacklisting IP or allowing denying IP.

This is currently only supported by the API Gateway API, AWS console, AWS SDK and not yet by CloudFormation, which I'm guessing is why it is not yet supported by Serverless. 

[Announcement](https://aws.amazon.com/about-aws/whats-new/2018/04/amazon-api-gateway-supports-resource-policies/)

[Blog for console-based setup](https://lobster1234.github.io/2018/04/14/amazon-api-gateway-ip-whitelisting/)

Probably best to wait for CloudFormation to catch up on this so I'm just registering for a future task.


For feature proposals:
* IP Blacklisting or allowing a certain type of method. Basically IAM type policy but without creating IAM role/policy.
* Json input or parameter based information can be set up.  

Note
** [Forum Question in relation](https://forum.serverless.com/t/api-gateway-resource-policy/4215
) 


Issue discussion:
@ptrivedi9400 
Sounds good to introduce this functionality :smile: 
I just added `wait-cf-support` tag so that someone can get started to implement it after support.

Solution patch:
diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md
index 60d3b3d7b16..f561e4cf58b 100644
--- a/docs/providers/aws/events/apigateway.md
+++ b/docs/providers/aws/events/apigateway.md
@@ -43,6 +43,7 @@ layout: Doc
     - [Manually Configuring shared API Gateway](#manually-configuring-shared-api-gateway)
       - [Note while using authorizers with shared API Gateway](#note-while-using-authorizers-with-shared-api-gateway)
   - [Share Authorizer](#share-authorizer)
+  - [Resource Policy](#resource-policy)
 
 _Are you looking for tutorials on using API Gateway? Check out the following resources:_
 
@@ -1151,3 +1152,25 @@ resources:
           - arn:aws:cognito-idp:${self:provider.region}:xxxxxx:userpool/abcdef 
           
 ```
+
+## Resource Policy
+
+Resource policies are policy documents that are used to control the invocation of the API. Find more use cases from the [Apigateway Resource Policies](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-resource-policies.html) documentation.
+
+```yml
+provider:
+  name: aws
+  runtime: nodejs6.10
+
+  resourcePolicy:
+    - Effect: Allow
+      Principal: "*"
+      Action: execute-api:Invoke
+      Resource:
+        - execute-api:/*/*/*
+      Condition:
+        IpAddress:
+          aws:SourceIp:
+            - "123.123.123.123"
+
+```
\ No newline at end of file
diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md
index 7939b6fb136..d0bcb131ef3 100644
--- a/docs/providers/aws/guide/serverless.yml.md
+++ b/docs/providers/aws/guide/serverless.yml.md
@@ -99,6 +99,16 @@ provider:
       - subnetId2
   notificationArns: # List of existing Amazon SNS topics in the same region where notifications about stack events are sent.
     - 'arn:aws:sns:us-east-1:XXXXXX:mytopic'
+  resourcePolicy:
+    - Effect: Allow
+      Principal: "*"
+      Action: execute-api:Invoke
+      Resource:
+        - execute-api:/*/*/*
+      Condition:
+        IpAddress:
+          aws:SourceIp:
+            - "123.123.123.123"
   tags: # Optional service wide function tags
     foo: bar
     baz: qux
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
index 2b6dc4f2bcb..d28348159c5 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
@@ -43,6 +43,17 @@ module.exports = {
       },
     });
 
+    if (!_.isEmpty(this.serverless.service.provider.resourcePolicy)) {
+      const policy = {
+        Version: '2012-10-17',
+        Statement: this.serverless.service.provider.resourcePolicy,
+      };
+      _.merge(this.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources[this.apiGatewayRestApiLogicalId].Properties, {
+            Policy: policy,
+          });
+    }
+
     return BbPromise.resolve();
   },
 };


Test patch:
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js
index bca554cd1e6..98f665df6ae 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js
@@ -16,8 +16,36 @@ describe('#compileRestApi()', () => {
         Properties: {
           Name: 'dev-new-service',
           EndpointConfiguration: {
-            Types: [
-              'EDGE',
+            Types: ['EDGE'],
+          },
+        },
+      },
+    },
+  };
+
+  const serviceResourcesAwsResourcesObjectWithResourcePolicyMock = {
+    Resources: {
+      ApiGatewayRestApi: {
+        Type: 'AWS::ApiGateway::RestApi',
+        Properties: {
+          Name: 'dev-new-service',
+          EndpointConfiguration: {
+            Types: ['EDGE'],
+          },
+          Policy: {
+            Version: '2012-10-17',
+            Statement: [
+              {
+                Effect: 'Allow',
+                Principal: '*',
+                Action: 'execute-api:Invoke',
+                Resource: ['execute-api:/*/*/*'],
+                Condition: {
+                  IpAddress: {
+                    'aws:SourceIp': ['123.123.123.123'],
+                  },
+                },
+              },
             ],
           },
         },
@@ -49,32 +77,48 @@ describe('#compileRestApi()', () => {
     };
   });
 
-  it('should create a REST API resource', () => awsCompileApigEvents
-    .compileRestApi().then(() => {
-      expect(
-        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
-          .Resources
-      ).to.deep.equal(
-        serviceResourcesAwsResourcesObjectMock.Resources
-      );
-    })
-  );
+  it('should create a REST API resource', () =>
+    awsCompileApigEvents.compileRestApi().then(() => {
+      expect(awsCompileApigEvents.serverless.service
+        .provider.compiledCloudFormationTemplate.Resources).to.deep.equal(
+          serviceResourcesAwsResourcesObjectMock.Resources
+        );
+    }));
+
+  it('should create a REST API resource with resource policy', () => {
+    awsCompileApigEvents.serverless.service.provider.resourcePolicy = [
+      {
+        Effect: 'Allow',
+        Principal: '*',
+        Action: 'execute-api:Invoke',
+        Resource: ['execute-api:/*/*/*'],
+        Condition: {
+          IpAddress: {
+            'aws:SourceIp': ['123.123.123.123'],
+          },
+        },
+      },
+    ];
+    return awsCompileApigEvents.compileRestApi().then(() => {
+      expect(awsCompileApigEvents.serverless.service.provider
+        .compiledCloudFormationTemplate.Resources).to.deep.equal(
+          serviceResourcesAwsResourcesObjectWithResourcePolicyMock.Resources
+        );
+    });
+  });
 
-  it('should ignore REST API resource creation if there is predefined restApi config',
-    () => {
-      awsCompileApigEvents.serverless.service.provider.apiGateway = {
-        restApiId: '6fyzt1pfpk',
-        restApiRootResourceId: 'z5d4qh4oqi',
-      };
-      return awsCompileApigEvents
-      .compileRestApi().then(() => {
-        expect(
-          awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
-            .Resources
-        ).to.deep.equal({});
-      });
-    }
-  );
+  it('should ignore REST API resource creation if there is predefined restApi config', () => {
+    awsCompileApigEvents.serverless.service.provider.apiGateway = {
+      restApiId: '6fyzt1pfpk',
+      restApiRootResourceId: 'z5d4qh4oqi',
+    };
+    return awsCompileApigEvents.compileRestApi().then(() => {
+      expect(awsCompileApigEvents.serverless.service.provider
+        .compiledCloudFormationTemplate.Resources).to.deep.equal(
+          {}
+        );
+    });
+  });
 
   it('throw error if endpointType property is not a string', () => {
     awsCompileApigEvents.serverless.service.provider.endpointType = ['EDGE'];