Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for missing authorizer members #1426

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/source-2.0/aws/amazon-apigateway.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ An *authorizer* definition is a structure that supports the following members:
authorization caching is disabled. If it is greater than 0,
API Gateway will cache authorizer responses. If this field is not set,
the default value is 300. The maximum value is 3600, or 1 hour.
* - authorizerPayloadFormatVersion
- ``string``
- For HTTP APIs, specifies the format of the data that API Gateway
sends to a Lambda authorizer, and how API Gateway interprets the
response from Lambda. Supported values are ``1.0`` and ``2.0``.
For more information, see `Lambda Authorizers Payload Format`_.
* - enableSimpleResponses
- ``boolean``
- For HTTP APIs, specifies whether a request authorizer returns a
Boolean value or an IAM policy. Supported only for authorizers
with an ``authorizerPayloadFormatVersion`` of 2.0. If enabled, the
Lambda authorizer function returns a Boolean value.

.. code-block:: smithy

Expand All @@ -188,6 +200,8 @@ An *authorizer* definition is a structure that supports the following members:
identitySource: "mapping.expression"
identityValidationExpression: "[A-Z]+"
resultTtlInSeconds: 100
authorizerPayloadFormatVersion: "2.0"
enableSimpleResponses: true
}
)
service Weather {
Expand Down Expand Up @@ -871,3 +885,4 @@ integration response to two ``header`` parameters of the method response.
.. _x-amazon-apigateway-api-key-source: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-api-key-source.html
.. _IntegrationResponse: https://docs.aws.amazon.com/apigateway/api-reference/resource/integration-response/
.. _mapping templates: https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html#models-mappings-mappings
.. _Lambda Authorizers Payload Format: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.payload-format
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ private <T extends Trait> SecurityScheme convertAuthScheme(
.withOptionalMember("identitySource", authorizer.getIdentitySource().map(Node::from))
.withOptionalMember("authorizerResultTtlInSeconds",
authorizer.getResultTtlInSeconds().map(Node::from))
.withOptionalMember("authorizerPayloadFormatVersion",
authorizer.getAuthorizerPayloadFormatVersion().map(Node::from))
.withOptionalMember("enableSimpleResponses",
authorizer.getEnableSimpleResponses().map(Node::from))
.build();
if (authorizerNode.size() != 0) {
schemeBuilder.putExtension(EXTENSION_NAME, authorizerNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Optional;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -66,6 +65,8 @@ public void addsAuthorizers() {
assertThat(authorizer.getStringMember("identitySource").get().getValue(), equalTo("mapping.expression"));
assertThat(authorizer.getStringMember("identityValidationExpression").get().getValue(), equalTo("[A-Z]+"));
assertThat(authorizer.getNumberMember("authorizerResultTtlInSeconds").get().getValue(), equalTo(100));
assertThat(authorizer.getStringMember("authorizerPayloadFormatVersion").get().getValue(), equalTo("2.0"));
assertThat(authorizer.getBooleanMember("enableSimpleResponses").get().getValue(), equalTo(true));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"credentials": "arn:foo:bar",
"identitySource": "mapping.expression",
"identityValidationExpression": "[A-Z]+",
"resultTtlInSeconds": 100
"resultTtlInSeconds": 100,
"authorizerPayloadFormatVersion": "2.0",
"enableSimpleResponses": true
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public final class AuthorizerDefinition implements ToNode, ToSmithyBuilder<Autho
private final String identitySource;
private final String identityValidationExpression;
private final Integer resultTtlInSeconds;
private final String authorizerPayloadFormatVersion;
private final Boolean enableSimpleResponses;

private AuthorizerDefinition(Builder builder) {
scheme = SmithyBuilder.requiredState(SCHEME_KEY, builder.scheme);
Expand All @@ -51,6 +53,8 @@ private AuthorizerDefinition(Builder builder) {
identitySource = builder.identitySource;
identityValidationExpression = builder.identityValidationExpression;
resultTtlInSeconds = builder.resultTtlInSeconds;
authorizerPayloadFormatVersion = builder.authorizerPayloadFormatVersion;
enableSimpleResponses = builder.enableSimpleResponses;

if (builder.customAuthType != null) {
customAuthType = builder.customAuthType;
Expand Down Expand Up @@ -162,6 +166,25 @@ public Optional<Integer> getResultTtlInSeconds() {
return Optional.ofNullable(resultTtlInSeconds);
}

/**
* Gets the format of the payload returned by the authorizer.
*
* @return Returns payload type.
*/
public Optional<String> getAuthorizerPayloadFormatVersion() {
return Optional.ofNullable(authorizerPayloadFormatVersion);
}

/**
* Gets whether the authorizer returns simple responses.
*
* @return Returns true if authorizer returns a boolean,
* false if it returns an IAM policy.
*/
public Optional<Boolean> getEnableSimpleResponses() {
return Optional.ofNullable(enableSimpleResponses);
}

@Override
public Builder toBuilder() {
return builder()
Expand All @@ -172,7 +195,9 @@ public Builder toBuilder() {
.credentials(credentials)
.identitySource(identitySource)
.identityValidationExpression(identityValidationExpression)
.resultTtlInSeconds(resultTtlInSeconds);
.resultTtlInSeconds(resultTtlInSeconds)
.authorizerPayloadFormatVersion(authorizerPayloadFormatVersion)
.enableSimpleResponses(enableSimpleResponses);
}

@Override
Expand All @@ -198,7 +223,9 @@ public boolean equals(Object o) {
&& Objects.equals(credentials, that.credentials)
&& Objects.equals(identitySource, that.identitySource)
&& Objects.equals(identityValidationExpression, that.identityValidationExpression)
&& Objects.equals(resultTtlInSeconds, that.resultTtlInSeconds);
&& Objects.equals(resultTtlInSeconds, that.resultTtlInSeconds)
&& Objects.equals(authorizerPayloadFormatVersion, that.authorizerPayloadFormatVersion)
&& Objects.equals(enableSimpleResponses, that.enableSimpleResponses);
}

@Override
Expand All @@ -218,6 +245,8 @@ public static final class Builder implements SmithyBuilder<AuthorizerDefinition>
private String identitySource;
private String identityValidationExpression;
private Integer resultTtlInSeconds;
private String authorizerPayloadFormatVersion;
private Boolean enableSimpleResponses;

@Override
public AuthorizerDefinition build() {
Expand Down Expand Up @@ -333,5 +362,27 @@ public Builder resultTtlInSeconds(Integer resultTtlInSeconds) {
this.resultTtlInSeconds = resultTtlInSeconds;
return this;
}

/**
* Sets the format of the payload returned by the authorizer.
*
* @param authorizerPayloadFormatVersion format of the payload.
* @return Returns the builder.
*/
public Builder authorizerPayloadFormatVersion(String authorizerPayloadFormatVersion) {
this.authorizerPayloadFormatVersion = authorizerPayloadFormatVersion;
return this;
}

/**
* Sets whether the authorizer returns simple responses.
*
* @param enableSimpleResponses defines if authorizer should return simple responses.
* @return Returns the builder.
*/
public Builder enableSimpleResponses(Boolean enableSimpleResponses) {
this.enableSimpleResponses = enableSimpleResponses;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public Optional<AuthorizerDefinition> getAuthorizer(String name) {
}

/**
* Gets an immuatable map of authorizer names to their definitions.
* Gets an immutable map of authorizer names to their definitions.
*
* @return Returns the authorizers.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

package software.amazon.smithy.aws.apigateway.traits;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -27,30 +29,49 @@
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.model.validation.ValidationUtils;
import software.amazon.smithy.utils.OptionalUtils;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Each authorizer resolved within a service must use a scheme that
* matches one of the schemes of the protocols of the service.
* Validates if authorizers traits are well-defined.
*/
@SmithyInternalApi
public final class AuthorizersTraitValidator extends AbstractValidator {
@Override
public List<ValidationEvent> validate(Model model) {
return model.shapes(ServiceShape.class)
.flatMap(service -> OptionalUtils.stream(validateService(model, service)))
.map(service -> validate(model, service))
.flatMap(List::stream)
.collect(Collectors.toList());
}

private Optional<ValidationEvent> validateService(Model model, ServiceShape service) {
private List<ValidationEvent> validate(Model model, ServiceShape service) {
Map<String, AuthorizerDefinition> authorizers = service.getTrait(AuthorizersTrait.class)
.map(AuthorizersTrait::getAuthorizers)
.orElseGet(HashMap::new);

List<ValidationEvent> validationEvents = new ArrayList<>();

Optional<ValidationEvent> authSchemaValidation =
validateAuthSchema(authorizers, model, service);
authSchemaValidation.ifPresent(validationEvents::add);

Optional<ValidationEvent> enableSimpleResponsesValidation =
validateEnableSimpleResponsesConfig(authorizers, service);
enableSimpleResponsesValidation.ifPresent(validationEvents::add);

return validationEvents;
}

/**
* Each authorizer resolved within a service must use a scheme that
* matches one of the schemes of the protocols of the service.
*/
private Optional<ValidationEvent> validateAuthSchema(Map<String, AuthorizerDefinition> authorizers,
Model model,
ServiceShape service) {
Set<ShapeId> authSchemes = ServiceIndex.of(model).getAuthSchemes(service).keySet();

// Create a comma separated string of authorizer names to schemes.
String invalidMappings = service.getTrait(AuthorizersTrait.class)
.map(AuthorizersTrait::getAuthorizers)
.orElseGet(HashMap::new)
.entrySet().stream()
String invalidMappings = authorizers.entrySet().stream()
.filter(entry -> !authSchemes.contains(entry.getValue().getScheme()))
.map(entry -> entry.getKey() + " -> " + entry.getValue().getScheme())
.sorted()
Expand All @@ -68,4 +89,32 @@ private Optional<ValidationEvent> validateService(Model model, ServiceShape serv
ValidationUtils.tickedList(authSchemes),
invalidMappings)));
}

/**
* Each authorizer with the enableSimpleResponses member defined
* should have the authorizedPayloadFormatVersion member set to 2.0.
*/
private Optional<ValidationEvent> validateEnableSimpleResponsesConfig(Map<String, AuthorizerDefinition> authorizers,
ServiceShape service) {
String invalidConfigs = authorizers.entrySet().stream()
.filter(entry -> entry.getValue().getEnableSimpleResponses().isPresent())
.filter(entry -> entry.getValue().getAuthorizerPayloadFormatVersion().isPresent())
.filter(entry -> !entry.getValue().getAuthorizerPayloadFormatVersion().get().equals("2.0"))
.map(Map.Entry::getKey)
.sorted()
.collect(Collectors.joining(", "));

if (invalidConfigs.isEmpty()) {
return Optional.empty();
}

AuthorizersTrait authorizersTrait = service.getTrait(AuthorizersTrait.class).get();
return Optional.of(error(service, authorizersTrait, String.format(
"The enableSimpleResponses member of %s is only supported when authorizedPayloadFormatVersion "
+ "is 2.0. The following authorizers are misconfigured: %s",
AuthorizersTrait.ID,
invalidConfigs
)));
}
Comment on lines +97 to +118
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this validator is being overzealous or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't being overzealous. We do similar validation to assure compatible properties are set for other traits. The HttpApiKeyAuthTraitValidator does something similar for the @HttpApiKeyAuthTrait's in and scheme properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great (:


}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ structure AuthorizerDefinition {

/// The number of seconds for which the resulting IAM policy is cached.
resultTtlInSeconds: Integer

/// Format version of the payload sent from API Gateway to the authorizer
/// and how API Gateway interprets the response. Used only by HTTP APIs.
authorizerPayloadFormatVersion: PayloadFormatVersion

/// Specifies if the autorizer returns either a boolean or an IAM Policy.
/// If enabled, authorizer returns a boolean. Used only by HTTP APIs.
/// Only supported when authorizerPayloadFormatVersion is set to 2.0.
enableSimpleResponses: Boolean
}

/// Defines a response and specifies parameter mappings.
Expand Down Expand Up @@ -336,3 +345,10 @@ enum PassThroughBehavior {
/// request.
NEVER = "never"
}

/// Defines the payloadFormatVersion used by authorizers
@private
enum PayloadFormatVersion {
V1_0 = "1.0"
V2_0 = "2.0"
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/*
* Copyright 2022 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.traits;

import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -25,6 +40,8 @@ public void registersTrait() {
.withMember("identitySource", "mapping.expression")
.withMember("identityValidationExpression", "[A-Z]+")
.withMember("resultTtlInSeconds", 100)
.withMember("authorizerPayloadFormatVersion", "format.version")
.withMember("enableSimpleResponse", true)
.build())
.build();
Trait trait = factory.createTrait(AuthorizersTrait.ID, id, node).get();
Expand Down