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