Skip to content

Commit

Permalink
Add ServiceIndex method to account for smithy.api#noAuth
Browse files Browse the repository at this point in the history
  • Loading branch information
gosar committed Aug 15, 2023
1 parent 8635932 commit de0801c
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.AuthDefinitionTrait;
import software.amazon.smithy.model.traits.AuthTrait;
import software.amazon.smithy.model.traits.OptionalAuthTrait;
import software.amazon.smithy.model.traits.ProtocolDefinitionTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;

/**
* An index that resolves service protocols and auth schemes.
Expand Down Expand Up @@ -198,6 +200,41 @@ public Map<ShapeId, Trait> getEffectiveAuthSchemes(ToShapeId service, ToShapeId
.orElse(Collections.emptyMap());
}

/**
* Gets a list of effective authentication schemes applied to an operation bound within a service, that also
* includes the {@code smithy.api#noAuth} scheme when no authentication is to be used.
*
* <p>This behaves the same as {@link #getEffectiveAuthSchemes(ToShapeId, ToShapeId)} with the following
* differences:
* If the operation has no effective auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth}
* auth scheme.
* If the operation has the {@code smithy.api#optionalAuth} trait, it adds {@code smithy.api#noAuth} to the end.
*
* <p>This is a convenience wrapper around getEffectiveAuthSchemes, that accounts for
* {@code smithy.api#optionalAuth} and avoids having to special case handling empty result. The returned map will
* always contain at least 1 entry.
*
* <p>The {@code smithy.api#noAuth} scheme, if present, is always the last scheme.
*
* @param service Service the operation is within.
* @param operation Operation to get the effective authentication schemes of.
* @return Returns a map of the trait shape ID to the auth trait itself.
*/
public Map<ShapeId, Trait> getNoAuthAwareEffectiveAuthSchemes(ToShapeId service, ToShapeId operation) {
LinkedHashMap<ShapeId, Trait> authSchemes = new LinkedHashMap<>(getEffectiveAuthSchemes(service, operation));
if (authSchemes.isEmpty() || hasOptionalAuth(operation)) {
authSchemes.put(NoAuthTrait.ID, new NoAuthTrait());
}
return authSchemes;
}

private boolean hasOptionalAuth(ToShapeId operation) {
return getModel()
.getShape(operation.toShapeId())
.filter(shape -> shape.hasTrait(OptionalAuthTrait.class))
.isPresent();
}

private Map<ShapeId, Trait> getAuthTraitValues(Shape service, Shape subject) {
if (!subject.hasTrait(AuthTrait.class)) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package software.amazon.smithy.model.traits.synthetic;

import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.AnnotationTrait;

/**
* An auth scheme trait for {@code smithy.api#noAuth} which indicates no authentication. This is not a real trait
* in the semantic model, but a valid auth scheme for use in
* {@link ServiceIndex#getNoAuthAwareEffectiveAuthSchemes(ToShapeId, ToShapeId)}.
*/
public final class NoAuthTrait extends AnnotationTrait {

public static final ShapeId ID = ShapeId.from("smithy.api#noAuth");

public NoAuthTrait() {
super(ID, Node.objectNode());
}

@Override
public boolean isSynthetic() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
package software.amazon.smithy.model.knowledge;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.equalTo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterAll;
Expand All @@ -36,6 +36,8 @@

public class ServiceIndexTest {

private static final ShapeId CUSTOM_AUTH_ID = ShapeId.from("smithy.example#customAuth");

private static Model model;

@BeforeAll
Expand All @@ -52,7 +54,7 @@ public static void after() {
}

@Test
public void returnsProtocolsOfService() {
public void protocolsOfService() {
Model model = Model.assembler()
.addImport(getClass().getResource("service-index-loads-protocols.smithy"))
.assemble()
Expand All @@ -66,88 +68,166 @@ public void returnsProtocolsOfService() {
}

@Test
public void returnsAuthSchemesOfService() {
public void authSchemesOfService() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> auth = serviceIndex.getAuthSchemes(
ShapeId.from("smithy.example#ServiceWithNoAuthTrait"));

List<ShapeId> ids = new ArrayList<>(auth.keySet());
assertThat(ids, hasSize(3));
assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
ShapeId.from("smithy.example#ServiceWithoutAuthTrait"));
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
}

@Test
public void getsAuthSchemesOfServiceWithNoAuthTrait() {
public void authSchemesOfServiceWithoutAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
ShapeId.from("smithy.example#ServiceWithNoAuthTrait"));

List<ShapeId> ids = new ArrayList<>(auth.keySet());
assertThat(ids, hasSize(3));
assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
ShapeId.from("smithy.example#ServiceWithoutAuthTrait"));
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
}

@Test
public void getsAuthSchemesOfServiceWithAuthTrait() {
public void authSchemesOfServiceWithAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
ShapeId.from("smithy.example#ServiceWithAuthTrait"));

List<ShapeId> ids = new ArrayList<>(auth.keySet());
assertThat(auth.keySet(), hasSize(2));
assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
assertThat(ids.get(1), equalTo(HttpDigestAuthTrait.ID));
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
}

@Test
public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithNoAuthTrait() {
public void authSchemesOfServiceWithEmptyAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
ShapeId.from("smithy.example#ServiceWithNoAuthTrait"),
ShapeId.from("smithy.example#OperationWithNoAuthTrait"));
ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait"));
assertAuthSchemes(auth);
}

List<ShapeId> ids = new ArrayList<>(auth.keySet());
assertThat(ids, hasSize(3));
assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
@Test
public void authSchemesOfOperationWithoutAuthTraitAndServiceWithoutAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");

Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);

auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
}

@Test
public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithAuthTrait() {
public void authSchemesOfOperationWithoutAuthTraitAndServiceWithAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
ShapeId.from("smithy.example#ServiceWithAuthTrait"),
ShapeId.from("smithy.example#OperationWithNoAuthTrait"));
ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");

List<ShapeId> ids = new ArrayList<>(auth.keySet());
assertThat(ids, hasSize(2));
assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
assertThat(ids.get(1), equalTo(HttpDigestAuthTrait.ID));
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);

auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
}

@Test
public void getsAuthSchemesOfOperationWithAuthTrait() {
public void authSchemesOfOperationWithoutAuthTraitAndServiceWithEmptyAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
ShapeId service = ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait");
ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");

Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth);

auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, ServiceIndex.NoAuthTrait.ID);
}

@Test
public void authSchemesOfOperationWithAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
ShapeId.from("smithy.example#ServiceWithAuthTrait"),
ShapeId.from("smithy.example#OperationWithAuthTrait"));
assertAuthSchemes(auth, HttpDigestAuthTrait.ID);
}

@Test
public void authSchemesOfOperationWithEmptyAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
ShapeId operation = ShapeId.from("smithy.example#OperationWithEmptyAuthTrait");

Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth);

assertThat(auth.keySet(), hasSize(1));
assertThat(auth, hasKey(HttpDigestAuthTrait.ID));
auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, ServiceIndex.NoAuthTrait.ID);
}

@Test
public void returnsAnEmptyCollectionWhenTheServiceDoesNotExist() {
public void authSchemesOfOperationWithOptionalAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
ShapeId.from("smithy.example#Invalid"),
ShapeId.from("smithy.example#OperationWithAuthTrait"));
ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
ShapeId operation = ShapeId.from("smithy.example#OperationWithOptionalAuthTrait");

Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);

auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID, ServiceIndex.NoAuthTrait.ID);
}

// Test to assert that smithy.api#noAuth trait is not part of traits that are sorted alphabetically, but last.
// The authSchemesOfOperationWithOptionalAuthTrait() test above doesn't really assert that, because
// smithy.api#noAuth would have been last if included in sorting.
@Test
public void authSchemesOfOperationWithOptionalAuthTraitAndServiceWithoutAuthTrait() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
ShapeId operation = ShapeId.from("smithy.example#OperationWithOptionalAuthTrait");

assertThat(auth.keySet(), empty());
Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);

auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID,
ServiceIndex.NoAuthTrait.ID);
}

@Test
public void authSchemesOfInvalidService() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
ShapeId service = ShapeId.from("smithy.example#Invalid");

Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
assertAuthSchemes(auth);
}

@Test
public void authSchemesOfInvalidServiceWithInvalidOperation() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
ShapeId service = ShapeId.from("smithy.example#Invalid");
ShapeId operation = ShapeId.from("smithy.example#OperationWithAuthTrait");

Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth);

auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, ServiceIndex.NoAuthTrait.ID);
}

@Test
public void authSchemesOfServiceWithInvalidOperation() {
ServiceIndex serviceIndex = ServiceIndex.of(model);
ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
ShapeId operation = ShapeId.from("smithy.example#InvalidOperation");

Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth);

auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation);
assertAuthSchemes(auth, ServiceIndex.NoAuthTrait.ID);
}

private void assertAuthSchemes(Map<ShapeId, Trait> auth, ShapeId... authSchemes) {
List<ShapeId> ids = new ArrayList<>(auth.keySet());
assertThat(ids, hasSize(authSchemes.length));
assertThat(ids, equalTo(Arrays.asList(authSchemes)));
}
}
Loading

0 comments on commit de0801c

Please sign in to comment.