fluidattacks_core_git/clone/
codecommit.rs1use std::path::Path;
2use tokio::process::Command;
3
4use super::{build_clone_args, reason, redirect_config, stall_config, CloneOpts};
5use crate::aws::{self, AwsTempCreds};
6use crate::errors::{CloneError, GitCallError};
7use crate::types::CredentialKind;
8
9pub async fn clone_codecommit(opts: &CloneOpts, dest: &Path) -> anyhow::Result<()> {
10 let Some(CredentialKind::AwsRole {
11 arn,
12 external_id,
13 via,
14 }) = opts.credentials.as_ref().map(|c| &c.kind)
15 else {
16 anyhow::bail!("expected AwsRole credentials for CodeCommit clone");
17 };
18 Ok(run_git_clone(opts, dest, arn, external_id, via.as_deref()).await?)
19}
20
21pub(super) async fn run_git_clone(
22 opts: &CloneOpts,
23 dest: &Path,
24 arn: &str,
25 external_id: &str,
26 via: Option<&str>,
27) -> Result<(), CloneError> {
28 let creds = aws::assume_role(arn, external_id, &opts.repo_url, via).await?;
29 Ok(clone_with(opts, dest, &creds).await?)
30}
31
32async fn clone_with(
33 opts: &CloneOpts,
34 dest: &Path,
35 creds: &AwsTempCreds,
36) -> Result<(), GitCallError> {
37 let mut command = Command::new("git");
38 command
39 .args(clone_args(opts, dest))
40 .env("AWS_ACCESS_KEY_ID", &creds.access_key)
41 .env("AWS_SECRET_ACCESS_KEY", &creds.secret_key)
42 .env("AWS_SESSION_TOKEN", &creds.session_token)
43 .env("AWS_DEFAULT_REGION", &creds.region);
44 reason::run(&mut command, &[&creds.secret_key, &creds.session_token]).await
45}
46
47fn clone_args(opts: &CloneOpts, dest: &Path) -> Vec<std::ffi::OsString> {
48 let mut extra_config = redirect_config(opts.follow_redirects).to_vec();
49 extra_config.extend(stall_config());
50 build_clone_args(opts, &opts.repo_url, dest, &extra_config)
51}
52
53#[cfg(test)]
54mod tests {
55 use std::path::Path;
56
57 use super::{clone_args, clone_with};
58 use crate::aws::AwsTempCreds;
59 use crate::clone::tests::opts;
60 use crate::clone::{CloneOpts, CloneShape};
61 use crate::errors::GitCallError;
62
63 #[test]
64 fn a_redirect_is_followed_only_when_the_root_asks_for_one() {
65 for follow_redirects in [false, true] {
66 let opts = CloneOpts {
67 follow_redirects,
68 ..opts(CloneShape::SingleBranch)
69 };
70
71 let args = clone_args(&opts, Path::new("/tmp/a_root"));
72
73 let config = format!("http.followRedirects={follow_redirects}");
74 assert!(args.windows(2).any(|pair| pair == ["-c", config.as_str()]));
75 }
76 }
77
78 #[test]
79 fn git_checks_the_certificate() {
80 let args = clone_args(&opts(CloneShape::SingleBranch), Path::new("/tmp/a_root"));
81
82 assert!(!args.iter().any(|arg| arg == "http.sslVerify=false"));
83 }
84
85 #[tokio::test]
86 async fn the_session_stays_out_of_what_git_says() {
87 let tmp = tempfile::tempdir().unwrap();
88 let opts = CloneOpts {
90 repo_url: format!("{}/a-secret-key/a-token", tmp.path().display()),
91 ..opts(CloneShape::SingleBranch)
92 };
93 let creds = AwsTempCreds {
94 access_key: "an-access-key".to_owned(),
95 secret_key: "a-secret-key".to_owned(),
96 session_token: "a-token".to_owned(),
97 region: "us-east-1".to_owned(),
98 };
99
100 let outcome = clone_with(&opts, &tmp.path().join("a_root"), &creds).await;
101
102 let Err(GitCallError::Refused { stderr, .. }) = outcome else {
103 panic!("{outcome:?}");
104 };
105 assert!(stderr.contains("[REDACTED]/[REDACTED]"), "{stderr}");
106 }
107
108 #[test]
109 fn a_clone_that_stops_receiving_data_is_cut_after_ten_minutes() {
110 let args = clone_args(&opts(CloneShape::SingleBranch), Path::new("/tmp/a_root"));
111
112 assert!(args
113 .windows(2)
114 .any(|pair| pair == ["-c", "http.lowSpeedLimit=1000"]));
115 assert!(args
116 .windows(2)
117 .any(|pair| pair == ["-c", "http.lowSpeedTime=600"]));
118 }
119}