1use aws_smithy_json::deserialize::token::skip_value;
7use aws_smithy_json::deserialize::{json_token_iter, EscapeError, Token};
8use aws_smithy_types::date_time::Format;
9use aws_smithy_types::DateTime;
10use std::borrow::Cow;
11use std::error::Error;
12use std::fmt::{self, Display, Formatter};
13use std::time::SystemTime;
14
15#[derive(Debug)]
16pub(crate) enum InvalidJsonCredentials {
17 JsonError(Box<dyn Error + Send + Sync>),
19 MissingField(&'static str),
21
22 InvalidField {
24 field: &'static str,
25 err: Box<dyn Error + Send + Sync>,
26 },
27
28 Other(Cow<'static, str>),
30}
31
32impl From<EscapeError> for InvalidJsonCredentials {
33 fn from(err: EscapeError) -> Self {
34 InvalidJsonCredentials::JsonError(err.into())
35 }
36}
37
38impl From<aws_smithy_json::deserialize::error::DeserializeError> for InvalidJsonCredentials {
39 fn from(err: aws_smithy_json::deserialize::error::DeserializeError) -> Self {
40 InvalidJsonCredentials::JsonError(err.into())
41 }
42}
43
44impl Display for InvalidJsonCredentials {
45 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
46 match self {
47 InvalidJsonCredentials::JsonError(json) => {
48 write!(f, "invalid JSON in response: {json}")
49 }
50 InvalidJsonCredentials::MissingField(field) => {
51 write!(f, "Expected field `{field}` in response but it was missing",)
52 }
53 InvalidJsonCredentials::Other(msg) => write!(f, "{msg}"),
54 InvalidJsonCredentials::InvalidField { field, err } => {
55 write!(f, "Invalid field in response: `{field}`. {err}")
56 }
57 }
58 }
59}
60
61impl Error for InvalidJsonCredentials {}
62
63#[derive(PartialEq, Eq)]
64pub(crate) struct RefreshableCredentials<'a> {
65 pub(crate) access_key_id: Cow<'a, str>,
66 pub(crate) secret_access_key: Cow<'a, str>,
67 pub(crate) session_token: Cow<'a, str>,
68 pub(crate) account_id: Option<Cow<'a, str>>,
69 pub(crate) expiration: SystemTime,
70}
71
72impl fmt::Debug for RefreshableCredentials<'_> {
73 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
74 let mut debug = f.debug_struct("RefreshableCredentials");
75 debug
76 .field("access_key_id", &self.access_key_id)
77 .field("secret_access_key", &"** redacted **")
78 .field("session_token", &"** redacted **");
79 if let Some(account_id) = &self.account_id {
80 debug.field("account_id", account_id);
81 }
82 debug.field("expiration", &self.expiration).finish()
83 }
84}
85
86#[non_exhaustive]
87#[derive(Debug, PartialEq, Eq)]
88pub(crate) enum JsonCredentials<'a> {
89 RefreshableCredentials(RefreshableCredentials<'a>),
90 Error {
91 code: Cow<'a, str>,
92 message: Cow<'a, str>,
93 }, }
114
115pub(crate) fn parse_json_credentials(
124 credentials_response: &str,
125) -> Result<JsonCredentials<'_>, InvalidJsonCredentials> {
126 let mut code = None;
127 let mut access_key_id = None;
128 let mut secret_access_key = None;
129 let mut session_token = None;
130 let mut account_id = None;
131 let mut expiration = None;
132 let mut message = None;
133 json_parse_loop(credentials_response.as_bytes(), |key, value| {
134 match (key, value) {
135 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Code") => {
146 code = Some(value.to_unescaped()?);
147 }
148 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccessKeyId") => {
149 access_key_id = Some(value.to_unescaped()?);
150 }
151 (key, Token::ValueString { value, .. })
152 if key.eq_ignore_ascii_case("SecretAccessKey") =>
153 {
154 secret_access_key = Some(value.to_unescaped()?);
155 }
156 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Token") => {
157 session_token = Some(value.to_unescaped()?);
158 }
159 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccountId") => {
160 account_id = Some(value.to_unescaped()?);
161 }
162 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Expiration") => {
163 expiration = Some(value.to_unescaped()?);
164 }
165
166 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Message") => {
168 message = Some(value.to_unescaped()?);
169 }
170 _ => {}
171 };
172 Ok(())
173 })?;
174 match code {
175 None | Some(Cow::Borrowed("Success")) => {
178 let access_key_id =
179 access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?;
180 let secret_access_key =
181 secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?;
182 let session_token =
183 session_token.ok_or(InvalidJsonCredentials::MissingField("Token"))?;
184 let expiration =
185 expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?;
186 let expiration = SystemTime::try_from(
187 DateTime::from_str(expiration.as_ref(), Format::DateTime).map_err(|err| {
188 InvalidJsonCredentials::InvalidField {
189 field: "Expiration",
190 err: err.into(),
191 }
192 })?,
193 )
194 .map_err(|_| {
195 InvalidJsonCredentials::Other(
196 "credential expiration time cannot be represented by a SystemTime".into(),
197 )
198 })?;
199 Ok(JsonCredentials::RefreshableCredentials(
200 RefreshableCredentials {
201 access_key_id,
202 secret_access_key,
203 session_token,
204 account_id,
205 expiration,
206 },
207 ))
208 }
209 Some(other) => Ok(JsonCredentials::Error {
210 code: other,
211 message: message.unwrap_or_else(|| "no message".into()),
212 }),
213 }
214}
215
216pub(crate) fn json_parse_loop<'a>(
217 input: &'a [u8],
218 mut f: impl FnMut(Cow<'a, str>, &Token<'a>) -> Result<(), InvalidJsonCredentials>,
219) -> Result<(), InvalidJsonCredentials> {
220 let mut tokens = json_token_iter(input).peekable();
221 if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) {
222 return Err(InvalidJsonCredentials::JsonError(
223 "expected a JSON document starting with `{`".into(),
224 ));
225 }
226 loop {
227 match tokens.next().transpose()? {
228 Some(Token::EndObject { .. }) => break,
229 Some(Token::ObjectKey { key, .. }) => {
230 if let Some(Ok(token)) = tokens.peek() {
231 let key = key.to_unescaped()?;
232 f(key, token)?
233 }
234 skip_value(&mut tokens)?;
235 }
236 other => {
237 return Err(InvalidJsonCredentials::Other(
238 format!("expected object key, found: {other:?}").into(),
239 ));
240 }
241 }
242 }
243 if tokens.next().is_some() {
244 return Err(InvalidJsonCredentials::Other(
245 "found more JSON tokens after completing parsing".into(),
246 ));
247 }
248 Ok(())
249}
250
251#[cfg(test)]
252mod test {
253 use crate::json_credentials::{
254 parse_json_credentials, InvalidJsonCredentials, JsonCredentials, RefreshableCredentials,
255 };
256 use std::time::{Duration, UNIX_EPOCH};
257
258 #[test]
259 fn json_credentials_success_response() {
260 let response = r#"
261 {
262 "Code" : "Success",
263 "LastUpdated" : "2021-09-17T20:57:08Z",
264 "Type" : "AWS-HMAC",
265 "AccessKeyId" : "ASIARTEST",
266 "SecretAccessKey" : "xjtest",
267 "Token" : "IQote///test",
268 "AccountID" : "111122223333",
269 "Expiration" : "2021-09-18T03:31:56Z"
270 }"#;
271 let parsed = parse_json_credentials(response).expect("valid JSON");
272 assert_eq!(
273 parsed,
274 JsonCredentials::RefreshableCredentials(RefreshableCredentials {
275 access_key_id: "ASIARTEST".into(),
276 secret_access_key: "xjtest".into(),
277 session_token: "IQote///test".into(),
278 account_id: Some("111122223333".into()),
279 expiration: UNIX_EPOCH + Duration::from_secs(1631935916),
280 })
281 )
282 }
283
284 #[test]
285 fn json_credentials_invalid_json() {
286 let error = parse_json_credentials("404: not found").expect_err("no json");
287 match error {
288 InvalidJsonCredentials::JsonError(_) => {} err => panic!("incorrect error: {:?}", err),
290 }
291 }
292
293 #[test]
294 fn json_credentials_not_json_object() {
295 let error = parse_json_credentials("[1,2,3]").expect_err("no json");
296 match error {
297 InvalidJsonCredentials::JsonError(_) => {} _ => panic!("incorrect error"),
299 }
300 }
301
302 #[test]
303 fn json_credentials_missing_code() {
304 let resp = r#"{
305 "LastUpdated" : "2021-09-17T20:57:08Z",
306 "Type" : "AWS-HMAC",
307 "AccessKeyId" : "ASIARTEST",
308 "SecretAccessKey" : "xjtest",
309 "Token" : "IQote///test",
310 "AccountID" : "111122223333",
311 "Expiration" : "2021-09-18T03:31:56Z"
312 }"#;
313 let parsed = parse_json_credentials(resp).expect("code not required");
314 assert_eq!(
315 parsed,
316 JsonCredentials::RefreshableCredentials(RefreshableCredentials {
317 access_key_id: "ASIARTEST".into(),
318 secret_access_key: "xjtest".into(),
319 session_token: "IQote///test".into(),
320 account_id: Some("111122223333".into()),
321 expiration: UNIX_EPOCH + Duration::from_secs(1631935916),
322 })
323 )
324 }
325
326 #[test]
327 fn json_credentials_required_session_token() {
328 let resp = r#"{
329 "LastUpdated" : "2021-09-17T20:57:08Z",
330 "Type" : "AWS-HMAC",
331 "AccessKeyId" : "ASIARTEST",
332 "SecretAccessKey" : "xjtest",
333 "AccountID" : "111122223333",
334 "Expiration" : "2021-09-18T03:31:56Z"
335 }"#;
336 let parsed = parse_json_credentials(resp).expect_err("token missing");
337 assert_eq!(
338 format!("{}", parsed),
339 "Expected field `Token` in response but it was missing"
340 );
341 }
342
343 #[test]
344 fn json_credentials_missing_akid() {
345 let resp = r#"{
346 "Code": "Success",
347 "LastUpdated" : "2021-09-17T20:57:08Z",
348 "Type" : "AWS-HMAC",
349 "SecretAccessKey" : "xjtest",
350 "Token" : "IQote///test",
351 "AccountID" : "111122223333",
352 "Expiration" : "2021-09-18T03:31:56Z"
353 }"#;
354 match parse_json_credentials(resp).expect_err("no code") {
355 InvalidJsonCredentials::MissingField("AccessKeyId") => {} resp => panic!("incorrect json_credentials response: {:?}", resp),
357 }
358 }
359
360 #[test]
361 fn json_credentials_error_response() {
362 let response = r#"{
363 "Code" : "AssumeRoleUnauthorizedAccess",
364 "Message" : "EC2 cannot assume the role integration-test.",
365 "LastUpdated" : "2021-09-17T20:46:56Z"
366 }"#;
367 let parsed = parse_json_credentials(response).expect("valid JSON");
368 assert_eq!(
369 parsed,
370 JsonCredentials::Error {
371 code: "AssumeRoleUnauthorizedAccess".into(),
372 message: "EC2 cannot assume the role integration-test.".into(),
373 }
374 );
375 }
376
377 #[test]
379 fn json_credentials_ecs() {
380 let response = r#"{
382 "RoleArn":"arn:aws:iam::123456789:role/ecs-task-role",
383 "AccessKeyId":"ASIARTEST",
384 "SecretAccessKey":"SECRETTEST",
385 "Token":"tokenEaCXVzLXdlc3QtMiJGMEQCIHt47W18eF4dYfSlmKGiwuJnqmIS3LMXNYfODBCEhcnaAiAnuhGOpcdIDxin4QFzhtgaCR2MpcVqR8NFJdMgOt0/xyrnAwhhEAEaDDEzNDA5NTA2NTg1NiIM9M9GT+c5UfV/8r7PKsQDUa9xE9Eprz5N+jgxbFSD2aJR2iyXCcP9Q1cOh4fdZhyw2WNmq9XnIa2tkzrreiQ5R2t+kzergJHO1KRZPfesarfJ879aWJCSocsEKh7xXwwzTsVXrNo5eWkpwTh64q+Ksz15eoaBhtrvnGvPx6SmXv7SToi/DTHFafJlT/T9jITACZvZXSE9zfLka26Rna3rI4g0ugowha//j1f/c1XuKloqshpZvMKc561om9Y5fqBv1fRiS2KhetGTcmz3wUqNQAk8Dq9oINS7cCtdIO0atqCK69UaKeJ9uKY8mzY9dFWw2IrkpOoXmA9r955iU0NOz/95jVJiPZ/8aE8vb0t67gQfzBUCfky+mGSGWAfPRXQlFa5AEulCTHPd7IcTVCtasG033oKEKgB8QnTxvM2LaPlwaaHo7MHGYXeUKbn9NRKd8m1ShwmAlr4oKp1vQp6cPHDTsdTfPTzh/ZAjUPs+ljQbAwqXbPQdUUPpOk0vltY8k6Im9EA0pf80iUNoqrixpmPsR2hzI/ybUwdh+QhvCSBx+J8KHqF6X92u4qAVYIxLy/LGZKT9YC6Kr9Gywn+Ro+EK/xl3axHPzNpbjRDJnbW3HrMw5LmmiwY6pgGWgmD6IOq4QYUtu1uhaLQZyoI5o5PWn+d3kqqxifu8D0ykldB3lQGdlJ2rjKJjCdx8fce1SoXao9cc4hiwn39hUPuTqzVwv2zbzCKmNggIpXP6gqyRtUCakf6tI7ZwqTb2S8KF3t4ElIP8i4cPdNoI0JHSC+sT4LDPpUcX1CjGxfvo55mBHJedW3LXve8TRj4UckFXT1gLuTnzqPMrC5AHz4TAt+uv",
386 "AccountID" : "111122223333",
387 "Expiration" : "2009-02-13T23:31:30Z"
388 }"#;
389 let parsed = parse_json_credentials(response).expect("valid JSON");
390 use std::borrow::Cow;
391 assert!(
392 matches!(
393 &parsed,
394 JsonCredentials::RefreshableCredentials(RefreshableCredentials{
395 access_key_id: Cow::Borrowed("ASIARTEST"),
396 secret_access_key: Cow::Borrowed("SECRETTEST"),
397 session_token,
398 account_id: Some(Cow::Borrowed("111122223333")),
399 expiration
400 }) if session_token.starts_with("token") && *expiration == UNIX_EPOCH + Duration::from_secs(1234567890)
401 ),
402 "{:?}",
403 parsed
404 );
405 }
406
407 #[test]
408 fn case_insensitive_code_parsing() {
409 let response = r#"{
410 "code" : "AssumeRoleUnauthorizedAccess",
411 "message" : "EC2 cannot assume the role integration-test."
412 }"#;
413 let parsed = parse_json_credentials(response).expect("valid JSON");
414 assert_eq!(
415 parsed,
416 JsonCredentials::Error {
417 code: "AssumeRoleUnauthorizedAccess".into(),
418 message: "EC2 cannot assume the role integration-test.".into(),
419 }
420 );
421 }
422}