Skip to main content

fluidattacks_core_git/
aws.rs

1use anyhow::{bail, Context, Result};
2use regex::Regex;
3use uuid::Uuid;
4
5pub struct AwsTempCreds {
6    pub access_key: String,
7    pub secret_key: String,
8    pub session_token: String,
9    pub region: String,
10}
11
12struct Hop<'a> {
13    role_arn: &'a str,
14    external_id: Option<&'a str>,
15}
16
17/// Chained when the caller is not the principal the client's trust policy pins.
18const fn chain<'a>(
19    arn: &'a str,
20    external_id: &'a str,
21    via: Option<&'a str>,
22) -> (Hop<'a>, Option<Hop<'a>>) {
23    let client = Hop {
24        role_arn: arn,
25        external_id: Some(external_id),
26    };
27
28    match via {
29        Some(intermediate) => (
30            Hop {
31                role_arn: intermediate,
32                external_id: None,
33            },
34            Some(client),
35        ),
36        None => (client, None),
37    }
38}
39
40pub async fn assume_role(
41    arn: &str,
42    external_id: &str,
43    url: &str,
44    via: Option<&str>,
45) -> Result<AwsTempCreds> {
46    let region = extract_region(url)?;
47    let (first, then) = chain(arn, external_id, via);
48
49    let mut creds = assume(&first, &region, None).await?;
50    if let Some(hop) = then {
51        creds = assume(&hop, &region, Some(&creds)).await?;
52    }
53
54    Ok(creds)
55}
56
57async fn assume(hop: &Hop<'_>, region: &str, from: Option<&AwsTempCreds>) -> Result<AwsTempCreds> {
58    let mut config = aws_config::defaults(aws_config::BehaviorVersion::latest())
59        .region(aws_config::Region::new(region.to_owned()));
60
61    if let Some(session) = from {
62        config = config.credentials_provider(aws_sdk_sts::config::Credentials::new(
63            session.access_key.clone(),
64            session.secret_key.clone(),
65            Some(session.session_token.clone()),
66            None,
67            "fluidattacks-core-chain",
68        ));
69    }
70
71    let sts = aws_sdk_sts::Client::new(&config.load().await);
72
73    let mut request = sts
74        .assume_role()
75        .role_arn(hop.role_arn)
76        .role_session_name(format!("session-{}", Uuid::new_v4()));
77
78    if let Some(external_id) = hop.external_id {
79        request = request.external_id(external_id);
80    }
81
82    let resp = request
83        .send()
84        .await
85        .with_context(|| format!("STS AssumeRole failed for {}", hop.role_arn))?;
86
87    let creds = resp
88        .credentials()
89        .ok_or_else(|| anyhow::anyhow!("no credentials in AssumeRole response"))?;
90
91    let access_key = creds.access_key_id().to_owned();
92    let secret_key = creds.secret_access_key().to_owned();
93    let session_token = creds.session_token().to_owned();
94
95    if access_key.is_empty() || secret_key.is_empty() {
96        bail!("empty credentials from STS AssumeRole");
97    }
98
99    Ok(AwsTempCreds {
100        access_key,
101        secret_key,
102        session_token,
103        region: region.to_owned(),
104    })
105}
106
107fn extract_region(url: &str) -> Result<String> {
108    let re = Regex::new(r"codecommit::([a-z0-9-]+)://")?;
109    if let Some(caps) = re.captures(url) {
110        return Ok(caps[1].to_string());
111    }
112
113    // Try to find region in standard CodeCommit URLs
114    let re2 = Regex::new(r"git-codecommit\.([a-z0-9-]+)\.amazonaws\.com")?;
115    if let Some(caps) = re2.captures(url) {
116        return Ok(caps[1].to_string());
117    }
118
119    Ok("us-east-1".to_owned())
120}
121
122#[cfg(test)]
123mod tests {
124    use super::chain;
125
126    #[test]
127    fn one_call_takes_the_client_role_with_its_external_id() {
128        let (first, then) = chain("arn:aws:iam::1:role/client", "an-external-id", None);
129
130        assert_eq!(first.role_arn, "arn:aws:iam::1:role/client");
131        assert_eq!(first.external_id, Some("an-external-id"));
132        assert!(then.is_none());
133    }
134
135    #[test]
136    fn a_chain_takes_the_intermediate_role_first_and_without_the_external_id() {
137        let (first, then) = chain(
138            "arn:aws:iam::1:role/client",
139            "an-external-id",
140            Some("arn:aws:iam::2:role/intermediate"),
141        );
142
143        assert_eq!(first.role_arn, "arn:aws:iam::2:role/intermediate");
144        assert_eq!(first.external_id, None);
145
146        let client = then.expect("the client role is still taken");
147        assert_eq!(client.role_arn, "arn:aws:iam::1:role/client");
148        assert_eq!(client.external_id, Some("an-external-id"));
149    }
150}