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