Skip to content

Commit 19ab556

Browse files
authored
Merge pull request #67 from horike37/cors-support
Cors support
2 parents 567e322 + e505b10 commit 19ab556

File tree

9 files changed

+671
-7
lines changed

9 files changed

+671
-7
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,47 @@ stepFunctions:
115115
definition:
116116
```
117117

118+
#### Enabling CORS
119+
120+
To set CORS configurations for your HTTP endpoints, simply modify your event configurations as follows:
121+
122+
```yml
123+
stepFunctions:
124+
stateMachines:
125+
hello:
126+
events:
127+
- http:
128+
path: posts/create
129+
method: POST
130+
cors: true
131+
definition:
132+
```
133+
134+
Setting cors to true assumes a default configuration which is equivalent to:
135+
136+
```yml
137+
stepFunctions:
138+
stateMachines:
139+
hello:
140+
events:
141+
- http:
142+
path: posts/create
143+
method: POST
144+
cors:
145+
origin: '*'
146+
headers:
147+
- Content-Type
148+
- X-Amz-Date
149+
- Authorization
150+
- X-Api-Key
151+
- X-Amz-Security-Token
152+
- X-Amz-User-Agent
153+
allowCredentials: false
154+
definition:
155+
```
156+
157+
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.
158+
118159
#### Send request to an API
119160
You can input an value as json in request body, the value is passed as the input value of your statemachine
120161

lib/deploy/events/apiGateway/cors.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use strict';
2+
3+
const _ = require('lodash');
4+
const BbPromise = require('bluebird');
5+
6+
module.exports = {
7+
8+
compileCors() {
9+
_.forEach(this.pluginhttpValidated.corsPreflight, (config, path) => {
10+
const resourceName = this.getResourceName(path);
11+
const resourceRef = this.getResourceId(path);
12+
const corsMethodLogicalId = this.provider.naming
13+
.getMethodLogicalId(resourceName, 'options');
14+
15+
let origin = config.origin;
16+
if (config.origins && config.origins.length) {
17+
origin = config.origins.join(',');
18+
}
19+
20+
const preflightHeaders = {
21+
'Access-Control-Allow-Origin': `'${origin}'`,
22+
'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`,
23+
'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`,
24+
'Access-Control-Allow-Credentials': `'${config.allowCredentials}'`,
25+
};
26+
27+
if (_.includes(config.methods, 'ANY')) {
28+
preflightHeaders['Access-Control-Allow-Methods'] =
29+
preflightHeaders['Access-Control-Allow-Methods']
30+
.replace('ANY', 'DELETE,GET,HEAD,PATCH,POST,PUT');
31+
}
32+
33+
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
34+
[corsMethodLogicalId]: {
35+
Type: 'AWS::ApiGateway::Method',
36+
Properties: {
37+
AuthorizationType: 'NONE',
38+
HttpMethod: 'OPTIONS',
39+
MethodResponses: this.generateCorsMethodResponses(preflightHeaders),
40+
RequestParameters: {},
41+
Integration: {
42+
Type: 'MOCK',
43+
RequestTemplates: {
44+
'application/json': '{statusCode:200}',
45+
},
46+
IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders),
47+
},
48+
ResourceId: resourceRef,
49+
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
50+
},
51+
},
52+
});
53+
});
54+
55+
return BbPromise.resolve();
56+
},
57+
58+
generateCorsMethodResponses(preflightHeaders) {
59+
const methodResponseHeaders = {};
60+
61+
_.forEach(preflightHeaders, (value, header) => {
62+
methodResponseHeaders[`method.response.header.${header}`] = true;
63+
});
64+
65+
return [
66+
{
67+
StatusCode: '200',
68+
ResponseParameters: methodResponseHeaders,
69+
ResponseModels: {},
70+
},
71+
];
72+
},
73+
74+
generateCorsIntegrationResponses(preflightHeaders) {
75+
const responseParameters = _.mapKeys(preflightHeaders,
76+
(value, header) => `method.response.header.${header}`);
77+
78+
return [
79+
{
80+
StatusCode: '200',
81+
ResponseParameters: responseParameters,
82+
ResponseTemplates: {
83+
'application/json': '',
84+
},
85+
},
86+
];
87+
},
88+
89+
};
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
'use strict';
2+
3+
const expect = require('chai').expect;
4+
const Serverless = require('serverless/lib/Serverless');
5+
const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider');
6+
const ServerlessStepFunctions = require('./../../../index');
7+
8+
describe('#methods()', () => {
9+
let serverless;
10+
let serverlessStepFunctions;
11+
12+
beforeEach(() => {
13+
serverless = new Serverless();
14+
serverless.setProvider('aws', new AwsProvider(serverless));
15+
serverless.service.provider.compiledCloudFormationTemplate = {
16+
Resources: {},
17+
};
18+
19+
const options = {
20+
stage: 'dev',
21+
region: 'us-east-1',
22+
};
23+
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
24+
serverlessStepFunctions.serverless.service.stepFunctions = {
25+
stateMachines: {
26+
first: {},
27+
},
28+
};
29+
serverlessStepFunctions.apiGatewayResourceLogicalIds = {
30+
'users/create': 'ApiGatewayResourceUsersCreate',
31+
'users/list': 'ApiGatewayResourceUsersList',
32+
'users/update': 'ApiGatewayResourceUsersUpdate',
33+
'users/delete': 'ApiGatewayResourceUsersDelete',
34+
'users/any': 'ApiGatewayResourceUsersAny',
35+
};
36+
serverlessStepFunctions.apiGatewayResourceNames = {
37+
'users/create': 'UsersCreate',
38+
'users/list': 'UsersList',
39+
'users/update': 'UsersUpdate',
40+
'users/delete': 'UsersDelete',
41+
'users/any': 'UsersAny',
42+
};
43+
serverlessStepFunctions.pluginhttpValidated = {};
44+
});
45+
46+
it('should create preflight method for CORS enabled resource', () => {
47+
serverlessStepFunctions.pluginhttpValidated.corsPreflight = {
48+
'users/update': {
49+
origin: 'http://example.com',
50+
headers: ['*'],
51+
methods: ['OPTIONS', 'PUT'],
52+
allowCredentials: false,
53+
},
54+
'users/create': {
55+
origins: ['*', 'http://example.com'],
56+
headers: ['*'],
57+
methods: ['OPTIONS', 'POST'],
58+
allowCredentials: true,
59+
},
60+
'users/delete': {
61+
origins: ['*'],
62+
headers: ['CustomHeaderA', 'CustomHeaderB'],
63+
methods: ['OPTIONS', 'DELETE'],
64+
allowCredentials: false,
65+
},
66+
'users/any': {
67+
origins: ['http://example.com'],
68+
headers: ['*'],
69+
methods: ['OPTIONS', 'ANY'],
70+
allowCredentials: false,
71+
},
72+
};
73+
return serverlessStepFunctions.compileCors().then(() => {
74+
// users/create
75+
expect(
76+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
77+
.Resources.ApiGatewayMethodUsersCreateOptions
78+
.Properties.Integration.IntegrationResponses[0]
79+
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
80+
).to.equal('\'*,http://example.com\'');
81+
82+
expect(
83+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
84+
.Resources.ApiGatewayMethodUsersCreateOptions
85+
.Properties.Integration.IntegrationResponses[0]
86+
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
87+
).to.equal('\'*\'');
88+
89+
expect(
90+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
91+
.Resources.ApiGatewayMethodUsersCreateOptions
92+
.Properties.Integration.IntegrationResponses[0]
93+
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
94+
).to.equal('\'OPTIONS,POST\'');
95+
96+
expect(
97+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
98+
.Resources.ApiGatewayMethodUsersCreateOptions
99+
.Properties.Integration.IntegrationResponses[0]
100+
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
101+
).to.equal('\'true\'');
102+
103+
// users/update
104+
expect(
105+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
106+
.Resources.ApiGatewayMethodUsersUpdateOptions
107+
.Properties.Integration.IntegrationResponses[0]
108+
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
109+
).to.equal('\'http://example.com\'');
110+
111+
expect(
112+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
113+
.Resources.ApiGatewayMethodUsersUpdateOptions
114+
.Properties.Integration.IntegrationResponses[0]
115+
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
116+
).to.equal('\'OPTIONS,PUT\'');
117+
118+
expect(
119+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
120+
.Resources.ApiGatewayMethodUsersUpdateOptions
121+
.Properties.Integration.IntegrationResponses[0]
122+
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
123+
).to.equal('\'false\'');
124+
125+
// users/delete
126+
expect(
127+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
128+
.Resources.ApiGatewayMethodUsersDeleteOptions
129+
.Properties.Integration.IntegrationResponses[0]
130+
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
131+
).to.equal('\'*\'');
132+
133+
expect(
134+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
135+
.Resources.ApiGatewayMethodUsersDeleteOptions
136+
.Properties.Integration.IntegrationResponses[0]
137+
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
138+
).to.equal('\'CustomHeaderA,CustomHeaderB\'');
139+
140+
expect(
141+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
142+
.Resources.ApiGatewayMethodUsersDeleteOptions
143+
.Properties.Integration.IntegrationResponses[0]
144+
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
145+
).to.equal('\'OPTIONS,DELETE\'');
146+
147+
expect(
148+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
149+
.Resources.ApiGatewayMethodUsersDeleteOptions
150+
.Properties.Integration.IntegrationResponses[0]
151+
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
152+
).to.equal('\'false\'');
153+
154+
// users/any
155+
expect(
156+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
157+
.Resources.ApiGatewayMethodUsersAnyOptions
158+
.Properties.Integration.IntegrationResponses[0]
159+
.ResponseParameters['method.response.header.Access-Control-Allow-Origin']
160+
).to.equal('\'http://example.com\'');
161+
162+
expect(
163+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
164+
.Resources.ApiGatewayMethodUsersAnyOptions
165+
.Properties.Integration.IntegrationResponses[0]
166+
.ResponseParameters['method.response.header.Access-Control-Allow-Headers']
167+
).to.equal('\'*\'');
168+
169+
expect(
170+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
171+
.Resources.ApiGatewayMethodUsersAnyOptions
172+
.Properties.Integration.IntegrationResponses[0]
173+
.ResponseParameters['method.response.header.Access-Control-Allow-Methods']
174+
).to.equal('\'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT\'');
175+
176+
expect(
177+
serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
178+
.Resources.ApiGatewayMethodUsersAnyOptions
179+
.Properties.Integration.IntegrationResponses[0]
180+
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
181+
).to.equal('\'false\'');
182+
});
183+
});
184+
});

0 commit comments

Comments
 (0)