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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,47 @@ stepFunctions:
definition:
```

#### Enabling CORS

To set CORS configurations for your HTTP endpoints, simply modify your event configurations as follows:

```yml
stepFunctions:
stateMachines:
hello:
events:
- http:
path: posts/create
method: POST
cors: true
definition:
```

Setting cors to true assumes a default configuration which is equivalent to:

```yml
stepFunctions:
stateMachines:
hello:
events:
- http:
path: posts/create
method: POST
cors:
origin: '*'
headers:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
allowCredentials: false
definition:
```

Configuring the cors property sets Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods,Access-Control-Allow-Credentials headers in the CORS preflight response.

#### Send request to an API
You can input an value as json in request body, the value is passed as the input value of your statemachine

Expand Down
89 changes: 89 additions & 0 deletions lib/deploy/events/apiGateway/cors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict';

const _ = require('lodash');
const BbPromise = require('bluebird');

module.exports = {

compileCors() {
_.forEach(this.pluginhttpValidated.corsPreflight, (config, path) => {
const resourceName = this.getResourceName(path);
const resourceRef = this.getResourceId(path);
const corsMethodLogicalId = this.provider.naming
.getMethodLogicalId(resourceName, 'options');

let origin = config.origin;
if (config.origins && config.origins.length) {
origin = config.origins.join(',');
}

const preflightHeaders = {
'Access-Control-Allow-Origin': `'${origin}'`,
'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`,
'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`,
'Access-Control-Allow-Credentials': `'${config.allowCredentials}'`,
};

if (_.includes(config.methods, 'ANY')) {
preflightHeaders['Access-Control-Allow-Methods'] =
preflightHeaders['Access-Control-Allow-Methods']
.replace('ANY', 'DELETE,GET,HEAD,PATCH,POST,PUT');
}

_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[corsMethodLogicalId]: {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: 'NONE',
HttpMethod: 'OPTIONS',
MethodResponses: this.generateCorsMethodResponses(preflightHeaders),
RequestParameters: {},
Integration: {
Type: 'MOCK',
RequestTemplates: {
'application/json': '{statusCode:200}',
},
IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders),
},
ResourceId: resourceRef,
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
},
},
});
});

return BbPromise.resolve();
},

generateCorsMethodResponses(preflightHeaders) {
const methodResponseHeaders = {};

_.forEach(preflightHeaders, (value, header) => {
methodResponseHeaders[`method.response.header.${header}`] = true;
});

return [
{
StatusCode: '200',
ResponseParameters: methodResponseHeaders,
ResponseModels: {},
},
];
},

generateCorsIntegrationResponses(preflightHeaders) {
const responseParameters = _.mapKeys(preflightHeaders,
(value, header) => `method.response.header.${header}`);

return [
{
StatusCode: '200',
ResponseParameters: responseParameters,
ResponseTemplates: {
'application/json': '',
},
},
];
},

};
184 changes: 184 additions & 0 deletions lib/deploy/events/apiGateway/cors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use strict';

const expect = require('chai').expect;
const Serverless = require('serverless/lib/Serverless');
const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider');
const ServerlessStepFunctions = require('./../../../index');

describe('#methods()', () => {
let serverless;
let serverlessStepFunctions;

beforeEach(() => {
serverless = new Serverless();
serverless.setProvider('aws', new AwsProvider(serverless));
serverless.service.provider.compiledCloudFormationTemplate = {
Resources: {},
};

const options = {
stage: 'dev',
region: 'us-east-1',
};
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
serverlessStepFunctions.serverless.service.stepFunctions = {
stateMachines: {
first: {},
},
};
serverlessStepFunctions.apiGatewayResourceLogicalIds = {
'users/create': 'ApiGatewayResourceUsersCreate',
'users/list': 'ApiGatewayResourceUsersList',
'users/update': 'ApiGatewayResourceUsersUpdate',
'users/delete': 'ApiGatewayResourceUsersDelete',
'users/any': 'ApiGatewayResourceUsersAny',
};
serverlessStepFunctions.apiGatewayResourceNames = {
'users/create': 'UsersCreate',
'users/list': 'UsersList',
'users/update': 'UsersUpdate',
'users/delete': 'UsersDelete',
'users/any': 'UsersAny',
};
serverlessStepFunctions.pluginhttpValidated = {};
});

it('should create preflight method for CORS enabled resource', () => {
serverlessStepFunctions.pluginhttpValidated.corsPreflight = {
'users/update': {
origin: 'http://example.com',
headers: ['*'],
methods: ['OPTIONS', 'PUT'],
allowCredentials: false,
},
'users/create': {
origins: ['*', 'http://example.com'],
headers: ['*'],
methods: ['OPTIONS', 'POST'],
allowCredentials: true,
},
'users/delete': {
origins: ['*'],
headers: ['CustomHeaderA', 'CustomHeaderB'],
methods: ['OPTIONS', 'DELETE'],
allowCredentials: false,
},
'users/any': {
origins: ['http://example.com'],
headers: ['*'],
methods: ['OPTIONS', 'ANY'],
allowCredentials: false,
},
};
return serverlessStepFunctions.compileCors().then(() => {
// users/create
expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreateOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
).to.equal('\'*,http://example.com\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreateOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
).to.equal('\'*\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreateOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
).to.equal('\'OPTIONS,POST\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreateOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
).to.equal('\'true\'');

// users/update
expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersUpdateOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
).to.equal('\'http://example.com\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersUpdateOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
).to.equal('\'OPTIONS,PUT\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersUpdateOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
).to.equal('\'false\'');

// users/delete
expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersDeleteOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
).to.equal('\'*\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersDeleteOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
).to.equal('\'CustomHeaderA,CustomHeaderB\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersDeleteOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
).to.equal('\'OPTIONS,DELETE\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersDeleteOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
).to.equal('\'false\'');

// users/any
expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersAnyOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
).to.equal('\'http://example.com\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersAnyOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
).to.equal('\'*\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersAnyOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
).to.equal('\'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT\'');

expect(
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersAnyOptions
.Properties.Integration.IntegrationResponses[0]
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
).to.equal('\'false\'');
});
});
});
Loading