diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java index 0c32dc009dd..dcaf11e0807 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java @@ -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. @@ -198,6 +200,41 @@ public Map 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. + * + *

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. + * + *

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. + * + *

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 getNoAuthAwareEffectiveAuthSchemes(ToShapeId service, ToShapeId operation) { + LinkedHashMap 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 getAuthTraitValues(Shape service, Shape subject) { if (!subject.hasTrait(AuthTrait.class)) { return null; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java new file mode 100644 index 00000000000..4fa22797a89 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java @@ -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; + } +} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java index e7e899a6fdd..781adb4558d 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java @@ -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; @@ -36,6 +36,8 @@ public class ServiceIndexTest { + private static final ShapeId CUSTOM_AUTH_ID = ShapeId.from("smithy.example#customAuth"); + private static Model model; @BeforeAll @@ -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() @@ -66,88 +68,166 @@ public void returnsProtocolsOfService() { } @Test - public void returnsAuthSchemesOfService() { + public void authSchemesOfService() { ServiceIndex serviceIndex = ServiceIndex.of(model); Map auth = serviceIndex.getAuthSchemes( - ShapeId.from("smithy.example#ServiceWithNoAuthTrait")); - - List 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 auth = serviceIndex.getEffectiveAuthSchemes( - ShapeId.from("smithy.example#ServiceWithNoAuthTrait")); - - List 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 auth = serviceIndex.getEffectiveAuthSchemes( ShapeId.from("smithy.example#ServiceWithAuthTrait")); - - List 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 auth = serviceIndex.getEffectiveAuthSchemes( - ShapeId.from("smithy.example#ServiceWithNoAuthTrait"), - ShapeId.from("smithy.example#OperationWithNoAuthTrait")); + ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait")); + assertAuthSchemes(auth); + } - List 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 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 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 ids = new ArrayList<>(auth.keySet()); - assertThat(ids, hasSize(2)); - assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID)); - assertThat(ids.get(1), equalTo(HttpDigestAuthTrait.ID)); + Map 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 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 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 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 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 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 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 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 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 auth = serviceIndex.getEffectiveAuthSchemes(service, operation); + assertAuthSchemes(auth); + + auth = serviceIndex.getNoAuthAwareEffectiveAuthSchemes(service, operation); + assertAuthSchemes(auth, ServiceIndex.NoAuthTrait.ID); + } + + private void assertAuthSchemes(Map auth, ShapeId... authSchemes) { + List ids = new ArrayList<>(auth.keySet()); + assertThat(ids, hasSize(authSchemes.length)); + assertThat(ids, equalTo(Arrays.asList(authSchemes))); } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy index 95d5d6c8a5c..4f3127e113c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy @@ -2,17 +2,21 @@ $version: "2" namespace smithy.example +@customAuth @httpBasicAuth @httpDigestAuth @httpBearerAuth -service ServiceWithNoAuthTrait { +service ServiceWithoutAuthTrait { version: "2020-01-29", operations: [ - OperationWithNoAuthTrait, - OperationWithAuthTrait + OperationWithoutAuthTrait, + OperationWithAuthTrait, + OperationWithEmptyAuthTrait, + OperationWithOptionalAuthTrait ] } +@customAuth @httpBasicAuth @httpDigestAuth @httpBearerAuth @@ -20,12 +24,46 @@ service ServiceWithNoAuthTrait { service ServiceWithAuthTrait { version: "2020-01-29", operations: [ - OperationWithNoAuthTrait, - OperationWithAuthTrait + OperationWithoutAuthTrait, + OperationWithAuthTrait, + OperationWithEmptyAuthTrait, + OperationWithOptionalAuthTrait ] } -operation OperationWithNoAuthTrait {} +@customAuth +@httpBasicAuth +@httpDigestAuth +@httpBearerAuth +@auth([]) +service ServiceWithEmptyAuthTrait { + version: "2020-01-29", + operations: [ + OperationWithoutAuthTrait, + OperationWithAuthTrait, + OperationWithEmptyAuthTrait, + OperationWithOptionalAuthTrait + ] +} + +operation OperationWithoutAuthTrait {} @auth([httpDigestAuth]) operation OperationWithAuthTrait {} + +@auth([]) +operation OperationWithEmptyAuthTrait {} + +@optionalAuth +operation OperationWithOptionalAuthTrait {} + +// Defining a custom trait, to assert that alphabetical sorting of traits takes namespace into account, as well as +// smithy.api#noAuth is added to the end, and not included in sorting. +@trait( + selector: "service" + breakingChanges: [ + {change: "remove"} + ] +) +@authDefinition +structure customAuth {}