From 136034dd5b399e983a41d296744aa29b7a586d55 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 24 Sep 2019 00:24:09 +0300 Subject: [PATCH 01/19] feat(apigateway): cors support (wip) adds support for CORS preflight (OPTIONS) requests on resources. currently supports: origin, methods, headers, credentials, status code implemented during a live twitch stream on sep 24, 2019 --- packages/@aws-cdk/aws-apigateway/lib/cors.ts | 29 ++++++++++++ .../@aws-cdk/aws-apigateway/lib/resource.ts | 47 +++++++++++++++++++ .../test/integ.cors.handler/index.ts | 11 +++++ .../aws-apigateway/test/integ.cors.ts | 30 ++++++++++++ .../aws-apigateway/test/test.resource.ts | 7 ++- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/cors.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.cors.handler/index.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.cors.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/cors.ts b/packages/@aws-cdk/aws-apigateway/lib/cors.ts new file mode 100644 index 0000000000000..0e01f90fc934f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/cors.ts @@ -0,0 +1,29 @@ +export interface CorsOptions { + /** + * @default 204 + */ + readonly statusCode?: number; + + readonly allowOrigin: string; + + /** + * @default + */ + readonly allowHeaders?: string[]; + + /** + * @default + */ + readonly allowMethods?: string[]; + + /** + * @default false + */ + readonly allowCredentials?: boolean; +} + +export class Cors { + // TODO: dedup with utils.ts/ALLOWED_METHODS + public static readonly ALL_METHODS = [ 'OPTIONS', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'HEAD' ]; + public static readonly DEFAULT_HEADERS = [ 'Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token', 'X-Amz-User-Agent' ]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index f5cc99bc7fb67..61b783feed293 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -1,6 +1,8 @@ import { Construct, IResource as IResourceBase, Resource as ResourceConstruct } from '@aws-cdk/core'; import { CfnResource, CfnResourceProps } from './apigateway.generated'; +import { Cors, CorsOptions } from './cors'; import { Integration } from './integration'; +import { MockIntegration } from './integrations'; import { Method, MethodOptions } from './method'; import { RestApi } from './restapi'; @@ -85,6 +87,11 @@ export interface IResource extends IResourceBase { * @returns The newly created `Method` object. */ addMethod(httpMethod: string, target?: Integration, options?: MethodOptions): Method; + + /** + * TODO + */ + addCorsPreflight(options: CorsOptions): Method; } export interface ResourceOptions { @@ -144,6 +151,46 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc return new ProxyResource(this, '{proxy+}', { parent: this, ...options }); } + public addCorsPreflight(options: CorsOptions) { + + const headers = options.allowHeaders || Cors.DEFAULT_HEADERS; + const methods = options.allowMethods || Cors.ALL_METHODS; + const origin = options.allowOrigin; + + const integrationResponseParams: { [p: string]: string } = { }; + integrationResponseParams['method.response.header.Access-Control-Allow-Headers'] = `'${headers.join(',')}'`; + integrationResponseParams['method.response.header.Access-Control-Allow-Origin'] = `'${origin}'`; + integrationResponseParams['method.response.header.Access-Control-Allow-Methods'] = `'${methods.join(',')}'`; + + if (options.allowCredentials) { + integrationResponseParams['method.response.header.Access-Control-Allow-Credentials'] = `'true'`; + } + + const methodReponseParams: { [p: string]: boolean } = { }; + for (const key of Object.keys(integrationResponseParams)) { + methodReponseParams[key] = true; + } + + const statusCode = options.statusCode !== undefined ? options.statusCode : 204; + + return this.addMethod('OPTIONS', new MockIntegration({ + integrationResponses: [ + { + statusCode: `${statusCode}`, + responseParameters: integrationResponseParams, + } + ], + requestTemplates: { 'application/json': '{ statusCode: 200 }' } + }), { + methodResponses: [ + { + statusCode: `${statusCode}`, + responseParameters: methodReponseParams + } + ] + }); + } + public getResource(pathPart: string): IResource | undefined { return this.children[pathPart]; } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.handler/index.ts b/packages/@aws-cdk/aws-apigateway/test/integ.cors.handler/index.ts new file mode 100644 index 0000000000000..b2ebc6a65bcd7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.handler/index.ts @@ -0,0 +1,11 @@ +exports.handler = async (evt: any) => { + // tslint:disable-next-line:no-console + console.error(JSON.stringify(evt, undefined, 2)); + return { + statusCode: 200, + body: 'hello, cors!', + headers: { + 'Access-Control-Allow-Origin': '*' + } + }; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts b/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts new file mode 100644 index 0000000000000..8eae87253cdb3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts @@ -0,0 +1,30 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import { App, Construct, Stack, StackProps } from '@aws-cdk/core'; +import path = require('path'); +import apigw = require('../lib'); + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const api = new apigw.RestApi(this, 'cors-api-test'); + + const handler = new lambda.Function(this, 'handler', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'integ.cors.handler')) + }); + + const twitch = api.root.addResource('twitch'); + const backend = new apigw.LambdaIntegration(handler); + + twitch.addMethod('GET', backend); // GET /twitch + twitch.addMethod('POST', backend); // POST /twitch + twitch.addMethod('DELETE', backend); // DELETE /twitch + twitch.addCorsPreflight({ allowOrigin: '*' }); + } +} + +const app = new App(); +new TestStack(app, 'cors-twitch-test'); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index bc293c9c67706..a72561d51e545 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -1,5 +1,4 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import cdk = require('@aws-cdk/core'); import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import apigw = require('../lib'); @@ -9,7 +8,7 @@ import apigw = require('../lib'); export = { 'ProxyResource defines a "{proxy+}" resource with ANY method'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigw.RestApi(stack, 'api'); // WHEN @@ -50,7 +49,7 @@ export = { 'if "anyMethod" is false, then an ANY method will not be defined'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigw.RestApi(stack, 'api'); // WHEN @@ -71,7 +70,7 @@ export = { 'addProxy can be used on any resource to attach a proxy from that route'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigw.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false, From c546a31f1ea2e167434849f38a0dc4ffb04acb9b Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 25 Sep 2019 23:33:29 +0300 Subject: [PATCH 02/19] export types from cors.ts --- packages/@aws-cdk/aws-apigateway/lib/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index d174e58933827..f19a847f30556 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -16,6 +16,7 @@ export * from './authorizer'; export * from './json-schema'; export * from './domain-name'; export * from './base-path-mapping'; +export * from './cors'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; From 0933b82fa96aaca29288c0f466942b0b5cad96a9 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 25 Sep 2019 23:33:44 +0300 Subject: [PATCH 03/19] allow 'ANY' to be used in `allowMethods` --- packages/@aws-cdk/aws-apigateway/lib/resource.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 61b783feed293..c78e696984abd 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -152,11 +152,18 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc } public addCorsPreflight(options: CorsOptions) { - const headers = options.allowHeaders || Cors.DEFAULT_HEADERS; - const methods = options.allowMethods || Cors.ALL_METHODS; + let methods = options.allowMethods || Cors.ALL_METHODS; const origin = options.allowOrigin; + if (methods.includes('ANY')) { + if (methods.length > 1) { + throw new Error(`ANY cannot be used with any other method. Received: ${methods.join(',')}`); + } + + methods = Cors.ALL_METHODS; + } + const integrationResponseParams: { [p: string]: string } = { }; integrationResponseParams['method.response.header.Access-Control-Allow-Headers'] = `'${headers.join(',')}'`; integrationResponseParams['method.response.header.Access-Control-Allow-Origin'] = `'${origin}'`; From a4f430c1a7c023bc95475e95d7132ece9c54332f Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 25 Sep 2019 23:34:05 +0300 Subject: [PATCH 04/19] add a bunch of unit tests --- .../aws-apigateway/test/test.resource.ts | 254 +++++++++++++++++- 1 file changed, 253 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index a72561d51e545..fd07ea412901b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -167,7 +167,7 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', - ResourceId: { "Fn::GetAtt": [ "apiC8550315", "RootResourceId" ] }, + ResourceId: { "Fn::GetAtt": ["apiC8550315", "RootResourceId"] }, Integration: { RequestParameters: { foo: "bar" }, Type: 'MOCK' @@ -326,5 +326,257 @@ export = { } } + }, + + 'addCorsPreflight': { + + 'adds an OPTIONS method to a resource'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigin: 'https://amazon.com' + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ] + })); + test.done(); + }, + + 'allowCredentials'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigin: 'https://amazon.com', + allowCredentials: true + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Allow-Credentials": "'true'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Allow-Credentials": true + }, + "StatusCode": "204" + } + ] + })); + test.done(); + }, + + 'allowMethods'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigin: 'https://aws.amazon.com', + allowMethods: [ 'GET', 'PUT' ] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'GET,PUT'", + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + }, + "StatusCode": "204" + } + ] + })); + test.done(); + }, + + 'allowMethods ANY will expand to all supported methods'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigin: 'https://aws.amazon.com', + allowMethods: [ 'ANY' ] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + }, + "StatusCode": "204" + } + ] + })); + test.done(); + }, + + 'allowMethods ANY cannot be used with any other method'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // THEN + test.throws(() => resource.addCorsPreflight({ + allowOrigin: 'https://aws.amazon.com', + allowMethods: [ 'ANY', 'PUT' ] + }), /ANY cannot be used with any other method. Received: ANY,PUT/); + + test.done(); + }, + + 'statusCode can be used to set the response status code'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigin: 'https://aws.amazon.com', + statusCode: 200 + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + }, + "StatusCode": "200" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + }, + "StatusCode": "200" + } + ] + })); + test.done(); + }, + } + + }; From f415bf9d8dc3fc85352b7a22f892943cabdf5ae3 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 00:05:52 +0300 Subject: [PATCH 05/19] document api --- packages/@aws-cdk/aws-apigateway/lib/cors.ts | 33 ++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/cors.ts b/packages/@aws-cdk/aws-apigateway/lib/cors.ts index 0e01f90fc934f..ba3569a9656da 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/cors.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/cors.ts @@ -1,22 +1,51 @@ export interface CorsOptions { /** + * Specifies the response status code returned from the OPTIONS method. + * * @default 204 */ readonly statusCode?: number; - readonly allowOrigin: string; + /** + * The Access-Control-Allow-Origin response header indicates whether the + * response can be shared with requesting code from the given origin. + * + * Specifies the list of origins that are allowed to make requests to this resource. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + */ + readonly allowOrigins: string[]; /** + * The Access-Control-Allow-Headers response header is used in response to a + * preflight request which includes the Access-Control-Request-Headers to + * indicate which HTTP headers can be used during the actual request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers * @default */ readonly allowHeaders?: string[]; /** + * The Access-Control-Allow-Methods response header specifies the method or + * methods allowed when accessing the resource in response to a preflight request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods * @default */ readonly allowMethods?: string[]; /** + * The Access-Control-Allow-Credentials response header tells browsers whether + * to expose the response to frontend JavaScript code when the request's + * credentials mode (Request.credentials) is "include". + * + * When a request's credentials mode (Request.credentials) is "include", + * browsers will only expose the response to frontend JavaScript code if the + * Access-Control-Allow-Credentials value is true. + * + * Credentials are cookies, authorization headers or TLS client certificates. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials * @default false */ readonly allowCredentials?: boolean; @@ -26,4 +55,4 @@ export class Cors { // TODO: dedup with utils.ts/ALLOWED_METHODS public static readonly ALL_METHODS = [ 'OPTIONS', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'HEAD' ]; public static readonly DEFAULT_HEADERS = [ 'Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token', 'X-Amz-User-Agent' ]; -} \ No newline at end of file +} From 3fe3d8c5a45fff48dcd39274498cf2fa4e2c9876 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 00:06:08 +0300 Subject: [PATCH 06/19] support multiple origins --- .../@aws-cdk/aws-apigateway/lib/resource.ts | 32 +++++++- .../aws-apigateway/test/integ.cors.ts | 4 +- .../aws-apigateway/test/test.resource.ts | 80 ++++++++++++++++--- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index c78e696984abd..b567aafaa4938 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -154,7 +154,7 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc public addCorsPreflight(options: CorsOptions) { const headers = options.allowHeaders || Cors.DEFAULT_HEADERS; let methods = options.allowMethods || Cors.ALL_METHODS; - const origin = options.allowOrigin; + const origin = options.allowOrigins[0]; if (methods.includes('ANY')) { if (methods.length > 1) { @@ -164,6 +164,10 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc methods = Cors.ALL_METHODS; } + if (options.allowOrigins.length === 0) { + throw new Error('allowOrigins must contain at least one origin'); + } + const integrationResponseParams: { [p: string]: string } = { }; integrationResponseParams['method.response.header.Access-Control-Allow-Headers'] = `'${headers.join(',')}'`; integrationResponseParams['method.response.header.Access-Control-Allow-Origin'] = `'${origin}'`; @@ -185,9 +189,10 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc { statusCode: `${statusCode}`, responseParameters: integrationResponseParams, + responseTemplates: renderResponseTemplate() } ], - requestTemplates: { 'application/json': '{ statusCode: 200 }' } + requestTemplates: { 'application/json': '{ statusCode: 200 }' }, }), { methodResponses: [ { @@ -196,6 +201,29 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc } ] }); + + function renderResponseTemplate() { + const origins = options.allowOrigins.slice(1); + + if (origins.length === 0) { + return undefined; + } + + const template = new Array(); + + template.push(`#set($origin = $input.params("Origin"))`); + template.push(`#if($origin == "") #set($origin = $input.params("origin")) #end`); + + const condition = origins.map(o => `$origin.matches("${o}")`).join(' || '); + + template.push(`#if(${condition})`); + template.push(` #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)`); + template.push('#end'); + + return { + 'application/json': template.join('\n') + }; + } } public getResource(pathPart: string): IResource | undefined { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts b/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts index 8eae87253cdb3..ad441a3e7b28e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts @@ -21,10 +21,10 @@ class TestStack extends Stack { twitch.addMethod('GET', backend); // GET /twitch twitch.addMethod('POST', backend); // POST /twitch twitch.addMethod('DELETE', backend); // DELETE /twitch - twitch.addCorsPreflight({ allowOrigin: '*' }); + twitch.addCorsPreflight({ allowOrigins: [ 'https://google.com', 'https://www.test-cors.org' ] }); } } const app = new App(); new TestStack(app, 'cors-twitch-test'); -app.synth(); \ No newline at end of file +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index fd07ea412901b..965ba74c06a6b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -338,7 +338,7 @@ export = { // WHEN resource.addCorsPreflight({ - allowOrigin: 'https://amazon.com' + allowOrigins: ['https://amazon.com'] }); // THEN @@ -383,7 +383,7 @@ export = { // WHEN resource.addCorsPreflight({ - allowOrigin: 'https://amazon.com', + allowOrigins: ['https://amazon.com'], allowCredentials: true }); @@ -431,8 +431,8 @@ export = { // WHEN resource.addCorsPreflight({ - allowOrigin: 'https://aws.amazon.com', - allowMethods: [ 'GET', 'PUT' ] + allowOrigins: ['https://aws.amazon.com'], + allowMethods: ['GET', 'PUT'] }); // THEN @@ -477,8 +477,8 @@ export = { // WHEN resource.addCorsPreflight({ - allowOrigin: 'https://aws.amazon.com', - allowMethods: [ 'ANY' ] + allowOrigins: ['https://aws.amazon.com'], + allowMethods: ['ANY'] }); // THEN @@ -523,8 +523,8 @@ export = { // THEN test.throws(() => resource.addCorsPreflight({ - allowOrigin: 'https://aws.amazon.com', - allowMethods: [ 'ANY', 'PUT' ] + allowOrigins: ['https://aws.amazon.com'], + allowMethods: ['ANY', 'PUT'] }), /ANY cannot be used with any other method. Received: ANY,PUT/); test.done(); @@ -538,7 +538,7 @@ export = { // WHEN resource.addCorsPreflight({ - allowOrigin: 'https://aws.amazon.com', + allowOrigins: ['https://aws.amazon.com'], statusCode: 200 }); @@ -576,6 +576,68 @@ export = { test.done(); }, + 'allowOrigins must contain at least one origin'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + test.throws(() => resource.addCorsPreflight({ + allowOrigins: [] + }), /allowOrigins must contain at least one origin/); + + test.done(); + }, + + 'allowOrigins can be used to specify multiple origins'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://twitch.tv', 'https://amazon.com', 'https://aws.amazon.com'] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://twitch.tv'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "ResponseTemplates": { + "application/json": "#set($origin = $input.params(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params(\"origin\")) #end\n#if($origin.matches(\"https://amazon.com\") || $origin.matches(\"https://aws.amazon.com\"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ] + })); + test.done(); + } + } From 0c2c3844d9b8ece0938ad2268fee1c2928424239 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 17:00:35 +0300 Subject: [PATCH 07/19] add maxAge + disableCache --- packages/@aws-cdk/aws-apigateway/lib/cors.ts | 23 ++++ .../@aws-cdk/aws-apigateway/lib/resource.ts | 70 ++++++++--- .../aws-apigateway/test/test.resource.ts | 115 +++++++++++++++++- 3 files changed, 187 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/cors.ts b/packages/@aws-cdk/aws-apigateway/lib/cors.ts index ba3569a9656da..93dbb653b2718 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/cors.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/cors.ts @@ -1,3 +1,5 @@ +import { Duration } from '@aws-cdk/core'; + export interface CorsOptions { /** * Specifies the response status code returned from the OPTIONS method. @@ -49,6 +51,27 @@ export interface CorsOptions { * @default false */ readonly allowCredentials?: boolean; + + /** + * The Access-Control-Max-Age response header indicates how long the results of + * a preflight request (that is the information contained in the + * Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) + * can be cached. + * + * To disable caching altogther use `disableCache: true`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + * @default - browser-specific (see reference) + */ + readonly maxAge?: Duration; + + /** + * Sets Access-Control-Max-Age to -1, which means that caching is disabled. + * This option cannot be used with `maxAge`. + * + * @default - cache is enabled + */ + readonly disableCache?: boolean; } export class Cors { diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index b567aafaa4938..313a07964be56 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -152,9 +152,29 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc } public addCorsPreflight(options: CorsOptions) { + const integrationResponseParams: { [p: string]: string } = { }; + + // + // Access-Control-Allow-Headers + const headers = options.allowHeaders || Cors.DEFAULT_HEADERS; + integrationResponseParams['method.response.header.Access-Control-Allow-Headers'] = `'${headers.join(',')}'`; + + // + // Access-Control-Allow-Origin + + if (options.allowOrigins.length === 0) { + throw new Error('allowOrigins must contain at least one origin'); + } + + // we use the first origin here and if there are more origins in the list, we + // will match against them in the response velocity template + integrationResponseParams['method.response.header.Access-Control-Allow-Origin'] = `'${options.allowOrigins[0]}'`; + + // + // Access-Control-Allow-Methods + let methods = options.allowMethods || Cors.ALL_METHODS; - const origin = options.allowOrigins[0]; if (methods.includes('ANY')) { if (methods.length > 1) { @@ -164,44 +184,58 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc methods = Cors.ALL_METHODS; } - if (options.allowOrigins.length === 0) { - throw new Error('allowOrigins must contain at least one origin'); - } - - const integrationResponseParams: { [p: string]: string } = { }; - integrationResponseParams['method.response.header.Access-Control-Allow-Headers'] = `'${headers.join(',')}'`; - integrationResponseParams['method.response.header.Access-Control-Allow-Origin'] = `'${origin}'`; integrationResponseParams['method.response.header.Access-Control-Allow-Methods'] = `'${methods.join(',')}'`; + // + // Access-Control-Allow-Credentials + if (options.allowCredentials) { integrationResponseParams['method.response.header.Access-Control-Allow-Credentials'] = `'true'`; } + // + // Access-Control-Max-Age + + let maxAgeSeconds; + + if (options.maxAge && options.disableCache) { + throw new Error(`The options "maxAge" and "disableCache" are mutually exclusive`); + } + + if (options.maxAge) { + maxAgeSeconds = options.maxAge.toSeconds(); + } + + if (options.disableCache) { + maxAgeSeconds = -1; + } + + if (maxAgeSeconds) { + integrationResponseParams['method.response.header.Access-Control-Max-Age'] = `'${maxAgeSeconds}'`; + } + const methodReponseParams: { [p: string]: boolean } = { }; for (const key of Object.keys(integrationResponseParams)) { methodReponseParams[key] = true; } + // + // statusCode + const statusCode = options.statusCode !== undefined ? options.statusCode : 204; return this.addMethod('OPTIONS', new MockIntegration({ + requestTemplates: { 'application/json': '{ statusCode: 200 }' }, integrationResponses: [ - { - statusCode: `${statusCode}`, - responseParameters: integrationResponseParams, - responseTemplates: renderResponseTemplate() - } + { statusCode: `${statusCode}`, responseParameters: integrationResponseParams, responseTemplates: renderResponseTemplate() } ], - requestTemplates: { 'application/json': '{ statusCode: 200 }' }, }), { methodResponses: [ - { - statusCode: `${statusCode}`, - responseParameters: methodReponseParams - } + { statusCode: `${statusCode}`, responseParameters: methodReponseParams } ] }); + // renders the response template to match all possible origins (if we have more than one) function renderResponseTemplate() { const origins = options.allowOrigins.slice(1); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index 965ba74c06a6b..639a8ef61dba3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Stack } from '@aws-cdk/core'; +import { Stack, Duration } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import apigw = require('../lib'); @@ -636,9 +636,118 @@ export = { ] })); test.done(); - } + }, - } + 'maxAge can be used to specify Access-Control-Max-Age'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + maxAge: Duration.minutes(60) + }); + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Max-Age": `'${60 * 60}'` + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Max-Age": true + }, + "StatusCode": "204" + } + ] + })); + test.done(); + }, + + 'disableCache will set Max-Age to -1'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + disableCache: true + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Max-Age": `'-1'` + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Max-Age": true + }, + "StatusCode": "204" + } + ] + })); + test.done(); + }, + + 'maxAge and disableCache are mutually exclusive'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // THEN + test.throws(() => resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + disableCache: true, + maxAge: Duration.seconds(10) + }), /The options "maxAge" and "disableCache" are mutually exclusive/); + + test.done(); + } + } }; From 30e328e9643a296e5b3c0e8c324835ccb41e19fe Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 17:00:45 +0300 Subject: [PATCH 08/19] document addCorsPreflight --- packages/@aws-cdk/aws-apigateway/lib/resource.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 313a07964be56..fcbd582716deb 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -89,7 +89,17 @@ export interface IResource extends IResourceBase { addMethod(httpMethod: string, target?: Integration, options?: MethodOptions): Method; /** - * TODO + * Adds an OPTIONS method to this resource which responds to Cross-Origin + * Resource Sharing (CORS) preflight requests. + * + * Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional + * HTTP headers to tell browsers to give a web application running at one + * origin, access to selected resources from a different origin. A web + * application executes a cross-origin HTTP request when it requests a + * resource that has a different origin (domain, protocol, or port) from its + * own. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS */ addCorsPreflight(options: CorsOptions): Method; } From 1a78d381688d14b01b860533d691142791e61518 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 17:25:32 +0300 Subject: [PATCH 09/19] exposeHeaders --- packages/@aws-cdk/aws-apigateway/lib/cors.ts | 15 ++++++ .../@aws-cdk/aws-apigateway/lib/resource.ts | 45 +++++++++++------ .../aws-apigateway/test/test.resource.ts | 48 +++++++++++++++++++ 3 files changed, 93 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/cors.ts b/packages/@aws-cdk/aws-apigateway/lib/cors.ts index 93dbb653b2718..f5f4b668258aa 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/cors.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/cors.ts @@ -72,6 +72,21 @@ export interface CorsOptions { * @default - cache is enabled */ readonly disableCache?: boolean; + + /** + * The Access-Control-Expose-Headers response header indicates which headers + * can be exposed as part of the response by listing their names. + * + * If you want clients to be able to access other headers, you have to list + * them using the Access-Control-Expose-Headers header. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + * + * @default - only the 6 CORS-safelisted response headers are exposed: + * Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, + * Pragma + */ + readonly exposeHeaders?: string[]; } export class Cors { diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index fcbd582716deb..08677a752bcf5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -162,13 +162,13 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc } public addCorsPreflight(options: CorsOptions) { - const integrationResponseParams: { [p: string]: string } = { }; + const headers: { [name: string]: string } = { }; // // Access-Control-Allow-Headers - const headers = options.allowHeaders || Cors.DEFAULT_HEADERS; - integrationResponseParams['method.response.header.Access-Control-Allow-Headers'] = `'${headers.join(',')}'`; + const allowHeaders = options.allowHeaders || Cors.DEFAULT_HEADERS; + headers['Access-Control-Allow-Headers'] = `'${allowHeaders.join(',')}'`; // // Access-Control-Allow-Origin @@ -179,28 +179,28 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc // we use the first origin here and if there are more origins in the list, we // will match against them in the response velocity template - integrationResponseParams['method.response.header.Access-Control-Allow-Origin'] = `'${options.allowOrigins[0]}'`; + headers['Access-Control-Allow-Origin'] = `'${options.allowOrigins[0]}'`; // // Access-Control-Allow-Methods - let methods = options.allowMethods || Cors.ALL_METHODS; + let allowMethods = options.allowMethods || Cors.ALL_METHODS; - if (methods.includes('ANY')) { - if (methods.length > 1) { - throw new Error(`ANY cannot be used with any other method. Received: ${methods.join(',')}`); + if (allowMethods.includes('ANY')) { + if (allowMethods.length > 1) { + throw new Error(`ANY cannot be used with any other method. Received: ${allowMethods.join(',')}`); } - methods = Cors.ALL_METHODS; + allowMethods = Cors.ALL_METHODS; } - integrationResponseParams['method.response.header.Access-Control-Allow-Methods'] = `'${methods.join(',')}'`; + headers['Access-Control-Allow-Methods'] = `'${allowMethods.join(',')}'`; // // Access-Control-Allow-Credentials if (options.allowCredentials) { - integrationResponseParams['method.response.header.Access-Control-Allow-Credentials'] = `'true'`; + headers['Access-Control-Allow-Credentials'] = `'true'`; } // @@ -221,12 +221,15 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc } if (maxAgeSeconds) { - integrationResponseParams['method.response.header.Access-Control-Max-Age'] = `'${maxAgeSeconds}'`; + headers['Access-Control-Max-Age'] = `'${maxAgeSeconds}'`; } - const methodReponseParams: { [p: string]: boolean } = { }; - for (const key of Object.keys(integrationResponseParams)) { - methodReponseParams[key] = true; + // + // Access-Control-Expose-Headers + // + + if (options.exposeHeaders) { + headers['Access-Control-Expose-Headers'] = `'${options.exposeHeaders.join(',')}'`; } // @@ -234,6 +237,18 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc const statusCode = options.statusCode !== undefined ? options.statusCode : 204; + // + // prepare responseParams + + const integrationResponseParams: { [p: string]: string } = { }; + const methodReponseParams: { [p: string]: boolean } = { }; + + for (const [ name, value ] of Object.entries(headers)) { + const key = `method.response.header.${name}`; + integrationResponseParams[key] = value; + methodReponseParams[key] = true; + } + return this.addMethod('OPTIONS', new MockIntegration({ requestTemplates: { 'application/json': '{ statusCode: 200 }' }, integrationResponses: [ diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index 639a8ef61dba3..187969f2b05c3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -747,6 +747,54 @@ export = { maxAge: Duration.seconds(10) }), /The options "maxAge" and "disableCache" are mutually exclusive/); + test.done(); + }, + + 'exposeHeaders can be used to specify Access-Control-Expose-Headers'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + exposeHeaders: [ 'Authorization', 'Foo' ] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Expose-Headers": "'Authorization,Foo'", + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + MethodResponses: [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Expose-Headers": true, + }, + "StatusCode": "204" + } + ] + })); test.done(); } } From 03e9723e8c25426970d64b1c0924aece7d350237 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 17:35:57 +0300 Subject: [PATCH 10/19] doc cleanups --- packages/@aws-cdk/aws-apigateway/lib/cors.ts | 15 +++++++++++---- packages/@aws-cdk/aws-apigateway/lib/util.ts | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/cors.ts b/packages/@aws-cdk/aws-apigateway/lib/cors.ts index f5f4b668258aa..2590785551980 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/cors.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/cors.ts @@ -1,4 +1,5 @@ import { Duration } from '@aws-cdk/core'; +import { ALL_METHODS } from './util'; export interface CorsOptions { /** @@ -24,7 +25,7 @@ export interface CorsOptions { * indicate which HTTP headers can be used during the actual request. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers - * @default + * @default Cors.DEFAULT_HEADERS */ readonly allowHeaders?: string[]; @@ -33,7 +34,7 @@ export interface CorsOptions { * methods allowed when accessing the resource in response to a preflight request. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods - * @default + * @default Cors.ALL_METHODS */ readonly allowMethods?: string[]; @@ -90,7 +91,13 @@ export interface CorsOptions { } export class Cors { - // TODO: dedup with utils.ts/ALLOWED_METHODS - public static readonly ALL_METHODS = [ 'OPTIONS', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'HEAD' ]; + /** + * All HTTP methods. + */ + public static readonly ALL_METHODS = ALL_METHODS; + + /** + * The set of default headers allowed for CORS and useful for API Gateway. + */ public static readonly DEFAULT_HEADERS = [ 'Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token', 'X-Amz-User-Agent' ]; } diff --git a/packages/@aws-cdk/aws-apigateway/lib/util.ts b/packages/@aws-cdk/aws-apigateway/lib/util.ts index 9b8fb97f001be..691c7b6cba0fc 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/util.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/util.ts @@ -1,7 +1,9 @@ import { format as formatUrl } from 'url'; import jsonSchema = require('./json-schema'); -const ALLOWED_METHODS = [ 'ANY', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]; +export const ALL_METHODS = [ 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]; + +const ALLOWED_METHODS = [ 'ANY', ...ALL_METHODS ]; export function validateHttpMethod(method: string, messagePrefix: string = '') { if (!ALLOWED_METHODS.includes(method)) { From bcff001a2c260d4eddfe995cc90535cf51ae3b0f Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 18:11:16 +0300 Subject: [PATCH 11/19] reorder methods --- packages/@aws-cdk/aws-apigateway/lib/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/util.ts b/packages/@aws-cdk/aws-apigateway/lib/util.ts index 691c7b6cba0fc..7748792bb2454 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/util.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/util.ts @@ -1,7 +1,7 @@ import { format as formatUrl } from 'url'; import jsonSchema = require('./json-schema'); -export const ALL_METHODS = [ 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]; +export const ALL_METHODS = [ 'OPTIONS', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'HEAD' ]; const ALLOWED_METHODS = [ 'ANY', ...ALL_METHODS ]; From 5ce14ac81bc58782357235bea5698cd1ce39a4c6 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Sep 2019 18:56:42 +0300 Subject: [PATCH 12/19] add integ test expectation --- .../test/integ.cors.expected.json | 621 ++++++++++++++++++ 1 file changed, 621 insertions(+) create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json new file mode 100644 index 0000000000000..86e89ec1739ac --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -0,0 +1,621 @@ +{ + "Resources": { + "corsapitest8682546E": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "cors-api-test" + } + }, + "corsapitestDeployment2BF1633Ab835c2bfdc07db9c80c58bb34e202728": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "corsapitesttwitchDELETEB4C94228", + "corsapitesttwitchGET4270341B", + "corsapitesttwitchOPTIONSE5EEB527", + "corsapitesttwitchPOSTB52CFB02", + "corsapitesttwitch0E3D1559" + ] + }, + "corsapitestDeploymentStageprod8F31F2AB": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "DeploymentId": { + "Ref": "corsapitestDeployment2BF1633Ab835c2bfdc07db9c80c58bb34e202728" + }, + "StageName": "prod" + } + }, + "corsapitestCloudWatchRole9AF5A81A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "corsapitestAccount7D1D6854": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "corsapitestCloudWatchRole9AF5A81A", + "Arn" + ] + } + }, + "DependsOn": [ + "corsapitest8682546E" + ] + }, + "corsapitesttwitch0E3D1559": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "corsapitest8682546E", + "RootResourceId" + ] + }, + "PathPart": "twitch", + "RestApiId": { + "Ref": "corsapitest8682546E" + } + } + }, + "corsapitesttwitchGETApiPermissioncorstwitchtestcorsapitest1E81FF74GETtwitchDD74718A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "corsapitest8682546E" + }, + "/", + { + "Ref": "corsapitestDeploymentStageprod8F31F2AB" + }, + "/GET/twitch" + ] + ] + } + } + }, + "corsapitesttwitchGETApiPermissionTestcorstwitchtestcorsapitest1E81FF74GETtwitch730CD01F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "corsapitest8682546E" + }, + "/test-invoke-stage/GET/twitch" + ] + ] + } + } + }, + "corsapitesttwitchGET4270341B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "corsapitesttwitch0E3D1559" + }, + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "corsapitesttwitchPOSTApiPermissioncorstwitchtestcorsapitest1E81FF74POSTtwitchD6548E1B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "corsapitest8682546E" + }, + "/", + { + "Ref": "corsapitestDeploymentStageprod8F31F2AB" + }, + "/POST/twitch" + ] + ] + } + } + }, + "corsapitesttwitchPOSTApiPermissionTestcorstwitchtestcorsapitest1E81FF74POSTtwitch9C9C1E14": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "corsapitest8682546E" + }, + "/test-invoke-stage/POST/twitch" + ] + ] + } + } + }, + "corsapitesttwitchPOSTB52CFB02": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "corsapitesttwitch0E3D1559" + }, + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "corsapitesttwitchDELETEApiPermissioncorstwitchtestcorsapitest1E81FF74DELETEtwitch2AF8A510": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "corsapitest8682546E" + }, + "/", + { + "Ref": "corsapitestDeploymentStageprod8F31F2AB" + }, + "/DELETE/twitch" + ] + ] + } + } + }, + "corsapitesttwitchDELETEApiPermissionTestcorstwitchtestcorsapitest1E81FF74DELETEtwitch0CD7A81B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "corsapitest8682546E" + }, + "/test-invoke-stage/DELETE/twitch" + ] + ] + } + } + }, + "corsapitesttwitchDELETEB4C94228": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "DELETE", + "ResourceId": { + "Ref": "corsapitesttwitch0E3D1559" + }, + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "handlerE1533BD5", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "corsapitesttwitchOPTIONSE5EEB527": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "OPTIONS", + "ResourceId": { + "Ref": "corsapitesttwitch0E3D1559" + }, + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://google.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "ResponseTemplates": { + "application/json": "#set($origin = $input.params(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params(\"origin\")) #end\n#if($origin.matches(\"https://www.test-cors.org\"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ] + } + }, + "handlerServiceRole187D5A5A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "handlerE1533BD5": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "handlerCodeS3Bucket9583A349" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "handlerCodeS3VersionKey5F6149CC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "handlerCodeS3VersionKey5F6149CC" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "handlerServiceRole187D5A5A", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "handlerServiceRole187D5A5A" + ] + } + }, + "Outputs": { + "corsapitestEndpointE63606AE": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "corsapitest8682546E" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "corsapitestDeploymentStageprod8F31F2AB" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "handlerCodeS3Bucket9583A349": { + "Type": "String", + "Description": "S3 bucket for asset \"cors-twitch-test/handler/Code\"" + }, + "handlerCodeS3VersionKey5F6149CC": { + "Type": "String", + "Description": "S3 key for asset version \"cors-twitch-test/handler/Code\"" + }, + "handlerCodeArtifactHashA96ADEB4": { + "Type": "String", + "Description": "Artifact hash for asset \"cors-twitch-test/handler/Code\"" + } + } +} \ No newline at end of file From 11ef4b603544127e2ddf09601cfaea91a9c4db3b Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 10:52:22 +0300 Subject: [PATCH 13/19] defaultCorsPreflightOptions --- .../@aws-cdk/aws-apigateway/lib/resource.ts | 22 + .../@aws-cdk/aws-apigateway/lib/restapi.ts | 7 + .../@aws-cdk/aws-apigateway/test/test.cors.ts | 530 ++++++++++++++++++ .../aws-apigateway/test/test.resource.ts | 472 +--------------- 4 files changed, 560 insertions(+), 471 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.cors.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 08677a752bcf5..47b56130a371b 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -45,6 +45,11 @@ export interface IResource extends IResourceBase { */ readonly defaultMethodOptions?: MethodOptions; + /** + * Default options for CORS preflight OPTIONS method. + */ + readonly defaultCorsPreflightOptions?: CorsOptions; + /** * Gets or create all resources leading up to the specified path. * @@ -120,6 +125,16 @@ export interface ResourceOptions { * @default - Inherited from parent. */ readonly defaultMethodOptions?: MethodOptions; + + /** + * Adds a CORS preflight OPTIONS method to this resource and all child + * resources. + * + * You can add CORS at the resource-level using `addCorsPreflight`. + * + * @default - CORS is disabled + */ + readonly defaultCorsPreflightOptions?: CorsOptions; } export interface ResourceProps extends ResourceOptions { @@ -142,6 +157,7 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc public abstract readonly path: string; public abstract readonly defaultIntegration?: Integration; public abstract readonly defaultMethodOptions?: MethodOptions; + public abstract readonly defaultCorsPreflightOptions?: CorsOptions; private readonly children: { [pathPart: string]: Resource } = { }; @@ -333,6 +349,7 @@ export class Resource extends ResourceBase { public readonly defaultIntegration?: Integration; public readonly defaultMethodOptions?: MethodOptions; + public readonly defaultCorsPreflightOptions?: CorsOptions; constructor(scope: Construct, id: string, props: ResourceProps) { super(scope, id); @@ -373,6 +390,11 @@ export class Resource extends ResourceBase { ...props.parent.defaultMethodOptions, ...props.defaultMethodOptions }; + this.defaultCorsPreflightOptions = props.defaultCorsPreflightOptions || props.parent.defaultCorsPreflightOptions; + + if (this.defaultCorsPreflightOptions) { + this.addCorsPreflight(this.defaultCorsPreflightOptions); + } } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 99b2193ccc4b0..cb3706d433056 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -2,6 +2,7 @@ import iam = require('@aws-cdk/aws-iam'); import { CfnOutput, Construct, IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; import { ApiKey, IApiKey } from './api-key'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; +import { CorsOptions } from './cors'; import { Deployment } from './deployment'; import { DomainName, DomainNameOptions } from './domain-name'; import { Integration } from './integration'; @@ -449,6 +450,7 @@ class RootResource extends ResourceBase { public readonly path: string; public readonly defaultIntegration?: Integration | undefined; public readonly defaultMethodOptions?: MethodOptions | undefined; + public readonly defaultCorsPreflightOptions?: CorsOptions | undefined; constructor(api: RestApi, props: RestApiProps, resourceId: string) { super(api, 'Default'); @@ -456,8 +458,13 @@ class RootResource extends ResourceBase { this.parentResource = undefined; this.defaultIntegration = props.defaultIntegration; this.defaultMethodOptions = props.defaultMethodOptions; + this.defaultCorsPreflightOptions = props.defaultCorsPreflightOptions; this.restApi = api; this.resourceId = resourceId; this.path = '/'; + + if (this.defaultCorsPreflightOptions) { + this.addCorsPreflight(this.defaultCorsPreflightOptions); + } } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.cors.ts b/packages/@aws-cdk/aws-apigateway/test/test.cors.ts new file mode 100644 index 0000000000000..b5b591f2cb1e2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.cors.ts @@ -0,0 +1,530 @@ +import { countResources, expect, haveResource, not } from '@aws-cdk/assert'; +import { Duration, Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import apigw = require('../lib'); + +export = { + 'adds an OPTIONS method to a resource'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'allowCredentials'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + allowCredentials: true + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Allow-Credentials": "'true'" + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Allow-Credentials": true + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'allowMethods'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://aws.amazon.com'], + allowMethods: ['GET', 'PUT'] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'GET,PUT'", + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'allowMethods ANY will expand to all supported methods'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://aws.amazon.com'], + allowMethods: ['ANY'] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'allowMethods ANY cannot be used with any other method'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // THEN + test.throws(() => resource.addCorsPreflight({ + allowOrigins: ['https://aws.amazon.com'], + allowMethods: ['ANY', 'PUT'] + }), /ANY cannot be used with any other method. Received: ANY,PUT/); + + test.done(); + }, + + 'statusCode can be used to set the response status code'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://aws.amazon.com'], + statusCode: 200 + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + }, + StatusCode: "200" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + }, + StatusCode: "200" + } + ] + })); + test.done(); + }, + + 'allowOrigins must contain at least one origin'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + test.throws(() => resource.addCorsPreflight({ + allowOrigins: [] + }), /allowOrigins must contain at least one origin/); + + test.done(); + }, + + 'allowOrigins can be used to specify multiple origins'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://twitch.tv', 'https://amazon.com', 'https://aws.amazon.com'] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://twitch.tv'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + ResponseTemplates: { + "application/json": "#set($origin = $input.params(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params(\"origin\")) #end\n#if($origin.matches(\"https://amazon.com\") || $origin.matches(\"https://aws.amazon.com\"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end" + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'maxAge can be used to specify Access-Control-Max-Age'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + maxAge: Duration.minutes(60) + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Max-Age": `'${60 * 60}'` + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Max-Age": true + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'disableCache will set Max-Age to -1'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + disableCache: true + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Max-Age": `'-1'` + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Max-Age": true + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'maxAge and disableCache are mutually exclusive'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // THEN + test.throws(() => resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + disableCache: true, + maxAge: Duration.seconds(10) + }), /The options "maxAge" and "disableCache" are mutually exclusive/); + + test.done(); + }, + + 'exposeHeaders can be used to specify Access-Control-Expose-Headers'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + const resource = api.root.addResource('MyResource'); + + // WHEN + resource.addCorsPreflight({ + allowOrigins: ['https://amazon.com'], + exposeHeaders: [ 'Authorization', 'Foo' ] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Expose-Headers": "'Authorization,Foo'", + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Expose-Headers": true, + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'defaultCorsPreflightOptions can be used to specify CORS for all resource tree'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + + // WHEN + const resource = api.root.addResource('MyResource', { + defaultCorsPreflightOptions: { + allowOrigins: ['https://amazon.com'], + } + }); + resource.addResource('MyChildResource'); + + // THEN + expect(stack).to(countResources('AWS::ApiGateway::Method', 2)); // on both resources + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceD5CDB490' } + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apiMyResourceMyChildResource2DC010C5' } + })); + test.done(); + }, + + 'defaultCorsPreflightOptions can be specified at the API level to apply to all resources'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.RestApi(stack, 'api', { + defaultCorsPreflightOptions: { + allowOrigins: ['https://amazon.com'], + } + }); + + const child1 = api.root.addResource('child1'); + const child2 = child1.addResource('child2'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { "Fn::GetAtt": [ "apiC8550315", "RootResourceId" ] } + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apichild1841A5840' } + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { Ref: 'apichild1child26A9A7C47' } + })); + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index 187969f2b05c3..d4d67ff2d99c1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Stack, Duration } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import apigw = require('../lib'); @@ -328,474 +328,4 @@ export = { } }, - 'addCorsPreflight': { - - 'adds an OPTIONS method to a resource'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://amazon.com'] - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true - }, - "StatusCode": "204" - } - ] - })); - test.done(); - }, - - 'allowCredentials'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://amazon.com'], - allowCredentials: true - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", - "method.response.header.Access-Control-Allow-Credentials": "'true'" - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true, - "method.response.header.Access-Control-Allow-Credentials": true - }, - "StatusCode": "204" - } - ] - })); - test.done(); - }, - - 'allowMethods'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://aws.amazon.com'], - allowMethods: ['GET', 'PUT'] - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'GET,PUT'", - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true, - }, - "StatusCode": "204" - } - ] - })); - test.done(); - }, - - 'allowMethods ANY will expand to all supported methods'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://aws.amazon.com'], - allowMethods: ['ANY'] - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true, - }, - "StatusCode": "204" - } - ] - })); - test.done(); - }, - - 'allowMethods ANY cannot be used with any other method'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // THEN - test.throws(() => resource.addCorsPreflight({ - allowOrigins: ['https://aws.amazon.com'], - allowMethods: ['ANY', 'PUT'] - }), /ANY cannot be used with any other method. Received: ANY,PUT/); - - test.done(); - }, - - 'statusCode can be used to set the response status code'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://aws.amazon.com'], - statusCode: 200 - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", - }, - "StatusCode": "200" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true, - }, - "StatusCode": "200" - } - ] - })); - test.done(); - }, - - 'allowOrigins must contain at least one origin'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - test.throws(() => resource.addCorsPreflight({ - allowOrigins: [] - }), /allowOrigins must contain at least one origin/); - - test.done(); - }, - - 'allowOrigins can be used to specify multiple origins'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://twitch.tv', 'https://amazon.com', 'https://aws.amazon.com'] - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://twitch.tv'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" - }, - "ResponseTemplates": { - "application/json": "#set($origin = $input.params(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params(\"origin\")) #end\n#if($origin.matches(\"https://amazon.com\") || $origin.matches(\"https://aws.amazon.com\"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end" - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true - }, - "StatusCode": "204" - } - ] - })); - test.done(); - }, - - 'maxAge can be used to specify Access-Control-Max-Age'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://amazon.com'], - maxAge: Duration.minutes(60) - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", - "method.response.header.Access-Control-Max-Age": `'${60 * 60}'` - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true, - "method.response.header.Access-Control-Max-Age": true - }, - "StatusCode": "204" - } - ] - })); - test.done(); - }, - - 'disableCache will set Max-Age to -1'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://amazon.com'], - disableCache: true - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", - "method.response.header.Access-Control-Max-Age": `'-1'` - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true, - "method.response.header.Access-Control-Max-Age": true - }, - "StatusCode": "204" - } - ] - })); - test.done(); - }, - - 'maxAge and disableCache are mutually exclusive'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // THEN - test.throws(() => resource.addCorsPreflight({ - allowOrigins: ['https://amazon.com'], - disableCache: true, - maxAge: Duration.seconds(10) - }), /The options "maxAge" and "disableCache" are mutually exclusive/); - - test.done(); - }, - - 'exposeHeaders can be used to specify Access-Control-Expose-Headers'(test: Test) { - // GIVEN - const stack = new Stack(); - const api = new apigw.RestApi(stack, 'api'); - const resource = api.root.addResource('MyResource'); - - // WHEN - resource.addCorsPreflight({ - allowOrigins: ['https://amazon.com'], - exposeHeaders: [ 'Authorization', 'Foo' ] - }); - - // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { Ref: 'apiMyResourceD5CDB490' }, - Integration: { - "IntegrationResponses": [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", - "method.response.header.Access-Control-Expose-Headers": "'Authorization,Foo'", - }, - "StatusCode": "204" - } - ], - "RequestTemplates": { - "application/json": "{ statusCode: 200 }" - }, - "Type": "MOCK" - }, - MethodResponses: [ - { - "ResponseParameters": { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Origin": true, - "method.response.header.Access-Control-Allow-Methods": true, - "method.response.header.Access-Control-Expose-Headers": true, - }, - "StatusCode": "204" - } - ] - })); - test.done(); - } - } }; From ea69fa1e550f1c1bf508148b5e361384b531eb7d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 10:52:28 +0300 Subject: [PATCH 14/19] README --- packages/@aws-cdk/aws-apigateway/README.md | 52 ++++++++++++++++++++ packages/@aws-cdk/aws-apigateway/lib/cors.ts | 2 + 2 files changed, 54 insertions(+) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index f6636896ea57d..896af3a88074f 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -466,6 +466,58 @@ new route53.ARecord(this, 'CustomDomainAliasRecord', { }); ``` +### Cross Origin Resource Sharing (CORS) + +[Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) is a mechanism +that uses additional HTTP headers to tell browsers to give a web application +running at one origin, access to selected resources from a different origin. A +web application executes a cross-origin HTTP request when it requests a resource +that has a different origin (domain, protocol, or port) from its own. + +You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS HTTP method to any API resource via the `addCorsPreflight` method. + +The following example will add an OPTIONS method to the `myResource` API resource, which +only allows GET and PUT HTTP requests from the origin https://amazon.com. + +```ts +myResource.addCorsPreflight({ + allowOrigins: [ 'https://amazon.com' ], + allowMethods: [ 'GET', 'PUT' ] +}); +``` + +See the +[`CorsOptions`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.CorsOptions.html) +API reference for a detailed list of supported configuration options. + +You can specify default options for all resources within an API or a sub-tree using +`defaultCorsPreflightOptions`: + + +```ts +new apigateway.RestApi(this, 'api', { + defaultCorsPreflightOptions: { + allowOrigins: [ 'https://amazon.com' ] + } +}); +``` + +This means that the construct will add a CORS preflight OPTIONS method to +**all** HTTP resources in this API gateway. + +Similarly, you can specify this at the resource level: + +```ts +const subtree = resource.addResource('subtree', { + defaultCorsPreflightOptions: { + allowOrigins: [ 'https://amazon.com' ] + } +}); +``` + +This means that all resources under `subtree` (inclusive) will have a preflight +OPTIONS added to them. + ---- This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/@aws-cdk/aws-apigateway/lib/cors.ts b/packages/@aws-cdk/aws-apigateway/lib/cors.ts index 2590785551980..da448207e1f8f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/cors.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/cors.ts @@ -33,6 +33,8 @@ export interface CorsOptions { * The Access-Control-Allow-Methods response header specifies the method or * methods allowed when accessing the resource in response to a preflight request. * + * If `ANY` is specified, it will be expanded to `Cors.ALL_METHODS`. + * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods * @default Cors.ALL_METHODS */ From 85a5434df8168708c15abae288c7715ec69c44f6 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 10:54:48 +0300 Subject: [PATCH 15/19] add missing features link --- packages/@aws-cdk/aws-apigateway/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 896af3a88074f..1d974c306963b 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -518,6 +518,9 @@ const subtree = resource.addResource('subtree', { This means that all resources under `subtree` (inclusive) will have a preflight OPTIONS added to them. +See [#906](https://github.com/aws/aws-cdk/issues/906) for a list of CORS +features which are not yet supported. + ---- This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. From 17908fbc5c4afb14c0646070408763c256811027 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 10:59:55 +0300 Subject: [PATCH 16/19] update integ test expectation --- .../test/integ.cors.expected.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json index 86e89ec1739ac..ab0fa92e17909 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -526,7 +526,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "handlerCodeS3Bucket9583A349" + "Ref": "AssetParameters90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4S3Bucket9DD8E176" }, "S3Key": { "Fn::Join": [ @@ -539,7 +539,7 @@ "Fn::Split": [ "||", { - "Ref": "handlerCodeS3VersionKey5F6149CC" + "Ref": "AssetParameters90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4S3VersionKey0275D4BE" } ] } @@ -552,7 +552,7 @@ "Fn::Split": [ "||", { - "Ref": "handlerCodeS3VersionKey5F6149CC" + "Ref": "AssetParameters90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4S3VersionKey0275D4BE" } ] } @@ -605,17 +605,17 @@ } }, "Parameters": { - "handlerCodeS3Bucket9583A349": { + "AssetParameters90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4S3Bucket9DD8E176": { "Type": "String", - "Description": "S3 bucket for asset \"cors-twitch-test/handler/Code\"" + "Description": "S3 bucket for asset \"90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4\"" }, - "handlerCodeS3VersionKey5F6149CC": { + "AssetParameters90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4S3VersionKey0275D4BE": { "Type": "String", - "Description": "S3 key for asset version \"cors-twitch-test/handler/Code\"" + "Description": "S3 key for asset version \"90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4\"" }, - "handlerCodeArtifactHashA96ADEB4": { + "AssetParameters90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4ArtifactHash1D8265F7": { "Type": "String", - "Description": "Artifact hash for asset \"cors-twitch-test/handler/Code\"" + "Description": "Artifact hash for asset \"90f989e3ccf22eb90814665d7725476b19c790c4f37045ad8d6fbe1dbde112f4\"" } } } \ No newline at end of file From 32553850ac607694ef4fa935e5bfa7a4d1abad47 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 11:01:05 +0300 Subject: [PATCH 17/19] fix compilation error --- packages/@aws-cdk/aws-apigateway/test/test.cors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/test.cors.ts b/packages/@aws-cdk/aws-apigateway/test/test.cors.ts index b5b591f2cb1e2..3a0e36f330fe3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.cors.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.cors.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource, not } from '@aws-cdk/assert'; +import { countResources, expect, haveResource } from '@aws-cdk/assert'; import { Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import apigw = require('../lib'); @@ -510,7 +510,7 @@ export = { }); const child1 = api.root.addResource('child1'); - const child2 = child1.addResource('child2'); + child1.addResource('child2'); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { From e27538ad8a753a7629f70b02fa231b28b074a5ba Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 11:24:36 +0300 Subject: [PATCH 18/19] respond with Vary if specific origin is specified --- .../@aws-cdk/aws-apigateway/lib/resource.ts | 13 +- .../@aws-cdk/aws-apigateway/test/test.cors.ts | 121 +++++++++++++++++- 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 47b56130a371b..53fccfcab8941 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -193,9 +193,20 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc throw new Error('allowOrigins must contain at least one origin'); } + if (options.allowOrigins.includes('*') && options.allowOrigins.length > 1) { + throw new Error(`Invalid "allowOrigins" - cannot mix "*" with specific origins: ${options.allowOrigins.join(',')}`); + } + // we use the first origin here and if there are more origins in the list, we // will match against them in the response velocity template - headers['Access-Control-Allow-Origin'] = `'${options.allowOrigins[0]}'`; + const initialOrigin = options.allowOrigins[0]; + headers['Access-Control-Allow-Origin'] = `'${initialOrigin}'`; + + // the "Vary" header is required if we allow a specific origin + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#CORS_and_caching + if (initialOrigin !== '*') { + headers.Vary = `'Origin'`; + } // // Access-Control-Allow-Methods diff --git a/packages/@aws-cdk/aws-apigateway/test/test.cors.ts b/packages/@aws-cdk/aws-apigateway/test/test.cors.ts index 3a0e36f330fe3..28c67b9780d32 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.cors.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.cors.ts @@ -25,7 +25,8 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", - "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + "method.response.header.Vary": "'Origin'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", }, StatusCode: "204" } @@ -40,6 +41,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true }, StatusCode: "204" @@ -71,6 +73,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", "method.response.header.Access-Control-Allow-Credentials": "'true'" }, @@ -87,6 +90,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true, "method.response.header.Access-Control-Allow-Credentials": true }, @@ -119,6 +123,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'GET,PUT'", }, StatusCode: "204" @@ -134,6 +139,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true, }, StatusCode: "204" @@ -165,6 +171,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", }, StatusCode: "204" @@ -180,6 +187,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true, }, StatusCode: "204" @@ -226,6 +234,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://aws.amazon.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", }, StatusCode: "200" @@ -241,6 +250,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true, }, StatusCode: "200" @@ -285,6 +295,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://twitch.tv'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" }, ResponseTemplates: { @@ -303,6 +314,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true }, StatusCode: "204" @@ -334,6 +346,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", "method.response.header.Access-Control-Max-Age": `'${60 * 60}'` }, @@ -350,6 +363,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true, "method.response.header.Access-Control-Max-Age": true }, @@ -382,6 +396,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", "method.response.header.Access-Control-Max-Age": `'-1'` }, @@ -398,6 +413,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true, "method.response.header.Access-Control-Max-Age": true }, @@ -446,6 +462,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://amazon.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", "method.response.header.Access-Control-Expose-Headers": "'Authorization,Foo'", }, @@ -462,6 +479,7 @@ export = { ResponseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true, "method.response.header.Access-Control-Expose-Headers": true, }, @@ -527,4 +545,105 @@ export = { })); test.done(); }, + + 'Vary: Origin is sent back if Allow-Origin is not "*"'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + + // WHEN + api.root.addResource('AllowAll', { + defaultCorsPreflightOptions: { + allowOrigins: [ '*' ] + } + }); + + api.root.addResource('AllowSpecific', { + defaultCorsPreflightOptions: { + allowOrigins: [ 'http://specific.com' ] + } + }); + + // THENB + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + ResourceId: { + Ref: "apiAllowAll2F5BC564" + }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + StatusCode: "204" + } + ] + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + ResourceId: { + Ref: "apiAllowSpecific77DD8AF1" + }, + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'http://specific.com'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Vary": "'Origin'" + }, + StatusCode: "204" + } + ], + RequestTemplates: { + "application/json": "{ statusCode: 200 }" + }, + Type: "MOCK" + }, + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Vary": true + }, + StatusCode: "204" + } + ] + })); + test.done(); + }, + + 'If "*" is specified in allow-origin, it cannot be mixed with specific origins'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + + // WHEN + test.throws(() => api.root.addResource('AllowAll', { + defaultCorsPreflightOptions: { + allowOrigins: [ 'https://bla.com', '*', 'https://specific' ] + } + }), /Invalid "allowOrigins" - cannot mix "\*" with specific origins: https:\/\/bla\.com,\*,https:\/\/specific/); + + test.done(); + } }; \ No newline at end of file From f80755d9eff4052ef31f46cae4ac89f267f37fc8 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 11:26:56 +0300 Subject: [PATCH 19/19] update expectation --- .../@aws-cdk/aws-apigateway/test/integ.cors.expected.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json index ab0fa92e17909..0c78af230bd90 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -6,7 +6,7 @@ "Name": "cors-api-test" } }, - "corsapitestDeployment2BF1633Ab835c2bfdc07db9c80c58bb34e202728": { + "corsapitestDeployment2BF1633Af56aad239353437f465d2090f2edab0c": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -29,7 +29,7 @@ "Ref": "corsapitest8682546E" }, "DeploymentId": { - "Ref": "corsapitestDeployment2BF1633Ab835c2bfdc07db9c80c58bb34e202728" + "Ref": "corsapitestDeployment2BF1633Af56aad239353437f465d2090f2edab0c" }, "StageName": "prod" } @@ -465,6 +465,7 @@ "ResponseParameters": { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'https://google.com'", + "method.response.header.Vary": "'Origin'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" }, "ResponseTemplates": { @@ -483,6 +484,7 @@ "ResponseParameters": { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Vary": true, "method.response.header.Access-Control-Allow-Methods": true }, "StatusCode": "204"