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

Added scheme property to HttpApiKeyAuth trait #893

Merged
merged 17 commits into from
Sep 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
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:

DavidOgunsAWS marked this conversation as resolved.
Show resolved Hide resolved
.. 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": [ ]
DavidOgunsAWS marked this conversation as resolved.
Show resolved Hide resolved
}
]
}