1#![cfg(feature = "credentials-process")]
7
8use crate::json_credentials::{json_parse_loop, InvalidJsonCredentials};
11use crate::sensitive_command::CommandWithSensitiveArgs;
12use aws_credential_types::attributes::AccountId;
13use aws_credential_types::credential_feature::AwsCredentialFeature;
14use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
15use aws_credential_types::Credentials;
16use aws_smithy_json::deserialize::Token;
17use std::borrow::Cow;
18use std::process::Command;
19use std::time::SystemTime;
20use time::format_description::well_known::Rfc3339;
21use time::OffsetDateTime;
22
23#[derive(Debug)]
57pub struct CredentialProcessProvider {
58 command: CommandWithSensitiveArgs<String>,
59 profile_account_id: Option<AccountId>,
60}
61
62impl ProvideCredentials for CredentialProcessProvider {
63 fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
64 where
65 Self: 'a,
66 {
67 future::ProvideCredentials::new(self.credentials())
68 }
69}
70
71impl CredentialProcessProvider {
72 pub fn new(command: String) -> Self {
74 Self {
75 command: CommandWithSensitiveArgs::new(command),
76 profile_account_id: None,
77 }
78 }
79
80 pub(crate) fn builder() -> Builder {
81 Builder::default()
82 }
83
84 async fn credentials(&self) -> provider::Result {
85 tracing::debug!(command = %self.command, "loading credentials from external process");
87
88 let command = if cfg!(windows) {
89 let mut command = Command::new("cmd.exe");
90 command.args(["/C", self.command.unredacted()]);
91 command
92 } else {
93 let mut command = Command::new("sh");
94 command.args(["-c", self.command.unredacted()]);
95 command
96 };
97 let output = tokio::process::Command::from(command)
98 .output()
99 .await
100 .map_err(|e| {
101 CredentialsError::provider_error(format!(
102 "Error retrieving credentials from external process: {}",
103 e
104 ))
105 })?;
106
107 tracing::trace!(command = ?self.command, status = ?output.status, "executed command (unredacted)");
109
110 if !output.status.success() {
111 let reason =
112 std::str::from_utf8(&output.stderr).unwrap_or("could not decode stderr as UTF-8");
113 return Err(CredentialsError::provider_error(format!(
114 "Error retrieving credentials: external process exited with code {}. Stderr: {}",
115 output.status, reason
116 )));
117 }
118
119 let output = std::str::from_utf8(&output.stdout).map_err(|e| {
120 CredentialsError::provider_error(format!(
121 "Error retrieving credentials from external process: could not decode output as UTF-8: {}",
122 e
123 ))
124 })?;
125
126 parse_credential_process_json_credentials(output, self.profile_account_id.as_ref())
127 .map(|mut creds| {
128 creds
129 .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
130 .push(AwsCredentialFeature::CredentialsProcess);
131 creds
132 })
133 .map_err(|invalid| {
134 CredentialsError::provider_error(format!(
135 "Error retrieving credentials from external process, could not parse response: {}",
136 invalid
137 ))
138 })
139 }
140}
141
142#[derive(Debug, Default)]
143pub(crate) struct Builder {
144 command: Option<CommandWithSensitiveArgs<String>>,
145 profile_account_id: Option<AccountId>,
146}
147
148impl Builder {
149 pub(crate) fn command(mut self, command: CommandWithSensitiveArgs<String>) -> Self {
150 self.command = Some(command);
151 self
152 }
153
154 #[allow(dead_code)] pub(crate) fn account_id(mut self, account_id: impl Into<AccountId>) -> Self {
156 self.set_account_id(Some(account_id.into()));
157 self
158 }
159
160 pub(crate) fn set_account_id(&mut self, account_id: Option<AccountId>) {
161 self.profile_account_id = account_id;
162 }
163
164 pub(crate) fn build(self) -> CredentialProcessProvider {
165 CredentialProcessProvider {
166 command: self.command.expect("should be set"),
167 profile_account_id: self.profile_account_id,
168 }
169 }
170}
171
172pub(crate) fn parse_credential_process_json_credentials(
180 credentials_response: &str,
181 profile_account_id: Option<&AccountId>,
182) -> Result<Credentials, InvalidJsonCredentials> {
183 let mut version = None;
184 let mut access_key_id = None;
185 let mut secret_access_key = None;
186 let mut session_token = None;
187 let mut expiration = None;
188 let mut account_id = profile_account_id
189 .as_ref()
190 .map(|id| Cow::Borrowed(id.as_str()));
191 json_parse_loop(credentials_response.as_bytes(), |key, value| {
192 match (key, value) {
193 (key, Token::ValueNumber { value, .. }) if key.eq_ignore_ascii_case("Version") => {
202 version = Some(i32::try_from(*value).map_err(|err| {
203 InvalidJsonCredentials::InvalidField {
204 field: "Version",
205 err: err.into(),
206 }
207 })?);
208 }
209 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccessKeyId") => {
210 access_key_id = Some(value.to_unescaped()?)
211 }
212 (key, Token::ValueString { value, .. })
213 if key.eq_ignore_ascii_case("SecretAccessKey") =>
214 {
215 secret_access_key = Some(value.to_unescaped()?)
216 }
217 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("SessionToken") => {
218 session_token = Some(value.to_unescaped()?)
219 }
220 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Expiration") => {
221 expiration = Some(value.to_unescaped()?)
222 }
223 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccountId") => {
224 account_id = Some(value.to_unescaped()?)
225 }
226
227 _ => {}
228 };
229 Ok(())
230 })?;
231
232 match version {
233 Some(1) => { }
234 None => return Err(InvalidJsonCredentials::MissingField("Version")),
235 Some(version) => {
236 return Err(InvalidJsonCredentials::InvalidField {
237 field: "version",
238 err: format!("unknown version number: {}", version).into(),
239 })
240 }
241 }
242
243 let access_key_id = access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?;
244 let secret_access_key =
245 secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?;
246 let expiration = expiration.map(parse_expiration).transpose()?;
247 if expiration.is_none() {
248 tracing::debug!("no expiration provided for credentials provider credentials. these credentials will never be refreshed.")
249 }
250 let mut builder = Credentials::builder()
251 .access_key_id(access_key_id)
252 .secret_access_key(secret_access_key)
253 .provider_name("CredentialProcess");
254 builder.set_session_token(session_token.map(String::from));
255 builder.set_expiry(expiration);
256 builder.set_account_id(account_id.map(AccountId::from));
257 Ok(builder.build())
258}
259
260fn parse_expiration(expiration: impl AsRef<str>) -> Result<SystemTime, InvalidJsonCredentials> {
261 OffsetDateTime::parse(expiration.as_ref(), &Rfc3339)
262 .map(SystemTime::from)
263 .map_err(|err| InvalidJsonCredentials::InvalidField {
264 field: "Expiration",
265 err: err.into(),
266 })
267}
268
269#[cfg(test)]
270mod test {
271 use crate::credential_process::CredentialProcessProvider;
272 use crate::sensitive_command::CommandWithSensitiveArgs;
273 use aws_credential_types::credential_feature::AwsCredentialFeature;
274 use aws_credential_types::provider::ProvideCredentials;
275 use std::time::{Duration, SystemTime};
276 use time::format_description::well_known::Rfc3339;
277 use time::OffsetDateTime;
278 use tokio::time::timeout;
279
280 #[tokio::test]
282 #[cfg_attr(windows, ignore)]
283 async fn test_credential_process() {
284 let provider = CredentialProcessProvider::new(String::from(
285 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "AccountId": "123456789001", "Expiration": "2022-05-02T18:36:00+00:00" }'"#,
286 ));
287 let creds = provider.provide_credentials().await.expect("valid creds");
288 assert_eq!(creds.access_key_id(), "ASIARTESTID");
289 assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
290 assert_eq!(creds.session_token(), Some("TESTSESSIONTOKEN"));
291 assert_eq!(creds.account_id().unwrap().as_str(), "123456789001");
292 assert_eq!(
293 creds.expiry(),
294 Some(
295 SystemTime::try_from(
296 OffsetDateTime::parse("2022-05-02T18:36:00+00:00", &Rfc3339)
297 .expect("static datetime")
298 )
299 .expect("static datetime")
300 )
301 );
302 }
303
304 #[tokio::test]
306 #[cfg_attr(windows, ignore)]
307 async fn test_credential_process_no_expiry() {
308 let provider = CredentialProcessProvider::new(String::from(
309 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
310 ));
311 let creds = provider.provide_credentials().await.expect("valid creds");
312 assert_eq!(creds.access_key_id(), "ASIARTESTID");
313 assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
314 assert_eq!(creds.session_token(), None);
315 assert_eq!(creds.expiry(), None);
316 }
317
318 #[tokio::test]
319 async fn credentials_process_timeouts() {
320 let provider = CredentialProcessProvider::new(String::from("sleep 1000"));
321 let _creds = timeout(Duration::from_millis(1), provider.provide_credentials())
322 .await
323 .expect_err("timeout forced");
324 }
325
326 #[tokio::test]
327 async fn credentials_with_fallback_account_id() {
328 let provider = CredentialProcessProvider::builder()
329 .command(CommandWithSensitiveArgs::new(String::from(
330 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
331 )))
332 .account_id("012345678901")
333 .build();
334 let creds = provider.provide_credentials().await.unwrap();
335 assert_eq!("012345678901", creds.account_id().unwrap().as_str());
336 }
337
338 #[tokio::test]
339 async fn fallback_account_id_shadowed_by_account_id_in_process_output() {
340 let provider = CredentialProcessProvider::builder()
341 .command(CommandWithSensitiveArgs::new(String::from(
342 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }'"#,
343 )))
344 .account_id("012345678901")
345 .build();
346 let creds = provider.provide_credentials().await.unwrap();
347 assert_eq!("111122223333", creds.account_id().unwrap().as_str());
348 }
349
350 #[tokio::test]
351 async fn credential_feature() {
352 let provider = CredentialProcessProvider::builder()
353 .command(CommandWithSensitiveArgs::new(String::from(
354 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }'"#,
355 )))
356 .account_id("012345678901")
357 .build();
358 let creds = provider.provide_credentials().await.unwrap();
359 assert_eq!(
360 &vec![AwsCredentialFeature::CredentialsProcess],
361 creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
362 );
363 }
364}