1use crate::json_credentials::{parse_json_credentials, JsonCredentials, RefreshableCredentials};
12use crate::provider_config::ProviderConfig;
13use aws_credential_types::provider::{self, error::CredentialsError};
14use aws_credential_types::Credentials;
15use aws_smithy_runtime::client::orchestrator::operation::Operation;
16use aws_smithy_runtime::client::retries::classifiers::{
17 HttpStatusCodeClassifier, TransientErrorClassifier,
18};
19use aws_smithy_runtime_api::client::http::HttpConnectorSettings;
20use aws_smithy_runtime_api::client::interceptors::context::{Error, InterceptorContext};
21use aws_smithy_runtime_api::client::orchestrator::{
22 HttpResponse, OrchestratorError, SensitiveOutput,
23};
24use aws_smithy_runtime_api::client::result::SdkError;
25use aws_smithy_runtime_api::client::retries::classifiers::ClassifyRetry;
26use aws_smithy_runtime_api::client::retries::classifiers::RetryAction;
27use aws_smithy_runtime_api::client::runtime_plugin::StaticRuntimePlugin;
28use aws_smithy_types::body::SdkBody;
29use aws_smithy_types::config_bag::Layer;
30use aws_smithy_types::retry::RetryConfig;
31use aws_smithy_types::timeout::TimeoutConfig;
32use http::header::{ACCEPT, AUTHORIZATION};
33use http::HeaderValue;
34use std::time::Duration;
35
36const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(5);
37const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
38
39#[derive(Debug)]
40struct HttpProviderAuth {
41 auth: Option<HeaderValue>,
42}
43
44#[derive(Debug)]
45pub(crate) struct HttpCredentialProvider {
46 operation: Operation<HttpProviderAuth, Credentials, CredentialsError>,
47}
48
49impl HttpCredentialProvider {
50 pub(crate) fn builder() -> Builder {
51 Builder::default()
52 }
53
54 pub(crate) async fn credentials(&self, auth: Option<HeaderValue>) -> provider::Result {
55 let credentials = self.operation.invoke(HttpProviderAuth { auth }).await;
56 match credentials {
57 Ok(creds) => Ok(creds),
58 Err(SdkError::ServiceError(context)) => Err(context.into_err()),
59 Err(other) => Err(CredentialsError::unhandled(other)),
60 }
61 }
62}
63
64#[derive(Default)]
65pub(crate) struct Builder {
66 provider_config: Option<ProviderConfig>,
67 http_connector_settings: Option<HttpConnectorSettings>,
68}
69
70impl Builder {
71 pub(crate) fn configure(mut self, provider_config: &ProviderConfig) -> Self {
72 self.provider_config = Some(provider_config.clone());
73 self
74 }
75
76 pub(crate) fn http_connector_settings(
77 mut self,
78 http_connector_settings: HttpConnectorSettings,
79 ) -> Self {
80 self.http_connector_settings = Some(http_connector_settings);
81 self
82 }
83
84 pub(crate) fn build(
85 self,
86 provider_name: &'static str,
87 endpoint: &str,
88 path: impl Into<String>,
89 ) -> HttpCredentialProvider {
90 let provider_config = self.provider_config.unwrap_or_default();
91
92 let mut builder = Operation::builder()
93 .service_name("HttpCredentialProvider")
94 .operation_name("LoadCredentials")
95 .with_connection_poisoning()
96 .endpoint_url(endpoint)
97 .no_auth()
98 .timeout_config(
99 TimeoutConfig::builder()
100 .connect_timeout(DEFAULT_CONNECT_TIMEOUT)
101 .read_timeout(DEFAULT_READ_TIMEOUT)
102 .build(),
103 )
104 .runtime_plugin(StaticRuntimePlugin::new().with_config({
105 let mut layer = Layer::new("SensitiveOutput");
106 layer.store_put(SensitiveOutput);
107 layer.freeze()
108 }));
109 if let Some(http_client) = provider_config.http_client() {
110 builder = builder.http_client(http_client);
111 }
112 if let Some(sleep_impl) = provider_config.sleep_impl() {
113 builder = builder
114 .standard_retry(&RetryConfig::standard())
115 .retry_classifier(HttpCredentialRetryClassifier)
121 .retry_classifier(TransientErrorClassifier::<Error>::new())
123 .retry_classifier(HttpStatusCodeClassifier::default())
125 .sleep_impl(sleep_impl);
126 } else {
127 builder = builder.no_retry();
128 }
129 let path = path.into();
130 let operation = builder
131 .serializer(move |input: HttpProviderAuth| {
132 let mut http_req = http::Request::builder()
133 .uri(path.clone())
134 .header(ACCEPT, "application/json");
135 if let Some(auth) = input.auth {
136 http_req = http_req.header(AUTHORIZATION, auth);
137 }
138 Ok(http_req
139 .body(SdkBody::empty())
140 .expect("valid request")
141 .try_into()
142 .unwrap())
143 })
144 .deserializer(move |response| parse_response(provider_name, response))
145 .build();
146 HttpCredentialProvider { operation }
147 }
148}
149
150fn parse_response(
151 provider_name: &'static str,
152 response: &HttpResponse,
153) -> Result<Credentials, OrchestratorError<CredentialsError>> {
154 if !response.status().is_success() {
155 return Err(OrchestratorError::operation(
156 CredentialsError::provider_error(format!(
157 "Non-success status from HTTP credential provider: {:?}",
158 response.status()
159 )),
160 ));
161 }
162 let resp_bytes = response.body().bytes().expect("non-streaming deserializer");
163 let str_resp = std::str::from_utf8(resp_bytes)
164 .map_err(|err| OrchestratorError::operation(CredentialsError::unhandled(err)))?;
165 let json_creds = parse_json_credentials(str_resp)
166 .map_err(|err| OrchestratorError::operation(CredentialsError::unhandled(err)))?;
167 match json_creds {
168 JsonCredentials::RefreshableCredentials(RefreshableCredentials {
169 access_key_id,
170 secret_access_key,
171 session_token,
172 expiration,
173 }) => Ok(Credentials::new(
174 access_key_id,
175 secret_access_key,
176 Some(session_token.to_string()),
177 Some(expiration),
178 provider_name,
179 )),
180 JsonCredentials::Error { code, message } => Err(OrchestratorError::operation(
181 CredentialsError::provider_error(format!(
182 "failed to load credentials [{}]: {}",
183 code, message
184 )),
185 )),
186 }
187}
188
189#[derive(Clone, Debug)]
190struct HttpCredentialRetryClassifier;
191
192impl ClassifyRetry for HttpCredentialRetryClassifier {
193 fn name(&self) -> &'static str {
194 "HttpCredentialRetryClassifier"
195 }
196
197 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
198 let output_or_error = ctx.output_or_error();
199 let error = match output_or_error {
200 Some(Ok(_)) | None => return RetryAction::NoActionIndicated,
201 Some(Err(err)) => err,
202 };
203
204 if let Some((err, status)) = error
206 .as_operation_error()
207 .and_then(|err| err.downcast_ref::<CredentialsError>())
208 .zip(ctx.response().map(HttpResponse::status))
209 {
210 if matches!(err, CredentialsError::Unhandled { .. }) && status.is_success() {
211 return RetryAction::server_error();
212 }
213 }
214
215 RetryAction::NoActionIndicated
216 }
217}
218
219#[cfg(test)]
220mod test {
221 use super::*;
222 use aws_credential_types::provider::error::CredentialsError;
223 use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient};
224 use aws_smithy_types::body::SdkBody;
225 use http::{Request, Response, Uri};
226 use std::time::SystemTime;
227
228 async fn provide_creds(
229 http_client: StaticReplayClient,
230 ) -> Result<Credentials, CredentialsError> {
231 let provider_config = ProviderConfig::default().with_http_client(http_client.clone());
232 let provider = HttpCredentialProvider::builder()
233 .configure(&provider_config)
234 .build("test", "http://localhost:1234/", "/some-creds");
235 provider.credentials(None).await
236 }
237
238 fn successful_req_resp() -> ReplayEvent {
239 ReplayEvent::new(
240 Request::builder()
241 .uri(Uri::from_static("http://localhost:1234/some-creds"))
242 .body(SdkBody::empty())
243 .unwrap(),
244 Response::builder()
245 .status(200)
246 .body(SdkBody::from(
247 r#"{
248 "AccessKeyId" : "MUA...",
249 "SecretAccessKey" : "/7PC5om....",
250 "Token" : "AQoDY....=",
251 "Expiration" : "2016-02-25T06:03:31Z"
252 }"#,
253 ))
254 .unwrap(),
255 )
256 }
257
258 #[tokio::test]
259 async fn successful_response() {
260 let http_client = StaticReplayClient::new(vec![successful_req_resp()]);
261 let creds = provide_creds(http_client.clone()).await.expect("success");
262 assert_eq!("MUA...", creds.access_key_id());
263 assert_eq!("/7PC5om....", creds.secret_access_key());
264 assert_eq!(Some("AQoDY....="), creds.session_token());
265 assert_eq!(
266 Some(SystemTime::UNIX_EPOCH + Duration::from_secs(1456380211)),
267 creds.expiry()
268 );
269 http_client.assert_requests_match(&[]);
270 }
271
272 #[tokio::test]
273 async fn retry_nonparseable_response() {
274 let http_client = StaticReplayClient::new(vec![
275 ReplayEvent::new(
276 Request::builder()
277 .uri(Uri::from_static("http://localhost:1234/some-creds"))
278 .body(SdkBody::empty())
279 .unwrap(),
280 Response::builder()
281 .status(200)
282 .body(SdkBody::from(r#"not json"#))
283 .unwrap(),
284 ),
285 successful_req_resp(),
286 ]);
287 let creds = provide_creds(http_client.clone()).await.expect("success");
288 assert_eq!("MUA...", creds.access_key_id());
289 http_client.assert_requests_match(&[]);
290 }
291
292 #[tokio::test]
293 async fn retry_error_code() {
294 let http_client = StaticReplayClient::new(vec![
295 ReplayEvent::new(
296 Request::builder()
297 .uri(Uri::from_static("http://localhost:1234/some-creds"))
298 .body(SdkBody::empty())
299 .unwrap(),
300 Response::builder()
301 .status(500)
302 .body(SdkBody::from(r#"it broke"#))
303 .unwrap(),
304 ),
305 successful_req_resp(),
306 ]);
307 let creds = provide_creds(http_client.clone()).await.expect("success");
308 assert_eq!("MUA...", creds.access_key_id());
309 http_client.assert_requests_match(&[]);
310 }
311
312 #[tokio::test]
313 async fn explicit_error_not_retryable() {
314 let http_client = StaticReplayClient::new(vec![ReplayEvent::new(
315 Request::builder()
316 .uri(Uri::from_static("http://localhost:1234/some-creds"))
317 .body(SdkBody::empty())
318 .unwrap(),
319 Response::builder()
320 .status(400)
321 .body(SdkBody::from(
322 r#"{ "Code": "Error", "Message": "There was a problem, it was your fault" }"#,
323 ))
324 .unwrap(),
325 )]);
326 let err = provide_creds(http_client.clone())
327 .await
328 .expect_err("it should fail");
329 assert!(
330 matches!(err, CredentialsError::ProviderError { .. }),
331 "should be CredentialsError::ProviderError: {err}",
332 );
333 http_client.assert_requests_match(&[]);
334 }
335}