1use base64::Engine;
2
3use crate::state::{IamAccessKey, IamPolicy, IamRole, IamUser};
4
5use fakecloud_aws::xml::xml_escape;
6
7fn url_encode_policy(s: &str) -> String {
9 let mut result = String::new();
10 for byte in s.bytes() {
11 match byte {
12 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
13 result.push(byte as char);
14 }
15 _ => {
16 use std::fmt::Write;
17 write!(result, "%{:02X}", byte).unwrap();
18 }
19 }
20 }
21 result
22}
23
24fn tags_xml(tags: &[crate::state::Tag]) -> String {
25 if tags.is_empty() {
26 return String::new();
27 }
28 tags.iter()
29 .map(|t| {
30 format!(
31 " <member>\n <Key>{}</Key>\n <Value>{}</Value>\n </member>",
32 xml_escape(&t.key),
33 xml_escape(&t.value)
34 )
35 })
36 .collect::<Vec<_>>()
37 .join("\n")
38}
39
40fn user_xml(user: &IamUser) -> String {
41 let tags_section = if user.tags.is_empty() {
42 String::new()
43 } else {
44 let tags_members = tags_xml(&user.tags);
45 format!("\n <Tags>\n{tags_members}\n </Tags>")
46 };
47
48 let pb_section = user
49 .permissions_boundary
50 .as_ref()
51 .map(|pb| {
52 format!(
53 "\n <PermissionsBoundary>\n <PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>\n <PermissionsBoundaryArn>{pb}</PermissionsBoundaryArn>\n </PermissionsBoundary>"
54 )
55 })
56 .unwrap_or_default();
57
58 format!(
59 r#" <User>
60 <Path>{path}</Path>
61 <UserName>{name}</UserName>
62 <UserId>{id}</UserId>
63 <Arn>{arn}</Arn>
64 <CreateDate>{date}</CreateDate>{tags_section}{pb_section}
65 </User>"#,
66 path = user.path,
67 name = user.user_name,
68 id = user.user_id,
69 arn = user.arn,
70 date = user.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
71 )
72}
73
74fn role_xml(role: &IamRole) -> String {
75 let tags_section = if role.tags.is_empty() {
76 String::new()
77 } else {
78 let tags_members = tags_xml(&role.tags);
79 format!("\n <Tags>\n{tags_members}\n </Tags>")
80 };
81
82 let pb_section = role
83 .permissions_boundary
84 .as_ref()
85 .map(|pb| {
86 format!(
87 "\n <PermissionsBoundary>\n <PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>\n <PermissionsBoundaryArn>{pb}</PermissionsBoundaryArn>\n </PermissionsBoundary>"
88 )
89 })
90 .unwrap_or_default();
91
92 let description_section = match &role.description {
93 Some(desc) => format!("\n <Description>{}</Description>", xml_escape(desc)),
94 None => String::new(),
95 };
96
97 format!(
98 r#" <Role>
99 <Path>{path}</Path>
100 <RoleName>{name}</RoleName>
101 <RoleId>{id}</RoleId>
102 <Arn>{arn}</Arn>
103 <CreateDate>{date}</CreateDate>
104 <AssumeRolePolicyDocument>{policy}</AssumeRolePolicyDocument>{description_section}
105 <MaxSessionDuration>{max_session}</MaxSessionDuration>
106 <RoleLastUsed/>{tags_section}{pb_section}
107 </Role>"#,
108 path = role.path,
109 name = role.role_name,
110 id = role.role_id,
111 arn = role.arn,
112 date = role.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
113 policy = url_encode_policy(&role.assume_role_policy_document),
114 max_session = role.max_session_duration,
115 )
116}
117
118pub fn create_user_response(user: &IamUser, request_id: &str) -> String {
119 let user_xml = user_xml(user);
120 format!(
121 r#"<?xml version="1.0" encoding="UTF-8"?>
122<CreateUserResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
123 <CreateUserResult>
124{user_xml}
125 </CreateUserResult>
126 <ResponseMetadata>
127 <RequestId>{request_id}</RequestId>
128 </ResponseMetadata>
129</CreateUserResponse>"#,
130 )
131}
132
133pub fn get_user_response(user: &IamUser, request_id: &str) -> String {
134 let user_xml = user_xml(user);
135 format!(
136 r#"<?xml version="1.0" encoding="UTF-8"?>
137<GetUserResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
138 <GetUserResult>
139{user_xml}
140 </GetUserResult>
141 <ResponseMetadata>
142 <RequestId>{request_id}</RequestId>
143 </ResponseMetadata>
144</GetUserResponse>"#,
145 )
146}
147
148pub fn list_users_response(
149 users: &[IamUser],
150 is_truncated: bool,
151 marker: Option<&str>,
152 request_id: &str,
153) -> String {
154 let members: String = users
155 .iter()
156 .map(|u| {
157 format!(
159 r#" <member>
160 <Path>{path}</Path>
161 <UserName>{name}</UserName>
162 <UserId>{id}</UserId>
163 <Arn>{arn}</Arn>
164 <CreateDate>{date}</CreateDate>
165 </member>"#,
166 path = u.path,
167 name = u.user_name,
168 id = u.user_id,
169 arn = u.arn,
170 date = u.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
171 )
172 })
173 .collect::<Vec<_>>()
174 .join("\n");
175
176 let marker_section = if let Some(m) = marker {
177 format!("\n <Marker>{}</Marker>", xml_escape(m))
178 } else {
179 String::new()
180 };
181
182 format!(
183 r#"<?xml version="1.0" encoding="UTF-8"?>
184<ListUsersResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
185 <ListUsersResult>
186 <IsTruncated>{is_truncated}</IsTruncated>{marker_section}
187 <Users>
188{members}
189 </Users>
190 </ListUsersResult>
191 <ResponseMetadata>
192 <RequestId>{request_id}</RequestId>
193 </ResponseMetadata>
194</ListUsersResponse>"#,
195 )
196}
197
198pub fn create_access_key_response(key: &IamAccessKey, request_id: &str) -> String {
199 format!(
200 r#"<?xml version="1.0" encoding="UTF-8"?>
201<CreateAccessKeyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
202 <CreateAccessKeyResult>
203 <AccessKey>
204 <UserName>{user}</UserName>
205 <AccessKeyId>{key_id}</AccessKeyId>
206 <Status>{status}</Status>
207 <SecretAccessKey>{secret}</SecretAccessKey>
208 <CreateDate>{date}</CreateDate>
209 </AccessKey>
210 </CreateAccessKeyResult>
211 <ResponseMetadata>
212 <RequestId>{request_id}</RequestId>
213 </ResponseMetadata>
214</CreateAccessKeyResponse>"#,
215 user = key.user_name,
216 key_id = key.access_key_id,
217 status = key.status,
218 secret = key.secret_access_key,
219 date = key.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
220 )
221}
222
223pub fn list_access_keys_response(
224 keys: &[IamAccessKey],
225 user_name: &str,
226 is_truncated: bool,
227 marker: Option<&str>,
228 request_id: &str,
229) -> String {
230 let members: String = keys
231 .iter()
232 .map(|k| {
233 format!(
234 r#" <member>
235 <UserName>{user}</UserName>
236 <AccessKeyId>{key_id}</AccessKeyId>
237 <Status>{status}</Status>
238 <CreateDate>{date}</CreateDate>
239 </member>"#,
240 user = k.user_name,
241 key_id = k.access_key_id,
242 status = k.status,
243 date = k.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
244 )
245 })
246 .collect::<Vec<_>>()
247 .join("\n");
248
249 let marker_section = if let Some(m) = marker {
250 format!("\n <Marker>{}</Marker>", xml_escape(m))
251 } else {
252 String::new()
253 };
254
255 format!(
256 r#"<?xml version="1.0" encoding="UTF-8"?>
257<ListAccessKeysResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
258 <ListAccessKeysResult>
259 <UserName>{user_name}</UserName>
260 <IsTruncated>{is_truncated}</IsTruncated>{marker_section}
261 <AccessKeyMetadata>
262{members}
263 </AccessKeyMetadata>
264 </ListAccessKeysResult>
265 <ResponseMetadata>
266 <RequestId>{request_id}</RequestId>
267 </ResponseMetadata>
268</ListAccessKeysResponse>"#,
269 )
270}
271
272pub fn create_role_response(role: &IamRole, request_id: &str) -> String {
273 let role_xml = role_xml(role);
274 format!(
275 r#"<?xml version="1.0" encoding="UTF-8"?>
276<CreateRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
277 <CreateRoleResult>
278{role_xml}
279 </CreateRoleResult>
280 <ResponseMetadata>
281 <RequestId>{request_id}</RequestId>
282 </ResponseMetadata>
283</CreateRoleResponse>"#,
284 )
285}
286
287pub fn get_role_response(role: &IamRole, request_id: &str) -> String {
288 let role_xml = role_xml(role);
289 format!(
290 r#"<?xml version="1.0" encoding="UTF-8"?>
291<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
292 <GetRoleResult>
293{role_xml}
294 </GetRoleResult>
295 <ResponseMetadata>
296 <RequestId>{request_id}</RequestId>
297 </ResponseMetadata>
298</GetRoleResponse>"#,
299 )
300}
301
302pub fn list_roles_response(roles: &[IamRole], request_id: &str) -> String {
303 let members: String = roles
304 .iter()
305 .map(|r| {
306 let description_section = match &r.description {
308 Some(desc) => format!("\n <Description>{}</Description>", xml_escape(desc)),
309 None => String::new(),
310 };
311 format!(
312 r#" <member>
313 <Path>{path}</Path>
314 <RoleName>{name}</RoleName>
315 <RoleId>{id}</RoleId>
316 <Arn>{arn}</Arn>
317 <CreateDate>{date}</CreateDate>
318 <AssumeRolePolicyDocument>{policy}</AssumeRolePolicyDocument>{description_section}
319 <MaxSessionDuration>{max_session}</MaxSessionDuration>
320 </member>"#,
321 path = r.path,
322 name = r.role_name,
323 id = r.role_id,
324 arn = r.arn,
325 date = r.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
326 policy = url_encode_policy(&r.assume_role_policy_document),
327 max_session = r.max_session_duration,
328 )
329 })
330 .collect::<Vec<_>>()
331 .join("\n");
332
333 format!(
334 r#"<?xml version="1.0" encoding="UTF-8"?>
335<ListRolesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
336 <ListRolesResult>
337 <IsTruncated>false</IsTruncated>
338 <Roles>
339{members}
340 </Roles>
341 </ListRolesResult>
342 <ResponseMetadata>
343 <RequestId>{request_id}</RequestId>
344 </ResponseMetadata>
345</ListRolesResponse>"#,
346 )
347}
348
349pub fn list_roles_response_paginated(
350 roles: &[IamRole],
351 is_truncated: bool,
352 marker: Option<&str>,
353 request_id: &str,
354) -> String {
355 let members: String = roles
356 .iter()
357 .map(|r| {
358 let description_section = match &r.description {
360 Some(desc) => format!("\n <Description>{}</Description>", xml_escape(desc)),
361 None => String::new(),
362 };
363 format!(
364 r#" <member>
365 <Path>{path}</Path>
366 <RoleName>{name}</RoleName>
367 <RoleId>{id}</RoleId>
368 <Arn>{arn}</Arn>
369 <CreateDate>{date}</CreateDate>
370 <AssumeRolePolicyDocument>{policy}</AssumeRolePolicyDocument>{description_section}
371 <MaxSessionDuration>{max_session}</MaxSessionDuration>
372 </member>"#,
373 path = r.path,
374 name = r.role_name,
375 id = r.role_id,
376 arn = r.arn,
377 date = r.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
378 policy = url_encode_policy(&r.assume_role_policy_document),
379 max_session = r.max_session_duration,
380 )
381 })
382 .collect::<Vec<_>>()
383 .join("\n");
384
385 let marker_section = if let Some(m) = marker {
386 format!("\n <Marker>{}</Marker>", xml_escape(m))
387 } else {
388 String::new()
389 };
390
391 format!(
392 r#"<?xml version="1.0" encoding="UTF-8"?>
393<ListRolesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
394 <ListRolesResult>
395 <IsTruncated>{is_truncated}</IsTruncated>{marker_section}
396 <Roles>
397{members}
398 </Roles>
399 </ListRolesResult>
400 <ResponseMetadata>
401 <RequestId>{request_id}</RequestId>
402 </ResponseMetadata>
403</ListRolesResponse>"#,
404 )
405}
406
407pub fn create_policy_response(policy: &IamPolicy, request_id: &str) -> String {
408 let tags_section = if policy.tags.is_empty() {
409 String::new()
410 } else {
411 let tags_members = tags_xml(&policy.tags);
412 format!("\n <Tags>\n{tags_members}\n </Tags>")
413 };
414
415 format!(
416 r#"<?xml version="1.0" encoding="UTF-8"?>
417<CreatePolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
418 <CreatePolicyResult>
419 <Policy>
420 <PolicyName>{name}</PolicyName>
421 <PolicyId>{id}</PolicyId>
422 <Arn>{arn}</Arn>
423 <Path>{path}</Path>
424 <DefaultVersionId>{default_version}</DefaultVersionId>
425 <AttachmentCount>{attachment_count}</AttachmentCount>
426 <IsAttachable>true</IsAttachable>
427 <CreateDate>{date}</CreateDate>{tags_section}
428 </Policy>
429 </CreatePolicyResult>
430 <ResponseMetadata>
431 <RequestId>{request_id}</RequestId>
432 </ResponseMetadata>
433</CreatePolicyResponse>"#,
434 name = policy.policy_name,
435 id = policy.policy_id,
436 arn = policy.arn,
437 path = policy.path,
438 default_version = policy.default_version_id,
439 attachment_count = policy.attachment_count,
440 date = policy.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
441 )
442}
443
444pub fn list_policies_response(
445 policies: &[IamPolicy],
446 is_truncated: bool,
447 marker: Option<&str>,
448 request_id: &str,
449) -> String {
450 let members: String = policies
451 .iter()
452 .map(|p| {
453 format!(
455 r#" <member>
456 <PolicyName>{name}</PolicyName>
457 <PolicyId>{id}</PolicyId>
458 <Arn>{arn}</Arn>
459 <Path>{path}</Path>
460 <DefaultVersionId>{default_version}</DefaultVersionId>
461 <AttachmentCount>{attachment_count}</AttachmentCount>
462 <IsAttachable>true</IsAttachable>
463 <CreateDate>{date}</CreateDate>
464 </member>"#,
465 name = p.policy_name,
466 id = p.policy_id,
467 arn = p.arn,
468 path = p.path,
469 default_version = p.default_version_id,
470 attachment_count = p.attachment_count,
471 date = p.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
472 )
473 })
474 .collect::<Vec<_>>()
475 .join("\n");
476
477 let marker_section = if let Some(m) = marker {
478 format!("\n <Marker>{}</Marker>", xml_escape(m))
479 } else {
480 String::new()
481 };
482
483 format!(
484 r#"<?xml version="1.0" encoding="UTF-8"?>
485<ListPoliciesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
486 <ListPoliciesResult>
487 <IsTruncated>{is_truncated}</IsTruncated>{marker_section}
488 <Policies>
489{members}
490 </Policies>
491 </ListPoliciesResult>
492 <ResponseMetadata>
493 <RequestId>{request_id}</RequestId>
494 </ResponseMetadata>
495</ListPoliciesResponse>"#,
496 )
497}
498
499pub fn get_policy_response(policy: &IamPolicy, request_id: &str) -> String {
500 let tags_section = if policy.tags.is_empty() {
502 "\n <Tags/>".to_string()
503 } else {
504 let tags_members = tags_xml(&policy.tags);
505 format!("\n <Tags>\n{tags_members}\n </Tags>")
506 };
507
508 let description_section = if policy.description.is_empty() {
509 String::new()
510 } else {
511 format!(
512 "\n <Description>{}</Description>",
513 xml_escape(&policy.description)
514 )
515 };
516
517 format!(
518 r#"<?xml version="1.0" encoding="UTF-8"?>
519<GetPolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
520 <GetPolicyResult>
521 <Policy>
522 <PolicyName>{name}</PolicyName>
523 <PolicyId>{id}</PolicyId>
524 <Arn>{arn}</Arn>
525 <Path>{path}</Path>
526 <DefaultVersionId>{default_version}</DefaultVersionId>
527 <AttachmentCount>{attachment_count}</AttachmentCount>
528 <IsAttachable>true</IsAttachable>
529 <CreateDate>{date}</CreateDate>{description_section}{tags_section}
530 </Policy>
531 </GetPolicyResult>
532 <ResponseMetadata>
533 <RequestId>{request_id}</RequestId>
534 </ResponseMetadata>
535</GetPolicyResponse>"#,
536 name = policy.policy_name,
537 id = policy.policy_id,
538 arn = policy.arn,
539 path = policy.path,
540 default_version = policy.default_version_id,
541 attachment_count = policy.attachment_count,
542 date = policy.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
543 )
544}
545
546pub fn list_role_policies_response(policy_names: &[String], request_id: &str) -> String {
547 let members: String = policy_names
548 .iter()
549 .map(|name| format!(" <member>{name}</member>"))
550 .collect::<Vec<_>>()
551 .join("\n");
552
553 format!(
554 r#"<?xml version="1.0" encoding="UTF-8"?>
555<ListRolePoliciesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
556 <ListRolePoliciesResult>
557 <IsTruncated>false</IsTruncated>
558 <PolicyNames>
559{members}
560 </PolicyNames>
561 </ListRolePoliciesResult>
562 <ResponseMetadata>
563 <RequestId>{request_id}</RequestId>
564 </ResponseMetadata>
565</ListRolePoliciesResponse>"#,
566 )
567}
568
569pub fn get_caller_identity_response(
570 account_id: &str,
571 arn: &str,
572 user_id: &str,
573 request_id: &str,
574) -> String {
575 format!(
576 r#"<?xml version="1.0" encoding="UTF-8"?>
577<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
578 <GetCallerIdentityResult>
579 <Arn>{arn}</Arn>
580 <UserId>{user_id}</UserId>
581 <Account>{account_id}</Account>
582 </GetCallerIdentityResult>
583 <ResponseMetadata>
584 <RequestId>{request_id}</RequestId>
585 </ResponseMetadata>
586</GetCallerIdentityResponse>"#,
587 )
588}
589
590pub struct StsCredentials {
592 pub access_key_id: String,
593 pub secret_access_key: String,
594 pub session_token: String,
595}
596
597impl StsCredentials {
598 pub fn generate() -> Self {
599 Self {
600 access_key_id: generate_access_key_id(),
601 secret_access_key: generate_secret_access_key(),
602 session_token: generate_session_token(),
603 }
604 }
605}
606
607pub struct AssumedRoleInfo<'a> {
609 pub role_arn: &'a str,
610 pub role_session_name: &'a str,
611 pub assumed_role_id: &'a str,
612 pub account_id: &'a str,
613 pub partition: &'a str,
614 pub creds: &'a StsCredentials,
615 pub expiration: &'a str,
616 pub request_id: &'a str,
617}
618
619impl AssumedRoleInfo<'_> {
620 fn assumed_role_arn(&self) -> String {
621 let role_name = self.role_arn.rsplit('/').next().unwrap_or("unknown");
622 format!(
623 "arn:{}:sts::{}:assumed-role/{}/{}",
624 self.partition, self.account_id, role_name, self.role_session_name
625 )
626 }
627}
628
629pub fn assume_role_response(info: &AssumedRoleInfo<'_>) -> String {
630 let assumed_role_arn = info.assumed_role_arn();
631 format!(
632 r#"<?xml version="1.0" encoding="UTF-8"?>
633<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
634 <AssumeRoleResult>
635 <Credentials>
636 <AccessKeyId>{access_key_id}</AccessKeyId>
637 <SecretAccessKey>{secret_access_key}</SecretAccessKey>
638 <SessionToken>{session_token}</SessionToken>
639 <Expiration>{expiration}</Expiration>
640 </Credentials>
641 <AssumedRoleUser>
642 <AssumedRoleId>{role_id}:{session}</AssumedRoleId>
643 <Arn>{assumed_role_arn}</Arn>
644 </AssumedRoleUser>
645 </AssumeRoleResult>
646 <ResponseMetadata>
647 <RequestId>{request_id}</RequestId>
648 </ResponseMetadata>
649</AssumeRoleResponse>"#,
650 access_key_id = info.creds.access_key_id,
651 secret_access_key = info.creds.secret_access_key,
652 session_token = info.creds.session_token,
653 role_id = info.assumed_role_id,
654 assumed_role_arn = assumed_role_arn,
655 session = info.role_session_name,
656 expiration = info.expiration,
657 request_id = info.request_id,
658 )
659}
660
661pub fn assume_role_with_web_identity_response(info: &AssumedRoleInfo<'_>) -> String {
662 let assumed_role_arn = info.assumed_role_arn();
663 format!(
664 r#"<?xml version="1.0" encoding="UTF-8"?>
665<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
666 <AssumeRoleWithWebIdentityResult>
667 <Credentials>
668 <AccessKeyId>{access_key_id}</AccessKeyId>
669 <SecretAccessKey>{secret_access_key}</SecretAccessKey>
670 <SessionToken>{session_token}</SessionToken>
671 <Expiration>{expiration}</Expiration>
672 </Credentials>
673 <AssumedRoleUser>
674 <AssumedRoleId>{assumed_role_id}:{session}</AssumedRoleId>
675 <Arn>{assumed_role_arn}</Arn>
676 </AssumedRoleUser>
677 </AssumeRoleWithWebIdentityResult>
678 <ResponseMetadata>
679 <RequestId>{request_id}</RequestId>
680 </ResponseMetadata>
681</AssumeRoleWithWebIdentityResponse>"#,
682 access_key_id = info.creds.access_key_id,
683 secret_access_key = info.creds.secret_access_key,
684 session_token = info.creds.session_token,
685 assumed_role_id = info.assumed_role_id,
686 assumed_role_arn = assumed_role_arn,
687 session = info.role_session_name,
688 expiration = info.expiration,
689 request_id = info.request_id,
690 )
691}
692
693pub fn assume_role_with_saml_response(info: &AssumedRoleInfo<'_>) -> String {
694 let assumed_role_arn = info.assumed_role_arn();
695 format!(
696 r#"<?xml version="1.0" encoding="UTF-8"?>
697<AssumeRoleWithSAMLResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
698 <AssumeRoleWithSAMLResult>
699 <Credentials>
700 <AccessKeyId>{access_key_id}</AccessKeyId>
701 <SecretAccessKey>{secret_access_key}</SecretAccessKey>
702 <SessionToken>{session_token}</SessionToken>
703 <Expiration>{expiration}</Expiration>
704 </Credentials>
705 <AssumedRoleUser>
706 <AssumedRoleId>{assumed_role_id}:{session}</AssumedRoleId>
707 <Arn>{assumed_role_arn}</Arn>
708 </AssumedRoleUser>
709 </AssumeRoleWithSAMLResult>
710 <ResponseMetadata>
711 <RequestId>{request_id}</RequestId>
712 </ResponseMetadata>
713</AssumeRoleWithSAMLResponse>"#,
714 access_key_id = info.creds.access_key_id,
715 secret_access_key = info.creds.secret_access_key,
716 session_token = info.creds.session_token,
717 assumed_role_id = info.assumed_role_id,
718 assumed_role_arn = assumed_role_arn,
719 session = info.role_session_name,
720 expiration = info.expiration,
721 request_id = info.request_id,
722 )
723}
724
725pub fn get_session_token_response(
726 creds: &StsCredentials,
727 expiration: &str,
728 request_id: &str,
729) -> String {
730 let access_key_id = creds.access_key_id.as_str();
731 let secret_access_key = creds.secret_access_key.as_str();
732 let session_token = creds.session_token.as_str();
733
734 format!(
735 r#"<?xml version="1.0" encoding="UTF-8"?>
736<GetSessionTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
737 <GetSessionTokenResult>
738 <Credentials>
739 <AccessKeyId>{access_key_id}</AccessKeyId>
740 <SecretAccessKey>{secret_access_key}</SecretAccessKey>
741 <SessionToken>{session_token}</SessionToken>
742 <Expiration>{expiration}</Expiration>
743 </Credentials>
744 </GetSessionTokenResult>
745 <ResponseMetadata>
746 <RequestId>{request_id}</RequestId>
747 </ResponseMetadata>
748</GetSessionTokenResponse>"#,
749 access_key_id = access_key_id,
750 secret_access_key = secret_access_key,
751 session_token = session_token,
752 request_id = request_id,
753 )
754}
755
756pub fn get_federation_token_response(
757 creds: &StsCredentials,
758 name: &str,
759 account_id: &str,
760 partition: &str,
761 expiration: &str,
762 policy: Option<&str>,
763 request_id: &str,
764) -> String {
765 let access_key_id = creds.access_key_id.as_str();
766 let secret_access_key = creds.secret_access_key.as_str();
767 let session_token = creds.session_token.as_str();
768
769 let name = xml_escape(name);
770 let federated_user_arn = format!(
771 "arn:{}:sts::{}:federated-user/{}",
772 partition, account_id, name
773 );
774 let federated_user_id = format!("{}:{}", account_id, name);
775
776 let policy_section = if let Some(p) = policy {
777 format!(
778 "\n <PackedPolicySize>6</PackedPolicySize>\n <Policy>{}</Policy>",
779 xml_escape(p)
780 )
781 } else {
782 String::new()
783 };
784
785 format!(
786 r#"<?xml version="1.0" encoding="UTF-8"?>
787<GetFederationTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
788 <GetFederationTokenResult>
789 <Credentials>
790 <AccessKeyId>{access_key_id}</AccessKeyId>
791 <SecretAccessKey>{secret_access_key}</SecretAccessKey>
792 <SessionToken>{session_token}</SessionToken>
793 <Expiration>{expiration}</Expiration>
794 </Credentials>
795 <FederatedUser>
796 <FederatedUserId>{federated_user_id}</FederatedUserId>
797 <Arn>{federated_user_arn}</Arn>
798 </FederatedUser>{policy_section}
799 </GetFederationTokenResult>
800 <ResponseMetadata>
801 <RequestId>{request_id}</RequestId>
802 </ResponseMetadata>
803</GetFederationTokenResponse>"#,
804 access_key_id = access_key_id,
805 secret_access_key = secret_access_key,
806 session_token = session_token,
807 federated_user_arn = federated_user_arn,
808 federated_user_id = federated_user_id,
809 request_id = request_id,
810 )
811}
812
813pub fn assume_root_response(
814 creds: &StsCredentials,
815 expiration: &str,
816 source_identity: Option<&str>,
817 request_id: &str,
818) -> String {
819 let access_key_id = creds.access_key_id.as_str();
820 let secret_access_key = creds.secret_access_key.as_str();
821 let session_token = creds.session_token.as_str();
822 let source_identity_section = match source_identity {
823 Some(s) => format!("\n <SourceIdentity>{}</SourceIdentity>", xml_escape(s)),
824 None => String::new(),
825 };
826 format!(
827 r#"<?xml version="1.0" encoding="UTF-8"?>
828<AssumeRootResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
829 <AssumeRootResult>
830 <Credentials>
831 <AccessKeyId>{access_key_id}</AccessKeyId>
832 <SecretAccessKey>{secret_access_key}</SecretAccessKey>
833 <SessionToken>{session_token}</SessionToken>
834 <Expiration>{expiration}</Expiration>
835 </Credentials>{source_identity_section}
836 </AssumeRootResult>
837 <ResponseMetadata>
838 <RequestId>{request_id}</RequestId>
839 </ResponseMetadata>
840</AssumeRootResponse>"#,
841 )
842}
843
844pub fn get_web_identity_token_response(
845 web_identity_token: &str,
846 expiration: &str,
847 request_id: &str,
848) -> String {
849 format!(
850 r#"<?xml version="1.0" encoding="UTF-8"?>
851<GetWebIdentityTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
852 <GetWebIdentityTokenResult>
853 <WebIdentityToken>{web_identity_token}</WebIdentityToken>
854 <Expiration>{expiration}</Expiration>
855 </GetWebIdentityTokenResult>
856 <ResponseMetadata>
857 <RequestId>{request_id}</RequestId>
858 </ResponseMetadata>
859</GetWebIdentityTokenResponse>"#,
860 web_identity_token = xml_escape(web_identity_token),
861 expiration = expiration,
862 request_id = request_id,
863 )
864}
865
866pub fn get_delegated_access_token_response(
867 creds: &StsCredentials,
868 expiration: &str,
869 assumed_principal: &str,
870 request_id: &str,
871) -> String {
872 let access_key_id = creds.access_key_id.as_str();
873 let secret_access_key = creds.secret_access_key.as_str();
874 let session_token = creds.session_token.as_str();
875 format!(
876 r#"<?xml version="1.0" encoding="UTF-8"?>
877<GetDelegatedAccessTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
878 <GetDelegatedAccessTokenResult>
879 <Credentials>
880 <AccessKeyId>{access_key_id}</AccessKeyId>
881 <SecretAccessKey>{secret_access_key}</SecretAccessKey>
882 <SessionToken>{session_token}</SessionToken>
883 <Expiration>{expiration}</Expiration>
884 </Credentials>
885 <PackedPolicySize>0</PackedPolicySize>
886 <AssumedPrincipal>{assumed_principal}</AssumedPrincipal>
887 </GetDelegatedAccessTokenResult>
888 <ResponseMetadata>
889 <RequestId>{request_id}</RequestId>
890 </ResponseMetadata>
891</GetDelegatedAccessTokenResponse>"#,
892 assumed_principal = xml_escape(assumed_principal),
893 )
894}
895
896pub fn decode_authorization_message_response(decoded_message: &str, request_id: &str) -> String {
897 format!(
898 r#"<?xml version="1.0" encoding="UTF-8"?>
899<DecodeAuthorizationMessageResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
900 <DecodeAuthorizationMessageResult>
901 <DecodedMessage>{decoded_message}</DecodedMessage>
902 </DecodeAuthorizationMessageResult>
903 <ResponseMetadata>
904 <RequestId>{request_id}</RequestId>
905 </ResponseMetadata>
906</DecodeAuthorizationMessageResponse>"#,
907 decoded_message = xml_escape(decoded_message),
908 request_id = request_id,
909 )
910}
911
912pub fn get_access_key_info_response(account_id: &str, request_id: &str) -> String {
913 format!(
914 r#"<?xml version="1.0" encoding="UTF-8"?>
915<GetAccessKeyInfoResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
916 <GetAccessKeyInfoResult>
917 <Account>{account_id}</Account>
918 </GetAccessKeyInfoResult>
919 <ResponseMetadata>
920 <RequestId>{request_id}</RequestId>
921 </ResponseMetadata>
922</GetAccessKeyInfoResponse>"#,
923 account_id = account_id,
924 request_id = request_id,
925 )
926}
927
928pub fn generate_access_key_id() -> String {
930 let id = generate_alphanum_id(16);
931 format!("FSIA{}", id)
932}
933
934pub fn generate_secret_access_key() -> String {
936 generate_alphanum_id(40)
937}
938
939pub fn generate_role_id() -> String {
941 let id = generate_alphanum_id(17);
942 format!("AROA{}", id)
943}
944
945pub fn generate_session_token() -> String {
947 let prefix = "FQoGZXIvYXdzE";
949 let remaining = 356 - prefix.len(); let mut raw = Vec::with_capacity(288);
953 for _ in 0..18 {
954 raw.extend_from_slice(uuid::Uuid::new_v4().as_bytes());
955 }
956 let encoded = base64::engine::general_purpose::STANDARD.encode(&raw);
957 let suffix = &encoded[..remaining];
959 format!("{}{}", prefix, suffix)
960}
961
962fn generate_alphanum_id(len: usize) -> String {
964 let raw = format!(
965 "{}{}{}",
966 uuid::Uuid::new_v4(),
967 uuid::Uuid::new_v4(),
968 uuid::Uuid::new_v4(),
969 );
970 raw.replace('-', "")
971 .chars()
972 .filter(|c| c.is_alphanumeric())
973 .take(len)
974 .collect()
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[test]
982 fn url_encode_policy_escapes_special_chars() {
983 let encoded = url_encode_policy("a b");
984 assert!(encoded.contains("%20") || encoded.contains("+"));
985 }
986
987 #[test]
988 fn generate_alphanum_id_length() {
989 let id = generate_alphanum_id(20);
990 assert_eq!(id.len(), 20);
991 assert!(id.chars().all(|c| c.is_alphanumeric()));
992 }
993
994 #[test]
995 fn generate_alphanum_id_uniqueness() {
996 let a = generate_alphanum_id(16);
997 let b = generate_alphanum_id(16);
998 assert_ne!(a, b);
999 }
1000}