aws_config/
http_credential_provider.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Generalized HTTP credential provider. Currently, this cannot be used directly and can only
7//! be used via the ECS credential provider.
8//!
9//! Future work will stabilize this interface and enable it to be used directly.
10
11use 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                // The following errors are retryable:
116                //   - Socket errors
117                //   - Networking timeouts
118                //   - 5xx errors
119                //   - Non-parseable 200 responses.
120                .retry_classifier(HttpCredentialRetryClassifier)
121                // Socket errors and network timeouts
122                .retry_classifier(TransientErrorClassifier::<Error>::new())
123                // 5xx errors
124                .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        // Retry non-parseable 200 responses
205        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}