Skip to main content

fakecloud_iam/
xml_responses.rs

1use base64::Engine;
2
3use crate::state::{IamAccessKey, IamPolicy, IamRole, IamUser};
4
5use fakecloud_aws::xml::xml_escape;
6
7/// URL-encode a policy document for XML embedding (like AWS does).
8fn 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            // ListUsers never includes Tags or PermissionsBoundary (AWS behavior)
153            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            // ListRoles does NOT include Tags, PermissionsBoundary, or RoleLastUsed
296            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            // ListRoles does NOT include Tags, PermissionsBoundary, or RoleLastUsed
348            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            // ListPolicies never includes Tags or Description (AWS behavior)
438            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    // GetPolicy always includes Tags, even if empty (AWS behavior)
479    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
568/// Pre-generated STS credentials to be returned in XML responses.
569pub 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
585/// Inputs shared by all assume-role variants used to build an STS XML response.
586pub 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 decode_authorization_message_response(decoded_message: &str, request_id: &str) -> String {
792    format!(
793        r#"<?xml version="1.0" encoding="UTF-8"?>
794<DecodeAuthorizationMessageResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
795  <DecodeAuthorizationMessageResult>
796    <DecodedMessage>{decoded_message}</DecodedMessage>
797  </DecodeAuthorizationMessageResult>
798  <ResponseMetadata>
799    <RequestId>{request_id}</RequestId>
800  </ResponseMetadata>
801</DecodeAuthorizationMessageResponse>"#,
802        decoded_message = xml_escape(decoded_message),
803        request_id = request_id,
804    )
805}
806
807pub fn get_access_key_info_response(account_id: &str, request_id: &str) -> String {
808    format!(
809        r#"<?xml version="1.0" encoding="UTF-8"?>
810<GetAccessKeyInfoResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
811  <GetAccessKeyInfoResult>
812    <Account>{account_id}</Account>
813  </GetAccessKeyInfoResult>
814  <ResponseMetadata>
815    <RequestId>{request_id}</RequestId>
816  </ResponseMetadata>
817</GetAccessKeyInfoResponse>"#,
818        account_id = account_id,
819        request_id = request_id,
820    )
821}
822
823/// Generate an FSIA-prefixed temporary access key ID (20 chars total).
824pub fn generate_access_key_id() -> String {
825    let id = generate_alphanum_id(16);
826    format!("FSIA{}", id)
827}
828
829/// Generate a 40-character secret access key.
830pub fn generate_secret_access_key() -> String {
831    generate_alphanum_id(40)
832}
833
834/// Generate an AROA-prefixed role ID (21 chars total).
835pub fn generate_role_id() -> String {
836    let id = generate_alphanum_id(17);
837    format!("AROA{}", id)
838}
839
840/// Generate a session token that is exactly 356 characters starting with "FQoGZXIvYXdzE".
841pub fn generate_session_token() -> String {
842    // AWS session tokens are typically 356 chars and start with "FQoGZXIvYXdzE"
843    let prefix = "FQoGZXIvYXdzE";
844    let remaining = 356 - prefix.len(); // 343 chars needed
845                                        // Generate enough random bytes: we need at least ceil(343*3/4) = 258 bytes
846                                        // 18 UUIDs * 16 bytes = 288 bytes -> base64 = 384 chars (plenty)
847    let mut raw = Vec::with_capacity(288);
848    for _ in 0..18 {
849        raw.extend_from_slice(uuid::Uuid::new_v4().as_bytes());
850    }
851    let encoded = base64::engine::general_purpose::STANDARD.encode(&raw);
852    // Take exactly what we need from the encoded data
853    let suffix = &encoded[..remaining];
854    format!("{}{}", prefix, suffix)
855}
856
857/// Generate alphanumeric ID of given length.
858fn generate_alphanum_id(len: usize) -> String {
859    let raw = format!(
860        "{}{}{}",
861        uuid::Uuid::new_v4(),
862        uuid::Uuid::new_v4(),
863        uuid::Uuid::new_v4(),
864    );
865    raw.replace('-', "")
866        .chars()
867        .filter(|c| c.is_alphanumeric())
868        .take(len)
869        .collect()
870}