Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
79 changes: 73 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@
[![Build Status](https://travis-ci.org/horike37/serverless-apigateway-service-proxy.svg?branch=master)](https://travis-ci.org/horike37/serverless-apigateway-service-proxy) [![npm version](https://badge.fury.io/js/serverless-apigateway-service-proxy.svg)](https://badge.fury.io/js/serverless-apigateway-service-proxy) [![Coverage Status](https://coveralls.io/repos/github/horike37/serverless-apigateway-service-proxy/badge.svg?branch=master)](https://coveralls.io/github/horike37/serverless-apigateway-service-proxy?branch=master) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)

# Serverless APIGateway Service Proxy(BETA)

This Serverless Framewrok plugin supports the AWS service proxy integration feature of API Gateway. You can directly connect API Gateway to AWS services without Lambda.

## Install

Run `servelress plugin install` in your Serverless project.

```
$ serverless plugin install -n serverless-apigateway-service-proxy
```bash
serverless plugin install -n serverless-apigateway-service-proxy
```

## Supported AWS services

Here is a services list which this plugin supports for now. But will expand to other services in the feature.
Please pull request if you are intersted in it.

- Kinesis Streams
- SQS

## How to use

Define settings of the AWS services you want to integrate under `custom > apiGatewayServiceProxies` and run `serverless deploy`.

### Kinesis

Sample syntax for Kinesis proxy in serverless.yml.

```yaml
custom:
apiGatewayServiceProxies:
Expand All @@ -41,34 +47,38 @@ resources:
```

Sample request after deploying.
```

```bash
curl -XPOST https://xxxxxxx.execute-api.us-east-1.amazonaws.com/dev/kinesis -d '{"Data": "some data","PartitionKey": "some key"}' -H 'Content-Type:application/json'
```

### SQS

Sample syntax for SQS proxy in serverless.yml.

```yaml
custom:
apiGatewayServiceProxies:
- sqs:
path: /sqs
method: post
queueName: {"Fn::GetAtt":[ "SQSQueue", "QueueName" ]}
queueName: { 'Fn::GetAtt': ['SQSQueue', 'QueueName'] }
cors: true

resources:
Resources:
SQSQueue:
Type: "AWS::SQS::Queue"
Type: 'AWS::SQS::Queue'
```

Sample request after deploying.
```

```bash
curl -XPOST https://xxxxxx.execute-api.us-east-1.amazonaws.com/dev/sqs -d '{"message": "testtest"}' -H 'Content-Type:application/json'
```

## Common API Gateway features

### Enabling CORS

To set CORS configurations for your HTTP endpoints, simply modify your event configurations as follows:
Expand Down Expand Up @@ -142,8 +152,40 @@ custom:
allowCredentials: false
cacheControl: 'max-age=600, s-maxage=600, proxy-revalidate' # Caches on browser and proxy for 10 minutes and doesnt allow proxy to serve out of date content
```

### Adding Authorization

You can pass in any supported authorization type:

```yml
custom:
apiGatewayServiceProxies:
- sqs:
path: /sqs
method: post
queueName: { 'Fn::GetAtt': ['SQSQueue', 'QueueName'] }
cors: true

# optional - defaults to 'NONE'
authorizationType: 'AWS_IAM' # can be one of ['NONE', 'AWS_IAM', 'CUSTOM', 'COGNITO_USER_POOLS']

# when using 'CUSTOM' authorization type, one should specify authorizerId
# authorizerId: { Ref: 'AuthorizerLogicalId' }
# when using 'COGNITO_USER_POOLS' authorization type, one can specify a list of authorization scopes
# authorizationScopes: ['scope1','scope2']

resources:
Resources:
SQSQueue:
Type: 'AWS::SQS::Queue'
```

Source: [AWS::ApiGateway::Method docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-authorizationtype)

## Specific features

### Kinesis

#### Customizing request body mapping templates

If you'd like to add content types or customize the default templates, you can do so by including your custom [API Gateway request mapping template](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html) in `serverless.yml` like so:
Expand All @@ -170,4 +212,29 @@ custom:
- MyStreamArn:
Fn::GetAtt: [MyStream, Arn]
```

Source: [How to connect SNS to Kinesis for cross-account delivery via API Gateway](https://theburningmonk.com/2019/07/how-to-connect-sns-to-kinesis-for-cross-account-delivery-via-api-gateway/)

### SQS

#### Customizing request parameters

If you'd like to pass additional data to the integration request, you can do so by including your custom [API Gateway request parameters](https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html) in `serverless.yml` like so:

```yml
custom:
apiGatewayServiceProxies:
- sqs:
path: /queue
method: post
queueName: !GetAtt MyQueue.QueueName
cors: true

requestParameters:
'integration.request.querystring.MessageAttribute.1.Name': "'cognitoIdentityId'"
'integration.request.querystring.MessageAttribute.1.Value.StringValue': 'context.identity.cognitoIdentityId'
'integration.request.querystring.MessageAttribute.1.Value.DataType': "'String'"
'integration.request.querystring.MessageAttribute.2.Name': "'cognitoAuthenticationProvider'"
'integration.request.querystring.MessageAttribute.2.Value.StringValue': 'context.identity.cognitoAuthenticationProvider'
'integration.request.querystring.Me]
```
85 changes: 83 additions & 2 deletions lib/apiGateway/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ module.exports = {
const http = serviceProxy[serviceName]
http.path = await this.getProxyPath(serviceProxy[serviceName], serviceName)
http.method = await this.getProxyMethod(serviceProxy[serviceName], serviceName)
http.auth = await this.getAuth(serviceProxy[serviceName], serviceName)

await this.validateRequestParameters(serviceProxy[serviceName], serviceName)

if (serviceProxy[serviceName].cors) {
http.cors = await this.getCors(serviceProxy[serviceName])
Expand Down Expand Up @@ -59,7 +62,7 @@ module.exports = {
},

async getProxyPath(proxy, serviceName) {
if (proxy.path && typeof proxy.path === 'string') {
if (proxy.path && _.isString(proxy.path)) {
return proxy.path.replace(/^\//, '').replace(/\/$/, '')
}

Expand All @@ -71,7 +74,7 @@ module.exports = {
},

async getProxyMethod(proxy, serviceName) {
if (proxy.method && typeof proxy.method === 'string') {
if (proxy.method && _.isString(proxy.method)) {
const method = proxy.method.toLowerCase()

const allowedMethods = ['get', 'post', 'put', 'patch', 'options', 'head', 'delete', 'any']
Expand Down Expand Up @@ -154,5 +157,83 @@ module.exports = {
}

return cors
},

async getAuth(proxy, serviceName) {
const auth = {
authorizationType: 'NONE'
}

if (!_.isUndefined(proxy.authorizationType)) {
if (_.isString(proxy.authorizationType)) {
const allowedTypes = ['NONE', 'AWS_IAM', 'CUSTOM', 'COGNITO_USER_POOLS']
if (allowedTypes.indexOf(proxy.authorizationType) === NOT_FOUND) {
const errorMessage = [
`Invalid APIG authorization type "${proxy.authorizationType}" in AWS service proxy.`,
` AWS supported types are: ${allowedTypes.join(', ')}.`
].join('')
return BbPromise.reject(new this.serverless.classes.Error(errorMessage))
}

auth.authorizationType = proxy.authorizationType
} else {
return BbPromise.reject(
new this.serverless.classes.Error(
`Invalid "authorizationType" property in ${serviceName} proxy`
)
)
}
}

if (!_.isUndefined(proxy.authorizerId)) {
if (auth.authorizationType !== 'CUSTOM') {
const errorMessage = `Expecting 'CUSTOM' authorization type when 'authorizerId' is set in service ${serviceName}`
return BbPromise.reject(new this.serverless.classes.Error(errorMessage))
}

auth.authorizerId = proxy.authorizerId
}

if (!_.isUndefined(proxy.authorizationScopes)) {
if (_.isArray(proxy.authorizationScopes)) {
if (auth.authorizationType !== 'COGNITO_USER_POOLS') {
const errorMessage = `Expecting 'COGNITO_USER_POOLS' authorization type when 'authorizationScopes' is set in service ${serviceName}`
return BbPromise.reject(new this.serverless.classes.Error(errorMessage))
}

auth.authorizationScopes = proxy.authorizationScopes
} else {
return BbPromise.reject(
new this.serverless.classes.Error(
`Invalid "authorizationScopes" property in ${serviceName} proxy`
)
)
}
}

return auth
},

async validateRequestParameters(proxy, serviceName) {
if (!_.isUndefined(proxy.requestParameters)) {
if (serviceName !== 'sqs') {
return BbPromise.reject(
new this.serverless.classes.Error(
'requestParameters property is only valid for "sqs" service proxy'
)
)
}

if (
!_.isPlainObject(proxy.requestParameters) ||
_.some(_.values(proxy.requestParameters), (v) => !_.isString(v))
) {
return BbPromise.reject(
new this.serverless.classes.Error(
'requestParameters property must be a string to string mapping'
)
)
}
}
}
}
Loading