Skip to main content

alien_aws_clients/aws/
sts.rs

1use crate::aws::AwsClientConfigExt;
2use std::fmt::Debug;
3
4use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsSignConfig};
5use crate::aws::{AwsClientConfig, AwsCredentials};
6use alien_client_core::{ErrorData, Result};
7use alien_error::ContextError;
8use bon::Builder;
9use form_urlencoded;
10
11#[cfg(feature = "test-utils")]
12use mockall::automock;
13use quick_xml;
14use reqwest::{Client, StatusCode};
15use serde::de::DeserializeOwned;
16use serde::{Deserialize, Serialize};
17
18#[cfg_attr(feature = "test-utils", automock)]
19#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
20#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
21pub trait StsApi: Send + Sync + Debug {
22    async fn assume_role(&self, request: AssumeRoleRequest) -> Result<AssumeRoleResponse>;
23    async fn assume_role_with_web_identity(
24        &self,
25        request: AssumeRoleWithWebIdentityRequest,
26    ) -> Result<AssumeRoleWithWebIdentityResponse>;
27    async fn get_caller_identity(&self) -> Result<GetCallerIdentityResponse>;
28}
29
30/// AWS STS client using the new request/error abstractions.
31#[derive(Debug, Clone)]
32pub struct StsClient {
33    client: Client,
34    config: AwsClientConfig,
35}
36
37impl StsClient {
38    pub fn new(client: Client, config: AwsClientConfig) -> Self {
39        Self { client, config }
40    }
41
42    async fn sign_config(&self, operation_name: &str) -> Result<AwsSignConfig> {
43        let config = if operation_name == "AssumeRoleWithWebIdentity" {
44            self.config.clone()
45        } else if matches!(self.config.credentials, AwsCredentials::WebIdentity { .. }) {
46            self.config.get_web_identity_credentials().await?
47        } else {
48            self.config.clone()
49        };
50
51        Ok(AwsSignConfig {
52            service_name: "sts".into(),
53            region: config.region.clone(),
54            credentials: config.get_credentials(),
55            signing_region: None,
56        })
57    }
58
59    fn get_base_url(&self) -> String {
60        if let Some(override_url) = self.config.get_service_endpoint_option("sts") {
61            override_url.to_string()
62        } else {
63            format!("https://sts.{}.amazonaws.com", self.config.region)
64        }
65    }
66
67    fn build_form_body(action: &str, version: &str, params: Vec<(String, String)>) -> String {
68        let mut all = vec![
69            ("Action".to_string(), action.to_string()),
70            ("Version".to_string(), version.to_string()),
71        ];
72        all.extend(params);
73
74        all.into_iter()
75            .map(|(k, v)| {
76                format!(
77                    "{}={}",
78                    k,
79                    form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
80                )
81            })
82            .collect::<Vec<String>>()
83            .join("&")
84    }
85
86    // ---- Internal helpers ------------------------------------------------
87    async fn post_xml<T: DeserializeOwned + Send + 'static>(
88        &self,
89        body: String,
90        operation_name: &str,
91        resource_name: &str,
92    ) -> Result<T> {
93        let base_url = self.get_base_url();
94        let url = format!("{}/", base_url.trim_end_matches('/'));
95        let builder = self
96            .client
97            .post(&url)
98            .content_type_form()
99            .body(body.clone());
100
101        let sign_config = self.sign_config(operation_name).await?;
102        let result = crate::aws::aws_request_utils::sign_send_xml(builder, &sign_config).await;
103
104        match result {
105            Ok(v) => Ok(v),
106            Err(e) => {
107                if let Some(ErrorData::HttpResponseError {
108                    http_status,
109                    http_response_text: Some(ref text),
110                    ..
111                }) = &e.error
112                {
113                    let status = StatusCode::from_u16(*http_status)
114                        .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
115                    if let Some(mapped) =
116                        Self::map_sts_error(status, text, operation_name, resource_name, &body)
117                    {
118                        Err(e.context(mapped))
119                    } else {
120                        // Couldn't parse STS error, use original error
121                        Err(e)
122                    }
123                } else {
124                    Err(e)
125                }
126            }
127        }
128    }
129
130    fn map_sts_error(
131        status: StatusCode,
132        error_body: &str,
133        operation: &str,
134        resource_name: &str,
135        request_body: &str,
136    ) -> Option<ErrorData> {
137        // Attempt to parse the canonical AWS error XML.
138        let parsed_error: std::result::Result<StsErrorResponse, _> =
139            quick_xml::de::from_str(error_body);
140
141        let (error_code, error_message) = match parsed_error {
142            Ok(e) => (
143                e.error.code.unwrap_or_else(|| "UnknownErrorCode".into()),
144                e.error.message.unwrap_or_else(|| "Unknown error".into()),
145            ),
146            Err(_) => {
147                // If we can't parse the response, return None to use original error
148                return None;
149            }
150        };
151
152        Some(match error_code.as_str() {
153            // Access & auth
154            "AccessDenied"
155            | "AccessDeniedException"
156            | "UnauthorizedOperation"
157            | "InvalidUserID.NotFound"
158            | "AuthFailure"
159            | "SignatureDoesNotMatch"
160            | "TokenRefreshRequired"
161            | "NotAuthorized"
162            | "InvalidClientTokenId"
163            | "MissingAuthenticationToken"
164            | "OptInRequired" => ErrorData::RemoteAccessDenied {
165                resource_type: "STS Resource".into(),
166                resource_name: resource_name.into(),
167            },
168
169            // Rate limiting / throttling
170            "Throttling" | "ThrottlingException" | "RequestLimitExceeded" => {
171                ErrorData::RateLimitExceeded {
172                    message: error_message,
173                }
174            }
175
176            // Service unavailable
177            "ServiceUnavailable" | "InternalFailure" | "ServiceFailure" => {
178                ErrorData::RemoteServiceUnavailable {
179                    message: error_message,
180                }
181            }
182
183            // STS-specific errors
184            "ExpiredToken" => ErrorData::AuthenticationError {
185                message: "Security token has expired".into(),
186            },
187
188            "MalformedPolicyDocument" => ErrorData::InvalidInput {
189                message: format!("Malformed policy document: {}", error_message),
190                field_name: Some("PolicyDocument".into()),
191            },
192
193            "PackedPolicyTooLarge" => ErrorData::InvalidInput {
194                message: format!("Policy document is too large: {}", error_message),
195                field_name: Some("PolicyDocument".into()),
196            },
197
198            "RegionDisabled" => ErrorData::RemoteServiceUnavailable {
199                message: format!("STS is not enabled in this region: {}", error_message),
200            },
201
202            "InvalidParameterValue" => ErrorData::InvalidInput {
203                message: error_message,
204                field_name: None,
205            },
206
207            // Generic fallback categories
208            _ => match status {
209                StatusCode::CONFLICT => ErrorData::RemoteResourceConflict {
210                    message: error_message,
211                    resource_type: "STS Resource".into(),
212                    resource_name: resource_name.into(),
213                },
214                StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound {
215                    resource_type: "STS Resource".into(),
216                    resource_name: resource_name.into(),
217                },
218                StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied {
219                    resource_type: "STS Resource".into(),
220                    resource_name: resource_name.into(),
221                },
222                StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded {
223                    message: error_message,
224                },
225                StatusCode::SERVICE_UNAVAILABLE
226                | StatusCode::BAD_GATEWAY
227                | StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable {
228                    message: error_message,
229                },
230                _ => ErrorData::HttpResponseError {
231                    message: format!("STS {operation} failed: {error_message}"),
232                    url: "sts.amazonaws.com".into(),
233                    http_status: status.as_u16(),
234                    http_response_text: Some(error_body.into()),
235                    http_request_text: Some(request_body.into()),
236                },
237            },
238        })
239    }
240}
241
242#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
243#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
244impl StsApi for StsClient {
245    async fn assume_role(&self, request: AssumeRoleRequest) -> Result<AssumeRoleResponse> {
246        let mut params: Vec<(String, String)> = vec![
247            ("RoleArn".to_string(), request.role_arn.clone()),
248            (
249                "RoleSessionName".to_string(),
250                request.role_session_name.clone(),
251            ),
252        ];
253
254        if let Some(duration) = request.duration_seconds {
255            params.push(("DurationSeconds".to_string(), duration.to_string()));
256        }
257        if let Some(ref external_id) = request.external_id {
258            params.push(("ExternalId".to_string(), external_id.clone()));
259        }
260        if let Some(ref policy) = request.policy {
261            params.push(("Policy".to_string(), policy.clone()));
262        }
263        if let Some(ref serial_number) = request.serial_number {
264            params.push(("SerialNumber".to_string(), serial_number.clone()));
265        }
266        if let Some(ref token_code) = request.token_code {
267            params.push(("TokenCode".to_string(), token_code.clone()));
268        }
269        if let Some(ref source_identity) = request.source_identity {
270            params.push(("SourceIdentity".to_string(), source_identity.clone()));
271        }
272
273        // Handle policy ARNs array
274        if let Some(ref policy_arns) = request.policy_arns {
275            for (i, policy_arn) in policy_arns.iter().enumerate() {
276                params.push((
277                    format!("PolicyArns.member.{}.arn", i + 1),
278                    policy_arn.clone(),
279                ));
280            }
281        }
282
283        // Handle tags array
284        if let Some(ref tags) = request.tags {
285            for (i, tag) in tags.iter().enumerate() {
286                params.push((format!("Tags.member.{}.Key", i + 1), tag.key.clone()));
287                params.push((format!("Tags.member.{}.Value", i + 1), tag.value.clone()));
288            }
289        }
290
291        // Handle transitive tag keys array
292        if let Some(ref transitive_tag_keys) = request.transitive_tag_keys {
293            for (i, key) in transitive_tag_keys.iter().enumerate() {
294                params.push((format!("TransitiveTagKeys.member.{}", i + 1), key.clone()));
295            }
296        }
297
298        let body = Self::build_form_body("AssumeRole", "2011-06-15", params);
299        self.post_xml(body, "AssumeRole", &request.role_arn).await
300    }
301
302    async fn assume_role_with_web_identity(
303        &self,
304        request: AssumeRoleWithWebIdentityRequest,
305    ) -> Result<AssumeRoleWithWebIdentityResponse> {
306        let mut params: Vec<(String, String)> = vec![
307            ("RoleArn".to_string(), request.role_arn.clone()),
308            (
309                "RoleSessionName".to_string(),
310                request.role_session_name.clone(),
311            ),
312            (
313                "WebIdentityToken".to_string(),
314                request.web_identity_token.clone(),
315            ),
316        ];
317
318        if let Some(duration) = request.duration_seconds {
319            params.push(("DurationSeconds".to_string(), duration.to_string()));
320        }
321        if let Some(ref policy) = request.policy {
322            params.push(("Policy".to_string(), policy.clone()));
323        }
324        if let Some(ref provider_id) = request.provider_id {
325            params.push(("ProviderId".to_string(), provider_id.clone()));
326        }
327
328        // Handle policy ARNs array
329        if let Some(ref policy_arns) = request.policy_arns {
330            for (i, policy_arn) in policy_arns.iter().enumerate() {
331                params.push((
332                    format!("PolicyArns.member.{}.arn", i + 1),
333                    policy_arn.clone(),
334                ));
335            }
336        }
337
338        let body = Self::build_form_body("AssumeRoleWithWebIdentity", "2011-06-15", params);
339        self.post_xml(body, "AssumeRoleWithWebIdentity", &request.role_arn)
340            .await
341    }
342
343    async fn get_caller_identity(&self) -> Result<GetCallerIdentityResponse> {
344        let params = vec![];
345        let body = Self::build_form_body("GetCallerIdentity", "2011-06-15", params);
346        self.post_xml(body, "GetCallerIdentity", "caller").await
347    }
348}
349
350// -------------------------------------------------------------------------
351// Error XML structs (PascalCase matching AWS STS)
352// -------------------------------------------------------------------------
353
354#[derive(Deserialize, Debug)]
355#[serde(rename_all = "PascalCase")]
356struct StsErrorResponse {
357    pub error: StsErrorDetails,
358}
359
360#[derive(Deserialize, Debug)]
361#[serde(rename_all = "PascalCase")]
362struct StsErrorDetails {
363    pub code: Option<String>,
364    pub message: Option<String>,
365}
366
367// -------------------------------------------------------------------------
368// Request / response payloads
369// -------------------------------------------------------------------------
370
371#[derive(Serialize, Debug, Clone, Builder)]
372#[serde(rename_all = "PascalCase")]
373pub struct AssumeRoleRequest {
374    /// The ARN of the role to assume
375    pub role_arn: String,
376    /// An identifier for the assumed role session
377    pub role_session_name: String,
378    /// The duration, in seconds, of the role session (900-43200)
379    pub duration_seconds: Option<i32>,
380    /// A unique identifier used when assuming a role in another account
381    pub external_id: Option<String>,
382    /// An IAM policy in JSON format to use as an inline session policy
383    pub policy: Option<String>,
384    /// The Amazon Resource Names (ARNs) of IAM managed policies to use as managed session policies
385    pub policy_arns: Option<Vec<String>>,
386    /// The identification number of the MFA device
387    pub serial_number: Option<String>,
388    /// The value provided by the MFA device
389    pub token_code: Option<String>,
390    /// The source identity specified by the principal
391    pub source_identity: Option<String>,
392    /// A list of session tags
393    pub tags: Option<Vec<Tag>>,
394    /// A list of keys for session tags that you want to set as transitive
395    pub transitive_tag_keys: Option<Vec<String>>,
396}
397
398#[derive(Serialize, Debug, Clone, Builder)]
399#[serde(rename_all = "PascalCase")]
400pub struct AssumeRoleWithWebIdentityRequest {
401    /// The ARN of the role to assume
402    pub role_arn: String,
403    /// An identifier for the assumed role session
404    pub role_session_name: String,
405    /// The OAuth 2.0 access token or OpenID Connect ID token
406    pub web_identity_token: String,
407    /// The duration, in seconds, of the role session (900-43200)
408    pub duration_seconds: Option<i32>,
409    /// An IAM policy in JSON format to use as an inline session policy
410    pub policy: Option<String>,
411    /// The Amazon Resource Names (ARNs) of IAM managed policies to use as managed session policies
412    pub policy_arns: Option<Vec<String>>,
413    /// The fully qualified host component of the domain name of the OAuth 2.0 identity provider
414    pub provider_id: Option<String>,
415}
416
417#[derive(Serialize, Deserialize, Debug, Clone)]
418#[serde(rename_all = "PascalCase")]
419pub struct Tag {
420    pub key: String,
421    pub value: String,
422}
423
424#[derive(Deserialize, Debug)]
425#[serde(rename_all = "PascalCase")]
426pub struct AssumeRoleResponse {
427    pub assume_role_result: AssumeRoleResult,
428}
429
430#[derive(Deserialize, Debug)]
431#[serde(rename_all = "PascalCase")]
432pub struct AssumeRoleWithWebIdentityResponse {
433    pub assume_role_with_web_identity_result: AssumeRoleWithWebIdentityResult,
434}
435
436#[derive(Deserialize, Debug)]
437#[serde(rename_all = "PascalCase")]
438pub struct AssumeRoleWithWebIdentityResult {
439    pub assumed_role_user: AssumedRoleUser,
440    pub credentials: Credentials,
441    pub packed_policy_size: Option<i32>,
442    pub provider: Option<String>,
443    pub audience: Option<String>,
444    pub source_identity: Option<String>,
445    pub subject_from_web_identity_token: Option<String>,
446}
447
448#[derive(Deserialize, Debug)]
449#[serde(rename_all = "PascalCase")]
450pub struct AssumeRoleResult {
451    pub assumed_role_user: AssumedRoleUser,
452    pub credentials: Credentials,
453    pub packed_policy_size: Option<i32>,
454    pub source_identity: Option<String>,
455}
456
457#[derive(Deserialize, Debug)]
458#[serde(rename_all = "PascalCase")]
459pub struct AssumedRoleUser {
460    pub arn: String,
461    pub assumed_role_id: String,
462}
463
464#[derive(Deserialize, Debug)]
465#[serde(rename_all = "PascalCase")]
466pub struct Credentials {
467    pub access_key_id: String,
468    pub secret_access_key: String,
469    pub session_token: String,
470    pub expiration: String,
471}
472
473#[derive(Deserialize, Debug)]
474#[serde(rename_all = "PascalCase")]
475pub struct GetCallerIdentityResponse {
476    pub get_caller_identity_result: GetCallerIdentityResult,
477}
478
479#[derive(Deserialize, Debug)]
480#[serde(rename_all = "PascalCase")]
481pub struct GetCallerIdentityResult {
482    pub arn: Option<String>,
483    pub user_id: Option<String>,
484    pub account: Option<String>,
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use alien_core::{AwsServiceOverrides, AwsWebIdentityConfig};
491    use std::collections::HashMap;
492    use std::io::{Read, Write};
493    use std::net::{TcpListener, TcpStream};
494    use std::sync::{Arc, Mutex};
495
496    #[tokio::test]
497    async fn get_caller_identity_exchanges_web_identity_before_signing() {
498        let observed = Arc::new(Mutex::new(Vec::new()));
499        let (endpoint, server) = start_sts_test_server(observed.clone());
500
501        let token_file = tempfile::NamedTempFile::new().expect("create token file");
502        std::fs::write(token_file.path(), "test-web-identity-token").expect("write token file");
503
504        let config = AwsClientConfig {
505            account_id: "123456789012".to_string(),
506            region: "us-east-2".to_string(),
507            credentials: AwsCredentials::WebIdentity {
508                config: AwsWebIdentityConfig {
509                    role_arn: "arn:aws:iam::123456789012:role/test-role".to_string(),
510                    session_name: Some("test-session".to_string()),
511                    web_identity_token_file: token_file.path().display().to_string(),
512                    duration_seconds: Some(900),
513                },
514            },
515            service_overrides: Some(AwsServiceOverrides {
516                endpoints: HashMap::from([("sts".to_string(), endpoint)]),
517            }),
518        };
519
520        let response = StsClient::new(Client::new(), config)
521            .get_caller_identity()
522            .await
523            .expect("get caller identity should use exchanged credentials");
524
525        assert_eq!(
526            response.get_caller_identity_result.account.as_deref(),
527            Some("123456789012")
528        );
529
530        server.join().expect("server thread should finish");
531        let observed = observed.lock().expect("observed requests lock");
532        assert_eq!(observed.len(), 2);
533        assert!(observed[0]
534            .body
535            .contains("Action=AssumeRoleWithWebIdentity"));
536        assert!(observed[1].body.contains("Action=GetCallerIdentity"));
537        assert!(
538            observed[1].authorization.contains("ASIATESTACCESS"),
539            "GetCallerIdentity must be signed with credentials returned by AssumeRoleWithWebIdentity, got: {}",
540            observed[1].authorization
541        );
542    }
543
544    #[derive(Debug)]
545    struct ObservedRequest {
546        body: String,
547        authorization: String,
548    }
549
550    fn start_sts_test_server(
551        observed: Arc<Mutex<Vec<ObservedRequest>>>,
552    ) -> (String, std::thread::JoinHandle<()>) {
553        let listener = TcpListener::bind("127.0.0.1:0").expect("bind test STS server");
554        let endpoint = format!("http://{}", listener.local_addr().expect("local addr"));
555        let server = std::thread::spawn(move || {
556            for _ in 0..2 {
557                let (mut stream, _) = listener.accept().expect("accept STS request");
558                let (headers, body) = read_http_request(&mut stream);
559                let authorization = headers
560                    .lines()
561                    .find_map(|line| {
562                        line.split_once(':').and_then(|(name, value)| {
563                            name.eq_ignore_ascii_case("authorization")
564                                .then(|| value.trim())
565                        })
566                    })
567                    .unwrap_or_default()
568                    .to_string();
569                observed
570                    .lock()
571                    .expect("observed requests lock")
572                    .push(ObservedRequest {
573                        body: body.clone(),
574                        authorization: authorization.clone(),
575                    });
576
577                if body.contains("Action=AssumeRoleWithWebIdentity") {
578                    write_xml_response(&mut stream, assume_role_with_web_identity_response());
579                } else if body.contains("Action=GetCallerIdentity")
580                    && authorization.contains("ASIATESTACCESS")
581                {
582                    write_xml_response(&mut stream, get_caller_identity_response());
583                } else {
584                    write_forbidden_response(&mut stream);
585                }
586            }
587        });
588        (endpoint, server)
589    }
590
591    fn read_http_request(stream: &mut TcpStream) -> (String, String) {
592        let mut buffer = Vec::new();
593        let mut scratch = [0_u8; 4096];
594        let header_end;
595        loop {
596            let read = stream.read(&mut scratch).expect("read request");
597            assert!(read > 0, "connection closed before headers");
598            buffer.extend_from_slice(&scratch[..read]);
599            if let Some(position) = buffer.windows(4).position(|w| w == b"\r\n\r\n") {
600                header_end = position + 4;
601                break;
602            }
603        }
604
605        let headers = String::from_utf8(buffer[..header_end].to_vec()).expect("headers utf8");
606        let content_length = headers
607            .lines()
608            .find_map(|line| {
609                line.to_ascii_lowercase()
610                    .strip_prefix("content-length: ")
611                    .and_then(|value| value.trim().parse::<usize>().ok())
612            })
613            .expect("content-length header");
614
615        while buffer.len() < header_end + content_length {
616            let read = stream.read(&mut scratch).expect("read request body");
617            assert!(read > 0, "connection closed before body");
618            buffer.extend_from_slice(&scratch[..read]);
619        }
620
621        let body = String::from_utf8(buffer[header_end..header_end + content_length].to_vec())
622            .expect("body utf8");
623        (headers, body)
624    }
625
626    fn write_xml_response(stream: &mut TcpStream, body: String) {
627        let response = format!(
628            "HTTP/1.1 200 OK\r\ncontent-type: text/xml\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
629            body.len(),
630            body
631        );
632        stream
633            .write_all(response.as_bytes())
634            .expect("write XML response");
635    }
636
637    fn write_forbidden_response(stream: &mut TcpStream) {
638        let body = r#"<ErrorResponse><Error><Code>AccessDenied</Code><Message>denied</Message></Error></ErrorResponse>"#;
639        let response = format!(
640            "HTTP/1.1 403 Forbidden\r\ncontent-type: text/xml\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
641            body.len(),
642            body
643        );
644        stream
645            .write_all(response.as_bytes())
646            .expect("write forbidden response");
647    }
648
649    fn assume_role_with_web_identity_response() -> String {
650        r#"<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
651  <AssumeRoleWithWebIdentityResult>
652    <SubjectFromWebIdentityToken>system:serviceaccount:test:agent</SubjectFromWebIdentityToken>
653    <Audience>sts.amazonaws.com</Audience>
654    <Provider>provider</Provider>
655    <AssumedRoleUser>
656      <Arn>arn:aws:sts::123456789012:assumed-role/test-role/test-session</Arn>
657      <AssumedRoleId>AROA:test-session</AssumedRoleId>
658    </AssumedRoleUser>
659    <Credentials>
660      <AccessKeyId>ASIATESTACCESS</AccessKeyId>
661      <SecretAccessKey>test-secret</SecretAccessKey>
662      <SessionToken>test-session-token</SessionToken>
663      <Expiration>2026-05-27T11:00:00Z</Expiration>
664    </Credentials>
665  </AssumeRoleWithWebIdentityResult>
666  <ResponseMetadata><RequestId>request-1</RequestId></ResponseMetadata>
667</AssumeRoleWithWebIdentityResponse>"#
668            .to_string()
669    }
670
671    fn get_caller_identity_response() -> String {
672        r#"<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
673  <GetCallerIdentityResult>
674    <Arn>arn:aws:sts::123456789012:assumed-role/test-role/test-session</Arn>
675    <UserId>AROA:test-session</UserId>
676    <Account>123456789012</Account>
677  </GetCallerIdentityResult>
678  <ResponseMetadata><RequestId>request-2</RequestId></ResponseMetadata>
679</GetCallerIdentityResponse>"#
680            .to_string()
681    }
682}