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 extends Trait> 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 extends Trait> 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 extends Trait> 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 77a63fbc64b..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,21 +66,6 @@ public OperationObject updateOperation(
}
private ObjectNode createIntegration(
- Context extends Trait> context,
- OperationObject operationObject,
- OperationShape shape,
- Trait integration
- ) {
- ObjectNode integrationObject = getIntegrationAsObject(context, shape, integration);
- 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 extends Trait> context,
OperationShape shape,
Trait integration
@@ -136,80 +104,4 @@ private static void validateTraitConfiguration(IntegrationTrait trait,
+ "'payloadFormatVersion' must be set on the aws.apigateway#integration trait.");
}
}
-
- private ObjectNode updateIntegrationWithCors(
- Context extends Trait> 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 cd0bc2f9679..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,11 +32,18 @@ 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 AddCorsToGatewayResponses()),
+
+ ApiGatewayMapper.wrap(new AddRequestValidators()),
+ ApiGatewayMapper.wrap(new CloudFormationSubstitution()),
+
+ // 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 extends Trait> 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 extends Trait> 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 extends Trait> context, OpenApi openapi) {
+ return context.getService().getTrait(CorsTrait.class)
+ .map(corsTrait -> addCors(context, openapi, corsTrait))
+ .orElse(openapi);
+ }
+
+ private OpenApi addCors(Context extends Trait> 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..0a81b202c84
--- /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,230 @@
+{
+ "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",
+ "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"
+ }
+ }
+ },
+ "/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",
+ "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"
+ }
+ },
+ "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",
+ "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"
+ }
+ },
+ "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",
+ "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"
+ }
+ }
+ }
+ },
+ "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..0f37889f46a
--- /dev/null
+++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/http-api-cors.openapi.json
@@ -0,0 +1,238 @@
+{
+ "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",
+ "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"
+ }
+ }
+ },
+ "/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",
+ "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"
+ }
+ },
+ "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",
+ "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"
+ }
+ },
+ "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",
+ "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"
+ }
+ }
+ }
+ },
+ "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 extends Trait> context,
+ SecuritySchemeConverter converter
+ ) {
+ T t = context.getService().expectTrait(converter.getAuthSchemeType());
+ return converter.getAuthRequestHeaders(context, t);
+ }
+
+ private static Set getSecuritySchemeResponseHeaders(
+ Context extends Trait> 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.