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