1 1 | /*
|
2 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 3 | * SPDX-License-Identifier: Apache-2.0
|
4 4 | */
|
5 5 |
|
6 6 | use crate::client::auth::no_auth::NO_AUTH_SCHEME_ID;
|
7 7 | use crate::client::identity::IdentityCache;
|
8 8 | use aws_smithy_runtime_api::box_error::BoxError;
|
9 9 | use aws_smithy_runtime_api::client::auth::{
|
10 - | AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOptionResolverParams,
|
11 - | ResolveAuthSchemeOptions,
|
10 + | AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOption,
|
11 + | AuthSchemeOptionResolverParams, AuthSchemePreference, ResolveAuthSchemeOptions,
|
12 12 | };
|
13 13 | use aws_smithy_runtime_api::client::endpoint::{EndpointResolverParams, ResolveEndpoint};
|
14 14 | use aws_smithy_runtime_api::client::identity::{Identity, ResolveIdentity};
|
15 15 | use aws_smithy_runtime_api::client::identity::{IdentityCacheLocation, ResolveCachedIdentity};
|
16 16 | use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
|
17 17 | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
|
18 18 | use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
|
19 19 | use aws_smithy_types::endpoint::Endpoint;
|
20 20 | use aws_smithy_types::Document;
|
21 21 | use std::borrow::Cow;
|
22 + | use std::collections::HashMap;
|
22 23 | use std::error::Error as StdError;
|
23 24 | use std::fmt;
|
24 25 | use tracing::trace;
|
25 26 |
|
26 27 | #[derive(Debug)]
|
27 28 | struct NoMatchingAuthSchemeError(ExploredList);
|
28 29 |
|
29 30 | impl fmt::Display for NoMatchingAuthSchemeError {
|
30 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
31 32 | let explored = &self.0;
|
118 119 | runtime_components: &RuntimeComponents,
|
119 120 | cfg: &mut ConfigBag,
|
120 121 | ) -> Result<(AuthSchemeId, Identity, Option<Endpoint>), BoxError> {
|
121 122 | let params = cfg
|
122 123 | .load::<AuthSchemeOptionResolverParams>()
|
123 124 | .expect("auth scheme option resolver params must be set");
|
124 125 | let option_resolver = runtime_components.auth_scheme_option_resolver();
|
125 126 | let options = option_resolver
|
126 127 | .resolve_auth_scheme_options_v2(params, cfg, runtime_components)
|
127 128 | .await?;
|
129 + | let options =
|
130 + | reprioritize_with_auth_scheme_preference(options, cfg.load::<AuthSchemePreference>()).await;
|
128 131 |
|
129 132 | trace!(
|
130 133 | auth_scheme_option_resolver_params = ?params,
|
131 134 | auth_scheme_options = ?options,
|
132 135 | "orchestrating auth",
|
133 136 | );
|
134 137 |
|
135 138 | let mut explored = ExploredList::default();
|
136 139 |
|
137 140 | // Iterate over IDs of possibly-supported auth schemes
|
138 141 | for auth_scheme_option in &options {
|
139 142 | let scheme_id = auth_scheme_option.scheme_id();
|
140 143 | // For each ID, try to resolve the corresponding auth scheme.
|
141 144 | if let Some(auth_scheme) = runtime_components.auth_scheme(scheme_id) {
|
142 145 | // Use the resolved auth scheme to resolve an identity
|
143 146 | if let Some(identity_resolver) = auth_scheme.identity_resolver(runtime_components) {
|
144 147 | match legacy_try_resolve_endpoint(runtime_components, cfg, scheme_id).await {
|
145 148 | Ok(endpoint) => {
|
146 149 | trace!(scheme_id= ?scheme_id, "resolving identity");
|
147 150 | let identity_cache = if identity_resolver.cache_location()
|
148 151 | == IdentityCacheLocation::RuntimeComponents
|
149 152 | {
|
150 153 | runtime_components.identity_cache()
|
151 154 | } else {
|
152 155 | IdentityCache::no_cache()
|
153 156 | };
|
154 157 | // Apply properties from the selected auth scheme option
|
155 158 | if let Some(properties) = auth_scheme_option.properties() {
|
156 159 | cfg.push_shared_layer(properties);
|
157 160 | }
|
158 161 | let identity = identity_cache
|
159 162 | .resolve_cached_identity(identity_resolver, runtime_components, cfg)
|
160 163 | .await?;
|
161 164 | trace!(identity = ?identity, "resolved identity");
|
162 165 | return Ok((scheme_id.clone(), identity, endpoint));
|
163 166 | }
|
164 167 | Err(AuthOrchestrationError::MissingEndpointConfig) => {
|
165 168 | explored.push(scheme_id.clone(), ExploreResult::MissingEndpointConfig);
|
166 169 | continue;
|
167 170 | }
|
168 171 | Err(AuthOrchestrationError::FailedToResolveEndpoint(source)) => {
|
169 172 | // Some negative endpoint tests expect an endpoint resolution error,
|
170 173 | // so we need to return it to satisfy them.
|
171 174 | return Err(source);
|
172 175 | }
|
173 176 | Err(other_err) => {
|
174 177 | return Err(other_err.into());
|
175 178 | }
|
176 179 | }
|
177 180 | } else {
|
178 181 | explored.push(scheme_id.clone(), ExploreResult::NoIdentityResolver);
|
179 182 | }
|
180 183 | } else {
|
181 184 | explored.push(scheme_id.clone(), ExploreResult::NoAuthScheme);
|
182 185 | }
|
183 186 | }
|
184 187 |
|
185 188 | Err(NoMatchingAuthSchemeError(explored).into())
|
186 189 | }
|
187 190 |
|
191 + | // Re-prioritize `supported_auth_scheme_options` based on `auth_scheme_preference`
|
192 + | //
|
193 + | // Schemes in `auth_scheme_preference` that are not present in `supported_auth_scheme_options` will be ignored.
|
194 + | async fn reprioritize_with_auth_scheme_preference(
|
195 + | supported_auth_scheme_options: Vec<AuthSchemeOption>,
|
196 + | auth_scheme_preference: Option<&AuthSchemePreference>,
|
197 + | ) -> Vec<AuthSchemeOption> {
|
198 + | match auth_scheme_preference {
|
199 + | Some(preference) => {
|
200 + | // maps auth scheme ID to the index in the preference list
|
201 + | let preference_map: HashMap<_, _> = preference
|
202 + | .clone()
|
203 + | .into_iter()
|
204 + | .enumerate()
|
205 + | .map(|(i, s)| (s, i))
|
206 + | .collect();
|
207 + | let (mut preferred, non_preferred): (Vec<_>, Vec<_>) = supported_auth_scheme_options
|
208 + | .into_iter()
|
209 + | .partition(|auth_scheme_option| {
|
210 + | preference_map.contains_key(auth_scheme_option.scheme_id())
|
211 + | });
|
212 + |
|
213 + | preferred.sort_by_key(|opt| {
|
214 + | preference_map
|
215 + | .get(opt.scheme_id())
|
216 + | .expect("guaranteed by `partition`")
|
217 + | });
|
218 + | preferred.extend(non_preferred);
|
219 + | preferred
|
220 + | }
|
221 + | None => supported_auth_scheme_options,
|
222 + | }
|
223 + | }
|
224 + |
|
188 225 | pub(super) fn sign_request(
|
189 226 | scheme_id: &AuthSchemeId,
|
190 227 | identity: &Identity,
|
191 228 | ctx: &mut InterceptorContext,
|
192 229 | runtime_components: &RuntimeComponents,
|
193 230 | cfg: &ConfigBag,
|
194 231 | ) -> Result<(), BoxError> {
|
195 232 | trace!("signing request");
|
196 233 | let request = ctx.request_mut().expect("set during serialization");
|
197 234 | let endpoint = cfg
|
878 915 | ExploreResult::MissingEndpointConfig,
|
879 916 | );
|
880 917 | }
|
881 918 | let err = NoMatchingAuthSchemeError(list).to_string();
|
882 919 | if !err.contains(
|
883 920 | "Note: there were other auth schemes that were evaluated that weren't listed here",
|
884 921 | ) {
|
885 922 | panic!("The error should indicate that the explored list was truncated.");
|
886 923 | }
|
887 924 | }
|
925 + |
|
926 + | #[cfg(feature = "http-auth")]
|
927 + | #[tokio::test]
|
928 + | async fn test_resolve_identity() {
|
929 + | use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation, BasicAuthScheme};
|
930 + | use aws_smithy_runtime_api::client::auth::http::{
|
931 + | HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID,
|
932 + | };
|
933 + | use aws_smithy_runtime_api::client::identity::http::Token;
|
934 + | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
|
935 + |
|
936 + | #[derive(Debug)]
|
937 + | struct Cache;
|
938 + | impl ResolveCachedIdentity for Cache {
|
939 + | fn resolve_cached_identity<'a>(
|
940 + | &'a self,
|
941 + | identity_resolver: SharedIdentityResolver,
|
942 + | rc: &'a RuntimeComponents,
|
943 + | cfg: &'a ConfigBag,
|
944 + | ) -> IdentityFuture<'a> {
|
945 + | IdentityFuture::new(
|
946 + | async move { identity_resolver.resolve_identity(rc, cfg).await },
|
947 + | )
|
948 + | }
|
949 + | }
|
950 + |
|
951 + | let mut layer = Layer::new("test");
|
952 + | layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
|
953 + | layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
|
954 + | let mut cfg = ConfigBag::of_layers(vec![layer]);
|
955 + |
|
956 + | let runtime_components_builder = RuntimeComponentsBuilder::for_tests()
|
957 + | .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new()))
|
958 + | .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new(
|
959 + | "result:",
|
960 + | ApiKeyLocation::Header,
|
961 + | "Authorization",
|
962 + | )))
|
963 + | .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
|
964 + | StaticAuthSchemeOptionResolver::new(vec![
|
965 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
966 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
967 + | ]),
|
968 + | )))
|
969 + | .with_identity_cache(Some(Cache));
|
970 + |
|
971 + | struct TestCase {
|
972 + | builder_updater: Box<dyn Fn(RuntimeComponentsBuilder) -> RuntimeComponents>,
|
973 + | resolved_auth_scheme: AuthSchemeId,
|
974 + | should_error: bool,
|
975 + | }
|
976 + |
|
977 + | for test_case in [
|
978 + | TestCase {
|
979 + | builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
|
980 + | rcb.with_identity_resolver(
|
981 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
982 + | SharedIdentityResolver::new(Token::new("basic", None)),
|
983 + | )
|
984 + | .with_identity_resolver(
|
985 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
986 + | SharedIdentityResolver::new(Token::new("api-key", None)),
|
987 + | )
|
988 + | .build()
|
989 + | .unwrap()
|
990 + | }),
|
991 + | resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID,
|
992 + | should_error: false,
|
993 + | },
|
994 + | TestCase {
|
995 + | builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
|
996 + | rcb.with_identity_resolver(
|
997 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
998 + | SharedIdentityResolver::new(Token::new("basic", None)),
|
999 + | )
|
1000 + | .build()
|
1001 + | .unwrap()
|
1002 + | }),
|
1003 + | resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID,
|
1004 + | should_error: false,
|
1005 + | },
|
1006 + | TestCase {
|
1007 + | builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
|
1008 + | rcb.with_identity_resolver(
|
1009 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1010 + | SharedIdentityResolver::new(Token::new("api-key", None)),
|
1011 + | )
|
1012 + | .build()
|
1013 + | .unwrap()
|
1014 + | }),
|
1015 + | resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID,
|
1016 + | should_error: false,
|
1017 + | },
|
1018 + | TestCase {
|
1019 + | builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| rcb.build().unwrap()),
|
1020 + | resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID,
|
1021 + | should_error: true,
|
1022 + | },
|
1023 + | ]
|
1024 + | .into_iter()
|
1025 + | {
|
1026 + | let runtime_components =
|
1027 + | (test_case.builder_updater)(runtime_components_builder.clone());
|
1028 + | match resolve_identity(&runtime_components, &mut cfg).await {
|
1029 + | Ok(resolved) => assert_eq!(test_case.resolved_auth_scheme, resolved.0),
|
1030 + | Err(e) if test_case.should_error => {
|
1031 + | assert!(e.downcast_ref::<NoMatchingAuthSchemeError>().is_some());
|
1032 + | }
|
1033 + | _ => {
|
1034 + | panic!("`resolve_identity` returned an `Err` when no error was expected in the test.");
|
1035 + | }
|
1036 + | }
|
1037 + | }
|
1038 + | }
|
1039 + |
|
1040 + | #[cfg(feature = "http-auth")]
|
1041 + | #[tokio::test]
|
1042 + | async fn auth_scheme_preference() {
|
1043 + | use aws_smithy_runtime_api::client::auth::http::{
|
1044 + | HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
|
1045 + | };
|
1046 + |
|
1047 + | struct TestCase {
|
1048 + | supported: Vec<AuthSchemeOption>,
|
1049 + | preference: Option<AuthSchemePreference>,
|
1050 + | expected_resolved_auths: Vec<AuthSchemeId>,
|
1051 + | }
|
1052 + |
|
1053 + | for test_case in [
|
1054 + | TestCase {
|
1055 + | supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
|
1056 + | .map(AuthSchemeOption::from)
|
1057 + | .to_vec(),
|
1058 + | preference: None,
|
1059 + | expected_resolved_auths: vec![
|
1060 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1061 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
1062 + | ],
|
1063 + | },
|
1064 + | TestCase {
|
1065 + | supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
|
1066 + | .map(AuthSchemeOption::from)
|
1067 + | .to_vec(),
|
1068 + | preference: Some([].into()),
|
1069 + | expected_resolved_auths: vec![
|
1070 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1071 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
1072 + | ],
|
1073 + | },
|
1074 + | TestCase {
|
1075 + | supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
|
1076 + | .map(AuthSchemeOption::from)
|
1077 + | .to_vec(),
|
1078 + | preference: Some(["bogus"].map(AuthSchemeId::from).into()),
|
1079 + | expected_resolved_auths: vec![
|
1080 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1081 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
1082 + | ],
|
1083 + | },
|
1084 + | TestCase {
|
1085 + | supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
|
1086 + | .map(AuthSchemeOption::from)
|
1087 + | .to_vec(),
|
1088 + | preference: Some([HTTP_BASIC_AUTH_SCHEME_ID].into()),
|
1089 + | expected_resolved_auths: vec![
|
1090 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
1091 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1092 + | ],
|
1093 + | },
|
1094 + | TestCase {
|
1095 + | supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
|
1096 + | .map(AuthSchemeOption::from)
|
1097 + | .to_vec(),
|
1098 + | preference: Some([HTTP_BASIC_AUTH_SCHEME_ID, HTTP_API_KEY_AUTH_SCHEME_ID].into()),
|
1099 + | expected_resolved_auths: vec![
|
1100 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
1101 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1102 + | ],
|
1103 + | },
|
1104 + | TestCase {
|
1105 + | supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
|
1106 + | .map(AuthSchemeOption::from)
|
1107 + | .to_vec(),
|
1108 + | preference: Some(
|
1109 + | [
|
1110 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
1111 + | HTTP_BEARER_AUTH_SCHEME_ID,
|
1112 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1113 + | ]
|
1114 + | .into(),
|
1115 + | ),
|
1116 + | expected_resolved_auths: vec![
|
1117 + | HTTP_BASIC_AUTH_SCHEME_ID,
|
1118 + | HTTP_API_KEY_AUTH_SCHEME_ID,
|
1119 + | ],
|
1120 + | },
|
1121 + | ] {
|
1122 + | let actual = reprioritize_with_auth_scheme_preference(
|
1123 + | test_case.supported,
|
1124 + | test_case.preference.as_ref(),
|
1125 + | )
|
1126 + | .await;
|
1127 + | let actual = actual
|
1128 + | .iter()
|
1129 + | .map(|opt| opt.scheme_id())
|
1130 + | .cloned()
|
1131 + | .collect::<Vec<_>>();
|
1132 + | assert_eq!(test_case.expected_resolved_auths, actual);
|
1133 + | }
|
1134 + | }
|
888 1135 | }
|