aws_smithy_runtime/client/orchestrator/
auth.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::client::auth::no_auth::NO_AUTH_SCHEME_ID;
7use crate::client::identity::IdentityCache;
8use aws_smithy_runtime_api::box_error::BoxError;
9use aws_smithy_runtime_api::client::auth::{
10    AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOptionResolverParams,
11    ResolveAuthSchemeOptions,
12};
13use aws_smithy_runtime_api::client::identity::ResolveIdentity;
14use aws_smithy_runtime_api::client::identity::{IdentityCacheLocation, ResolveCachedIdentity};
15use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
16use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
17use aws_smithy_types::config_bag::ConfigBag;
18use aws_smithy_types::endpoint::Endpoint;
19use aws_smithy_types::Document;
20use std::borrow::Cow;
21use std::error::Error as StdError;
22use std::fmt;
23use tracing::trace;
24
25#[derive(Debug)]
26struct NoMatchingAuthSchemeError(ExploredList);
27
28impl fmt::Display for NoMatchingAuthSchemeError {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        let explored = &self.0;
31
32        // Use the information we have about the auth options that were explored to construct
33        // as helpful of an error message as possible.
34        if explored.items().count() == 0 {
35            return f.write_str(
36                "no auth options are available. This can happen if there's \
37                    a problem with the service model, or if there is a codegen bug.",
38            );
39        }
40        if explored
41            .items()
42            .all(|explored| matches!(explored.result, ExploreResult::NoAuthScheme))
43        {
44            return f.write_str(
45                "no auth schemes are registered. This can happen if there's \
46                    a problem with the service model, or if there is a codegen bug.",
47            );
48        }
49
50        let mut try_add_identity = false;
51        let mut likely_bug = false;
52        f.write_str("failed to select an auth scheme to sign the request with.")?;
53        for item in explored.items() {
54            write!(
55                f,
56                " \"{}\" wasn't a valid option because ",
57                item.scheme_id.as_str()
58            )?;
59            f.write_str(match item.result {
60                ExploreResult::NoAuthScheme => {
61                    likely_bug = true;
62                    "no auth scheme was registered for it."
63                }
64                ExploreResult::NoIdentityResolver => {
65                    try_add_identity = true;
66                    "there was no identity resolver for it."
67                }
68                ExploreResult::MissingEndpointConfig => {
69                    likely_bug = true;
70                    "there is auth config in the endpoint config, but this scheme wasn't listed in it \
71                    (see https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details)."
72                }
73                ExploreResult::NotExplored => {
74                    debug_assert!(false, "this should be unreachable");
75                    "<unknown>"
76                }
77            })?;
78        }
79        if try_add_identity {
80            f.write_str(" Be sure to set an identity, such as credentials, auth token, or other identity type that is required for this service.")?;
81        } else if likely_bug {
82            f.write_str(" This is likely a bug.")?;
83        }
84        if explored.truncated {
85            f.write_str(" Note: there were other auth schemes that were evaluated that weren't listed here.")?;
86        }
87
88        Ok(())
89    }
90}
91
92impl StdError for NoMatchingAuthSchemeError {}
93
94#[derive(Debug)]
95enum AuthOrchestrationError {
96    MissingEndpointConfig,
97    BadAuthSchemeEndpointConfig(Cow<'static, str>),
98}
99
100impl fmt::Display for AuthOrchestrationError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            // This error is never bubbled up
104            Self::MissingEndpointConfig => f.write_str("missing endpoint config"),
105            Self::BadAuthSchemeEndpointConfig(message) => f.write_str(message),
106        }
107    }
108}
109
110impl StdError for AuthOrchestrationError {}
111
112pub(super) async fn orchestrate_auth(
113    ctx: &mut InterceptorContext,
114    runtime_components: &RuntimeComponents,
115    cfg: &ConfigBag,
116) -> Result<(), BoxError> {
117    let params = cfg
118        .load::<AuthSchemeOptionResolverParams>()
119        .expect("auth scheme option resolver params must be set");
120    let option_resolver = runtime_components.auth_scheme_option_resolver();
121    let options = option_resolver.resolve_auth_scheme_options(params)?;
122    let endpoint = cfg
123        .load::<Endpoint>()
124        .expect("endpoint added to config bag by endpoint orchestrator");
125
126    trace!(
127        auth_scheme_option_resolver_params = ?params,
128        auth_scheme_options = ?options,
129        "orchestrating auth",
130    );
131
132    let mut explored = ExploredList::default();
133
134    // Iterate over IDs of possibly-supported auth schemes
135    for &scheme_id in options.as_ref() {
136        // For each ID, try to resolve the corresponding auth scheme.
137        if let Some(auth_scheme) = runtime_components.auth_scheme(scheme_id) {
138            // Use the resolved auth scheme to resolve an identity
139            if let Some(identity_resolver) = auth_scheme.identity_resolver(runtime_components) {
140                let identity_cache = if identity_resolver.cache_location()
141                    == IdentityCacheLocation::RuntimeComponents
142                {
143                    runtime_components.identity_cache()
144                } else {
145                    IdentityCache::no_cache()
146                };
147                let signer = auth_scheme.signer();
148                trace!(
149                    auth_scheme = ?auth_scheme,
150                    identity_cache = ?identity_cache,
151                    identity_resolver = ?identity_resolver,
152                    signer = ?signer,
153                    "resolved auth scheme, identity cache, identity resolver, and signing implementation"
154                );
155
156                match extract_endpoint_auth_scheme_config(endpoint, scheme_id) {
157                    Ok(auth_scheme_endpoint_config) => {
158                        trace!(auth_scheme_endpoint_config = ?auth_scheme_endpoint_config, "extracted auth scheme endpoint config");
159
160                        let identity = identity_cache
161                            .resolve_cached_identity(identity_resolver, runtime_components, cfg)
162                            .await?;
163                        trace!(identity = ?identity, "resolved identity");
164
165                        trace!("signing request");
166                        let request = ctx.request_mut().expect("set during serialization");
167                        signer.sign_http_request(
168                            request,
169                            &identity,
170                            auth_scheme_endpoint_config,
171                            runtime_components,
172                            cfg,
173                        )?;
174                        return Ok(());
175                    }
176                    Err(AuthOrchestrationError::MissingEndpointConfig) => {
177                        explored.push(scheme_id, ExploreResult::MissingEndpointConfig);
178                        continue;
179                    }
180                    Err(other_err) => return Err(other_err.into()),
181                }
182            } else {
183                explored.push(scheme_id, ExploreResult::NoIdentityResolver);
184            }
185        } else {
186            explored.push(scheme_id, ExploreResult::NoAuthScheme);
187        }
188    }
189
190    Err(NoMatchingAuthSchemeError(explored).into())
191}
192
193fn extract_endpoint_auth_scheme_config(
194    endpoint: &Endpoint,
195    scheme_id: AuthSchemeId,
196) -> Result<AuthSchemeEndpointConfig<'_>, AuthOrchestrationError> {
197    // TODO(P96049742): Endpoint config doesn't currently have a concept of optional auth or "no auth", so
198    // we are short-circuiting lookup of endpoint auth scheme config if that is the selected scheme.
199    if scheme_id == NO_AUTH_SCHEME_ID {
200        return Ok(AuthSchemeEndpointConfig::empty());
201    }
202    let auth_schemes = match endpoint.properties().get("authSchemes") {
203        Some(Document::Array(schemes)) => schemes,
204        // no auth schemes:
205        None => return Ok(AuthSchemeEndpointConfig::empty()),
206        _other => {
207            return Err(AuthOrchestrationError::BadAuthSchemeEndpointConfig(
208                "expected an array for `authSchemes` in endpoint config".into(),
209            ))
210        }
211    };
212    let auth_scheme_config = auth_schemes
213        .iter()
214        .find(|doc| {
215            let config_scheme_id = doc
216                .as_object()
217                .and_then(|object| object.get("name"))
218                .and_then(Document::as_string);
219            config_scheme_id == Some(scheme_id.as_str())
220        })
221        .ok_or(AuthOrchestrationError::MissingEndpointConfig)?;
222    Ok(AuthSchemeEndpointConfig::from(Some(auth_scheme_config)))
223}
224
225#[derive(Debug)]
226enum ExploreResult {
227    NotExplored,
228    NoAuthScheme,
229    NoIdentityResolver,
230    MissingEndpointConfig,
231}
232
233/// Information about an evaluated auth option.
234/// This should be kept small so it can fit in an array on the stack.
235#[derive(Debug)]
236struct ExploredAuthOption {
237    scheme_id: AuthSchemeId,
238    result: ExploreResult,
239}
240impl Default for ExploredAuthOption {
241    fn default() -> Self {
242        Self {
243            scheme_id: AuthSchemeId::new(""),
244            result: ExploreResult::NotExplored,
245        }
246    }
247}
248
249const MAX_EXPLORED_LIST_LEN: usize = 8;
250
251/// Stack allocated list of explored auth options for error messaging
252#[derive(Default)]
253struct ExploredList {
254    items: [ExploredAuthOption; MAX_EXPLORED_LIST_LEN],
255    len: usize,
256    truncated: bool,
257}
258impl ExploredList {
259    fn items(&self) -> impl Iterator<Item = &ExploredAuthOption> {
260        self.items.iter().take(self.len)
261    }
262
263    fn push(&mut self, scheme_id: AuthSchemeId, result: ExploreResult) {
264        if self.len + 1 >= self.items.len() {
265            self.truncated = true;
266        } else {
267            self.items[self.len] = ExploredAuthOption { scheme_id, result };
268            self.len += 1;
269        }
270    }
271}
272impl fmt::Debug for ExploredList {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        f.debug_struct("ExploredList")
275            .field("items", &&self.items[0..self.len])
276            .field("truncated", &self.truncated)
277            .finish()
278    }
279}
280
281#[cfg(all(test, feature = "test-util"))]
282mod tests {
283    use super::*;
284    use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
285    use aws_smithy_runtime_api::client::auth::{
286        AuthScheme, AuthSchemeId, AuthSchemeOptionResolverParams, SharedAuthScheme,
287        SharedAuthSchemeOptionResolver, Sign,
288    };
289    use aws_smithy_runtime_api::client::identity::{
290        Identity, IdentityFuture, ResolveIdentity, SharedIdentityResolver,
291    };
292    use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
293    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
294    use aws_smithy_runtime_api::client::runtime_components::{
295        GetIdentityResolver, RuntimeComponents, RuntimeComponentsBuilder,
296    };
297    use aws_smithy_types::config_bag::Layer;
298    use std::collections::HashMap;
299
300    #[tokio::test]
301    async fn basic_case() {
302        #[derive(Debug)]
303        struct TestIdentityResolver;
304        impl ResolveIdentity for TestIdentityResolver {
305            fn resolve_identity<'a>(
306                &'a self,
307                _runtime_components: &'a RuntimeComponents,
308                _config_bag: &'a ConfigBag,
309            ) -> IdentityFuture<'a> {
310                IdentityFuture::ready(Ok(Identity::new("doesntmatter", None)))
311            }
312        }
313
314        #[derive(Debug)]
315        struct TestSigner;
316
317        impl Sign for TestSigner {
318            fn sign_http_request(
319                &self,
320                request: &mut HttpRequest,
321                _identity: &Identity,
322                _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
323                _runtime_components: &RuntimeComponents,
324                _config_bag: &ConfigBag,
325            ) -> Result<(), BoxError> {
326                request
327                    .headers_mut()
328                    .insert(http_02x::header::AUTHORIZATION, "success!");
329                Ok(())
330            }
331        }
332
333        const TEST_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("test-scheme");
334
335        #[derive(Debug)]
336        struct TestAuthScheme {
337            signer: TestSigner,
338        }
339        impl AuthScheme for TestAuthScheme {
340            fn scheme_id(&self) -> AuthSchemeId {
341                TEST_SCHEME_ID
342            }
343
344            fn identity_resolver(
345                &self,
346                identity_resolvers: &dyn GetIdentityResolver,
347            ) -> Option<SharedIdentityResolver> {
348                identity_resolvers.identity_resolver(self.scheme_id())
349            }
350
351            fn signer(&self) -> &dyn Sign {
352                &self.signer
353            }
354        }
355
356        let mut ctx = InterceptorContext::new(Input::doesnt_matter());
357        ctx.enter_serialization_phase();
358        ctx.set_request(HttpRequest::empty());
359        let _ = ctx.take_input();
360        ctx.enter_before_transmit_phase();
361
362        let runtime_components = RuntimeComponentsBuilder::for_tests()
363            .with_auth_scheme(SharedAuthScheme::new(TestAuthScheme { signer: TestSigner }))
364            .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
365                StaticAuthSchemeOptionResolver::new(vec![TEST_SCHEME_ID]),
366            )))
367            .with_identity_resolver(
368                TEST_SCHEME_ID,
369                SharedIdentityResolver::new(TestIdentityResolver),
370            )
371            .build()
372            .unwrap();
373
374        let mut layer: Layer = Layer::new("test");
375        layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
376        layer.store_put(Endpoint::builder().url("dontcare").build());
377        let cfg = ConfigBag::of_layers(vec![layer]);
378
379        orchestrate_auth(&mut ctx, &runtime_components, &cfg)
380            .await
381            .expect("success");
382
383        assert_eq!(
384            "success!",
385            ctx.request()
386                .expect("request is set")
387                .headers()
388                .get("Authorization")
389                .unwrap()
390        );
391    }
392
393    #[cfg(feature = "http-auth")]
394    #[tokio::test]
395    async fn select_best_scheme_for_available_identity_resolvers() {
396        use crate::client::auth::http::{BasicAuthScheme, BearerAuthScheme};
397        use aws_smithy_runtime_api::client::auth::http::{
398            HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
399        };
400        use aws_smithy_runtime_api::client::identity::http::{Login, Token};
401
402        let mut ctx = InterceptorContext::new(Input::doesnt_matter());
403        ctx.enter_serialization_phase();
404        ctx.set_request(HttpRequest::empty());
405        let _ = ctx.take_input();
406        ctx.enter_before_transmit_phase();
407
408        fn config_with_identity(
409            scheme_id: AuthSchemeId,
410            identity: impl ResolveIdentity + 'static,
411        ) -> (RuntimeComponents, ConfigBag) {
412            let runtime_components = RuntimeComponentsBuilder::for_tests()
413                .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new()))
414                .with_auth_scheme(SharedAuthScheme::new(BearerAuthScheme::new()))
415                .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
416                    StaticAuthSchemeOptionResolver::new(vec![
417                        HTTP_BASIC_AUTH_SCHEME_ID,
418                        HTTP_BEARER_AUTH_SCHEME_ID,
419                    ]),
420                )))
421                .with_identity_resolver(scheme_id, SharedIdentityResolver::new(identity))
422                .build()
423                .unwrap();
424
425            let mut layer = Layer::new("test");
426            layer.store_put(Endpoint::builder().url("dontcare").build());
427            layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
428
429            (runtime_components, ConfigBag::of_layers(vec![layer]))
430        }
431
432        // First, test the presence of a basic auth login and absence of a bearer token
433        let (runtime_components, cfg) =
434            config_with_identity(HTTP_BASIC_AUTH_SCHEME_ID, Login::new("a", "b", None));
435        orchestrate_auth(&mut ctx, &runtime_components, &cfg)
436            .await
437            .expect("success");
438        assert_eq!(
439            // "YTpi" == "a:b" in base64
440            "Basic YTpi",
441            ctx.request()
442                .expect("request is set")
443                .headers()
444                .get("Authorization")
445                .unwrap()
446        );
447
448        // Next, test the presence of a bearer token and absence of basic auth
449        let (runtime_components, cfg) =
450            config_with_identity(HTTP_BEARER_AUTH_SCHEME_ID, Token::new("t", None));
451        let mut ctx = InterceptorContext::new(Input::erase("doesnt-matter"));
452        ctx.enter_serialization_phase();
453        ctx.set_request(HttpRequest::empty());
454        let _ = ctx.take_input();
455        ctx.enter_before_transmit_phase();
456        orchestrate_auth(&mut ctx, &runtime_components, &cfg)
457            .await
458            .expect("success");
459        assert_eq!(
460            "Bearer t",
461            ctx.request()
462                .expect("request is set")
463                .headers()
464                .get("Authorization")
465                .unwrap()
466        );
467    }
468
469    #[test]
470    fn extract_endpoint_auth_scheme_config_no_config() {
471        let endpoint = Endpoint::builder()
472            .url("dontcare")
473            .property("something-unrelated", Document::Null)
474            .build();
475        let config = extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
476            .expect("success");
477        assert!(config.as_document().is_none());
478    }
479
480    #[test]
481    fn extract_endpoint_auth_scheme_config_wrong_type() {
482        let endpoint = Endpoint::builder()
483            .url("dontcare")
484            .property("authSchemes", Document::String("bad".into()))
485            .build();
486        extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
487            .expect_err("should fail because authSchemes is the wrong type");
488    }
489
490    #[test]
491    fn extract_endpoint_auth_scheme_config_no_matching_scheme() {
492        let endpoint = Endpoint::builder()
493            .url("dontcare")
494            .property(
495                "authSchemes",
496                vec![
497                    Document::Object({
498                        let mut out = HashMap::new();
499                        out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
500                        out
501                    }),
502                    Document::Object({
503                        let mut out = HashMap::new();
504                        out.insert(
505                            "name".to_string(),
506                            "another-wrong-scheme-id".to_string().into(),
507                        );
508                        out
509                    }),
510                ],
511            )
512            .build();
513        extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
514            .expect_err("should fail because authSchemes doesn't include the desired scheme");
515    }
516
517    #[test]
518    fn extract_endpoint_auth_scheme_config_successfully() {
519        let endpoint = Endpoint::builder()
520            .url("dontcare")
521            .property(
522                "authSchemes",
523                vec![
524                    Document::Object({
525                        let mut out = HashMap::new();
526                        out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
527                        out
528                    }),
529                    Document::Object({
530                        let mut out = HashMap::new();
531                        out.insert("name".to_string(), "test-scheme-id".to_string().into());
532                        out.insert(
533                            "magicString".to_string(),
534                            "magic string value".to_string().into(),
535                        );
536                        out
537                    }),
538                ],
539            )
540            .build();
541        let config = extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
542            .expect("should find test-scheme-id");
543        assert_eq!(
544            "magic string value",
545            config
546                .as_document()
547                .expect("config is set")
548                .as_object()
549                .expect("it's an object")
550                .get("magicString")
551                .expect("magicString is set")
552                .as_string()
553                .expect("gimme the string, dammit!")
554        );
555    }
556
557    #[cfg(feature = "http-auth")]
558    #[tokio::test]
559    async fn use_identity_cache() {
560        use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation};
561        use aws_smithy_runtime_api::client::auth::http::HTTP_API_KEY_AUTH_SCHEME_ID;
562        use aws_smithy_runtime_api::client::identity::http::Token;
563        use aws_smithy_types::body::SdkBody;
564
565        let mut ctx = InterceptorContext::new(Input::doesnt_matter());
566        ctx.enter_serialization_phase();
567        ctx.set_request(
568            http_02x::Request::builder()
569                .body(SdkBody::empty())
570                .unwrap()
571                .try_into()
572                .unwrap(),
573        );
574        let _ = ctx.take_input();
575        ctx.enter_before_transmit_phase();
576
577        #[derive(Debug)]
578        struct Cache;
579        impl ResolveCachedIdentity for Cache {
580            fn resolve_cached_identity<'a>(
581                &'a self,
582                _resolver: SharedIdentityResolver,
583                _: &'a RuntimeComponents,
584                _config_bag: &'a ConfigBag,
585            ) -> IdentityFuture<'a> {
586                IdentityFuture::ready(Ok(Identity::new(Token::new("cached (pass)", None), None)))
587            }
588        }
589
590        let runtime_components = RuntimeComponentsBuilder::for_tests()
591            .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new(
592                "result:",
593                ApiKeyLocation::Header,
594                "Authorization",
595            )))
596            .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
597                StaticAuthSchemeOptionResolver::new(vec![HTTP_API_KEY_AUTH_SCHEME_ID]),
598            )))
599            .with_identity_cache(Some(Cache))
600            .with_identity_resolver(
601                HTTP_API_KEY_AUTH_SCHEME_ID,
602                SharedIdentityResolver::new(Token::new("uncached (fail)", None)),
603            )
604            .build()
605            .unwrap();
606        let mut layer = Layer::new("test");
607        layer.store_put(Endpoint::builder().url("dontcare").build());
608        layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
609        let config_bag = ConfigBag::of_layers(vec![layer]);
610
611        orchestrate_auth(&mut ctx, &runtime_components, &config_bag)
612            .await
613            .expect("success");
614        assert_eq!(
615            "result: cached (pass)",
616            ctx.request()
617                .expect("request is set")
618                .headers()
619                .get("Authorization")
620                .unwrap()
621        );
622    }
623
624    #[test]
625    fn friendly_error_messages() {
626        let err = NoMatchingAuthSchemeError(ExploredList::default());
627        assert_eq!(
628            "no auth options are available. This can happen if there's a problem with \
629            the service model, or if there is a codegen bug.",
630            err.to_string()
631        );
632
633        let mut list = ExploredList::default();
634        list.push(
635            AuthSchemeId::new("SigV4"),
636            ExploreResult::NoIdentityResolver,
637        );
638        list.push(
639            AuthSchemeId::new("SigV4a"),
640            ExploreResult::NoIdentityResolver,
641        );
642        let err = NoMatchingAuthSchemeError(list);
643        assert_eq!(
644            "failed to select an auth scheme to sign the request with. \
645            \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
646            \"SigV4a\" wasn't a valid option because there was no identity resolver for it. \
647            Be sure to set an identity, such as credentials, auth token, or other identity \
648            type that is required for this service.",
649            err.to_string()
650        );
651
652        // It should prioritize the suggestion to try an identity before saying it's a bug
653        let mut list = ExploredList::default();
654        list.push(
655            AuthSchemeId::new("SigV4"),
656            ExploreResult::NoIdentityResolver,
657        );
658        list.push(
659            AuthSchemeId::new("SigV4a"),
660            ExploreResult::MissingEndpointConfig,
661        );
662        let err = NoMatchingAuthSchemeError(list);
663        assert_eq!(
664            "failed to select an auth scheme to sign the request with. \
665            \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
666            \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
667            config, but this scheme wasn't listed in it (see \
668            https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
669            Be sure to set an identity, such as credentials, auth token, or other identity \
670            type that is required for this service.",
671            err.to_string()
672        );
673
674        // Otherwise, it should suggest it's a bug
675        let mut list = ExploredList::default();
676        list.push(
677            AuthSchemeId::new("SigV4a"),
678            ExploreResult::MissingEndpointConfig,
679        );
680        let err = NoMatchingAuthSchemeError(list);
681        assert_eq!(
682            "failed to select an auth scheme to sign the request with. \
683            \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
684            config, but this scheme wasn't listed in it (see \
685            https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
686            This is likely a bug.",
687            err.to_string()
688        );
689
690        // Truncation should be indicated
691        let mut list = ExploredList::default();
692        for _ in 0..=MAX_EXPLORED_LIST_LEN {
693            list.push(
694                AuthSchemeId::new("dontcare"),
695                ExploreResult::MissingEndpointConfig,
696            );
697        }
698        let err = NoMatchingAuthSchemeError(list).to_string();
699        if !err.contains(
700            "Note: there were other auth schemes that were evaluated that weren't listed here",
701        ) {
702            panic!("The error should indicate that the explored list was truncated.");
703        }
704    }
705}