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(SystemTime::from(
292 OffsetDateTime::parse("2022-05-02T18:36:00+00:00", &Rfc3339)
293 .expect("static datetime"),
294 ))
295 );
296 }
297
298 #[tokio::test]
300 #[cfg_attr(windows, ignore)]
301 async fn test_credential_process_no_expiry() {
302 let provider = CredentialProcessProvider::new(String::from(
303 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
304 ));
305 let creds = provider.provide_credentials().await.expect("valid creds");
306 assert_eq!(creds.access_key_id(), "ASIARTESTID");
307 assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
308 assert_eq!(creds.session_token(), None);
309 assert_eq!(creds.expiry(), None);
310 }
311
312 #[tokio::test]
313 async fn credentials_process_timeouts() {
314 let provider = CredentialProcessProvider::new(String::from("sleep 1000"));
315 let _creds = timeout(Duration::from_millis(1), provider.provide_credentials())
316 .await
317 .expect_err("timeout forced");
318 }
319
320 #[tokio::test]
321 async fn credentials_with_fallback_account_id() {
322 let provider = CredentialProcessProvider::builder()
323 .command(CommandWithSensitiveArgs::new(String::from(
324 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
325 )))
326 .account_id("012345678901")
327 .build();
328 let creds = provider.provide_credentials().await.unwrap();
329 assert_eq!("012345678901", creds.account_id().unwrap().as_str());
330 }
331
332 #[tokio::test]
333 async fn fallback_account_id_shadowed_by_account_id_in_process_output() {
334 let provider = CredentialProcessProvider::builder()
335 .command(CommandWithSensitiveArgs::new(String::from(
336 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }'"#,
337 )))
338 .account_id("012345678901")
339 .build();
340 let creds = provider.provide_credentials().await.unwrap();
341 assert_eq!("111122223333", creds.account_id().unwrap().as_str());
342 }
343
344 #[tokio::test]
345 async fn credential_feature() {
346 let provider = CredentialProcessProvider::builder()
347 .command(CommandWithSensitiveArgs::new(String::from(
348 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }'"#,
349 )))
350 .account_id("012345678901")
351 .build();
352 let creds = provider.provide_credentials().await.unwrap();
353 assert_eq!(
354 &vec![AwsCredentialFeature::CredentialsProcess],
355 creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
356 );
357 }
358}