test all the keys

This commit is contained in:
Nikita Galaiko 2023-12-08 11:02:17 +01:00 committed by GitButler
parent 346685cc64
commit a1d8b8cb8c
6 changed files with 196 additions and 30 deletions

View File

@ -149,6 +149,7 @@ fn main() {
commands::git_set_global_config,
commands::git_get_global_config,
commands::project_flush_and_push,
commands::test_git_connection,
zip::commands::get_logs_archive_path,
zip::commands::get_project_archive_path,
zip::commands::get_project_data_archive_path,

View File

@ -167,3 +167,59 @@ pub async fn project_flush_and_push(handle: tauri::AppHandle, id: &str) -> Resul
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn test_git_connection(
handle: tauri::AppHandle,
project_id: &str,
) -> Result<serde_json::Value, Error> {
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
let users = handle.state::<users::Controller>().inner().clone();
let projects = handle.state::<projects::Controller>().inner().clone();
let local_data_dir = DataDir::try_from(&handle)?;
let project = projects.get(&project_id).context("failed to get project")?;
let user = users.get_user()?;
let project_repository = project_repository::Repository::open(&project)?;
let gb_repo =
gb_repository::Repository::open(&local_data_dir, &project_repository, user.as_ref())
.context("failed to open repository")?;
let default_target = gb_repo
.default_target()
.context("failed to get default target")?
.ok_or_else(|| Error::UserError {
code: Code::Branches,
message: "No default target set".into(),
})?;
let remote_name = default_target.branch.remote();
let helper = git::credentials::Helper::from(&handle);
let mut results = HashMap::new();
let refspec = &format!("+refs/heads/*:refs/remotes/{}/*", remote_name);
for (mut remote, credentials) in helper.enumerate(&project_repository, remote_name)? {
let remote_url = remote.url().context("failed to get remote url")?.unwrap();
let results = results
.entry(remote_url.to_string())
.or_insert_with(Vec::new);
for callback in credentials {
let mut fetch_opts = git2::FetchOptions::new();
fetch_opts.remote_callbacks(callback.clone().into());
fetch_opts.prune(git2::FetchPrune::On);
results.push(serde_json::json!({
"callback": format!("{:?}", callback),
"error": remote.fetch(&[refspec], Some(&mut fetch_opts)).err().map(|e| e.to_string()),
}));
}
}
Ok(serde_json::json!(results))
}

View File

@ -4,18 +4,23 @@ use tauri::AppHandle;
use crate::{keys, paths, project_repository, projects, users};
#[derive(Debug, Clone)]
pub enum SshCredential {
Keyfile {
key_path: path::PathBuf,
passphrase: Option<String>,
},
MemoryKey(Box<keys::PrivateKey>),
GitButlerKey(Box<keys::PrivateKey>),
}
#[derive(Debug, Clone)]
pub enum HttpsCredential {
UsernamePassword { username: String, password: String },
CredentialHelper { username: String, password: String },
GitHubToken(String),
}
#[derive(Debug, Clone)]
pub enum Credential {
Noop,
Ssh(SshCredential),
@ -40,7 +45,7 @@ impl From<Credential> for git2::RemoteCallbacks<'_> {
git2::Cred::ssh_key("git", None, &key_path, passphrase.as_deref())
});
}
Credential::Ssh(SshCredential::MemoryKey(key)) => {
Credential::Ssh(SshCredential::GitButlerKey(key)) => {
remote_callbacks.credentials(move |url, _username_from_url, _allowed_types| {
tracing::info!("authenticating with {} using gitbutler's key", url);
git2::Cred::ssh_key_from_memory("git", None, &key.to_string(), None)
@ -52,6 +57,18 @@ impl From<Credential> for git2::RemoteCallbacks<'_> {
git2::Cred::userpass_plaintext(&username, &password)
});
}
Credential::Https(HttpsCredential::CredentialHelper { username, password }) => {
remote_callbacks.credentials(move |url, _username_from_url, _allowed_types| {
tracing::info!("authenticating with {url} as '{username}' with password using credential helper");
git2::Cred::userpass_plaintext(&username, &password)
});
}
Credential::Https(HttpsCredential::GitHubToken(token)) => {
remote_callbacks.credentials(move |url, _username_from_url, _allowed_types| {
tracing::info!("authenticating with {url} using github token");
git2::Cred::userpass_plaintext("git", &token)
});
}
};
remote_callbacks
}
@ -117,6 +134,89 @@ impl From<HelpError> for crate::error::Error {
}
impl Helper {
/// returns all possible credentials for a remote, without trying to be smart.
pub fn enumerate<'a>(
&'a self,
project_repository: &'a project_repository::Repository,
remote: &str,
) -> Result<Vec<(super::Remote, Vec<Credential>)>, HelpError> {
let remote = project_repository.git_repository.find_remote(remote)?;
let remote_url = remote.url()?.ok_or(HelpError::NoUrlSet)?;
let mut flow = vec![];
if let projects::AuthKey::Local {
private_key_path,
passphrase,
} = &project_repository.project().preferred_key
{
let ssh_remote = if remote_url.scheme == super::Scheme::Ssh {
project_repository
.git_repository
.remote_anonymous(&remote_url)
} else {
let ssh_url = remote_url.as_ssh()?;
project_repository.git_repository.remote_anonymous(&ssh_url)
}?;
flow.push((
ssh_remote,
vec![Credential::Ssh(SshCredential::Keyfile {
key_path: private_key_path
.canonicalize()
.unwrap_or(private_key_path.clone()),
passphrase: passphrase.clone(),
})],
));
}
// is github is authenticated, only try github.
if remote_url.is_github() {
if let Some(github_access_token) = self
.users
.get_user()?
.and_then(|user| user.github_access_token)
{
let https_remote = if remote_url.scheme == super::Scheme::Https {
project_repository
.git_repository
.remote_anonymous(&remote_url)
} else {
let ssh_url = remote_url.as_ssh()?;
project_repository.git_repository.remote_anonymous(&ssh_url)
}?;
flow.push((
https_remote,
vec![Credential::Https(HttpsCredential::GitHubToken(
github_access_token,
))],
));
}
}
if let Ok(https_url) = remote_url.as_https() {
flow.push((
project_repository
.git_repository
.remote_anonymous(&https_url)?,
Self::https_flow(project_repository, &https_url)?
.into_iter()
.map(Credential::Https)
.collect(),
));
}
if let Ok(ssh_url) = remote_url.as_ssh() {
flow.push((
project_repository
.git_repository
.remote_anonymous(&ssh_url)?,
self.ssh_flow()?.into_iter().map(Credential::Ssh).collect(),
));
}
Ok(flow)
}
pub fn help<'a>(
&'a self,
project_repository: &'a project_repository::Repository,
@ -142,11 +242,12 @@ impl Helper {
let ssh_url = remote_url.as_ssh()?;
project_repository.git_repository.remote_anonymous(&ssh_url)
}?;
return Ok(vec![(
ssh_remote,
vec![Credential::Ssh(SshCredential::Keyfile {
key_path: private_key_path
.read_link()
.canonicalize()
.unwrap_or(private_key_path.clone()),
passphrase: passphrase.clone(),
})],
@ -163,10 +264,8 @@ impl Helper {
let https_remote = if remote_url.scheme == super::Scheme::Https {
Ok(remote)
} else {
let https_url = remote_url.as_ssh()?;
project_repository
.git_repository
.remote_anonymous(&https_url)
let ssh_url = remote_url.as_ssh()?;
project_repository.git_repository.remote_anonymous(&ssh_url)
}?;
return Ok(vec![(
https_remote,
@ -205,7 +304,7 @@ impl Helper {
self.ssh_flow()?.into_iter().map(Credential::Ssh).collect(),
)];
if let Ok(https_url) = remote_url.as_ssh() {
if let Ok(https_url) = remote_url.as_https() {
flow.push((
project_repository
.git_repository
@ -222,7 +321,7 @@ impl Helper {
_ => {
let mut flow = vec![];
if let Ok(https_url) = remote_url.as_ssh() {
if let Ok(https_url) = remote_url.as_https() {
flow.push((
project_repository
.git_repository
@ -258,7 +357,7 @@ impl Helper {
let config = project_repository.git_repository.config()?;
helper.config(&git2::Config::from(config));
if let Some((username, password)) = helper.execute() {
flow.push(HttpsCredential::UsernamePassword { username, password });
flow.push(HttpsCredential::CredentialHelper { username, password });
}
Ok(flow)
@ -270,7 +369,7 @@ impl Helper {
let home_path = std::path::Path::new(&home_path);
let id_rsa_path = home_path.join(".ssh").join("id_rsa");
let id_rsa_path = id_rsa_path.read_link().unwrap_or(id_rsa_path);
let id_rsa_path = id_rsa_path.canonicalize().unwrap_or(id_rsa_path);
if id_rsa_path.exists() {
flow.push(SshCredential::Keyfile {
key_path: id_rsa_path.clone(),
@ -279,7 +378,7 @@ impl Helper {
}
let id_ed25519_path = home_path.join(".ssh").join("id_ed25519");
let id_ed25519_path = id_ed25519_path.read_link().unwrap_or(id_ed25519_path);
let id_ed25519_path = id_ed25519_path.canonicalize().unwrap_or(id_ed25519_path);
if id_ed25519_path.exists() {
flow.push(SshCredential::Keyfile {
key_path: id_ed25519_path.clone(),
@ -288,7 +387,7 @@ impl Helper {
}
let id_ecdsa_path = home_path.join(".ssh").join("id_ecdsa");
let id_ecdsa_path = id_ecdsa_path.read_link().unwrap_or(id_ecdsa_path);
let id_ecdsa_path = id_ecdsa_path.canonicalize().unwrap_or(id_ecdsa_path);
if id_ecdsa_path.exists() {
flow.push(SshCredential::Keyfile {
key_path: id_ecdsa_path.clone(),
@ -298,7 +397,7 @@ impl Helper {
}
let key = self.keys.get_or_create()?;
flow.push(SshCredential::MemoryKey(Box::new(key)));
flow.push(SshCredential::GitButlerKey(Box::new(key)));
Ok(flow)
}
}

View File

@ -9,7 +9,7 @@ pub use convert::ConvertError;
pub use parse::Error as ParseError;
pub use scheme::Scheme;
#[derive(Default, Clone, Debug, thiserror::Error)]
#[derive(Default, Clone, Hash, PartialEq, Eq, Debug, thiserror::Error)]
pub struct Url {
/// The URL scheme.
pub scheme: Scheme,

View File

@ -60,7 +60,7 @@ mod tests {
#[test]
fn to_https_url_test() {
vec![
for (input, expected) in [
(
"https://github.com/gitbutlerapp/gitbutler-client.git",
"https://github.com/gitbutlerapp/gitbutler-client.git",
@ -77,19 +77,24 @@ mod tests {
"ssh://git@github.com/gitbutlerapp/gitbutler-client.git",
"https://github.com/gitbutlerapp/gitbutler-client.git",
),
]
.into_iter()
.enumerate()
.for_each(|(i, (input, expected))| {
(
"git@bitbucket.org:gitbutler-nikita/test.git",
"https://bitbucket.org/gitbutler-nikita/test.git",
),
(
"https://bitbucket.org/gitbutler-nikita/test.git",
"https://bitbucket.org/gitbutler-nikita/test.git",
),
] {
let url = input.parse().unwrap();
let https_url = to_https_url(&url).unwrap();
assert_eq!(https_url.to_string(), expected, "test case {}", i);
});
assert_eq!(https_url.to_string(), expected, "test case {}", url);
}
}
#[test]
fn to_ssh_url_test() {
vec![
for (input, expected) in [
(
"git@github.com:gitbutlerapp/gitbutler-client.git",
"git@github.com:gitbutlerapp/gitbutler-client.git",
@ -106,13 +111,18 @@ mod tests {
"ssh://git@github.com/gitbutlerapp/gitbutler-client.git",
"ssh://git@github.com/gitbutlerapp/gitbutler-client.git",
),
]
.into_iter()
.enumerate()
.for_each(|(i, (input, expected))| {
(
"https://bitbucket.org/gitbutler-nikita/test.git",
"git@bitbucket.org:gitbutler-nikita/test.git",
),
(
"git@bitbucket.org:gitbutler-nikita/test.git",
"git@bitbucket.org:gitbutler-nikita/test.git",
),
] {
let url = input.parse().unwrap();
let ssh_url = to_ssh_url(&url).unwrap();
assert_eq!(ssh_url.to_string(), expected, "test case {}", i);
});
assert_eq!(ssh_url.to_string(), expected, "test case {}", url);
}
}
}

View File

@ -6,7 +6,7 @@ use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use ssh_key;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct PrivateKey(ssh_key::PrivateKey);
#[derive(Debug, thiserror::Error)]