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