aws_config/
credential_process.rs1#![cfg(feature = "credentials-process")]
7
8use crate::json_credentials::{json_parse_loop, InvalidJsonCredentials};
11use crate::sensitive_command::CommandWithSensitiveArgs;
12use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
13use aws_credential_types::Credentials;
14use aws_smithy_json::deserialize::Token;
15use std::process::Command;
16use std::time::SystemTime;
17use time::format_description::well_known::Rfc3339;
18use time::OffsetDateTime;
19
20#[derive(Debug)]
54pub struct CredentialProcessProvider {
55 command: CommandWithSensitiveArgs<String>,
56}
57
58impl ProvideCredentials for CredentialProcessProvider {
59 fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
60 where
61 Self: 'a,
62 {
63 future::ProvideCredentials::new(self.credentials())
64 }
65}
66
67impl CredentialProcessProvider {
68 pub fn new(command: String) -> Self {
70 Self {
71 command: CommandWithSensitiveArgs::new(command),
72 }
73 }
74
75 pub(crate) fn from_command(command: &CommandWithSensitiveArgs<&str>) -> Self {
76 Self {
77 command: command.to_owned_string(),
78 }
79 }
80
81 async fn credentials(&self) -> provider::Result {
82 tracing::debug!(command = %self.command, "loading credentials from external process");
84
85 let command = if cfg!(windows) {
86 let mut command = Command::new("cmd.exe");
87 command.args(["/C", self.command.unredacted()]);
88 command
89 } else {
90 let mut command = Command::new("sh");
91 command.args(["-c", self.command.unredacted()]);
92 command
93 };
94 let output = tokio::process::Command::from(command)
95 .output()
96 .await
97 .map_err(|e| {
98 CredentialsError::provider_error(format!(
99 "Error retrieving credentials from external process: {}",
100 e
101 ))
102 })?;
103
104 tracing::trace!(command = ?self.command, status = ?output.status, "executed command (unredacted)");
106
107 if !output.status.success() {
108 let reason =
109 std::str::from_utf8(&output.stderr).unwrap_or("could not decode stderr as UTF-8");
110 return Err(CredentialsError::provider_error(format!(
111 "Error retrieving credentials: external process exited with code {}. Stderr: {}",
112 output.status, reason
113 )));
114 }
115
116 let output = std::str::from_utf8(&output.stdout).map_err(|e| {
117 CredentialsError::provider_error(format!(
118 "Error retrieving credentials from external process: could not decode output as UTF-8: {}",
119 e
120 ))
121 })?;
122
123 parse_credential_process_json_credentials(output).map_err(|invalid| {
124 CredentialsError::provider_error(format!(
125 "Error retrieving credentials from external process, could not parse response: {}",
126 invalid
127 ))
128 })
129 }
130}
131
132pub(crate) fn parse_credential_process_json_credentials(
138 credentials_response: &str,
139) -> Result<Credentials, InvalidJsonCredentials> {
140 let mut version = None;
141 let mut access_key_id = None;
142 let mut secret_access_key = None;
143 let mut session_token = None;
144 let mut expiration = None;
145 json_parse_loop(credentials_response.as_bytes(), |key, value| {
146 match (key, value) {
147 (key, Token::ValueNumber { value, .. }) if key.eq_ignore_ascii_case("Version") => {
155 version = Some(i32::try_from(*value).map_err(|err| {
156 InvalidJsonCredentials::InvalidField {
157 field: "Version",
158 err: err.into(),
159 }
160 })?);
161 }
162 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccessKeyId") => {
163 access_key_id = Some(value.to_unescaped()?)
164 }
165 (key, Token::ValueString { value, .. })
166 if key.eq_ignore_ascii_case("SecretAccessKey") =>
167 {
168 secret_access_key = Some(value.to_unescaped()?)
169 }
170 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("SessionToken") => {
171 session_token = Some(value.to_unescaped()?)
172 }
173 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Expiration") => {
174 expiration = Some(value.to_unescaped()?)
175 }
176
177 _ => {}
178 };
179 Ok(())
180 })?;
181
182 match version {
183 Some(1) => { }
184 None => return Err(InvalidJsonCredentials::MissingField("Version")),
185 Some(version) => {
186 return Err(InvalidJsonCredentials::InvalidField {
187 field: "version",
188 err: format!("unknown version number: {}", version).into(),
189 })
190 }
191 }
192
193 let access_key_id = access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?;
194 let secret_access_key =
195 secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?;
196 let expiration = expiration.map(parse_expiration).transpose()?;
197 if expiration.is_none() {
198 tracing::debug!("no expiration provided for credentials provider credentials. these credentials will never be refreshed.")
199 }
200 Ok(Credentials::new(
201 access_key_id,
202 secret_access_key,
203 session_token.map(|tok| tok.to_string()),
204 expiration,
205 "CredentialProcess",
206 ))
207}
208
209fn parse_expiration(expiration: impl AsRef<str>) -> Result<SystemTime, InvalidJsonCredentials> {
210 OffsetDateTime::parse(expiration.as_ref(), &Rfc3339)
211 .map(SystemTime::from)
212 .map_err(|err| InvalidJsonCredentials::InvalidField {
213 field: "Expiration",
214 err: err.into(),
215 })
216}
217
218#[cfg(test)]
219mod test {
220 use crate::credential_process::CredentialProcessProvider;
221 use aws_credential_types::provider::ProvideCredentials;
222 use std::time::{Duration, SystemTime};
223 use time::format_description::well_known::Rfc3339;
224 use time::OffsetDateTime;
225 use tokio::time::timeout;
226
227 #[tokio::test]
229 #[cfg_attr(windows, ignore)]
230 async fn test_credential_process() {
231 let provider = CredentialProcessProvider::new(String::from(
232 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "Expiration": "2022-05-02T18:36:00+00:00" }'"#,
233 ));
234 let creds = provider.provide_credentials().await.expect("valid creds");
235 assert_eq!(creds.access_key_id(), "ASIARTESTID");
236 assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
237 assert_eq!(creds.session_token(), Some("TESTSESSIONTOKEN"));
238 assert_eq!(
239 creds.expiry(),
240 Some(
241 SystemTime::try_from(
242 OffsetDateTime::parse("2022-05-02T18:36:00+00:00", &Rfc3339)
243 .expect("static datetime")
244 )
245 .expect("static datetime")
246 )
247 );
248 }
249
250 #[tokio::test]
252 #[cfg_attr(windows, ignore)]
253 async fn test_credential_process_no_expiry() {
254 let provider = CredentialProcessProvider::new(String::from(
255 r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
256 ));
257 let creds = provider.provide_credentials().await.expect("valid creds");
258 assert_eq!(creds.access_key_id(), "ASIARTESTID");
259 assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
260 assert_eq!(creds.session_token(), None);
261 assert_eq!(creds.expiry(), None);
262 }
263
264 #[tokio::test]
265 async fn credentials_process_timeouts() {
266 let provider = CredentialProcessProvider::new(String::from("sleep 1000"));
267 let _creds = timeout(Duration::from_millis(1), provider.provide_credentials())
268 .await
269 .expect_err("timeout forced");
270 }
271}