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) => write!(
51 f,
52 "Expected field `{}` in response but it was missing",
53 field
54 ),
55 InvalidJsonCredentials::Other(msg) => write!(f, "{}", msg),
56 InvalidJsonCredentials::InvalidField { field, err } => {
57 write!(f, "Invalid field in response: `{}`. {}", field, err)
58 }
59 }
60 }
61}
62
63impl Error for InvalidJsonCredentials {}
64
65#[derive(PartialEq, Eq)]
66pub(crate) struct RefreshableCredentials<'a> {
67 pub(crate) access_key_id: Cow<'a, str>,
68 pub(crate) secret_access_key: Cow<'a, str>,
69 pub(crate) session_token: Cow<'a, str>,
70 pub(crate) expiration: SystemTime,
71}
72
73impl<'a> fmt::Debug for RefreshableCredentials<'a> {
74 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
75 f.debug_struct("RefreshableCredentials")
76 .field("access_key_id", &self.access_key_id)
77 .field("secret_access_key", &"** redacted **")
78 .field("session_token", &"** redacted **")
79 .field("expiration", &self.expiration)
80 .finish()
81 }
82}
83
84#[non_exhaustive]
85#[derive(Debug, PartialEq, Eq)]
86pub(crate) enum JsonCredentials<'a> {
87 RefreshableCredentials(RefreshableCredentials<'a>),
88 Error {
89 code: Cow<'a, str>,
90 message: Cow<'a, str>,
91 }, }
112
113pub(crate) fn parse_json_credentials(
122 credentials_response: &str,
123) -> Result<JsonCredentials<'_>, InvalidJsonCredentials> {
124 let mut code = None;
125 let mut access_key_id = None;
126 let mut secret_access_key = None;
127 let mut session_token = None;
128 let mut expiration = None;
129 let mut message = None;
130 json_parse_loop(credentials_response.as_bytes(), |key, value| {
131 match (key, value) {
132 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Code") => {
142 code = Some(value.to_unescaped()?);
143 }
144 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccessKeyId") => {
145 access_key_id = Some(value.to_unescaped()?);
146 }
147 (key, Token::ValueString { value, .. })
148 if key.eq_ignore_ascii_case("SecretAccessKey") =>
149 {
150 secret_access_key = Some(value.to_unescaped()?);
151 }
152 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Token") => {
153 session_token = Some(value.to_unescaped()?);
154 }
155 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Expiration") => {
156 expiration = Some(value.to_unescaped()?);
157 }
158
159 (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Message") => {
161 message = Some(value.to_unescaped()?);
162 }
163 _ => {}
164 };
165 Ok(())
166 })?;
167 match code {
168 None | Some(Cow::Borrowed("Success")) => {
171 let access_key_id =
172 access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?;
173 let secret_access_key =
174 secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?;
175 let session_token =
176 session_token.ok_or(InvalidJsonCredentials::MissingField("Token"))?;
177 let expiration =
178 expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?;
179 let expiration = SystemTime::try_from(
180 DateTime::from_str(expiration.as_ref(), Format::DateTime).map_err(|err| {
181 InvalidJsonCredentials::InvalidField {
182 field: "Expiration",
183 err: err.into(),
184 }
185 })?,
186 )
187 .map_err(|_| {
188 InvalidJsonCredentials::Other(
189 "credential expiration time cannot be represented by a SystemTime".into(),
190 )
191 })?;
192 Ok(JsonCredentials::RefreshableCredentials(
193 RefreshableCredentials {
194 access_key_id,
195 secret_access_key,
196 session_token,
197 expiration,
198 },
199 ))
200 }
201 Some(other) => Ok(JsonCredentials::Error {
202 code: other,
203 message: message.unwrap_or_else(|| "no message".into()),
204 }),
205 }
206}
207
208pub(crate) fn json_parse_loop<'a>(
209 input: &'a [u8],
210 mut f: impl FnMut(Cow<'a, str>, &Token<'a>) -> Result<(), InvalidJsonCredentials>,
211) -> Result<(), InvalidJsonCredentials> {
212 let mut tokens = json_token_iter(input).peekable();
213 if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) {
214 return Err(InvalidJsonCredentials::JsonError(
215 "expected a JSON document starting with `{`".into(),
216 ));
217 }
218 loop {
219 match tokens.next().transpose()? {
220 Some(Token::EndObject { .. }) => break,
221 Some(Token::ObjectKey { key, .. }) => {
222 if let Some(Ok(token)) = tokens.peek() {
223 let key = key.to_unescaped()?;
224 f(key, token)?
225 }
226 skip_value(&mut tokens)?;
227 }
228 other => {
229 return Err(InvalidJsonCredentials::Other(
230 format!("expected object key, found: {:?}", other).into(),
231 ));
232 }
233 }
234 }
235 if tokens.next().is_some() {
236 return Err(InvalidJsonCredentials::Other(
237 "found more JSON tokens after completing parsing".into(),
238 ));
239 }
240 Ok(())
241}
242
243#[cfg(test)]
244mod test {
245 use crate::json_credentials::{
246 parse_json_credentials, InvalidJsonCredentials, JsonCredentials, RefreshableCredentials,
247 };
248 use std::time::{Duration, UNIX_EPOCH};
249
250 #[test]
251 fn json_credentials_success_response() {
252 let response = r#"
253 {
254 "Code" : "Success",
255 "LastUpdated" : "2021-09-17T20:57:08Z",
256 "Type" : "AWS-HMAC",
257 "AccessKeyId" : "ASIARTEST",
258 "SecretAccessKey" : "xjtest",
259 "Token" : "IQote///test",
260 "Expiration" : "2021-09-18T03:31:56Z"
261 }"#;
262 let parsed = parse_json_credentials(response).expect("valid JSON");
263 assert_eq!(
264 parsed,
265 JsonCredentials::RefreshableCredentials(RefreshableCredentials {
266 access_key_id: "ASIARTEST".into(),
267 secret_access_key: "xjtest".into(),
268 session_token: "IQote///test".into(),
269 expiration: UNIX_EPOCH + Duration::from_secs(1631935916),
270 })
271 )
272 }
273
274 #[test]
275 fn json_credentials_invalid_json() {
276 let error = parse_json_credentials("404: not found").expect_err("no json");
277 match error {
278 InvalidJsonCredentials::JsonError(_) => {} err => panic!("incorrect error: {:?}", err),
280 }
281 }
282
283 #[test]
284 fn json_credentials_not_json_object() {
285 let error = parse_json_credentials("[1,2,3]").expect_err("no json");
286 match error {
287 InvalidJsonCredentials::JsonError(_) => {} _ => panic!("incorrect error"),
289 }
290 }
291
292 #[test]
293 fn json_credentials_missing_code() {
294 let resp = r#"{
295 "LastUpdated" : "2021-09-17T20:57:08Z",
296 "Type" : "AWS-HMAC",
297 "AccessKeyId" : "ASIARTEST",
298 "SecretAccessKey" : "xjtest",
299 "Token" : "IQote///test",
300 "Expiration" : "2021-09-18T03:31:56Z"
301 }"#;
302 let parsed = parse_json_credentials(resp).expect("code not required");
303 assert_eq!(
304 parsed,
305 JsonCredentials::RefreshableCredentials(RefreshableCredentials {
306 access_key_id: "ASIARTEST".into(),
307 secret_access_key: "xjtest".into(),
308 session_token: "IQote///test".into(),
309 expiration: UNIX_EPOCH + Duration::from_secs(1631935916),
310 })
311 )
312 }
313
314 #[test]
315 fn json_credentials_required_session_token() {
316 let resp = r#"{
317 "LastUpdated" : "2021-09-17T20:57:08Z",
318 "Type" : "AWS-HMAC",
319 "AccessKeyId" : "ASIARTEST",
320 "SecretAccessKey" : "xjtest",
321 "Expiration" : "2021-09-18T03:31:56Z"
322 }"#;
323 let parsed = parse_json_credentials(resp).expect_err("token missing");
324 assert_eq!(
325 format!("{}", parsed),
326 "Expected field `Token` in response but it was missing"
327 );
328 }
329
330 #[test]
331 fn json_credentials_missing_akid() {
332 let resp = r#"{
333 "Code": "Success",
334 "LastUpdated" : "2021-09-17T20:57:08Z",
335 "Type" : "AWS-HMAC",
336 "SecretAccessKey" : "xjtest",
337 "Token" : "IQote///test",
338 "Expiration" : "2021-09-18T03:31:56Z"
339 }"#;
340 match parse_json_credentials(resp).expect_err("no code") {
341 InvalidJsonCredentials::MissingField("AccessKeyId") => {} resp => panic!("incorrect json_credentials response: {:?}", resp),
343 }
344 }
345
346 #[test]
347 fn json_credentials_error_response() {
348 let response = r#"{
349 "Code" : "AssumeRoleUnauthorizedAccess",
350 "Message" : "EC2 cannot assume the role integration-test.",
351 "LastUpdated" : "2021-09-17T20:46:56Z"
352 }"#;
353 let parsed = parse_json_credentials(response).expect("valid JSON");
354 assert_eq!(
355 parsed,
356 JsonCredentials::Error {
357 code: "AssumeRoleUnauthorizedAccess".into(),
358 message: "EC2 cannot assume the role integration-test.".into(),
359 }
360 );
361 }
362
363 #[test]
365 fn json_credentials_ecs() {
366 let response = r#"{
368 "RoleArn":"arn:aws:iam::123456789:role/ecs-task-role",
369 "AccessKeyId":"ASIARTEST",
370 "SecretAccessKey":"SECRETTEST",
371 "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",
372 "Expiration" : "2009-02-13T23:31:30Z"
373 }"#;
374 let parsed = parse_json_credentials(response).expect("valid JSON");
375 use std::borrow::Cow;
376 assert!(
377 matches!(
378 &parsed,
379 JsonCredentials::RefreshableCredentials(RefreshableCredentials{
380 access_key_id: Cow::Borrowed("ASIARTEST"),
381 secret_access_key: Cow::Borrowed("SECRETTEST"),
382 session_token,
383 expiration
384 }) if session_token.starts_with("token") && *expiration == UNIX_EPOCH + Duration::from_secs(1234567890)
385 ),
386 "{:?}",
387 parsed
388 );
389 }
390
391 #[test]
392 fn case_insensitive_code_parsing() {
393 let response = r#"{
394 "code" : "AssumeRoleUnauthorizedAccess",
395 "message" : "EC2 cannot assume the role integration-test."
396 }"#;
397 let parsed = parse_json_credentials(response).expect("valid JSON");
398 assert_eq!(
399 parsed,
400 JsonCredentials::Error {
401 code: "AssumeRoleUnauthorizedAccess".into(),
402 message: "EC2 cannot assume the role integration-test.".into(),
403 }
404 );
405 }
406}