Skip to content

Commit

Permalink
Support API Gateway API key usage plans
Browse files Browse the repository at this point in the history
This commit adds support for enabling API Gateway's API key usage
plans. A non-custom httpApiKeyAuth based authorizer on an operation
will now be directly set on every operation in the OpenAPI document,
even if it's the same as the service level authorizer.
  • Loading branch information
kstich committed Oct 16, 2020
1 parent 9ca75c4 commit 68bfe46
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 2 deletions.
32 changes: 32 additions & 0 deletions docs/source/1.0/guides/converting-to-openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,37 @@ entry:
In the entry, ``providerARNs`` will be populated from the ``providerArns`` list
from the trait.

Amazon API Gateway API key usage plans
======================================

Smithy enables `API Gateway's API key usage plans`_ when a scheme based on the
:ref:`httpApiKeyAuth-trait` is set and configured as :ref:`an authorizer
<aws.apigateway#authorizers-trait>` with an empty ``customAuthType``.

The following Smithy model enables API Gateway's API key usage plans on the
``OperationA`` operation:

.. code-block:: smithy
namespace smithy.example
use aws.apigateway#authorizer
use aws.apigateway#authorizers
use aws.protocols#restJson1
@restJson1
@httpApiKeyAuth(name: "x-api-key", in: "header")
@authorizer("api_key")
// Note the empty `customAuthType` property.
@authorizers(api_key: {scheme: "smithy.api#httpApiKeyAuth", customAuthType: ""})
service Example {
version: "2019-06-17",
operations: [OperationA],
}
operation OperationA {}
.. _other-traits:

Other traits that influence API Gateway
Expand Down Expand Up @@ -1152,3 +1183,4 @@ The conversion process is highly extensible through
.. _OpenAPI security schemes: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#securitySchemeObject
.. _x-amazon-apigateway-authorizer: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html
.. _Lambda authorizers: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html
.. _API Gateway's API key usage plans: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Logger;
import software.amazon.smithy.aws.apigateway.traits.AuthorizerDefinition;
import software.amazon.smithy.aws.apigateway.traits.AuthorizerIndex;
Expand All @@ -29,6 +30,7 @@
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.openapi.fromsmithy.Context;
import software.amazon.smithy.openapi.fromsmithy.SecuritySchemeConverter;
Expand Down Expand Up @@ -103,11 +105,19 @@ public OperationObject updateOperation(
AuthorizerIndex authorizerIndex = AuthorizerIndex.of(context.getModel());

// Get the resolved security schemes of the service and operation, and
// only add security if it's different than the service.
// only add security if it's different than the service or...
String serviceAuth = authorizerIndex.getAuthorizer(service).orElse(null);
String operationAuth = authorizerIndex.getAuthorizer(service, shape).orElse(null);

if (operationAuth == null || Objects.equals(operationAuth, serviceAuth)) {
// Short circuit if we have no authorizer for the operation.
if (operationAuth == null) {
return operation;
}

// ...API Gateway's built-in API keys are being used. It requires the
// security to be specified on every operation.
// See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-setup-api-key-with-console.html#api-gateway-usage-plan-configure-apikey-on-method
if (Objects.equals(operationAuth, serviceAuth) && !usesApiGatewayApiKeys(service, operationAuth)) {
return operation;
}

Expand All @@ -116,6 +126,24 @@ public OperationObject updateOperation(
.build();
}

private boolean usesApiGatewayApiKeys(ServiceShape service, String operationAuth) {
// Get the authorizer for this operation if its customAuthType is disabled,
// as is required for API Gateway's API keys.
Optional<AuthorizerDefinition> definitionOptional = service.getTrait(AuthorizersTrait.class)
.flatMap(authorizers -> authorizers.getAuthorizer(operationAuth)
.filter(authorizer -> authorizer.getCustomAuthType().isPresent()
&& authorizer.getCustomAuthType().get().isEmpty()));

if (!definitionOptional.isPresent()) {
return false;
}
AuthorizerDefinition definition = definitionOptional.get();

// We then need to validate that a no-"type" variant of the @httpApiKeyAuth trait
// has been set to authenticate the operation, declaring it's a built-in scheme.
return definition.getScheme().equals(HttpApiKeyAuthTrait.ID) && !definition.getType().isPresent();
}

@Override
public OpenApi after(Context<? extends Trait> context, OpenApi openapi) {
return context.getService().getTrait(AuthorizersTrait.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter;
import software.amazon.smithy.openapi.model.OpenApi;
import software.amazon.smithy.openapi.model.SecurityScheme;
import software.amazon.smithy.utils.IoUtils;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.MapUtils;

Expand Down Expand Up @@ -134,6 +135,25 @@ public void emptyCustomAuthTypeNotSet() {
assertFalse(apiKey.getExtension("x-amazon-apigateway-authorizer").isPresent());
}

@Test
public void addsOperationLevelApiKeyScheme() {
Model model = Model.assembler()
.discoverModels(getClass().getClassLoader())
.addImport(getClass().getResource("operation-http-api-key-security.json"))
.assemble()
.unwrap();
OpenApiConfig config = new OpenApiConfig();
config.setService(ShapeId.from("smithy.example#Service"));
OpenApi result = OpenApiConverter.create()
.config(config)
.classLoader(getClass().getClassLoader())
.convert(model);
Node expectedNode = Node.parse(IoUtils.toUtf8String(
getClass().getResourceAsStream("operation-http-api-key-security.openapi.json")));

Node.assertEquals(result, expectedNode);
}

@Test
public void resolvesEffectiveAuthorizersForEachOperation() {
Model model = Model.assembler()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"smithy": "1.0",
"shapes": {
"smithy.example#Service": {
"type": "service",
"version": "2006-03-01",
"operations": [
{
"target": "smithy.example#Operation1"
}
],
"traits": {
"aws.protocols#restJson1": {},
"smithy.api#httpApiKeyAuth": {
"name": "x-api-key",
"in": "header"
},
"aws.apigateway#authorizer": "api_key",
"aws.apigateway#authorizers": {
"api_key": {
"scheme": "smithy.api#httpApiKeyAuth",
"customAuthType": ""
}
}
}
},
"smithy.example#Operation1": {
"type": "operation",
"traits": {
"smithy.api#http": {
"uri": "/",
"method": "GET"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"openapi": "3.0.2",
"info": {
"title": "Service",
"version": "2006-03-01"
},
"paths": {
"/": {
"get": {
"operationId": "Operation1",
"responses": {
"200": {
"description": "Operation1 response"
}
},
"security": [
{
"api_key": [ ]
}
]
}
}
},
"components": {
"securitySchemes": {
"api_key": {
"type": "apiKey",
"description": "X-Api-Key authentication",
"name": "x-api-key",
"in": "header"
}
}
},
"security": [
{
"api_key": [ ]
}
]
}

0 comments on commit 68bfe46

Please sign in to comment.