Skip to content

Commit

Permalink
Added scheme property to HttpApiKeyAuth trait (#893)
Browse files Browse the repository at this point in the history
Add scheme property to HttpApiKeyAuth trait
  • Loading branch information
DavidOgunsAWS authored Sep 1, 2021
1 parent d404932 commit 7742723
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 3 deletions.
14 changes: 14 additions & 0 deletions docs/source/1.0/spec/core/auth-traits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -29,13 +30,19 @@ 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;

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<String> getScheme() {
return Optional.ofNullable(scheme);
}

public String getName() {
Expand All @@ -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() {
Expand Down Expand Up @@ -101,13 +110,15 @@ 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();
}
}

public static final class Builder extends AbstractTraitBuilder<HttpApiKeyAuthTrait, Builder> {
private String scheme;
private String name;
private Location in;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ValidationEvent> validate(Model model) {
Set<ServiceShape> serviceShapesWithTrait = model.getServiceShapesWithTrait(HttpApiKeyAuthTrait.class);
List<ValidationEvent> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
Expand All @@ -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> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ public Class<HttpApiKeyAuthTrait> getAuthSchemeType() {

@Override
public SecurityScheme createSecurityScheme(Context<? extends Trait> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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": [ ]
}
]
}

0 comments on commit 7742723

Please sign in to comment.