sley_remote/
credentials.rs1use sley_config::GitConfig;
6use sley_core::Result;
7use sley_transport::{
8 GitCredential, RemoteTransport, RemoteUrl,
9 credential::{
10 credential_approve, credential_fill as transport_credential_fill, credential_reject,
11 },
12};
13
14use crate::CredentialProvider;
15
16pub fn http_protocol_name(remote: &RemoteUrl) -> Option<String> {
18 match remote.transport {
19 RemoteTransport::Https => Some("https".to_string()),
20 RemoteTransport::Http => Some("http".to_string()),
21 _ => None,
22 }
23}
24
25pub fn http_credential_host(remote: &RemoteUrl) -> Option<String> {
27 remote.host.clone().map(|host| match remote.port {
28 Some(port) => format!("{host}:{port}"),
29 None => host,
30 })
31}
32
33pub fn http_url_credential(remote: &RemoteUrl) -> Option<GitCredential> {
35 let username = remote.user.clone()?;
36 Some(GitCredential {
37 protocol: http_protocol_name(remote),
38 host: http_credential_host(remote),
39 username: Some(username),
40 password: remote.password.clone(),
41 ..GitCredential::default()
42 })
43}
44
45pub fn credential_request_for_url(remote: &RemoteUrl) -> GitCredential {
47 GitCredential {
48 protocol: http_protocol_name(remote),
49 host: http_credential_host(remote),
50 username: remote.user.clone(),
51 ..GitCredential::default()
52 }
53}
54
55pub fn credential_fill(
58 config: Option<&GitConfig>,
59 mut request: GitCredential,
60) -> Result<Option<GitCredential>> {
61 if let Err(err) = transport_credential_fill(config, None, &mut request, true) {
62 if request.username.is_some() && request.password.is_some() {
63 return Ok(Some(request));
64 }
65 return Err(err);
66 }
67 if request.username.is_some() && request.password.is_some() {
68 Ok(Some(request))
69 } else {
70 Ok(None)
71 }
72}
73
74pub fn credential_store(config: Option<&GitConfig>, credential: &GitCredential, approve: bool) {
76 let mut working = credential.clone();
77 if approve {
78 let _ = credential_approve(config, None, &mut working);
79 } else {
80 let _ = credential_reject(config, None, &mut working);
81 }
82}
83
84pub struct CredentialHelperProvider<'a> {
87 config: Option<&'a GitConfig>,
88}
89
90impl<'a> CredentialHelperProvider<'a> {
91 pub fn new(config: Option<&'a GitConfig>) -> Self {
92 Self { config }
93 }
94}
95
96impl CredentialProvider for CredentialHelperProvider<'_> {
97 fn fill(&mut self, request: GitCredential) -> Result<Option<GitCredential>> {
98 credential_fill(self.config, request)
99 }
100
101 fn approve(&mut self, credential: &GitCredential) -> Result<()> {
102 credential_store(self.config, credential, true);
103 Ok(())
104 }
105
106 fn reject(&mut self, credential: &GitCredential) -> Result<()> {
107 credential_store(self.config, credential, false);
108 Ok(())
109 }
110}
111
112#[cfg(all(test, unix))]
113mod credential_dispatch_parity_tests {
114 use std::fs;
115 use std::os::unix::fs::PermissionsExt;
116 use std::path::Path;
117
118 use sley_config::GitConfig;
119 use sley_transport::GitCredential;
120 use sley_transport::credential::credential_helper_command;
121
122 use super::credential_fill;
123
124 fn config_with_helper(helper: &str) -> GitConfig {
125 let escaped = helper.replace('\\', "\\\\").replace('"', "\\\"");
126 let body = format!("[credential]\n\thelper = \"{escaped}\"\n");
127 GitConfig::parse(body.as_bytes()).expect("config parses")
128 }
129
130 fn write_script(dir: &Path, name: &str, body: &str) -> std::path::PathBuf {
131 let path = dir.join(name);
132 fs::write(&path, body).expect("write script");
133 let mut perms = fs::metadata(&path).expect("metadata").permissions();
134 perms.set_mode(0o755);
135 fs::set_permissions(&path, perms).expect("chmod");
136 path
137 }
138
139 fn base_request() -> GitCredential {
140 GitCredential {
141 protocol: Some("https".to_string()),
142 host: Some("example.com".to_string()),
143 ..GitCredential::default()
144 }
145 }
146
147 #[test]
148 fn absolute_path_form_passes_args_and_op() {
149 let tmp = tempdir();
150 let marker = tmp.path().join("abs.out");
151 let script = write_script(
152 tmp.path(),
153 "abs-helper.sh",
154 &format!(
155 "#!/bin/sh\ncat >/dev/null\nprintf 'ARGS:[%s]\\n' \"$*\" >> '{}'\necho username=abs-user\necho password=abs-pass\n",
156 marker.display()
157 ),
158 );
159 let cfg = config_with_helper(&format!("{} --flag", script.display()));
160 let filled = credential_fill(Some(&cfg), base_request())
161 .expect("fill ok")
162 .expect("credential filled");
163 assert_eq!(filled.username.as_deref(), Some("abs-user"));
164 assert_eq!(filled.password.as_deref(), Some("abs-pass"));
165 let recorded = fs::read_to_string(&marker).expect("marker written");
166 assert_eq!(recorded.trim(), "ARGS:[--flag get]");
167 }
168
169 #[test]
170 fn shell_snippet_form_runs_through_shell_with_op_arg() {
171 let tmp = tempdir();
172 let marker = tmp.path().join("snip.out");
173 let helper = format!(
174 "!f() {{ cat >/dev/null; printf 'GOT:[%s]\\n' \"$*\" >> '{}'; echo username=snip-user; echo password=snip-pass; }}; f",
175 marker.display()
176 );
177 let cfg = config_with_helper(&helper);
178 let filled = credential_fill(Some(&cfg), base_request())
179 .expect("fill ok")
180 .expect("credential filled");
181 assert_eq!(filled.username.as_deref(), Some("snip-user"));
182 assert_eq!(filled.password.as_deref(), Some("snip-pass"));
183 let recorded = fs::read_to_string(&marker).expect("marker written");
184 assert_eq!(recorded.trim(), "GOT:[get]");
185 }
186
187 #[test]
188 fn relative_slash_name_is_bare_not_path() {
189 let cmd = credential_helper_command("sub/relhelper", "get").expect("command built");
190 let argv = command_argv(&cmd);
191 assert_ne!(
192 argv[0], "sub/relhelper",
193 "relative slash name must not be exec'd directly"
194 );
195 assert!(
196 argv.iter().any(|arg| arg == "credential-sub/relhelper")
197 || argv[0] == "sh"
198 || argv[0] == "/bin/sh",
199 "expected credential-<name> dispatch, got argv {argv:?}"
200 );
201 }
202
203 #[test]
204 fn plain_bare_name_maps_to_credential_binary() {
205 let cmd = credential_helper_command("myhelper --opt val", "get").expect("command built");
206 let argv = command_argv(&cmd);
207 assert!(
208 argv.iter().any(|arg| arg == "credential-myhelper")
209 || argv[0] == "sh"
210 || argv[0] == "/bin/sh",
211 "expected credential-<name> dispatch, got argv {argv:?}"
212 );
213 let rendered = argv.join(" ");
214 assert!(
215 rendered.contains("credential-myhelper")
216 && rendered.contains("--opt")
217 && rendered.contains("val")
218 && rendered.contains("get"),
219 "expected `credential-myhelper --opt val get` dispatch, got {rendered:?}"
220 );
221 }
222
223 fn command_argv(cmd: &std::process::Command) -> Vec<String> {
224 let mut out = vec![cmd.get_program().to_string_lossy().into_owned()];
225 out.extend(cmd.get_args().map(|a| a.to_string_lossy().into_owned()));
226 out
227 }
228
229 struct TempDir {
230 path: std::path::PathBuf,
231 }
232 impl TempDir {
233 fn path(&self) -> &Path {
234 &self.path
235 }
236 }
237 impl Drop for TempDir {
238 fn drop(&mut self) {
239 let _ = fs::remove_dir_all(&self.path);
240 }
241 }
242 fn tempdir() -> TempDir {
243 use std::sync::atomic::{AtomicU64, Ordering};
244 static COUNTER: AtomicU64 = AtomicU64::new(0);
245 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
246 let pid = std::process::id();
247 let path = std::env::temp_dir().join(format!("sley-cred-parity-{pid}-{n}"));
248 fs::create_dir_all(&path).expect("mkdir tempdir");
249 TempDir { path }
250 }
251}