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..b458719ed0c 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,11 @@ 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; +import software.amazon.smithy.utils.MapUtils; /** * An index that resolves service protocols and auth schemes. @@ -63,6 +66,25 @@ public static ServiceIndex of(Model model) { return model.getKnowledge(ServiceIndex.class, ServiceIndex::new); } + /** + * Defines the type of auth schemes returned by {@link #getEffectiveAuthSchemes}. + */ + public enum AuthSchemeMode { + + /** + * Use only the modeled auth schemes. This is the default. + */ + MODELED, + + /** + * Use the modeled auth schemes, as well as the synthetic {@link NoAuthTrait} where applicable. + * + *

The Smithy Reference Architecture recommends using the {@code smithy.api#noAuth} auth scheme to represent + * no authentication which is available as the {@link NoAuthTrait}. + */ + NO_AUTH_AWARE; + } + /** * Get all protocol traits attached to a service. * @@ -154,6 +176,30 @@ public Map getEffectiveAuthSchemes(ToShapeId service) { .orElse(Collections.emptyMap()); } + /** + * Gets a list of effective authentication schemes applied to a service, based on the AuthSchemeMode. + * + *

If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as + * {@link #getEffectiveAuthSchemes(ToShapeId)}. + * + *

If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, except that if the service has no effective + * auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth} auth scheme. It avoids having to + * special case handling an empty result. The returned map will always contain at least 1 entry. + * + * @param service Service to get the effective authentication schemes of. + * @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include. + * @return Returns a map of the trait shape ID to the auth trait itself. + */ + public Map getEffectiveAuthSchemes(ToShapeId service, AuthSchemeMode authSchemeMode) { + Map authSchemes = getEffectiveAuthSchemes(service); + if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) { + if (authSchemes.isEmpty()) { + authSchemes = MapUtils.of(NoAuthTrait.ID, new NoAuthTrait()); + } + } + return authSchemes; + } + /** * Gets a list of effective authentication schemes applied to an operation * bound within a service. @@ -198,7 +244,49 @@ public Map getEffectiveAuthSchemes(ToShapeId service, ToShapeId .orElse(Collections.emptyMap()); } - private Map getAuthTraitValues(Shape service, Shape subject) { + /** + * Gets a list of effective authentication schemes applied to an operation + * bound within a service, based on the AuthSchemeMode. + * + *

If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as + * {@link #getEffectiveAuthSchemes(ToShapeId, ToShapeId)}. + * + *

If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, 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. + * + *

Using {@code NO_AUTH_AWARE} accounts for {@code smithy.api#optionalAuth} and avoids having to special case + * handling an 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. + * @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include. + * @return Returns a map of the trait shape ID to the auth trait itself. + */ + public Map getEffectiveAuthSchemes(ToShapeId service, + ToShapeId operation, + AuthSchemeMode authSchemeMode) { + Map authSchemes = getEffectiveAuthSchemes(service, operation); + if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) { + if (authSchemes.isEmpty() || hasOptionalAuth(operation)) { + authSchemes = new LinkedHashMap<>(authSchemes); + 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 static 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..d49a472e4fa --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java @@ -0,0 +1,41 @@ +/* + * Copyright 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.traits.synthetic; + +import software.amazon.smithy.model.knowledge.ServiceIndex; +import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +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#getEffectiveAuthSchemes} with + * {@link AuthSchemeMode#NO_AUTH_AWARE}. + */ +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..e58285e6923 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,13 @@ 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 static software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode.NO_AUTH_AWARE; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import org.junit.jupiter.api.AfterAll; @@ -33,9 +34,12 @@ import software.amazon.smithy.model.traits.HttpBearerAuthTrait; import software.amazon.smithy.model.traits.HttpDigestAuthTrait; import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.synthetic.NoAuthTrait; public class ServiceIndexTest { + private static final ShapeId CUSTOM_AUTH_ID = ShapeId.from("smithy.example#customAuth"); + private static Model model; @BeforeAll @@ -52,7 +56,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 +70,193 @@ public void returnsProtocolsOfService() { } @Test - public void returnsAuthSchemesOfService() { + public void authSchemesOfService() { ServiceIndex serviceIndex = ServiceIndex.of(model); Map auth = serviceIndex.getAuthSchemes( - ShapeId.from("smithy.example#ServiceWithNoAuthTrait")); + ShapeId.from("smithy.example#ServiceWithoutAuthTrait")); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID); + } - 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 authSchemesOfServiceWithoutAuthTrait() { + ServiceIndex serviceIndex = ServiceIndex.of(model); + ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait"); + + Map auth = serviceIndex.getEffectiveAuthSchemes(service); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID); + + auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID); } @Test - public void getsAuthSchemesOfServiceWithNoAuthTrait() { + public void authSchemesOfServiceWithAuthTrait() { ServiceIndex serviceIndex = ServiceIndex.of(model); - Map auth = serviceIndex.getEffectiveAuthSchemes( - ShapeId.from("smithy.example#ServiceWithNoAuthTrait")); + ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait"); - 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)); + Map auth = serviceIndex.getEffectiveAuthSchemes(service); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID); + + auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID); } @Test - public void getsAuthSchemesOfServiceWithAuthTrait() { + public void authSchemesOfServiceWithEmptyAuthTrait() { ServiceIndex serviceIndex = ServiceIndex.of(model); - Map auth = serviceIndex.getEffectiveAuthSchemes( - ShapeId.from("smithy.example#ServiceWithAuthTrait")); + ShapeId service = ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait"); - 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)); + Map auth = serviceIndex.getEffectiveAuthSchemes(service); + assertAuthSchemes(auth); + + auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE); + assertAuthSchemes(auth, NoAuthTrait.ID); } @Test - public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithNoAuthTrait() { + public void authSchemesOfServiceWithoutAuthDefinitionTraits() { ServiceIndex serviceIndex = ServiceIndex.of(model); - Map auth = serviceIndex.getEffectiveAuthSchemes( - ShapeId.from("smithy.example#ServiceWithNoAuthTrait"), - ShapeId.from("smithy.example#OperationWithNoAuthTrait")); + ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthDefinitionTraits"); - 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)); + Map auth = serviceIndex.getEffectiveAuthSchemes(service); + assertAuthSchemes(auth); + + auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE); + assertAuthSchemes(auth, NoAuthTrait.ID); } @Test - public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithAuthTrait() { + public void authSchemesOfOperationWithoutAuthTraitAndServiceWithoutAuthTrait() { 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#ServiceWithoutAuthTrait"); + 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, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID); + + auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID); } @Test - public void getsAuthSchemesOfOperationWithAuthTrait() { + public void authSchemesOfOperationWithoutAuthTraitAndServiceWithAuthTrait() { + ServiceIndex serviceIndex = ServiceIndex.of(model); + ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait"); + ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait"); + + Map auth = serviceIndex.getEffectiveAuthSchemes(service, operation); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID); + + auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID); + } + + @Test + 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.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, 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); + } - assertThat(auth.keySet(), hasSize(1)); - assertThat(auth, hasKey(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); + + auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, 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.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID, 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"); + + Map auth = serviceIndex.getEffectiveAuthSchemes(service, operation); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID); + + auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID, + 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); - assertThat(auth.keySet(), empty()); + auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE); + assertAuthSchemes(auth, NoAuthTrait.ID); + } + + @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.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, 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.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE); + assertAuthSchemes(auth, 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..1a2ccb168b3 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,30 +2,77 @@ $version: "2" namespace smithy.example +@customAuth @httpBasicAuth @httpDigestAuth @httpBearerAuth -service ServiceWithNoAuthTrait { - version: "2020-01-29", +service ServiceWithoutAuthTrait { + version: "2020-01-29" operations: [ - OperationWithNoAuthTrait, + OperationWithoutAuthTrait OperationWithAuthTrait + OperationWithEmptyAuthTrait + OperationWithOptionalAuthTrait ] } +@customAuth @httpBasicAuth @httpDigestAuth @httpBearerAuth @auth([httpBasicAuth, httpDigestAuth]) service ServiceWithAuthTrait { - version: "2020-01-29", + version: "2020-01-29" operations: [ - OperationWithNoAuthTrait, + OperationWithoutAuthTrait OperationWithAuthTrait + OperationWithEmptyAuthTrait + OperationWithOptionalAuthTrait ] } -operation OperationWithNoAuthTrait {} +@customAuth +@httpBasicAuth +@httpDigestAuth +@httpBearerAuth +@auth([]) +service ServiceWithEmptyAuthTrait { + version: "2020-01-29" + operations: [ + OperationWithoutAuthTrait + OperationWithAuthTrait + OperationWithEmptyAuthTrait + OperationWithOptionalAuthTrait + ] +} + +service ServiceWithoutAuthDefinitionTraits { + version: "2020-01-29" + operations: [ + OperationWithoutAuthTrait + 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 {}