Skip to main content

fakecloud_iam/
xml_responses.rs

1use base64::Engine;
2
3use crate::state::{IamAccessKey, IamPolicy, IamRole, IamUser};
4
5fn xml_escape(s: &str) -> String {
6    s.replace('&', "&")
7        .replace('<', "&lt;")
8        .replace('>', "&gt;")
9        .replace('"', "&quot;")
10}
11
12/// URL-encode a policy document for XML embedding (like AWS does).
13fn url_encode_policy(s: &str) -> String {
14    let mut result = String::new();
15    for byte in s.bytes() {
16        match byte {
17            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
18                result.push(byte as char);
19            }
20            _ => {
21                use std::fmt::Write;
22                write!(result, "%{:02X}", byte).unwrap();
23            }
24        }
25    }
26    result
27}
28
29fn tags_xml(tags: &[crate::state::Tag]) -> String {
30    if tags.is_empty() {
31        return String::new();
32    }
33    tags.iter()
34        .map(|t| {
35            format!(
36                "        <member>\n          <Key>{}</Key>\n          <Value>{}</Value>\n        </member>",
37                xml_escape(&t.key),
38                xml_escape(&t.value)
39            )
40        })
41        .collect::<Vec<_>>()
42        .join("\n")
43}
44
45fn user_xml(user: &IamUser) -> String {
46    let tags_section = if user.tags.is_empty() {
47        String::new()
48    } else {
49        let tags_members = tags_xml(&user.tags);
50        format!("\n      <Tags>\n{tags_members}\n      </Tags>")
51    };
52
53    let pb_section = user
54        .permissions_boundary
55        .as_ref()
56        .map(|pb| {
57            format!(
58                "\n      <PermissionsBoundary>\n        <PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>\n        <PermissionsBoundaryArn>{pb}</PermissionsBoundaryArn>\n      </PermissionsBoundary>"
59            )
60        })
61        .unwrap_or_default();
62
63    format!(
64        r#"    <User>
65      <Path>{path}</Path>
66      <UserName>{name}</UserName>
67      <UserId>{id}</UserId>
68      <Arn>{arn}</Arn>
69      <CreateDate>{date}</CreateDate>{tags_section}{pb_section}
70    </User>"#,
71        path = user.path,
72        name = user.user_name,
73        id = user.user_id,
74        arn = user.arn,
75        date = user.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
76    )
77}
78
79fn role_xml(role: &IamRole) -> String {
80    let tags_section = if role.tags.is_empty() {
81        String::new()
82    } else {
83        let tags_members = tags_xml(&role.tags);
84        format!("\n      <Tags>\n{tags_members}\n      </Tags>")
85    };
86
87    let pb_section = role
88        .permissions_boundary
89        .as_ref()
90        .map(|pb| {
91            format!(
92                "\n      <PermissionsBoundary>\n        <PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>\n        <PermissionsBoundaryArn>{pb}</PermissionsBoundaryArn>\n      </PermissionsBoundary>"
93            )
94        })
95        .unwrap_or_default();
96
97    let description_section = match &role.description {
98        Some(desc) => format!("\n      <Description>{}</Description>", xml_escape(desc)),
99        None => String::new(),
100    };
101
102    format!(
103        r#"    <Role>
104      <Path>{path}</Path>
105      <RoleName>{name}</RoleName>
106      <RoleId>{id}</RoleId>
107      <Arn>{arn}</Arn>
108      <CreateDate>{date}</CreateDate>
109      <AssumeRolePolicyDocument>{policy}</AssumeRolePolicyDocument>{description_section}
110      <MaxSessionDuration>{max_session}</MaxSessionDuration>
111      <RoleLastUsed/>{tags_section}{pb_section}
112    </Role>"#,
113        path = role.path,
114        name = role.role_name,
115        id = role.role_id,
116        arn = role.arn,
117        date = role.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
118        policy = url_encode_policy(&role.assume_role_policy_document),
119        max_session = role.max_session_duration,
120    )
121}
122
123pub fn create_user_response(user: &IamUser, request_id: &str) -> String {
124    let user_xml = user_xml(user);
125    format!(
126        r#"<?xml version="1.0" encoding="UTF-8"?>
127<CreateUserResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
128  <CreateUserResult>
129{user_xml}
130  </CreateUserResult>
131  <ResponseMetadata>
132    <RequestId>{request_id}</RequestId>
133  </ResponseMetadata>
134</CreateUserResponse>"#,
135    )
136}
137
138pub fn get_user_response(user: &IamUser, request_id: &str) -> String {
139    let user_xml = user_xml(user);
140    format!(
141        r#"<?xml version="1.0" encoding="UTF-8"?>
142<GetUserResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
143  <GetUserResult>
144{user_xml}
145  </GetUserResult>
146  <ResponseMetadata>
147    <RequestId>{request_id}</RequestId>
148  </ResponseMetadata>
149</GetUserResponse>"#,
150    )
151}
152
153pub fn list_users_response(users: &[IamUser], request_id: &str) -> String {
154    let members: String = users
155        .iter()
156        .map(|u| {
157            // ListUsers never includes Tags or PermissionsBoundary (AWS behavior)
158            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    format!(
177        r#"<?xml version="1.0" encoding="UTF-8"?>
178<ListUsersResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
179  <ListUsersResult>
180    <IsTruncated>false</IsTruncated>
181    <Users>
182{members}
183    </Users>
184  </ListUsersResult>
185  <ResponseMetadata>
186    <RequestId>{request_id}</RequestId>
187  </ResponseMetadata>
188</ListUsersResponse>"#,
189    )
190}
191
192pub fn create_access_key_response(key: &IamAccessKey, request_id: &str) -> String {
193    format!(
194        r#"<?xml version="1.0" encoding="UTF-8"?>
195<CreateAccessKeyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
196  <CreateAccessKeyResult>
197    <AccessKey>
198      <UserName>{user}</UserName>
199      <AccessKeyId>{key_id}</AccessKeyId>
200      <Status>{status}</Status>
201      <SecretAccessKey>{secret}</SecretAccessKey>
202      <CreateDate>{date}</CreateDate>
203    </AccessKey>
204  </CreateAccessKeyResult>
205  <ResponseMetadata>
206    <RequestId>{request_id}</RequestId>
207  </ResponseMetadata>
208</CreateAccessKeyResponse>"#,
209        user = key.user_name,
210        key_id = key.access_key_id,
211        status = key.status,
212        secret = key.secret_access_key,
213        date = key.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
214    )
215}
216
217pub fn list_access_keys_response(
218    keys: &[IamAccessKey],
219    user_name: &str,
220    request_id: &str,
221) -> String {
222    let members: String = keys
223        .iter()
224        .map(|k| {
225            format!(
226                r#"      <member>
227        <UserName>{user}</UserName>
228        <AccessKeyId>{key_id}</AccessKeyId>
229        <Status>{status}</Status>
230        <CreateDate>{date}</CreateDate>
231      </member>"#,
232                user = k.user_name,
233                key_id = k.access_key_id,
234                status = k.status,
235                date = k.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
236            )
237        })
238        .collect::<Vec<_>>()
239        .join("\n");
240
241    format!(
242        r#"<?xml version="1.0" encoding="UTF-8"?>
243<ListAccessKeysResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
244  <ListAccessKeysResult>
245    <UserName>{user_name}</UserName>
246    <IsTruncated>false</IsTruncated>
247    <AccessKeyMetadata>
248{members}
249    </AccessKeyMetadata>
250  </ListAccessKeysResult>
251  <ResponseMetadata>
252    <RequestId>{request_id}</RequestId>
253  </ResponseMetadata>
254</ListAccessKeysResponse>"#,
255    )
256}
257
258pub fn create_role_response(role: &IamRole, request_id: &str) -> String {
259    let role_xml = role_xml(role);
260    format!(
261        r#"<?xml version="1.0" encoding="UTF-8"?>
262<CreateRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
263  <CreateRoleResult>
264{role_xml}
265  </CreateRoleResult>
266  <ResponseMetadata>
267    <RequestId>{request_id}</RequestId>
268  </ResponseMetadata>
269</CreateRoleResponse>"#,
270    )
271}
272
273pub fn get_role_response(role: &IamRole, request_id: &str) -> String {
274    let role_xml = role_xml(role);
275    format!(
276        r#"<?xml version="1.0" encoding="UTF-8"?>
277<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
278  <GetRoleResult>
279{role_xml}
280  </GetRoleResult>
281  <ResponseMetadata>
282    <RequestId>{request_id}</RequestId>
283  </ResponseMetadata>
284</GetRoleResponse>"#,
285    )
286}
287
288pub fn list_roles_response(roles: &[IamRole], request_id: &str) -> String {
289    let members: String = roles
290        .iter()
291        .map(|r| {
292            // ListRoles does NOT include Tags, PermissionsBoundary, or RoleLastUsed
293            let description_section = match &r.description {
294                Some(desc) => format!("\n        <Description>{}</Description>", xml_escape(desc)),
295                None => String::new(),
296            };
297            format!(
298                r#"      <member>
299        <Path>{path}</Path>
300        <RoleName>{name}</RoleName>
301        <RoleId>{id}</RoleId>
302        <Arn>{arn}</Arn>
303        <CreateDate>{date}</CreateDate>
304        <AssumeRolePolicyDocument>{policy}</AssumeRolePolicyDocument>{description_section}
305        <MaxSessionDuration>{max_session}</MaxSessionDuration>
306      </member>"#,
307                path = r.path,
308                name = r.role_name,
309                id = r.role_id,
310                arn = r.arn,
311                date = r.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
312                policy = url_encode_policy(&r.assume_role_policy_document),
313                max_session = r.max_session_duration,
314            )
315        })
316        .collect::<Vec<_>>()
317        .join("\n");
318
319    format!(
320        r#"<?xml version="1.0" encoding="UTF-8"?>
321<ListRolesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
322  <ListRolesResult>
323    <IsTruncated>false</IsTruncated>
324    <Roles>
325{members}
326    </Roles>
327  </ListRolesResult>
328  <ResponseMetadata>
329    <RequestId>{request_id}</RequestId>
330  </ResponseMetadata>
331</ListRolesResponse>"#,
332    )
333}
334
335pub fn list_roles_response_paginated(
336    roles: &[IamRole],
337    is_truncated: bool,
338    marker: Option<&str>,
339    request_id: &str,
340) -> String {
341    let members: String = roles
342        .iter()
343        .map(|r| {
344            // ListRoles does NOT include Tags, PermissionsBoundary, or RoleLastUsed
345            let description_section = match &r.description {
346                Some(desc) => format!("\n        <Description>{}</Description>", xml_escape(desc)),
347                None => String::new(),
348            };
349            format!(
350                r#"      <member>
351        <Path>{path}</Path>
352        <RoleName>{name}</RoleName>
353        <RoleId>{id}</RoleId>
354        <Arn>{arn}</Arn>
355        <CreateDate>{date}</CreateDate>
356        <AssumeRolePolicyDocument>{policy}</AssumeRolePolicyDocument>{description_section}
357        <MaxSessionDuration>{max_session}</MaxSessionDuration>
358      </member>"#,
359                path = r.path,
360                name = r.role_name,
361                id = r.role_id,
362                arn = r.arn,
363                date = r.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
364                policy = url_encode_policy(&r.assume_role_policy_document),
365                max_session = r.max_session_duration,
366            )
367        })
368        .collect::<Vec<_>>()
369        .join("\n");
370
371    let marker_section = if let Some(m) = marker {
372        format!("\n    <Marker>{}</Marker>", xml_escape(m))
373    } else {
374        String::new()
375    };
376
377    format!(
378        r#"<?xml version="1.0" encoding="UTF-8"?>
379<ListRolesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
380  <ListRolesResult>
381    <IsTruncated>{is_truncated}</IsTruncated>{marker_section}
382    <Roles>
383{members}
384    </Roles>
385  </ListRolesResult>
386  <ResponseMetadata>
387    <RequestId>{request_id}</RequestId>
388  </ResponseMetadata>
389</ListRolesResponse>"#,
390    )
391}
392
393pub fn create_policy_response(policy: &IamPolicy, request_id: &str) -> String {
394    let tags_section = if policy.tags.is_empty() {
395        String::new()
396    } else {
397        let tags_members = tags_xml(&policy.tags);
398        format!("\n      <Tags>\n{tags_members}\n      </Tags>")
399    };
400
401    format!(
402        r#"<?xml version="1.0" encoding="UTF-8"?>
403<CreatePolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
404  <CreatePolicyResult>
405    <Policy>
406      <PolicyName>{name}</PolicyName>
407      <PolicyId>{id}</PolicyId>
408      <Arn>{arn}</Arn>
409      <Path>{path}</Path>
410      <DefaultVersionId>{default_version}</DefaultVersionId>
411      <AttachmentCount>{attachment_count}</AttachmentCount>
412      <IsAttachable>true</IsAttachable>
413      <CreateDate>{date}</CreateDate>{tags_section}
414    </Policy>
415  </CreatePolicyResult>
416  <ResponseMetadata>
417    <RequestId>{request_id}</RequestId>
418  </ResponseMetadata>
419</CreatePolicyResponse>"#,
420        name = policy.policy_name,
421        id = policy.policy_id,
422        arn = policy.arn,
423        path = policy.path,
424        default_version = policy.default_version_id,
425        attachment_count = policy.attachment_count,
426        date = policy.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
427    )
428}
429
430pub fn list_policies_response(policies: &[IamPolicy], request_id: &str) -> String {
431    let members: String = policies
432        .iter()
433        .map(|p| {
434            // ListPolicies never includes Tags or Description (AWS behavior)
435            format!(
436                r#"      <member>
437        <PolicyName>{name}</PolicyName>
438        <PolicyId>{id}</PolicyId>
439        <Arn>{arn}</Arn>
440        <Path>{path}</Path>
441        <DefaultVersionId>{default_version}</DefaultVersionId>
442        <AttachmentCount>{attachment_count}</AttachmentCount>
443        <IsAttachable>true</IsAttachable>
444        <CreateDate>{date}</CreateDate>
445      </member>"#,
446                name = p.policy_name,
447                id = p.policy_id,
448                arn = p.arn,
449                path = p.path,
450                default_version = p.default_version_id,
451                attachment_count = p.attachment_count,
452                date = p.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
453            )
454        })
455        .collect::<Vec<_>>()
456        .join("\n");
457
458    format!(
459        r#"<?xml version="1.0" encoding="UTF-8"?>
460<ListPoliciesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
461  <ListPoliciesResult>
462    <IsTruncated>false</IsTruncated>
463    <Policies>
464{members}
465    </Policies>
466  </ListPoliciesResult>
467  <ResponseMetadata>
468    <RequestId>{request_id}</RequestId>
469  </ResponseMetadata>
470</ListPoliciesResponse>"#,
471    )
472}
473
474pub fn get_policy_response(policy: &IamPolicy, request_id: &str) -> String {
475    // GetPolicy always includes Tags, even if empty (AWS behavior)
476    let tags_section = if policy.tags.is_empty() {
477        "\n      <Tags/>".to_string()
478    } else {
479        let tags_members = tags_xml(&policy.tags);
480        format!("\n      <Tags>\n{tags_members}\n      </Tags>")
481    };
482
483    let description_section = if policy.description.is_empty() {
484        String::new()
485    } else {
486        format!(
487            "\n      <Description>{}</Description>",
488            xml_escape(&policy.description)
489        )
490    };
491
492    format!(
493        r#"<?xml version="1.0" encoding="UTF-8"?>
494<GetPolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
495  <GetPolicyResult>
496    <Policy>
497      <PolicyName>{name}</PolicyName>
498      <PolicyId>{id}</PolicyId>
499      <Arn>{arn}</Arn>
500      <Path>{path}</Path>
501      <DefaultVersionId>{default_version}</DefaultVersionId>
502      <AttachmentCount>{attachment_count}</AttachmentCount>
503      <IsAttachable>true</IsAttachable>
504      <CreateDate>{date}</CreateDate>{description_section}{tags_section}
505    </Policy>
506  </GetPolicyResult>
507  <ResponseMetadata>
508    <RequestId>{request_id}</RequestId>
509  </ResponseMetadata>
510</GetPolicyResponse>"#,
511        name = policy.policy_name,
512        id = policy.policy_id,
513        arn = policy.arn,
514        path = policy.path,
515        default_version = policy.default_version_id,
516        attachment_count = policy.attachment_count,
517        date = policy.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
518    )
519}
520
521pub fn list_role_policies_response(policy_names: &[String], request_id: &str) -> String {
522    let members: String = policy_names
523        .iter()
524        .map(|name| format!("      <member>{name}</member>"))
525        .collect::<Vec<_>>()
526        .join("\n");
527
528    format!(
529        r#"<?xml version="1.0" encoding="UTF-8"?>
530<ListRolePoliciesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
531  <ListRolePoliciesResult>
532    <IsTruncated>false</IsTruncated>
533    <PolicyNames>
534{members}
535    </PolicyNames>
536  </ListRolePoliciesResult>
537  <ResponseMetadata>
538    <RequestId>{request_id}</RequestId>
539  </ResponseMetadata>
540</ListRolePoliciesResponse>"#,
541    )
542}
543
544pub fn get_caller_identity_response(
545    account_id: &str,
546    arn: &str,
547    user_id: &str,
548    request_id: &str,
549) -> String {
550    format!(
551        r#"<?xml version="1.0" encoding="UTF-8"?>
552<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
553  <GetCallerIdentityResult>
554    <Arn>{arn}</Arn>
555    <UserId>{user_id}</UserId>
556    <Account>{account_id}</Account>
557  </GetCallerIdentityResult>
558  <ResponseMetadata>
559    <RequestId>{request_id}</RequestId>
560  </ResponseMetadata>
561</GetCallerIdentityResponse>"#,
562    )
563}
564
565/// Pre-generated STS credentials to be returned in XML responses.
566pub struct StsCredentials {
567    pub access_key_id: String,
568    pub secret_access_key: String,
569    pub session_token: String,
570}
571
572impl StsCredentials {
573    pub fn generate() -> Self {
574        Self {
575            access_key_id: generate_access_key_id(),
576            secret_access_key: generate_secret_access_key(),
577            session_token: generate_session_token(),
578        }
579    }
580}
581
582pub fn assume_role_response(
583    role_arn: &str,
584    role_session_name: &str,
585    role_id: &str,
586    account_id: &str,
587    partition: &str,
588    creds: &StsCredentials,
589    request_id: &str,
590) -> String {
591    // Extract role name from ARN
592    let role_name = role_arn.rsplit('/').next().unwrap_or("unknown");
593    let assumed_role_arn = format!(
594        "arn:{}:sts::{}:assumed-role/{}/{}",
595        partition, account_id, role_name, role_session_name
596    );
597
598    format!(
599        r#"<?xml version="1.0" encoding="UTF-8"?>
600<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
601  <AssumeRoleResult>
602    <Credentials>
603      <AccessKeyId>{access_key_id}</AccessKeyId>
604      <SecretAccessKey>{secret_access_key}</SecretAccessKey>
605      <SessionToken>{session_token}</SessionToken>
606      <Expiration>2099-12-31T23:59:59Z</Expiration>
607    </Credentials>
608    <AssumedRoleUser>
609      <AssumedRoleId>{role_id}:{session}</AssumedRoleId>
610      <Arn>{assumed_role_arn}</Arn>
611    </AssumedRoleUser>
612  </AssumeRoleResult>
613  <ResponseMetadata>
614    <RequestId>{request_id}</RequestId>
615  </ResponseMetadata>
616</AssumeRoleResponse>"#,
617        access_key_id = creds.access_key_id,
618        secret_access_key = creds.secret_access_key,
619        session_token = creds.session_token,
620        role_id = role_id,
621        assumed_role_arn = assumed_role_arn,
622        session = role_session_name,
623    )
624}
625
626pub fn assume_role_with_web_identity_response(
627    role_arn: &str,
628    role_session_name: &str,
629    account_id: &str,
630    partition: &str,
631    creds: &StsCredentials,
632    assumed_role_id: &str,
633    request_id: &str,
634) -> String {
635    let role_name = role_arn.rsplit('/').next().unwrap_or("unknown");
636    let assumed_role_arn = format!(
637        "arn:{}:sts::{}:assumed-role/{}/{}",
638        partition, account_id, role_name, role_session_name
639    );
640
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>2099-12-31T23:59:59Z</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 = creds.access_key_id,
661        secret_access_key = creds.secret_access_key,
662        session_token = creds.session_token,
663        assumed_role_id = assumed_role_id,
664        assumed_role_arn = assumed_role_arn,
665        session = role_session_name,
666        request_id = request_id,
667    )
668}
669
670pub fn assume_role_with_saml_response(
671    role_arn: &str,
672    role_session_name: &str,
673    account_id: &str,
674    partition: &str,
675    creds: &StsCredentials,
676    assumed_role_id: &str,
677    request_id: &str,
678) -> String {
679    let role_name = role_arn.rsplit('/').next().unwrap_or("unknown");
680    let assumed_role_arn = format!(
681        "arn:{}:sts::{}:assumed-role/{}/{}",
682        partition, account_id, role_name, role_session_name
683    );
684
685    format!(
686        r#"<?xml version="1.0" encoding="UTF-8"?>
687<AssumeRoleWithSAMLResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
688  <AssumeRoleWithSAMLResult>
689    <Credentials>
690      <AccessKeyId>{access_key_id}</AccessKeyId>
691      <SecretAccessKey>{secret_access_key}</SecretAccessKey>
692      <SessionToken>{session_token}</SessionToken>
693      <Expiration>2099-12-31T23:59:59Z</Expiration>
694    </Credentials>
695    <AssumedRoleUser>
696      <AssumedRoleId>{assumed_role_id}:{session}</AssumedRoleId>
697      <Arn>{assumed_role_arn}</Arn>
698    </AssumedRoleUser>
699  </AssumeRoleWithSAMLResult>
700  <ResponseMetadata>
701    <RequestId>{request_id}</RequestId>
702  </ResponseMetadata>
703</AssumeRoleWithSAMLResponse>"#,
704        access_key_id = creds.access_key_id,
705        secret_access_key = creds.secret_access_key,
706        session_token = creds.session_token,
707        assumed_role_id = assumed_role_id,
708        assumed_role_arn = assumed_role_arn,
709        session = role_session_name,
710        request_id = request_id,
711    )
712}
713
714pub fn get_session_token_response(request_id: &str) -> String {
715    // AWS docs example credentials (deterministic for local testing)
716    let access_key_id = "FSIAIOSFODNN7EXAMPLE";
717    let secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY";
718    let session_token = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE";
719
720    format!(
721        r#"<?xml version="1.0" encoding="UTF-8"?>
722<GetSessionTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
723  <GetSessionTokenResult>
724    <Credentials>
725      <AccessKeyId>{access_key_id}</AccessKeyId>
726      <SecretAccessKey>{secret_access_key}</SecretAccessKey>
727      <SessionToken>{session_token}</SessionToken>
728      <Expiration>2099-12-31T23:59:59Z</Expiration>
729    </Credentials>
730  </GetSessionTokenResult>
731  <ResponseMetadata>
732    <RequestId>{request_id}</RequestId>
733  </ResponseMetadata>
734</GetSessionTokenResponse>"#,
735        access_key_id = access_key_id,
736        secret_access_key = secret_access_key,
737        session_token = session_token,
738        request_id = request_id,
739    )
740}
741
742pub fn get_federation_token_response(
743    name: &str,
744    account_id: &str,
745    partition: &str,
746    request_id: &str,
747) -> String {
748    // AWS docs example credentials (deterministic for local testing)
749    let access_key_id = "FSIAIOSFODNN7EXAMPLE";
750    let secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY";
751    let session_token = "AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==";
752
753    let name = xml_escape(name);
754    let federated_user_arn = format!(
755        "arn:{}:sts::{}:federated-user/{}",
756        partition, account_id, name
757    );
758    let federated_user_id = format!("{}:{}", account_id, name);
759
760    format!(
761        r#"<?xml version="1.0" encoding="UTF-8"?>
762<GetFederationTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
763  <GetFederationTokenResult>
764    <Credentials>
765      <AccessKeyId>{access_key_id}</AccessKeyId>
766      <SecretAccessKey>{secret_access_key}</SecretAccessKey>
767      <SessionToken>{session_token}</SessionToken>
768      <Expiration>2099-12-31T23:59:59Z</Expiration>
769    </Credentials>
770    <FederatedUser>
771      <FederatedUserId>{federated_user_id}</FederatedUserId>
772      <Arn>{federated_user_arn}</Arn>
773    </FederatedUser>
774  </GetFederationTokenResult>
775  <ResponseMetadata>
776    <RequestId>{request_id}</RequestId>
777  </ResponseMetadata>
778</GetFederationTokenResponse>"#,
779        access_key_id = access_key_id,
780        secret_access_key = secret_access_key,
781        session_token = session_token,
782        federated_user_arn = federated_user_arn,
783        federated_user_id = federated_user_id,
784        request_id = request_id,
785    )
786}
787
788pub fn get_access_key_info_response(account_id: &str, request_id: &str) -> String {
789    format!(
790        r#"<?xml version="1.0" encoding="UTF-8"?>
791<GetAccessKeyInfoResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
792  <GetAccessKeyInfoResult>
793    <Account>{account_id}</Account>
794  </GetAccessKeyInfoResult>
795  <ResponseMetadata>
796    <RequestId>{request_id}</RequestId>
797  </ResponseMetadata>
798</GetAccessKeyInfoResponse>"#,
799        account_id = account_id,
800        request_id = request_id,
801    )
802}
803
804/// Generate an FSIA-prefixed temporary access key ID (20 chars total).
805pub fn generate_access_key_id() -> String {
806    let id = generate_alphanum_id(16);
807    format!("FSIA{}", id)
808}
809
810/// Generate a 40-character secret access key.
811pub fn generate_secret_access_key() -> String {
812    generate_alphanum_id(40)
813}
814
815/// Generate an AROA-prefixed role ID (21 chars total).
816pub fn generate_role_id() -> String {
817    let id = generate_alphanum_id(17);
818    format!("AROA{}", id)
819}
820
821/// Generate a session token that is exactly 356 characters starting with "FQoGZXIvYXdzE".
822pub fn generate_session_token() -> String {
823    // AWS session tokens are typically 356 chars and start with "FQoGZXIvYXdzE"
824    let prefix = "FQoGZXIvYXdzE";
825    let remaining = 356 - prefix.len(); // 343 chars needed
826                                        // Generate enough random bytes: we need at least ceil(343*3/4) = 258 bytes
827                                        // 18 UUIDs * 16 bytes = 288 bytes -> base64 = 384 chars (plenty)
828    let mut raw = Vec::with_capacity(288);
829    for _ in 0..18 {
830        raw.extend_from_slice(uuid::Uuid::new_v4().as_bytes());
831    }
832    let encoded = base64::engine::general_purpose::STANDARD.encode(&raw);
833    // Take exactly what we need from the encoded data
834    let suffix = &encoded[..remaining];
835    format!("{}{}", prefix, suffix)
836}
837
838/// Generate alphanumeric ID of given length.
839fn generate_alphanum_id(len: usize) -> String {
840    let raw = format!(
841        "{}{}{}",
842        uuid::Uuid::new_v4(),
843        uuid::Uuid::new_v4(),
844        uuid::Uuid::new_v4(),
845    );
846    raw.replace('-', "")
847        .chars()
848        .filter(|c| c.is_alphanumeric())
849        .take(len)
850        .collect()
851}