1use 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, AuthSchemeOption,
11 AuthSchemeOptionResolverParams, AuthSchemePreference, ResolveAuthSchemeOptions,
12};
13use aws_smithy_runtime_api::client::endpoint::{EndpointResolverParams, ResolveEndpoint};
14use aws_smithy_runtime_api::client::identity::{Identity, ResolveIdentity};
15use aws_smithy_runtime_api::client::identity::{IdentityCacheLocation, ResolveCachedIdentity};
16use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
17use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
18use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Storable, StoreReplace};
19use aws_smithy_types::endpoint::Endpoint;
20use aws_smithy_types::Document;
21use std::borrow::Cow;
22use std::collections::HashMap;
23use std::error::Error as StdError;
24use std::fmt;
25use tracing::trace;
26
27#[derive(Debug)]
28struct NoMatchingAuthSchemeError(ExploredList);
29
30impl fmt::Display for NoMatchingAuthSchemeError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 let explored = &self.0;
33
34 if explored.items().count() == 0 {
37 return f.write_str(
38 "no auth options are available. This can happen if there's \
39 a problem with the service model, or if there is a codegen bug.",
40 );
41 }
42 if explored
43 .items()
44 .all(|explored| matches!(explored.result, ExploreResult::NoAuthScheme))
45 {
46 return f.write_str(
47 "no auth schemes are registered. This can happen if there's \
48 a problem with the service model, or if there is a codegen bug.",
49 );
50 }
51
52 let mut try_add_identity = false;
53 let mut likely_bug = false;
54 f.write_str("failed to select an auth scheme to sign the request with.")?;
55 for item in explored.items() {
56 write!(
57 f,
58 " \"{}\" wasn't a valid option because ",
59 item.scheme_id.inner()
60 )?;
61 f.write_str(match item.result {
62 ExploreResult::NoAuthScheme => {
63 likely_bug = true;
64 "no auth scheme was registered for it."
65 }
66 ExploreResult::NoIdentityResolver => {
67 try_add_identity = true;
68 "there was no identity resolver for it."
69 }
70 ExploreResult::MissingEndpointConfig => {
71 likely_bug = true;
72 "there is auth config in the endpoint config, but this scheme wasn't listed in it \
73 (see https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details)."
74 }
75 ExploreResult::NotExplored => {
76 debug_assert!(false, "this should be unreachable");
77 "<unknown>"
78 }
79 })?;
80 }
81 if try_add_identity {
82 f.write_str(" Be sure to set an identity, such as credentials, auth token, or other identity type that is required for this service.")?;
83 } else if likely_bug {
84 f.write_str(" This is likely a bug.")?;
85 }
86 if explored.truncated {
87 f.write_str(" Note: there were other auth schemes that were evaluated that weren't listed here.")?;
88 }
89
90 Ok(())
91 }
92}
93
94impl StdError for NoMatchingAuthSchemeError {}
95
96#[derive(Debug)]
97enum AuthOrchestrationError {
98 MissingEndpointConfig,
99 BadAuthSchemeEndpointConfig(Cow<'static, str>),
100 FailedToResolveEndpoint(BoxError),
101}
102
103impl fmt::Display for AuthOrchestrationError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::MissingEndpointConfig => f.write_str("missing endpoint config"),
108 Self::BadAuthSchemeEndpointConfig(message) => f.write_str(message),
109 Self::FailedToResolveEndpoint(source) => {
110 write!(f, "failed to resolve endpoint: {source}")
111 }
112 }
113 }
114}
115
116impl StdError for AuthOrchestrationError {}
117
118pub(super) async fn resolve_identity(
119 runtime_components: &RuntimeComponents,
120 cfg: &mut ConfigBag,
121) -> Result<(AuthSchemeId, Identity, Option<Endpoint>), BoxError> {
122 let params = cfg
123 .load::<AuthSchemeOptionResolverParams>()
124 .expect("auth scheme option resolver params must be set");
125 let option_resolver = runtime_components.auth_scheme_option_resolver();
126 let options = option_resolver
127 .resolve_auth_scheme_options_v2(params, cfg, runtime_components)
128 .await?;
129 let options =
130 reprioritize_with_auth_scheme_preference(options, cfg.load::<AuthSchemePreference>()).await;
131
132 trace!(
133 auth_scheme_option_resolver_params = ?params,
134 auth_scheme_options = ?options,
135 "orchestrating auth",
136 );
137
138 let mut explored = ExploredList::default();
139
140 for auth_scheme_option in &options {
142 let scheme_id = auth_scheme_option.scheme_id();
143 if let Some(auth_scheme) = runtime_components.auth_scheme(scheme_id) {
145 if let Some(identity_resolver) = auth_scheme.identity_resolver(runtime_components) {
147 match legacy_try_resolve_endpoint(runtime_components, cfg, scheme_id).await {
148 Ok(endpoint) => {
149 trace!(scheme_id= ?scheme_id, "resolving identity");
150 let identity_cache = if identity_resolver.cache_location()
151 == IdentityCacheLocation::RuntimeComponents
152 {
153 runtime_components.identity_cache()
154 } else {
155 IdentityCache::no_cache()
156 };
157 if let Some(properties) = auth_scheme_option.properties() {
159 cfg.push_shared_layer(properties);
160 }
161 let identity = identity_cache
162 .resolve_cached_identity(identity_resolver, runtime_components, cfg)
163 .await?;
164 trace!(identity = ?identity, "resolved identity");
165 if let Some(layer) = identity.property::<FrozenLayer>().cloned() {
168 cfg.push_shared_layer(layer);
169 }
170 return Ok((scheme_id.clone(), identity, endpoint));
171 }
172 Err(AuthOrchestrationError::MissingEndpointConfig) => {
173 explored.push(scheme_id.clone(), ExploreResult::MissingEndpointConfig);
174 continue;
175 }
176 Err(AuthOrchestrationError::FailedToResolveEndpoint(source)) => {
177 return Err(source);
180 }
181 Err(other_err) => {
182 return Err(other_err.into());
183 }
184 }
185 } else {
186 explored.push(scheme_id.clone(), ExploreResult::NoIdentityResolver);
187 }
188 } else {
189 explored.push(scheme_id.clone(), ExploreResult::NoAuthScheme);
190 }
191 }
192
193 Err(NoMatchingAuthSchemeError(explored).into())
194}
195
196async fn reprioritize_with_auth_scheme_preference(
200 supported_auth_scheme_options: Vec<AuthSchemeOption>,
201 auth_scheme_preference: Option<&AuthSchemePreference>,
202) -> Vec<AuthSchemeOption> {
203 match auth_scheme_preference {
204 Some(preference) => {
205 let preference_map: HashMap<_, _> = preference
207 .clone()
208 .into_iter()
209 .enumerate()
210 .map(|(i, s)| (s, i))
211 .collect();
212 let (mut preferred, non_preferred): (Vec<_>, Vec<_>) = supported_auth_scheme_options
213 .into_iter()
214 .partition(|auth_scheme_option| {
215 preference_map.contains_key(auth_scheme_option.scheme_id())
216 });
217
218 preferred.sort_by_key(|opt| {
219 preference_map
220 .get(opt.scheme_id())
221 .expect("guaranteed by `partition`")
222 });
223 preferred.extend(non_preferred);
224 preferred
225 }
226 None => supported_auth_scheme_options,
227 }
228}
229
230pub(super) fn sign_request(
231 scheme_id: &AuthSchemeId,
232 identity: &Identity,
233 ctx: &mut InterceptorContext,
234 runtime_components: &RuntimeComponents,
235 cfg: &ConfigBag,
236) -> Result<(), BoxError> {
237 trace!("signing request");
238 let request = ctx.request_mut().expect("set during serialization");
239 let endpoint = cfg
240 .load::<Endpoint>()
241 .expect("endpoint added to config bag by endpoint orchestrator");
242 let auth_scheme = runtime_components
243 .auth_scheme(scheme_id)
244 .ok_or("should be configured")?;
245 let signer = auth_scheme.signer();
246 let auth_scheme_endpoint_config = extract_endpoint_auth_scheme_config(endpoint, scheme_id)?;
247 trace!(
248 signer = ?signer,
249 "signing implementation"
250 );
251 signer.sign_http_request(
252 request,
253 identity,
254 auth_scheme_endpoint_config,
255 runtime_components,
256 cfg,
257 )?;
258 Ok(())
259}
260
261#[doc(hidden)]
277#[derive(Clone, Debug)]
278pub struct AuthSchemeAndEndpointOrchestrationV2;
279
280impl Storable for AuthSchemeAndEndpointOrchestrationV2 {
281 type Storer = StoreReplace<Self>;
282}
283
284async fn legacy_try_resolve_endpoint(
294 runtime_components: &RuntimeComponents,
295 cfg: &ConfigBag,
296 scheme_id: &AuthSchemeId,
297) -> Result<Option<Endpoint>, AuthOrchestrationError> {
298 if cfg.load::<AuthSchemeAndEndpointOrchestrationV2>().is_some() {
299 return Ok(None);
302 }
303
304 let params = cfg
305 .load::<EndpointResolverParams>()
306 .expect("endpoint resolver params must be set");
307
308 tracing::debug!(scheme_id = ?scheme_id, endpoint_params = ?params, "using legacy auth and endpoint orchestration, resolving endpoint for auth scheme selection");
309
310 let endpoint = runtime_components
311 .endpoint_resolver()
312 .resolve_endpoint(params)
313 .await
314 .map_err(AuthOrchestrationError::FailedToResolveEndpoint)?;
315
316 let _ = extract_endpoint_auth_scheme_config(&endpoint, scheme_id)?;
323
324 Ok(Some(endpoint))
325}
326
327fn extract_endpoint_auth_scheme_config<'a>(
328 endpoint: &'a Endpoint,
329 scheme_id: &AuthSchemeId,
330) -> Result<AuthSchemeEndpointConfig<'a>, AuthOrchestrationError> {
331 if scheme_id == &NO_AUTH_SCHEME_ID {
334 return Ok(AuthSchemeEndpointConfig::empty());
335 }
336 let auth_schemes = match endpoint.properties().get("authSchemes") {
337 Some(Document::Array(schemes)) => schemes,
338 None => return Ok(AuthSchemeEndpointConfig::empty()),
340 _other => {
341 return Err(AuthOrchestrationError::BadAuthSchemeEndpointConfig(
342 "expected an array for `authSchemes` in endpoint config".into(),
343 ))
344 }
345 };
346 let auth_scheme_config = auth_schemes
347 .iter()
348 .find(|doc| {
349 let config_scheme_id = doc
350 .as_object()
351 .and_then(|object| object.get("name"))
352 .and_then(Document::as_string);
353 config_scheme_id == Some(scheme_id.inner())
354 })
355 .ok_or(AuthOrchestrationError::MissingEndpointConfig)?;
356 Ok(AuthSchemeEndpointConfig::from(Some(auth_scheme_config)))
357}
358
359#[derive(Debug)]
360enum ExploreResult {
361 NotExplored,
362 NoAuthScheme,
363 NoIdentityResolver,
364 MissingEndpointConfig,
365}
366
367#[derive(Debug)]
370struct ExploredAuthOption {
371 scheme_id: AuthSchemeId,
372 result: ExploreResult,
373}
374impl Default for ExploredAuthOption {
375 fn default() -> Self {
376 Self {
377 scheme_id: AuthSchemeId::new(""),
378 result: ExploreResult::NotExplored,
379 }
380 }
381}
382
383const MAX_EXPLORED_LIST_LEN: usize = 8;
384
385#[derive(Default)]
387struct ExploredList {
388 items: [ExploredAuthOption; MAX_EXPLORED_LIST_LEN],
389 len: usize,
390 truncated: bool,
391}
392impl ExploredList {
393 fn items(&self) -> impl Iterator<Item = &ExploredAuthOption> {
394 self.items.iter().take(self.len)
395 }
396
397 fn push(&mut self, scheme_id: AuthSchemeId, result: ExploreResult) {
398 if self.len + 1 >= self.items.len() {
399 self.truncated = true;
400 } else {
401 self.items[self.len] = ExploredAuthOption { scheme_id, result };
402 self.len += 1;
403 }
404 }
405}
406impl fmt::Debug for ExploredList {
407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408 f.debug_struct("ExploredList")
409 .field("items", &&self.items[0..self.len])
410 .field("truncated", &self.truncated)
411 .finish()
412 }
413}
414
415#[cfg(all(test, any(feature = "test-util", feature = "legacy-test-util")))]
416mod tests {
417 use super::*;
418 use crate::client::orchestrator::endpoints::{
419 StaticUriEndpointResolver, StaticUriEndpointResolverParams,
420 };
421 use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
422 use aws_smithy_runtime_api::client::auth::{
423 AuthScheme, AuthSchemeId, AuthSchemeOptionResolverParams, SharedAuthScheme,
424 SharedAuthSchemeOptionResolver, Sign,
425 };
426 use aws_smithy_runtime_api::client::endpoint::SharedEndpointResolver;
427 use aws_smithy_runtime_api::client::identity::{
428 Identity, IdentityFuture, ResolveIdentity, SharedIdentityResolver,
429 };
430 use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
431 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
432 use aws_smithy_runtime_api::client::runtime_components::{
433 GetIdentityResolver, RuntimeComponents, RuntimeComponentsBuilder,
434 };
435 use aws_smithy_types::config_bag::Layer;
436 use std::collections::HashMap;
437
438 #[tokio::test]
439 async fn basic_case() {
440 #[derive(Debug)]
441 struct TestIdentityResolver;
442 impl ResolveIdentity for TestIdentityResolver {
443 fn resolve_identity<'a>(
444 &'a self,
445 _runtime_components: &'a RuntimeComponents,
446 _config_bag: &'a ConfigBag,
447 ) -> IdentityFuture<'a> {
448 IdentityFuture::ready(Ok(Identity::new("doesntmatter", None)))
449 }
450 }
451
452 #[derive(Debug)]
453 struct TestSigner;
454
455 impl Sign for TestSigner {
456 fn sign_http_request(
457 &self,
458 request: &mut HttpRequest,
459 _identity: &Identity,
460 _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
461 _runtime_components: &RuntimeComponents,
462 _config_bag: &ConfigBag,
463 ) -> Result<(), BoxError> {
464 request
465 .headers_mut()
466 .insert(http_02x::header::AUTHORIZATION, "success!");
467 Ok(())
468 }
469 }
470
471 const TEST_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("test-scheme");
472
473 #[derive(Debug)]
474 struct TestAuthScheme {
475 signer: TestSigner,
476 }
477 impl AuthScheme for TestAuthScheme {
478 fn scheme_id(&self) -> AuthSchemeId {
479 TEST_SCHEME_ID
480 }
481
482 fn identity_resolver(
483 &self,
484 identity_resolvers: &dyn GetIdentityResolver,
485 ) -> Option<SharedIdentityResolver> {
486 identity_resolvers.identity_resolver(self.scheme_id())
487 }
488
489 fn signer(&self) -> &dyn Sign {
490 &self.signer
491 }
492 }
493
494 async fn run_test(add_more_to_layer: impl Fn(Layer) -> Layer) {
495 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
496 ctx.enter_serialization_phase();
497 ctx.set_request(HttpRequest::empty());
498 let _ = ctx.take_input();
499 ctx.enter_before_transmit_phase();
500
501 let runtime_components = RuntimeComponentsBuilder::for_tests()
502 .with_auth_scheme(SharedAuthScheme::new(TestAuthScheme { signer: TestSigner }))
503 .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
504 StaticAuthSchemeOptionResolver::new(vec![TEST_SCHEME_ID]),
505 )))
506 .with_identity_resolver(
507 TEST_SCHEME_ID,
508 SharedIdentityResolver::new(TestIdentityResolver),
509 )
510 .with_endpoint_resolver(Some(SharedEndpointResolver::new(
511 StaticUriEndpointResolver::http_localhost(8080),
512 )))
513 .build()
514 .unwrap();
515
516 let mut layer: Layer = Layer::new("test");
517 layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
518 layer.store_put(Endpoint::builder().url("dontcare").build());
519 let layer = add_more_to_layer(layer);
520 let mut cfg = ConfigBag::of_layers(vec![layer]);
521
522 let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut cfg)
523 .await
524 .expect("success");
525
526 sign_request(&scheme_id, &identity, &mut ctx, &runtime_components, &cfg)
527 .expect("success");
528
529 assert_eq!(
530 "success!",
531 ctx.request()
532 .expect("request is set")
533 .headers()
534 .get("Authorization")
535 .unwrap()
536 );
537 }
538
539 run_test(|mut layer| {
541 layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
542 layer
543 })
544 .await;
545
546 run_test(|mut layer| {
548 layer.store_put(EndpointResolverParams::from(
549 StaticUriEndpointResolverParams::new(),
550 ));
551 layer
552 })
553 .await;
554 }
555
556 #[cfg(feature = "http-auth")]
557 #[tokio::test]
558 async fn select_best_scheme_for_available_identity_resolvers() {
559 use crate::client::auth::http::{BasicAuthScheme, BearerAuthScheme};
560 use aws_smithy_runtime_api::client::auth::http::{
561 HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
562 };
563 use aws_smithy_runtime_api::client::identity::http::{Login, Token};
564
565 async fn run_test(add_more_to_layer: impl Fn(Layer) -> Layer) {
566 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
567 ctx.enter_serialization_phase();
568 ctx.set_request(HttpRequest::empty());
569 let _ = ctx.take_input();
570 ctx.enter_before_transmit_phase();
571
572 let (runtime_components, layer) =
574 config_with_identity(HTTP_BASIC_AUTH_SCHEME_ID, Login::new("a", "b", None));
575 let layer = add_more_to_layer(layer);
576 let mut cfg = ConfigBag::of_layers(vec![layer]);
577
578 let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut cfg)
579 .await
580 .expect("success");
581 sign_request(&scheme_id, &identity, &mut ctx, &runtime_components, &cfg)
582 .expect("success");
583 assert_eq!(
584 "Basic YTpi",
586 ctx.request()
587 .expect("request is set")
588 .headers()
589 .get("Authorization")
590 .unwrap()
591 );
592
593 let (runtime_components, layer) =
595 config_with_identity(HTTP_BEARER_AUTH_SCHEME_ID, Token::new("t", None));
596 let layer = add_more_to_layer(layer);
597 let mut cfg = ConfigBag::of_layers(vec![layer]);
598 let mut ctx = InterceptorContext::new(Input::erase("doesnt-matter"));
599 ctx.enter_serialization_phase();
600 ctx.set_request(HttpRequest::empty());
601 let _ = ctx.take_input();
602 ctx.enter_before_transmit_phase();
603 let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut cfg)
604 .await
605 .expect("success");
606 sign_request(&scheme_id, &identity, &mut ctx, &runtime_components, &cfg)
607 .expect("success");
608 assert_eq!(
609 "Bearer t",
610 ctx.request()
611 .expect("request is set")
612 .headers()
613 .get("Authorization")
614 .unwrap()
615 );
616 }
617
618 fn config_with_identity(
619 scheme_id: AuthSchemeId,
620 identity: impl ResolveIdentity + 'static,
621 ) -> (RuntimeComponents, Layer) {
622 let runtime_components = RuntimeComponentsBuilder::for_tests()
623 .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new()))
624 .with_auth_scheme(SharedAuthScheme::new(BearerAuthScheme::new()))
625 .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
626 StaticAuthSchemeOptionResolver::new(vec![
627 HTTP_BASIC_AUTH_SCHEME_ID,
628 HTTP_BEARER_AUTH_SCHEME_ID,
629 ]),
630 )))
631 .with_identity_resolver(scheme_id, SharedIdentityResolver::new(identity))
632 .with_endpoint_resolver(Some(SharedEndpointResolver::new(
633 StaticUriEndpointResolver::http_localhost(8080),
634 )))
635 .build()
636 .unwrap();
637
638 let mut layer = Layer::new("test");
639 layer.store_put(Endpoint::builder().url("dontcare").build());
640 layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
641
642 (runtime_components, layer)
643 }
644
645 run_test(|mut layer| {
647 layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
648 layer
649 })
650 .await;
651
652 run_test(|mut layer| {
654 layer.store_put(EndpointResolverParams::from(
655 StaticUriEndpointResolverParams::new(),
656 ));
657 layer
658 })
659 .await;
660 }
661
662 #[test]
663 fn extract_endpoint_auth_scheme_config_no_config() {
664 let endpoint = Endpoint::builder()
665 .url("dontcare")
666 .property("something-unrelated", Document::Null)
667 .build();
668 let config =
669 extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
670 .expect("success");
671 assert!(config.as_document().is_none());
672 }
673
674 #[test]
675 fn extract_endpoint_auth_scheme_config_wrong_type() {
676 let endpoint = Endpoint::builder()
677 .url("dontcare")
678 .property("authSchemes", Document::String("bad".into()))
679 .build();
680 extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
681 .expect_err("should fail because authSchemes is the wrong type");
682 }
683
684 #[test]
685 fn extract_endpoint_auth_scheme_config_no_matching_scheme() {
686 let endpoint = Endpoint::builder()
687 .url("dontcare")
688 .property(
689 "authSchemes",
690 vec![
691 Document::Object({
692 let mut out = HashMap::new();
693 out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
694 out
695 }),
696 Document::Object({
697 let mut out = HashMap::new();
698 out.insert(
699 "name".to_string(),
700 "another-wrong-scheme-id".to_string().into(),
701 );
702 out
703 }),
704 ],
705 )
706 .build();
707 extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
708 .expect_err("should fail because authSchemes doesn't include the desired scheme");
709 }
710
711 #[test]
712 fn extract_endpoint_auth_scheme_config_successfully() {
713 let endpoint = Endpoint::builder()
714 .url("dontcare")
715 .property(
716 "authSchemes",
717 vec![
718 Document::Object({
719 let mut out = HashMap::new();
720 out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
721 out
722 }),
723 Document::Object({
724 let mut out = HashMap::new();
725 out.insert("name".to_string(), "test-scheme-id".to_string().into());
726 out.insert(
727 "magicString".to_string(),
728 "magic string value".to_string().into(),
729 );
730 out
731 }),
732 ],
733 )
734 .build();
735 let config =
736 extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
737 .expect("should find test-scheme-id");
738 assert_eq!(
739 "magic string value",
740 config
741 .as_document()
742 .expect("config is set")
743 .as_object()
744 .expect("it's an object")
745 .get("magicString")
746 .expect("magicString is set")
747 .as_string()
748 .expect("gimme the string, dammit!")
749 );
750 }
751
752 #[cfg(feature = "http-auth")]
753 #[tokio::test]
754 async fn use_identity_cache() {
755 use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation};
756 use aws_smithy_runtime_api::client::auth::http::HTTP_API_KEY_AUTH_SCHEME_ID;
757 use aws_smithy_runtime_api::client::identity::http::Token;
758 use aws_smithy_types::body::SdkBody;
759
760 #[derive(Debug)]
761 struct Cache;
762 impl ResolveCachedIdentity for Cache {
763 fn resolve_cached_identity<'a>(
764 &'a self,
765 _resolver: SharedIdentityResolver,
766 _: &'a RuntimeComponents,
767 _config_bag: &'a ConfigBag,
768 ) -> IdentityFuture<'a> {
769 IdentityFuture::ready(Ok(Identity::new(Token::new("cached (pass)", None), None)))
770 }
771 }
772
773 async fn run_test(add_more_to_layer: impl Fn(Layer) -> Layer) {
774 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
775 ctx.enter_serialization_phase();
776 ctx.set_request(
777 http_02x::Request::builder()
778 .body(SdkBody::empty())
779 .unwrap()
780 .try_into()
781 .unwrap(),
782 );
783 let _ = ctx.take_input();
784 ctx.enter_before_transmit_phase();
785
786 let runtime_components = RuntimeComponentsBuilder::for_tests()
787 .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new(
788 "result:",
789 ApiKeyLocation::Header,
790 "Authorization",
791 )))
792 .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
793 StaticAuthSchemeOptionResolver::new(vec![HTTP_API_KEY_AUTH_SCHEME_ID]),
794 )))
795 .with_identity_cache(Some(Cache))
796 .with_identity_resolver(
797 HTTP_API_KEY_AUTH_SCHEME_ID,
798 SharedIdentityResolver::new(Token::new("uncached (fail)", None)),
799 )
800 .with_endpoint_resolver(Some(SharedEndpointResolver::new(
801 StaticUriEndpointResolver::http_localhost(8080),
802 )))
803 .build()
804 .unwrap();
805 let mut layer = Layer::new("test");
806 layer.store_put(Endpoint::builder().url("dontcare").build());
807 layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
808 let layer = add_more_to_layer(layer);
809 let mut config_bag = ConfigBag::of_layers(vec![layer]);
810
811 let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut config_bag)
812 .await
813 .expect("success");
814 sign_request(
815 &scheme_id,
816 &identity,
817 &mut ctx,
818 &runtime_components,
819 &config_bag,
820 )
821 .expect("success");
822 assert_eq!(
823 "result: cached (pass)",
824 ctx.request()
825 .expect("request is set")
826 .headers()
827 .get("Authorization")
828 .unwrap()
829 );
830 }
831
832 run_test(|mut layer| {
834 layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
835 layer
836 })
837 .await;
838
839 run_test(|mut layer| {
841 layer.store_put(EndpointResolverParams::from(
842 StaticUriEndpointResolverParams::new(),
843 ));
844 layer
845 })
846 .await;
847 }
848
849 #[test]
850 fn friendly_error_messages() {
851 let err = NoMatchingAuthSchemeError(ExploredList::default());
852 assert_eq!(
853 "no auth options are available. This can happen if there's a problem with \
854 the service model, or if there is a codegen bug.",
855 err.to_string()
856 );
857
858 let mut list = ExploredList::default();
859 list.push(
860 AuthSchemeId::new("SigV4"),
861 ExploreResult::NoIdentityResolver,
862 );
863 list.push(
864 AuthSchemeId::new("SigV4a"),
865 ExploreResult::NoIdentityResolver,
866 );
867 let err = NoMatchingAuthSchemeError(list);
868 assert_eq!(
869 "failed to select an auth scheme to sign the request with. \
870 \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
871 \"SigV4a\" wasn't a valid option because there was no identity resolver for it. \
872 Be sure to set an identity, such as credentials, auth token, or other identity \
873 type that is required for this service.",
874 err.to_string()
875 );
876
877 let mut list = ExploredList::default();
879 list.push(
880 AuthSchemeId::new("SigV4"),
881 ExploreResult::NoIdentityResolver,
882 );
883 list.push(
884 AuthSchemeId::new("SigV4a"),
885 ExploreResult::MissingEndpointConfig,
886 );
887 let err = NoMatchingAuthSchemeError(list);
888 assert_eq!(
889 "failed to select an auth scheme to sign the request with. \
890 \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
891 \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
892 config, but this scheme wasn't listed in it (see \
893 https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
894 Be sure to set an identity, such as credentials, auth token, or other identity \
895 type that is required for this service.",
896 err.to_string()
897 );
898
899 let mut list = ExploredList::default();
901 list.push(
902 AuthSchemeId::new("SigV4a"),
903 ExploreResult::MissingEndpointConfig,
904 );
905 let err = NoMatchingAuthSchemeError(list);
906 assert_eq!(
907 "failed to select an auth scheme to sign the request with. \
908 \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
909 config, but this scheme wasn't listed in it (see \
910 https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
911 This is likely a bug.",
912 err.to_string()
913 );
914
915 let mut list = ExploredList::default();
917 for _ in 0..=MAX_EXPLORED_LIST_LEN {
918 list.push(
919 AuthSchemeId::new("dontcare"),
920 ExploreResult::MissingEndpointConfig,
921 );
922 }
923 let err = NoMatchingAuthSchemeError(list).to_string();
924 if !err.contains(
925 "Note: there were other auth schemes that were evaluated that weren't listed here",
926 ) {
927 panic!("The error should indicate that the explored list was truncated.");
928 }
929 }
930
931 #[cfg(feature = "http-auth")]
932 #[tokio::test]
933 async fn test_resolve_identity() {
934 use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation, BasicAuthScheme};
935 use aws_smithy_runtime_api::client::auth::http::{
936 HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID,
937 };
938 use aws_smithy_runtime_api::client::identity::http::Token;
939 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
940
941 #[derive(Debug)]
942 struct Cache;
943 impl ResolveCachedIdentity for Cache {
944 fn resolve_cached_identity<'a>(
945 &'a self,
946 identity_resolver: SharedIdentityResolver,
947 rc: &'a RuntimeComponents,
948 cfg: &'a ConfigBag,
949 ) -> IdentityFuture<'a> {
950 IdentityFuture::new(
951 async move { identity_resolver.resolve_identity(rc, cfg).await },
952 )
953 }
954 }
955
956 let mut layer = Layer::new("test");
957 layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
958 layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
959 let mut cfg = ConfigBag::of_layers(vec![layer]);
960
961 let runtime_components_builder = RuntimeComponentsBuilder::for_tests()
962 .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new()))
963 .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new(
964 "result:",
965 ApiKeyLocation::Header,
966 "Authorization",
967 )))
968 .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
969 StaticAuthSchemeOptionResolver::new(vec![
970 HTTP_BASIC_AUTH_SCHEME_ID,
971 HTTP_API_KEY_AUTH_SCHEME_ID,
972 ]),
973 )))
974 .with_identity_cache(Some(Cache));
975
976 struct TestCase {
977 builder_updater: Box<dyn Fn(RuntimeComponentsBuilder) -> RuntimeComponents>,
978 resolved_auth_scheme: AuthSchemeId,
979 should_error: bool,
980 }
981
982 for test_case in [
983 TestCase {
984 builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
985 rcb.with_identity_resolver(
986 HTTP_BASIC_AUTH_SCHEME_ID,
987 SharedIdentityResolver::new(Token::new("basic", None)),
988 )
989 .with_identity_resolver(
990 HTTP_API_KEY_AUTH_SCHEME_ID,
991 SharedIdentityResolver::new(Token::new("api-key", None)),
992 )
993 .build()
994 .unwrap()
995 }),
996 resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID,
997 should_error: false,
998 },
999 TestCase {
1000 builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
1001 rcb.with_identity_resolver(
1002 HTTP_BASIC_AUTH_SCHEME_ID,
1003 SharedIdentityResolver::new(Token::new("basic", None)),
1004 )
1005 .build()
1006 .unwrap()
1007 }),
1008 resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID,
1009 should_error: false,
1010 },
1011 TestCase {
1012 builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
1013 rcb.with_identity_resolver(
1014 HTTP_API_KEY_AUTH_SCHEME_ID,
1015 SharedIdentityResolver::new(Token::new("api-key", None)),
1016 )
1017 .build()
1018 .unwrap()
1019 }),
1020 resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID,
1021 should_error: false,
1022 },
1023 TestCase {
1024 builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| rcb.build().unwrap()),
1025 resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID,
1026 should_error: true,
1027 },
1028 ]
1029 .into_iter()
1030 {
1031 let runtime_components =
1032 (test_case.builder_updater)(runtime_components_builder.clone());
1033 match resolve_identity(&runtime_components, &mut cfg).await {
1034 Ok(resolved) => assert_eq!(test_case.resolved_auth_scheme, resolved.0),
1035 Err(e) if test_case.should_error => {
1036 assert!(e.downcast_ref::<NoMatchingAuthSchemeError>().is_some());
1037 }
1038 _ => {
1039 panic!("`resolve_identity` returned an `Err` when no error was expected in the test.");
1040 }
1041 }
1042 }
1043 }
1044
1045 #[cfg(feature = "http-auth")]
1046 #[tokio::test]
1047 async fn auth_scheme_preference() {
1048 use aws_smithy_runtime_api::client::auth::http::{
1049 HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
1050 };
1051
1052 struct TestCase {
1053 supported: Vec<AuthSchemeOption>,
1054 preference: Option<AuthSchemePreference>,
1055 expected_resolved_auths: Vec<AuthSchemeId>,
1056 }
1057
1058 for test_case in [
1059 TestCase {
1060 supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1061 .map(AuthSchemeOption::from)
1062 .to_vec(),
1063 preference: None,
1064 expected_resolved_auths: vec![
1065 HTTP_API_KEY_AUTH_SCHEME_ID,
1066 HTTP_BASIC_AUTH_SCHEME_ID,
1067 ],
1068 },
1069 TestCase {
1070 supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1071 .map(AuthSchemeOption::from)
1072 .to_vec(),
1073 preference: Some([].into()),
1074 expected_resolved_auths: vec![
1075 HTTP_API_KEY_AUTH_SCHEME_ID,
1076 HTTP_BASIC_AUTH_SCHEME_ID,
1077 ],
1078 },
1079 TestCase {
1080 supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1081 .map(AuthSchemeOption::from)
1082 .to_vec(),
1083 preference: Some(["bogus"].map(AuthSchemeId::from).into()),
1084 expected_resolved_auths: vec![
1085 HTTP_API_KEY_AUTH_SCHEME_ID,
1086 HTTP_BASIC_AUTH_SCHEME_ID,
1087 ],
1088 },
1089 TestCase {
1090 supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1091 .map(AuthSchemeOption::from)
1092 .to_vec(),
1093 preference: Some([HTTP_BASIC_AUTH_SCHEME_ID].into()),
1094 expected_resolved_auths: vec![
1095 HTTP_BASIC_AUTH_SCHEME_ID,
1096 HTTP_API_KEY_AUTH_SCHEME_ID,
1097 ],
1098 },
1099 TestCase {
1100 supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1101 .map(AuthSchemeOption::from)
1102 .to_vec(),
1103 preference: Some([HTTP_BASIC_AUTH_SCHEME_ID, HTTP_API_KEY_AUTH_SCHEME_ID].into()),
1104 expected_resolved_auths: vec![
1105 HTTP_BASIC_AUTH_SCHEME_ID,
1106 HTTP_API_KEY_AUTH_SCHEME_ID,
1107 ],
1108 },
1109 TestCase {
1110 supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1111 .map(AuthSchemeOption::from)
1112 .to_vec(),
1113 preference: Some(
1114 [
1115 HTTP_BASIC_AUTH_SCHEME_ID,
1116 HTTP_BEARER_AUTH_SCHEME_ID,
1117 HTTP_API_KEY_AUTH_SCHEME_ID,
1118 ]
1119 .into(),
1120 ),
1121 expected_resolved_auths: vec![
1122 HTTP_BASIC_AUTH_SCHEME_ID,
1123 HTTP_API_KEY_AUTH_SCHEME_ID,
1124 ],
1125 },
1126 ] {
1127 let actual = reprioritize_with_auth_scheme_preference(
1128 test_case.supported,
1129 test_case.preference.as_ref(),
1130 )
1131 .await;
1132 let actual = actual
1133 .iter()
1134 .map(|opt| opt.scheme_id())
1135 .cloned()
1136 .collect::<Vec<_>>();
1137 assert_eq!(test_case.expected_resolved_auths, actual);
1138 }
1139 }
1140}