diff --git a/docs/source/1.0/spec/core/auth-traits.rst b/docs/source/1.0/spec/core/auth-traits.rst index 25a5b8bcc8f..5b7084f7c19 100644 --- a/docs/source/1.0/spec/core/auth-traits.rst +++ b/docs/source/1.0/spec/core/auth-traits.rst @@ -216,6 +216,10 @@ properties: - ``string`` - **Required**. Defines the location of where the key is serialized. This value can be set to ``header`` or ``query``. + * - scheme + - ``string`` + - Defines the security scheme to use on the ``Authorization`` header value + This can only be set if the "in" property is set to ``header``. The following example defines a service that accepts an API key in the "X-Api-Key" HTTP header: @@ -227,6 +231,16 @@ HTTP header: version: "2017-02-11", } +The following example defines a service that uses an API key auth scheme through +the HTTP ``Authorization`` header: + +.. code-block:: smithy + + @httpApiKeyAuth(scheme: "ApiKey", name: "Authorization", in: "header") + service WeatherService { + version: "2017-02-11", + } + .. smithy-trait:: smithy.api#optionalAuth .. _optionalAuth-trait: diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTrait.java index e1989e5928b..94f842789aa 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTrait.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2021 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. @@ -15,6 +15,7 @@ package software.amazon.smithy.model.traits; +import java.util.Optional; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.ShapeId; @@ -29,6 +30,7 @@ public final class HttpApiKeyAuthTrait extends AbstractTrait implements ToSmithy public static final ShapeId ID = ShapeId.from("smithy.api#httpApiKeyAuth"); + private final String scheme; private final String name; private final Location in; @@ -36,6 +38,11 @@ private HttpApiKeyAuthTrait(Builder builder) { super(ID, builder.getSourceLocation()); name = SmithyBuilder.requiredState("name", builder.name); in = SmithyBuilder.requiredState("in", builder.in); + scheme = builder.scheme; + } + + public Optional getScheme() { + return Optional.ofNullable(scheme); } public String getName() { @@ -50,17 +57,19 @@ public Location getIn() { public Builder toBuilder() { return builder() .sourceLocation(getSourceLocation()) + .scheme(getScheme().orElse(null)) .name(getName()) .in(getIn()); } @Override protected Node createNode() { - return Node.objectNodeBuilder() + ObjectNode.Builder builder = Node.objectNodeBuilder() .sourceLocation(getSourceLocation()) .withMember("name", getName()) .withMember("in", getIn().toString()) - .build(); + .withOptionalMember("scheme", getScheme().map(Node::from)); + return builder.build(); } public static Builder builder() { @@ -101,6 +110,7 @@ public Provider() { public Trait createTrait(ShapeId target, Node value) { ObjectNode objectNode = value.expectObjectNode(); Builder builder = builder().sourceLocation(value.getSourceLocation()); + builder.scheme(objectNode.getStringMemberOrDefault("scheme", null)); builder.name(objectNode.expectStringMember("name").getValue()); builder.in(Location.from(objectNode.expectStringMember("in").expectOneOf("header", "query"))); return builder.build(); @@ -108,6 +118,7 @@ public Trait createTrait(ShapeId target, Node value) { } public static final class Builder extends AbstractTraitBuilder { + private String scheme; private String name; private Location in; @@ -118,6 +129,11 @@ public HttpApiKeyAuthTrait build() { return new HttpApiKeyAuthTrait(this); } + public Builder scheme(String scheme) { + this.scheme = scheme; + return this; + } + public Builder name(String name) { this.name = name; return this; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpApiKeyAuthTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpApiKeyAuthTraitValidator.java new file mode 100644 index 00000000000..7b368a15f96 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpApiKeyAuthTraitValidator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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.model.validation.validators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Validates that if an HttpApiKeyAuth trait's scheme field is present then + * the 'in' field must specify "header". Scheme should only be used with the + * "Authorization" http header. + */ +public final class HttpApiKeyAuthTraitValidator extends AbstractValidator { + @Override + public List validate(Model model) { + Set serviceShapesWithTrait = model.getServiceShapesWithTrait(HttpApiKeyAuthTrait.class); + List events = new ArrayList<>(); + + for (ServiceShape serviceShape : serviceShapesWithTrait) { + HttpApiKeyAuthTrait trait = serviceShape.expectTrait(HttpApiKeyAuthTrait.class); + trait.getScheme().ifPresent(scheme -> { + if (trait.getIn() != HttpApiKeyAuthTrait.Location.HEADER) { + events.add(error(serviceShape, trait, + String.format("The httpApiKeyAuth trait must have an `in` value of `header` when a `scheme`" + + " is provided, found: %s", trait.getIn()))); + } + }); + } + + return events; + } +} diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index 174aef1ba4f..c275e2bc3c8 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -5,6 +5,7 @@ software.amazon.smithy.model.validation.validators.EventPayloadTraitValidator software.amazon.smithy.model.validation.validators.ExamplesTraitValidator software.amazon.smithy.model.validation.validators.ExclusiveStructureMemberTraitValidator software.amazon.smithy.model.validation.validators.HostLabelTraitValidator +software.amazon.smithy.model.validation.validators.HttpApiKeyAuthTraitValidator software.amazon.smithy.model.validation.validators.HttpBindingsMissingValidator software.amazon.smithy.model.validation.validators.HttpChecksumTraitValidator software.amazon.smithy.model.validation.validators.HttpHeaderTraitValidator diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 81b110620b1..99575207ec8 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -196,6 +196,10 @@ structure httpApiKeyAuth { /// can be set to `"header"` or `"query"`. @required in: HttpApiKeyLocations, + + /// Defines the security scheme to use on the ``Authorization`` header value + /// This can only be set if the "in" property is set to ``header``. + scheme: NonEmptyString, } @private diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTraitTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTraitTest.java index d2b73ee8ba3..17846ddd18b 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTraitTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/traits/HttpApiKeyAuthTraitTest.java @@ -18,6 +18,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Optional; @@ -39,6 +40,7 @@ public void loadsTraitWithHeader() { assertThat(trait.get(), instanceOf(HttpApiKeyAuthTrait.class)); HttpApiKeyAuthTrait auth = (HttpApiKeyAuthTrait) trait.get(); + assertFalse(auth.getScheme().isPresent()); assertThat(auth.getName(), equalTo("X-Foo")); assertThat(auth.getIn(), equalTo(HttpApiKeyAuthTrait.Location.HEADER)); assertThat(auth.toNode(), equalTo(node)); @@ -57,9 +59,30 @@ public void loadsTraitWithQuery() { assertThat(trait.get(), instanceOf(HttpApiKeyAuthTrait.class)); HttpApiKeyAuthTrait auth = (HttpApiKeyAuthTrait) trait.get(); + assertFalse(auth.getScheme().isPresent()); assertThat(auth.getName(), equalTo("blerg")); assertThat(auth.getIn(), equalTo(HttpApiKeyAuthTrait.Location.QUERY)); assertThat(auth.toNode(), equalTo(node)); assertThat(auth.toBuilder().build(), equalTo(auth)); } + + @Test + public void loadsTraitWithHeaderAndScheme() { + TraitFactory provider = TraitFactory.createServiceFactory(); + ObjectNode node = Node.objectNode() + .withMember("scheme", "fenty") + .withMember("name", "X-Foo") + .withMember("in", "header"); + Optional trait = provider.createTrait( + HttpApiKeyAuthTrait.ID, ShapeId.from("ns.qux#foo"), node); + assertTrue(trait.isPresent()); + assertThat(trait.get(), instanceOf(HttpApiKeyAuthTrait.class)); + HttpApiKeyAuthTrait auth = (HttpApiKeyAuthTrait) trait.get(); + + assertThat(auth.getScheme().get(), equalTo("fenty")); + assertThat(auth.getName(), equalTo("X-Foo")); + assertThat(auth.getIn(), equalTo(HttpApiKeyAuthTrait.Location.HEADER)); + assertThat(auth.toNode(), equalTo(node)); + assertThat(auth.toBuilder().build(), equalTo(auth)); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-api-key-scheme-trait-validator-test.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-api-key-scheme-trait-validator-test.errors new file mode 100644 index 00000000000..9aa23d834a5 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-api-key-scheme-trait-validator-test.errors @@ -0,0 +1 @@ +[ERROR] ns.foo#MyService: The httpApiKeyAuth trait must have an `in` value of `header` when a `scheme` is provided, found: query | HttpApiKeyAuthTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-api-key-scheme-trait-validator-test.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-api-key-scheme-trait-validator-test.json new file mode 100644 index 00000000000..8934b5cc2c0 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-api-key-scheme-trait-validator-test.json @@ -0,0 +1,39 @@ +{ + "smithy": "1.0", + "shapes": { + "ns.foo#MyService": { + "type": "service", + "version": "2017-01-17", + "operations": [ + { + "target": "ns.foo#A" + } + ], + "traits": { + "smithy.api#httpApiKeyAuth": { + "scheme": "Baz", + "name": "ApiKeyName", + "in": "query" + } + } + }, + "ns.foo#A": { + "type": "operation", + "input": { + "target": "ns.foo#AInput" + }, + "output": { + "target": "ns.foo#AOutput" + }, + "traits": { + "smithy.api#readonly": { } + } + }, + "ns.foo#AInput": { + "type": "structure" + }, + "ns.foo#AOutput": { + "type": "structure" + } + } +} diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverter.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverter.java index 614d3a69189..bfc9cf1a74c 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverter.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverter.java @@ -40,6 +40,16 @@ public Class getAuthSchemeType() { @Override public SecurityScheme createSecurityScheme(Context context, HttpApiKeyAuthTrait trait) { + if (trait.getScheme().isPresent()) { + return SecurityScheme.builder() + .type("http") + .scheme(trait.getScheme().get()) + .name(trait.getName()) + .in(trait.getIn().toString()) + .description("ApiKey authentication semantics via 'Authorization' header") + .build(); + } + return SecurityScheme.builder() .type("apiKey") .name(trait.getName()) diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverterTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverterTest.java index f8b7079fff5..59254868673 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverterTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/security/HttpApiKeyAuthConverterTest.java @@ -30,6 +30,23 @@ public void addsCustomApiKeyAuth() { Node.assertEquals(result, expectedNode); } + @Test + public void addsCustomApiKeyBearerAuth() { + Model model = Model.assembler() + .addImport(getClass().getResource("http-api-key-bearer-security.json")) + .discoverModels() + .assemble() + .unwrap(); + OpenApiConfig config = new OpenApiConfig(); + config.setService(ShapeId.from("smithy.example#Service")); + OpenApi result = OpenApiConverter.create().config(config).convert(model); + Node expectedNode = Node.parse(IoUtils.toUtf8String( + getClass().getResourceAsStream("http-api-key-bearer-security.openapi.json"))); + + Node.assertEquals(result, expectedNode); + } + + @Test public void returnsTraitHeader() { HttpApiKeyAuthConverter converter = new HttpApiKeyAuthConverter(); diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/security/http-api-key-bearer-security.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/security/http-api-key-bearer-security.json new file mode 100644 index 00000000000..fdbb07f9a60 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/security/http-api-key-bearer-security.json @@ -0,0 +1,31 @@ +{ + "smithy": "1.0", + "shapes": { + "smithy.example#Service": { + "type": "service", + "version": "2006-03-01", + "operations": [ + { + "target": "smithy.example#Operation" + } + ], + "traits": { + "aws.protocols#restJson1": {}, + "smithy.api#httpApiKeyAuth": { + "name": "Authorization", + "in": "header", + "scheme": "ApiKey" + } + } + }, + "smithy.example#Operation": { + "type": "operation", + "traits": { + "smithy.api#http": { + "uri": "/", + "method": "GET" + } + } + } + } +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/security/http-api-key-bearer-security.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/security/http-api-key-bearer-security.openapi.json new file mode 100644 index 00000000000..e732a628293 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/security/http-api-key-bearer-security.openapi.json @@ -0,0 +1,35 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Service", + "version": "2006-03-01" + }, + "paths": { + "/": { + "get": { + "operationId": "Operation", + "responses": { + "200": { + "description": "Operation response" + } + } + } + } + }, + "components": { + "securitySchemes": { + "smithy.api.httpApiKeyAuth": { + "type": "http", + "description": "ApiKey authentication semantics via 'Authorization' header", + "name": "Authorization", + "in": "header", + "scheme": "ApiKey" + } + } + }, + "security": [ + { + "smithy.api.httpApiKeyAuth": [ ] + } + ] +}