From 3cd4b52727f756f88dd92d95bad3c7ccc2ce465c Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 9 Dec 2020 17:02:26 -0800 Subject: [PATCH 1/3] Add CORS for API Gateway HTTP APIs --- .../apigateway/openapi/AddIntegrations.java | 10 + .../openapi/ApiGatewayExtension.java | 5 +- .../aws/apigateway/openapi/CorsHeader.java | 14 +- .../openapi/CorsHttpIntegration.java | 238 ++++++++++++++++++ .../openapi/CorsHttpIntegrationTest.java | 90 +++++++ .../http-api-cors-wildcards.openapi.json | 226 +++++++++++++++++ .../openapi/http-api-cors.openapi.json | 234 +++++++++++++++++ .../smithy/openapi/fromsmithy/Context.java | 50 ++++ 8 files changed, 853 insertions(+), 14 deletions(-) create mode 100644 smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegration.java create mode 100644 smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegrationTest.java create mode 100644 smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json create mode 100644 smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java index 77a63fbc64b..e680f81b535 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java @@ -89,6 +89,16 @@ private ObjectNode createIntegration( Trait integration ) { ObjectNode integrationObject = getIntegrationAsObject(context, shape, integration); + + ApiGatewayConfig.ApiType apiType = context.getConfig() + .getExtensions(ApiGatewayConfig.class) + .getApiGatewayType(); + + // TODO: Refactor this so it's self contained in a REST API CORS plugin. + if (apiType != ApiGatewayConfig.ApiType.REST) { + return integrationObject; + } + return context.getService().getTrait(CorsTrait.class) .map(cors -> { LOGGER.fine(() -> String.format("Adding CORS to `%s` operation responses", shape.getId())); diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java index cd0bc2f9679..15c706ccf2c 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java @@ -36,7 +36,10 @@ public List getOpenApiMappers() { ApiGatewayMapper.wrap(new CloudFormationSubstitution()), ApiGatewayMapper.wrap(new AddCorsResponseHeaders()), ApiGatewayMapper.wrap(new AddCorsPreflightIntegration()), - ApiGatewayMapper.wrap(new AddCorsToGatewayResponses()) + ApiGatewayMapper.wrap(new AddCorsToGatewayResponses()), + + // HTTP API mappers. + ApiGatewayMapper.wrap(new CorsHttpIntegration()) ); } diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java index b87de285ca9..e89efb5308c 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java @@ -21,7 +21,6 @@ import software.amazon.smithy.model.traits.CorsTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.openapi.fromsmithy.Context; -import software.amazon.smithy.openapi.fromsmithy.SecuritySchemeConverter; import software.amazon.smithy.openapi.model.OperationObject; import software.amazon.smithy.openapi.model.ResponseObject; @@ -56,10 +55,7 @@ static Set deduceOperationResponseHeaders( // and any headers explicitly modeled on the operation. Set result = new TreeSet<>(cors.getAdditionalExposedHeaders()); result.addAll(context.getOpenApiProtocol().getProtocolResponseHeaders(context, shape)); - - for (SecuritySchemeConverter converter : context.getSecuritySchemeConverters()) { - result.addAll(getSecuritySchemeResponseHeaders(context, converter)); - } + result.addAll(context.getAllSecuritySchemeResponseHeaders()); // Include all headers found in the generated OpenAPI response. for (ResponseObject responseObject : operationObject.getResponses().values()) { @@ -68,12 +64,4 @@ static Set deduceOperationResponseHeaders( return result; } - - private static Set getSecuritySchemeResponseHeaders( - Context context, - SecuritySchemeConverter converter - ) { - T t = context.getService().expectTrait(converter.getAuthSchemeType()); - return converter.getAuthResponseHeaders(context, t); - } } diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegration.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegration.java new file mode 100644 index 00000000000..b8056c7162b --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegration.java @@ -0,0 +1,238 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.apigateway.openapi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Logger; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.traits.CorsTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.model.OpenApi; +import software.amazon.smithy.openapi.model.OperationObject; +import software.amazon.smithy.openapi.model.ParameterObject; +import software.amazon.smithy.openapi.model.PathItem; +import software.amazon.smithy.openapi.model.Ref; +import software.amazon.smithy.openapi.model.ResponseObject; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SetUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds support for the API Gateway {@code x-amazon-apigateway-cors} + * extension for API Gateway HTTP APIs using values from the + * Smithy {@code cors} trait. + * + *
    + *
  • {@code allowOrigins} is populated based on the {@code origin} + * property of the {@code cors} trait.
  • + *
  • {@code maxAge} is populated based on the {@code maxAge} + * property of the {@code cors} trait.
  • + *
  • {@code allowMethods} is populated by scanning the generated + * OpenAPI definition for every defined method.
  • + *
  • {@code exposedHeaders} is set to "*" to expose all headers IFF + * the service does not use HTTP credentials, and no value is provided + * to the {@code additionalExposedHeaders} property of the Smithy + * {@code cors} trait. Otherwise, this value is populated by finding + * all of the response headers used by the protocol, modeled in the + * service, and used by auth schemes.
  • + *
  • {@code allowedHeaders} is set to "*" to allow all headers IFF + * the service does not use HTTP credentials, and no value is provided + * to the {@code additionalAllowedHeaders} property of the Smithy + * {@code cors} trait. Otherwise, this value is populated by finding + * all of the request headers used by the protocol, modeled in the + * service, and used by auth schemes.
  • + *
  • {@code allowCredentials} is set to true if any of the + * auth schemes used in the API use HTTP credentials according + * to {@link Context#usesHttpCredentials()}.
  • + *
+ * + * @see API Gateway documentation + */ +@SmithyInternalApi +public final class CorsHttpIntegration implements ApiGatewayMapper { + + private static final Logger LOGGER = Logger.getLogger(CorsHttpIntegration.class.getName()); + private static final String CORS_HTTP_EXTENSION = "x-amazon-apigateway-cors"; + + @Override + public List getApiTypes() { + return ListUtils.of(ApiGatewayConfig.ApiType.HTTP); + } + + @Override + public OpenApi after(Context context, OpenApi openapi) { + return context.getService().getTrait(CorsTrait.class) + .map(corsTrait -> addCors(context, openapi, corsTrait)) + .orElse(openapi); + } + + private OpenApi addCors(Context context, OpenApi openapi, CorsTrait trait) { + // Use any existing x-amazon-apigateway-cors value, if present. + Node alreadySetCorsValue = openapi.getExtension(CORS_HTTP_EXTENSION) + .flatMap(Node::asObjectNode) + .orElse(null); + + if (alreadySetCorsValue != null) { + return openapi; + } + + Set allowedMethodsInService = getMethodsUsedInApi(context, openapi); + Set allowedRequestHeaders = getAllowedHeaders(context, trait, openapi); + Set exposedHeaders = getExposedHeaders(context, trait, openapi); + + ObjectNode.Builder corsObjectBuilder = Node.objectNodeBuilder() + .withMember("allowOrigins", Node.fromStrings(trait.getOrigin())) + .withMember("maxAge", trait.getMaxAge()) + .withMember("allowMethods", Node.fromStrings(allowedMethodsInService)) + .withMember("exposeHeaders", Node.fromStrings(exposedHeaders)) + .withMember("allowHeaders", Node.fromStrings(allowedRequestHeaders)); + + if (context.usesHttpCredentials()) { + corsObjectBuilder.withMember("allowCredentials", true); + } + + return openapi.toBuilder() + .putExtension(CORS_HTTP_EXTENSION, corsObjectBuilder.build()) + .build(); + } + + private Set getMethodsUsedInApi(Context context, OpenApi openApi) { + Set methods = new TreeSet<>(); + + if (!context.usesHttpCredentials()) { + LOGGER.info("Using * for Access-Control-Allow-Methods because the service does not use HTTP credentials"); + return SetUtils.of("*"); + } + + LOGGER.info("Generating a value for Access-Control-Allow-Methods because the service uses HTTP credentials"); + for (PathItem pathItem : openApi.getPaths().values()) { + for (String method : pathItem.getOperations().keySet()) { + // No need to call out OPTIONS as supported. + if (!method.equalsIgnoreCase("OPTIONS")) { + methods.add(method.toUpperCase(Locale.ENGLISH)); + } + } + } + + return methods; + } + + private Set getAllowedHeaders(Context context, CorsTrait corsTrait, OpenApi openApi) { + Set headers = new TreeSet<>(corsTrait.getAdditionalAllowedHeaders()); + + // If no additionalAllowedHeaders are set on the trait and the + // service doesn't use HTTP credentials, then the simplest way + // to ensure that every header is allowed is using "*", which + // allows all headers. This can't be used when HTTP credentials are + // used since "*" then becomes a literal "*". + if (headers.isEmpty() && !context.usesHttpCredentials()) { + LOGGER.info("Using * for Access-Control-Allow-Headers because the service does not use HTTP credentials"); + return SetUtils.of("*"); + } + + LOGGER.info("Generating a value for Access-Control-Allow-Headers because the service uses HTTP credentials"); + + // Note: not all headers used in a service are defined in the OpenAPI model. + // That's generally true for any service, but that assumption is made here + // too because security scheme and protocol headers are not defined on operations. + + // Allow request headers needed by security schemes. + headers.addAll(context.getAllSecuritySchemeRequestHeaders()); + + // Allow any protocol-specific request headers for each operation. + TopDownIndex topDownIndex = TopDownIndex.of(context.getModel()); + for (OperationShape operation : topDownIndex.getContainedOperations(context.getService())) { + headers.addAll(context.getOpenApiProtocol().getProtocolRequestHeaders(context, operation)); + } + + // Allow all of the headers that were added to the generated OpenAPI definition. + for (PathItem item : openApi.getPaths().values()) { + headers.addAll(getHeadersFromParameterRefs(openApi, item.getParameters())); + for (OperationObject operationObject : item.getOperations().values()) { + headers.addAll(getHeadersFromParameters(operationObject.getParameters())); + } + } + + return headers; + } + + private Set getExposedHeaders(Context context, CorsTrait corsTrait, OpenApi openApi) { + Set headers = new TreeSet<>(corsTrait.getAdditionalExposedHeaders()); + + // If not additionalExposedHeaders are set on the trait and the + // service doesn't use HTTP credentials, then the simplest way + // to ensure that every header is exposed is using "*", which + // exposes all headers. This can't be used when HTTP credentials are + // used since "*" then becomes a literal "*". + if (headers.isEmpty() && !context.usesHttpCredentials()) { + LOGGER.info("Using * for Access-Control-Expose-Headers because the service does not use HTTP credentials"); + return SetUtils.of("*"); + } + + LOGGER.info("Generating a value for Access-Control-Expose-Headers because the service uses HTTP credentials"); + + // Note: not all headers used in a service are defined in the OpenAPI model. + // That's generally true for any service, but that assumption is made here + // too because security scheme and protocol headers are not defined on operations. + + // Expose response headers populated by security schemes. + headers.addAll(context.getAllSecuritySchemeResponseHeaders()); + + // Expose any protocol-specific response headers for each operation. + TopDownIndex topDownIndex = TopDownIndex.of(context.getModel()); + for (OperationShape operation : topDownIndex.getContainedOperations(context.getService())) { + headers.addAll(context.getOpenApiProtocol().getProtocolResponseHeaders(context, operation)); + } + + // Expose all of the headers that were added to the generated OpenAPI definition. + for (PathItem item : openApi.getPaths().values()) { + for (OperationObject operationObject : item.getOperations().values()) { + for (ResponseObject responseObject : operationObject.getResponses().values()) { + headers.addAll(responseObject.getHeaders().keySet()); + } + } + } + + return headers; + } + + private Set getHeadersFromParameterRefs(OpenApi openApi, Collection> params) { + Collection resolved = new ArrayList<>(); + for (Ref ref : params) { + resolved.add(ref.deref(openApi.getComponents())); + } + return getHeadersFromParameters(resolved); + } + + private Set getHeadersFromParameters(Collection params) { + Set result = new TreeSet<>(); + for (ParameterObject param : params) { + if (param.getIn().filter(in -> in.equals("header")).isPresent()) { + param.getName().ifPresent(result::add); + } + } + return result; + } +} diff --git a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegrationTest.java b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegrationTest.java new file mode 100644 index 00000000000..fabe2b6fc07 --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CorsHttpIntegrationTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.apigateway.openapi; + +import java.util.Collections; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.CorsTrait; +import software.amazon.smithy.model.transform.ModelTransformer; +import software.amazon.smithy.openapi.OpenApiConfig; +import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; +import software.amazon.smithy.utils.IoUtils; + +public class CorsHttpIntegrationTest { + @Test + public void generatesCorsForHttpApis() { + Model model = Model.assembler(getClass().getClassLoader()) + .discoverModels(getClass().getClassLoader()) + .addImport(getClass().getResource("cors-model.json")) + .assemble() + .unwrap(); + + OpenApiConfig config = new OpenApiConfig(); + ApiGatewayConfig apiGatewayConfig = new ApiGatewayConfig(); + apiGatewayConfig.setApiGatewayType(ApiGatewayConfig.ApiType.HTTP); + config.putExtensions(apiGatewayConfig); + config.setService(ShapeId.from("example.smithy#MyService")); + + ObjectNode result = OpenApiConverter.create().config(config).convertToNode(model); + Node expectedNode = Node.parse(IoUtils.toUtf8String( + getClass().getResourceAsStream("http-api-cors.openapi.json"))); + + Node.assertEquals(result, expectedNode); + } + + @Test + public void generatesCorsForHttpApisWithNoExplicitValues() { + // This test replaces the trait found in http-api-cors.openapi.json so + // that no explicit allowed and exposed headers are provided, causing + // the conversion to "*" for the corresponding CORS headers rather + // than needing to enumerate them all. + Model model = Model.assembler(getClass().getClassLoader()) + .discoverModels(getClass().getClassLoader()) + .addImport(getClass().getResource("cors-model.json")) + .assemble() + .unwrap(); + + ModelTransformer transformer = ModelTransformer.create(); + model = transformer.mapShapes(model, shape -> { + return shape.getTrait(CorsTrait.class) + .map(cors -> { + cors = cors.toBuilder() + .additionalAllowedHeaders(Collections.emptySet()) + .additionalExposedHeaders(Collections.emptySet()) + .build(); + return Shape.shapeToBuilder(shape).addTrait(cors).build(); + }) + .orElse(shape); + }); + + OpenApiConfig config = new OpenApiConfig(); + ApiGatewayConfig apiGatewayConfig = new ApiGatewayConfig(); + apiGatewayConfig.setApiGatewayType(ApiGatewayConfig.ApiType.HTTP); + config.putExtensions(apiGatewayConfig); + config.setService(ShapeId.from("example.smithy#MyService")); + + ObjectNode result = OpenApiConverter.create().config(config).convertToNode(model); + Node expectedNode = Node.parse(IoUtils.toUtf8String( + getClass().getResourceAsStream("http-api-cors-wildcards.openapi.json"))); + + Node.assertEquals(result, expectedNode); + } +} diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json new file mode 100644 index 00000000000..fad80e9f18c --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json @@ -0,0 +1,226 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "MyService", + "version": "2006-03-01" + }, + "paths": { + "/payload": { + "get": { + "operationId": "ListPayloads", + "responses": { + "200": { + "description": "ListPayloads 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListPayloadsResponseContent" + } + } + } + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServiceListPayloadsLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceListPayloads/invocations" + } + } + }, + "/payload/{id}": { + "delete": { + "operationId": "DeletePayload", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeletePayload response" + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServiceDeletePayloadLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceDeletePayload/invocations" + } + }, + "get": { + "operationId": "GetPayload", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetPayload 200 response", + "headers": { + "X-Foo-Header": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/GetPayloadOutputPayload" + } + } + } + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServiceGetPayloadLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceGetPayload/invocations" + } + }, + "put": { + "operationId": "PutPayload", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/PutPayloadInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "number", + "format": "int32", + "nullable": true + } + }, + { + "name": "X-EnumString", + "in": "header", + "schema": { + "$ref": "#/components/schemas/EnumString" + } + }, + { + "name": "X-Foo-Header", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "PutPayload response" + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServicePutPayloadLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServicePutPayload/invocations" + } + } + } + }, + "components": { + "schemas": { + "EnumString": { + "type": "string", + "enum": [ + "a", + "c" + ] + }, + "GetPayloadOutputPayload": { + "type": "string", + "format": "byte" + }, + "ListPayloadsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PayloadDescription" + } + } + } + }, + "PayloadDescription": { + "type": "object", + "properties": { + "createdAt": { + "type": "number" + }, + "id": { + "type": "string" + } + }, + "required": [ + "createdAt", + "id" + ] + }, + "PutPayloadInputPayload": { + "type": "string", + "format": "byte" + } + }, + "securitySchemes": { + "aws.auth.sigv4": { + "type": "apiKey", + "description": "AWS Signature Version 4 authentication", + "name": "Authorization", + "in": "header", + "x-amazon-apigateway-authtype": "awsSigv4" + } + } + }, + "security": [ + { + "aws.auth.sigv4": [] + } + ], + "x-amazon-apigateway-cors": { + "allowOrigins": [ + "https://www.example.com" + ], + "maxAge": 86400, + "allowMethods": [ + "*" + ], + "exposeHeaders": [ + "*" + ], + "allowHeaders": [ + "*" + ] + } +} diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json new file mode 100644 index 00000000000..39af93267bb --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json @@ -0,0 +1,234 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "MyService", + "version": "2006-03-01" + }, + "paths": { + "/payload": { + "get": { + "operationId": "ListPayloads", + "responses": { + "200": { + "description": "ListPayloads 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListPayloadsResponseContent" + } + } + } + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServiceListPayloadsLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceListPayloads/invocations" + } + } + }, + "/payload/{id}": { + "delete": { + "operationId": "DeletePayload", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeletePayload response" + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServiceDeletePayloadLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceDeletePayload/invocations" + } + }, + "get": { + "operationId": "GetPayload", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetPayload 200 response", + "headers": { + "X-Foo-Header": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/GetPayloadOutputPayload" + } + } + } + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServiceGetPayloadLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceGetPayload/invocations" + } + }, + "put": { + "operationId": "PutPayload", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/PutPayloadInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "number", + "format": "int32", + "nullable": true + } + }, + { + "name": "X-EnumString", + "in": "header", + "schema": { + "$ref": "#/components/schemas/EnumString" + } + }, + { + "name": "X-Foo-Header", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "PutPayload response" + } + }, + "x-amazon-apigateway-integration": { + "credentials": "arn:aws:iam::123456789012:role/MyServicePutPayloadLambdaRole", + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServicePutPayload/invocations" + } + } + } + }, + "components": { + "schemas": { + "EnumString": { + "type": "string", + "enum": [ + "a", + "c" + ] + }, + "GetPayloadOutputPayload": { + "type": "string", + "format": "byte" + }, + "ListPayloadsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PayloadDescription" + } + } + } + }, + "PayloadDescription": { + "type": "object", + "properties": { + "createdAt": { + "type": "number" + }, + "id": { + "type": "string" + } + }, + "required": [ + "createdAt", + "id" + ] + }, + "PutPayloadInputPayload": { + "type": "string", + "format": "byte" + } + }, + "securitySchemes": { + "aws.auth.sigv4": { + "type": "apiKey", + "description": "AWS Signature Version 4 authentication", + "name": "Authorization", + "in": "header", + "x-amazon-apigateway-authtype": "awsSigv4" + } + } + }, + "security": [ + { + "aws.auth.sigv4": [] + } + ], + "x-amazon-apigateway-cors": { + "allowOrigins": [ + "https://www.example.com" + ], + "maxAge": 86400, + "allowMethods": [ + "*" + ], + "exposeHeaders": [ + "X-Foo-Header", + "X-Service-Output-Metadata" + ], + "allowHeaders": [ + "Authorization", + "Date", + "X-Amz-Date", + "X-Amz-Security-Token", + "X-Amz-Target", + "X-EnumString", + "X-Foo-Header", + "X-Service-Input-Metadata" + ] + } +} diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java index e16c6f42b22..9c8e17677a3 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java @@ -19,7 +19,9 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; import software.amazon.smithy.jsonschema.JsonSchemaConverter; import software.amazon.smithy.jsonschema.Schema; import software.amazon.smithy.jsonschema.SchemaDocument; @@ -194,6 +196,54 @@ public boolean usesHttpCredentials() { return getSecuritySchemeConverters().stream().anyMatch(SecuritySchemeConverter::usesHttpCredentials); } + /** + * Gets an alphabetically sorted set of request headers used by every + * security scheme associated with the API. + * + *

This is useful when integrating with things like CORS.

+ * + * @return Returns the set of every request header used by every security scheme. + */ + public Set getAllSecuritySchemeRequestHeaders() { + Set headers = new TreeSet<>(); + for (SecuritySchemeConverter converter : getSecuritySchemeConverters()) { + headers.addAll(getSecuritySchemeRequestHeaders(this, converter)); + } + return headers; + } + + /** + * Gets an alphabetically sorted set of response headers used by every + * security scheme associated with the API. + * + *

This is useful when integrating with things like CORS.

+ * + * @return Returns the set of every response header used by every security scheme. + */ + public Set getAllSecuritySchemeResponseHeaders() { + Set headers = new TreeSet<>(); + for (SecuritySchemeConverter converter : getSecuritySchemeConverters()) { + headers.addAll(getSecuritySchemeResponseHeaders(this, converter)); + } + return headers; + } + + private static Set getSecuritySchemeRequestHeaders( + Context context, + SecuritySchemeConverter converter + ) { + T t = context.getService().expectTrait(converter.getAuthSchemeType()); + return converter.getAuthRequestHeaders(context, t); + } + + private static Set getSecuritySchemeResponseHeaders( + Context context, + SecuritySchemeConverter converter + ) { + T t = context.getService().expectTrait(converter.getAuthSchemeType()); + return converter.getAuthResponseHeaders(context, t); + } + /** * Gets all of the synthesized schemas that needed to be created while * generating the OpenAPI artifact. From 694900ad3430c5ebebf60de16c9d21e23d19bb9d Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 11 Dec 2020 10:40:22 -0800 Subject: [PATCH 2/3] Move REST API CORS support to own mapper The support for adding CORS REST API headers is now in its own mapper class to better distinguish between the AddIntegrations mapper that is applied to every type of API Gateway service, and our support for CORS in REST APIs which is different than CORS in HTTP APIs. --- .../openapi/AddCorsToRestIntegrations.java | 169 ++++++++++++++++++ .../apigateway/openapi/AddIntegrations.java | 124 +------------ .../openapi/ApiGatewayExtension.java | 8 +- 3 files changed, 178 insertions(+), 123 deletions(-) create mode 100644 smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsToRestIntegrations.java diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsToRestIntegrations.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsToRestIntegrations.java new file mode 100644 index 00000000000..417186405db --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsToRestIntegrations.java @@ -0,0 +1,169 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.apigateway.openapi; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Logger; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.traits.CorsTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.model.OperationObject; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.Pair; + +/** + * Adds CORS headers to REST integrations. + * + *

If the service is a REST API that has the {@link CorsTrait}, then + * integration responses will include a statically computed + * Access-Control-Expose-Headers CORS headers that contains every header + * exposed by the integration, and Access-Control-Allow-Credentials header + * if the operation uses a security scheme that needs it, and and + * Access-Control-Allow-Origin header that is the result of + * {@link CorsTrait#getOrigin()}. + */ +final class AddCorsToRestIntegrations implements ApiGatewayMapper { + + private static final Logger LOGGER = Logger.getLogger(AddCorsToRestIntegrations.class.getName()); + private static final String RESPONSES_KEY = "responses"; + private static final String HEADER_PREFIX = "method.response.header."; + private static final String DEFAULT_KEY = "default"; + private static final String STATUS_CODE_KEY = "statusCode"; + private static final String RESPONSE_PARAMETERS_KEY = "responseParameters"; + + @Override + public List getApiTypes() { + return ListUtils.of(ApiGatewayConfig.ApiType.REST); + } + + @Override + public OperationObject updateOperation( + Context context, + OperationShape shape, + OperationObject operationObject, + String httpMethod, + String path + ) { + CorsTrait cors = context.getService().getTrait(CorsTrait.class).orElse(null); + + if (cors == null) { + return operationObject; + } + + return operationObject.getExtension(AddIntegrations.INTEGRATION_EXTENSION_NAME) + .flatMap(Node::asObjectNode) + .map(integrationObject -> updateOperation(context, shape, operationObject, cors, integrationObject)) + .orElse(operationObject); + } + + private OperationObject updateOperation( + Context context, + OperationShape shape, + OperationObject operationObject, + CorsTrait cors, + ObjectNode integrationObject + ) { + ObjectNode updated = updateIntegrationWithCors( + context, operationObject, shape, integrationObject, cors); + + return operationObject.toBuilder() + .putExtension(AddIntegrations.INTEGRATION_EXTENSION_NAME, updated) + .build(); + } + + private ObjectNode updateIntegrationWithCors( + Context context, + OperationObject operationObject, + OperationShape shape, + ObjectNode integrationNode, + CorsTrait cors + ) { + ObjectNode responses = integrationNode.getObjectMember(RESPONSES_KEY).orElse(Node.objectNode()); + + // Always include a "default" response that has the same HTTP response code. + if (!responses.getMember(DEFAULT_KEY).isPresent()) { + responses = responses.withMember(DEFAULT_KEY, Node.objectNode().withMember(STATUS_CODE_KEY, "200")); + } + + Map corsHeaders = new HashMap<>(); + corsHeaders.put(CorsHeader.ALLOW_ORIGIN, cors.getOrigin()); + if (context.usesHttpCredentials()) { + corsHeaders.put(CorsHeader.ALLOW_CREDENTIALS, "true"); + } + + LOGGER.finer(() -> String.format("Adding the following CORS headers to the API Gateway integration of %s: %s", + shape.getId(), corsHeaders)); + Set deducedHeaders = CorsHeader.deduceOperationResponseHeaders(context, operationObject, shape, cors); + LOGGER.fine(() -> String.format("Detected the following headers for operation %s: %s", + shape.getId(), deducedHeaders)); + + // Update each response by adding CORS headers. + responses = responses.getMembers().entrySet().stream() + .peek(entry -> LOGGER.fine(() -> String.format( + "Updating integration response %s for `%s` with CORS", entry.getKey(), shape.getId()))) + .map(entry -> Pair.of(entry.getKey(), updateIntegrationResponse( + shape, corsHeaders, deducedHeaders, entry.getValue().expectObjectNode()))) + .collect(ObjectNode.collect(Pair::getLeft, Pair::getRight)); + + return integrationNode.withMember(RESPONSES_KEY, responses); + } + + private ObjectNode updateIntegrationResponse( + OperationShape shape, + Map corsHeaders, + Set deduced, + ObjectNode response + ) { + Map responseHeaders = new HashMap<>(corsHeaders); + ObjectNode responseParams = response.getObjectMember(RESPONSE_PARAMETERS_KEY).orElseGet(Node::objectNode); + + // Created a sorted set of all headers exposed in the integration. + Set headersToExpose = new TreeSet<>(deduced); + responseParams.getStringMap().keySet().stream() + .filter(parameterName -> parameterName.startsWith(HEADER_PREFIX)) + .map(parameterName -> parameterName.substring(HEADER_PREFIX.length())) + .forEach(headersToExpose::add); + String headersToExposeString = String.join(",", headersToExpose); + + // If there are exposed headers, then add a new header to the integration + // that lists all of them. See https://fetch.spec.whatwg.org/#http-access-control-expose-headers. + if (!headersToExposeString.isEmpty()) { + responseHeaders.put(CorsHeader.EXPOSE_HEADERS, headersToExposeString); + LOGGER.fine(() -> String.format("Adding `%s` header to `%s` with value of `%s`", + CorsHeader.EXPOSE_HEADERS, shape.getId(), headersToExposeString)); + } + + if (responseHeaders.isEmpty()) { + LOGGER.fine(() -> "No headers are exposed by " + shape.getId()); + return response; + } + + // Create an updated response that injects Access-Control-Expose-Headers. + ObjectNode.Builder builder = responseParams.toBuilder(); + for (Map.Entry entry : responseHeaders.entrySet()) { + builder.withMember(HEADER_PREFIX + entry.getKey(), "'" + entry.getValue() + "'"); + } + + return response.withMember(RESPONSE_PARAMETERS_KEY, builder.build()); + } +} diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java index e680f81b535..5ca412912cb 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java @@ -15,12 +15,8 @@ package software.amazon.smithy.aws.apigateway.openapi; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; import java.util.logging.Logger; import software.amazon.smithy.aws.apigateway.traits.IntegrationTrait; import software.amazon.smithy.aws.apigateway.traits.IntegrationTraitIndex; @@ -28,33 +24,20 @@ import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.OperationShape; -import software.amazon.smithy.model.traits.CorsTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.Context; import software.amazon.smithy.openapi.model.OperationObject; import software.amazon.smithy.utils.ListUtils; -import software.amazon.smithy.utils.Pair; /** * Adds API Gateway integrations to operations. - * - *

If the service has the {@link CorsTrait}, then integration responses - * will include a statically computed Access-Control-Expose-Headers - * CORS headers that contains every header exposed by the integration, - * and Access-Control-Allow-Credentials header if the operation uses - * a security scheme that needs it, and and Access-Control-Allow-Origin - * header that is the result of {@link CorsTrait#getOrigin()}. */ final class AddIntegrations implements ApiGatewayMapper { + static final String INTEGRATION_EXTENSION_NAME = "x-amazon-apigateway-integration"; + private static final Logger LOGGER = Logger.getLogger(AddIntegrations.class.getName()); - private static final String EXTENSION_NAME = "x-amazon-apigateway-integration"; - private static final String RESPONSES_KEY = "responses"; - private static final String HEADER_PREFIX = "method.response.header."; - private static final String DEFAULT_KEY = "default"; - private static final String STATUS_CODE_KEY = "statusCode"; - private static final String RESPONSE_PARAMETERS_KEY = "responseParameters"; private static final String PASSTHROUGH_BEHAVIOR = "passthroughBehavior"; private static final String INCORRECT_PASSTHROUGH_BEHAVIOR = "passThroughBehavior"; @@ -74,7 +57,7 @@ public OperationObject updateOperation( IntegrationTraitIndex index = IntegrationTraitIndex.of(context.getModel()); return index.getIntegrationTrait(context.getService(), shape) .map(trait -> operation.toBuilder() - .putExtension(EXTENSION_NAME, createIntegration(context, operation, shape, trait)) + .putExtension(INTEGRATION_EXTENSION_NAME, createIntegration(context, shape, trait)) .build()) .orElseGet(() -> { LOGGER.warning("No API Gateway integration trait found for " + shape.getId()); @@ -83,31 +66,6 @@ public OperationObject updateOperation( } private ObjectNode createIntegration( - Context context, - OperationObject operationObject, - OperationShape shape, - Trait integration - ) { - ObjectNode integrationObject = getIntegrationAsObject(context, shape, integration); - - ApiGatewayConfig.ApiType apiType = context.getConfig() - .getExtensions(ApiGatewayConfig.class) - .getApiGatewayType(); - - // TODO: Refactor this so it's self contained in a REST API CORS plugin. - if (apiType != ApiGatewayConfig.ApiType.REST) { - return integrationObject; - } - - return context.getService().getTrait(CorsTrait.class) - .map(cors -> { - LOGGER.fine(() -> String.format("Adding CORS to `%s` operation responses", shape.getId())); - return updateIntegrationWithCors(context, operationObject, shape, integrationObject, cors); - }) - .orElse(integrationObject); - } - - private static ObjectNode getIntegrationAsObject( Context context, OperationShape shape, Trait integration @@ -146,80 +104,4 @@ private static void validateTraitConfiguration(IntegrationTrait trait, + "'payloadFormatVersion' must be set on the aws.apigateway#integration trait."); } } - - private ObjectNode updateIntegrationWithCors( - Context context, - OperationObject operationObject, - OperationShape shape, - ObjectNode integrationNode, - CorsTrait cors - ) { - ObjectNode responses = integrationNode.getObjectMember(RESPONSES_KEY).orElse(Node.objectNode()); - - // Always include a "default" response that has the same HTTP response code. - if (!responses.getMember(DEFAULT_KEY).isPresent()) { - responses = responses.withMember(DEFAULT_KEY, Node.objectNode().withMember(STATUS_CODE_KEY, "200")); - } - - Map corsHeaders = new HashMap<>(); - corsHeaders.put(CorsHeader.ALLOW_ORIGIN, cors.getOrigin()); - if (context.usesHttpCredentials()) { - corsHeaders.put(CorsHeader.ALLOW_CREDENTIALS, "true"); - } - - LOGGER.finer(() -> String.format("Adding the following CORS headers to the API Gateway integration of %s: %s", - shape.getId(), corsHeaders)); - Set deducedHeaders = CorsHeader.deduceOperationResponseHeaders(context, operationObject, shape, cors); - LOGGER.fine(() -> String.format("Detected the following headers for operation %s: %s", - shape.getId(), deducedHeaders)); - - // Update each response by adding CORS headers. - responses = responses.getMembers().entrySet().stream() - .peek(entry -> LOGGER.fine(() -> String.format( - "Updating integration response %s for `%s` with CORS", entry.getKey(), shape.getId()))) - .map(entry -> Pair.of(entry.getKey(), updateIntegrationResponse( - shape, corsHeaders, deducedHeaders, entry.getValue().expectObjectNode()))) - .collect(ObjectNode.collect(Pair::getLeft, Pair::getRight)); - - return integrationNode.withMember(RESPONSES_KEY, responses); - } - - private ObjectNode updateIntegrationResponse( - OperationShape shape, - Map corsHeaders, - Set deduced, - ObjectNode response - ) { - Map responseHeaders = new HashMap<>(corsHeaders); - ObjectNode responseParams = response.getObjectMember(RESPONSE_PARAMETERS_KEY).orElseGet(Node::objectNode); - - // Created a sorted set of all headers exposed in the integration. - Set headersToExpose = new TreeSet<>(deduced); - responseParams.getStringMap().keySet().stream() - .filter(parameterName -> parameterName.startsWith(HEADER_PREFIX)) - .map(parameterName -> parameterName.substring(HEADER_PREFIX.length())) - .forEach(headersToExpose::add); - String headersToExposeString = String.join(",", headersToExpose); - - // If there are exposed headers, then add a new header to the integration - // that lists all of them. See https://fetch.spec.whatwg.org/#http-access-control-expose-headers. - if (!headersToExposeString.isEmpty()) { - responseHeaders.put(CorsHeader.EXPOSE_HEADERS, headersToExposeString); - LOGGER.fine(() -> String.format("Adding `%s` header to `%s` with value of `%s`", - CorsHeader.EXPOSE_HEADERS, shape.getId(), headersToExposeString)); - } - - if (responseHeaders.isEmpty()) { - LOGGER.fine(() -> "No headers are exposed by " + shape.getId()); - return response; - } - - // Create an updated response that injects Access-Control-Expose-Headers. - ObjectNode.Builder builder = responseParams.toBuilder(); - for (Map.Entry entry : responseHeaders.entrySet()) { - builder.withMember(HEADER_PREFIX + entry.getKey(), "'" + entry.getValue() + "'"); - } - - return response.withMember(RESPONSE_PARAMETERS_KEY, builder.build()); - } } diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java index 15c706ccf2c..0e3563f7e17 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayExtension.java @@ -32,12 +32,16 @@ public List getOpenApiMappers() { ApiGatewayMapper.wrap(new AddAuthorizers()), ApiGatewayMapper.wrap(new AddBinaryTypes()), ApiGatewayMapper.wrap(new AddIntegrations()), - ApiGatewayMapper.wrap(new AddRequestValidators()), - ApiGatewayMapper.wrap(new CloudFormationSubstitution()), + + // CORS For REST APIs + ApiGatewayMapper.wrap(new AddCorsToRestIntegrations()), ApiGatewayMapper.wrap(new AddCorsResponseHeaders()), ApiGatewayMapper.wrap(new AddCorsPreflightIntegration()), ApiGatewayMapper.wrap(new AddCorsToGatewayResponses()), + ApiGatewayMapper.wrap(new AddRequestValidators()), + ApiGatewayMapper.wrap(new CloudFormationSubstitution()), + // HTTP API mappers. ApiGatewayMapper.wrap(new CorsHttpIntegration()) ); From 8e39a33537f3f7030d94400266046376db951401 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 22 Jan 2021 15:52:52 -0800 Subject: [PATCH 3/3] Add missing payloadFormatVersion to tests --- .../apigateway/openapi/http-api-cors-wildcards.openapi.json | 4 ++++ .../smithy/aws/apigateway/openapi/http-api-cors.openapi.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json index fad80e9f18c..0a81b202c84 100644 --- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors-wildcards.openapi.json @@ -23,6 +23,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServiceListPayloadsLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceListPayloads/invocations" } @@ -49,6 +50,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServiceDeletePayloadLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceDeletePayload/invocations" } @@ -87,6 +89,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServiceGetPayloadLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceGetPayload/invocations" } @@ -143,6 +146,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServicePutPayloadLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServicePutPayload/invocations" } diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json index 39af93267bb..0f37889f46a 100644 --- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json @@ -23,6 +23,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServiceListPayloadsLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceListPayloads/invocations" } @@ -49,6 +50,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServiceDeletePayloadLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceDeletePayload/invocations" } @@ -87,6 +89,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServiceGetPayloadLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServiceGetPayload/invocations" } @@ -143,6 +146,7 @@ "x-amazon-apigateway-integration": { "credentials": "arn:aws:iam::123456789012:role/MyServicePutPayloadLambdaRole", "httpMethod": "POST", + "payloadFormatVersion": "1.0", "type": "aws_proxy", "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:MyServicePutPayload/invocations" }