From 4ddc9707477052c7bf294588185477bcbc15ac6b Mon Sep 17 00:00:00 2001 From: Nikita Galaiko Date: Thu, 14 Dec 2023 11:18:13 +0100 Subject: [PATCH] feat: add support for default and git credentials helper as preferred SSH key options in the UI --- packages/tauri/src/bin.rs | 1 - packages/tauri/src/commands.rs | 56 --- packages/tauri/src/git/credentials.rs | 358 ++++++++---------- packages/tauri/src/projects/project.rs | 2 + packages/ui/src/lib/backend/projects.ts | 2 + .../[projectId]/settings/KeysForm.svelte | 78 +++- 6 files changed, 224 insertions(+), 273 deletions(-) diff --git a/packages/tauri/src/bin.rs b/packages/tauri/src/bin.rs index 82386e00b..2a45c379e 100644 --- a/packages/tauri/src/bin.rs +++ b/packages/tauri/src/bin.rs @@ -148,7 +148,6 @@ 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, diff --git a/packages/tauri/src/commands.rs b/packages/tauri/src/commands.rs index 2987c419e..d0256b9ed 100644 --- a/packages/tauri/src/commands.rs +++ b/packages/tauri/src/commands.rs @@ -155,59 +155,3 @@ 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 { - let project_id = project_id.parse().map_err(|_| Error::UserError { - code: Code::Validation, - message: "Malformed project id".into(), - })?; - - let users = handle.state::().inner().clone(); - let projects = handle.state::().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)) -} diff --git a/packages/tauri/src/git/credentials.rs b/packages/tauri/src/git/credentials.rs index f46da1b5f..d631c94a1 100644 --- a/packages/tauri/src/git/credentials.rs +++ b/packages/tauri/src/git/credentials.rs @@ -15,7 +15,6 @@ pub enum SshCredential { #[derive(Debug, Clone, PartialEq, Eq)] pub enum HttpsCredential { - UsernamePassword { username: String, password: String }, CredentialHelper { username: String, password: String }, GitHubToken(String), } @@ -53,12 +52,6 @@ impl From for git2::RemoteCallbacks<'_> { git2::Cred::ssh_key_from_memory("git", None, &key.to_string(), None) }); } - Credential::Https(HttpsCredential::UsernamePassword { username, password }) => { - remote_callbacks.credentials(move |url, _username_from_url, _allowed_types| { - tracing::info!("authenticating with {url} as '{username}' with password"); - 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"); @@ -147,87 +140,6 @@ 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)>, 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.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, @@ -241,146 +153,174 @@ impl Helper { return Ok(vec![(remote, vec![Credential::Noop])]); } - // if prefernce set, only try that. - if let projects::AuthKey::Local { - private_key_path, - passphrase, - } = &project_repository.project().preferred_key - { - let ssh_remote = if remote_url.scheme == super::Scheme::Ssh { - Ok(remote) - } else { - let ssh_url = remote_url.as_ssh()?; - project_repository.git_repository.remote_anonymous(&ssh_url) - }?; + match &project_repository.project().preferred_key { + projects::AuthKey::Local { + private_key_path, + passphrase, + } => { + let ssh_remote = if remote_url.scheme == super::Scheme::Ssh { + Ok(remote) + } else { + 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.clone(), - passphrase: passphrase.clone(), - })], - )]); - } + Ok(vec![( + ssh_remote, + vec![Credential::Ssh(SshCredential::Keyfile { + key_path: private_key_path.clone(), + passphrase: passphrase.clone(), + })], + )]) + } + projects::AuthKey::Generated => { + let ssh_remote = if remote_url.scheme == super::Scheme::Ssh { + Ok(remote) + } else { + let ssh_url = remote_url.as_ssh()?; + project_repository.git_repository.remote_anonymous(&ssh_url) + }?; - // 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 key = self.keys.get_or_create()?; + Ok(vec![( + ssh_remote, + vec![Credential::Ssh(SshCredential::GitButlerKey(Box::new(key)))], + )]) + } + projects::AuthKey::GitCredentialsHelper => { let https_remote = if remote_url.scheme == super::Scheme::Https { Ok(remote) } else { let url = remote_url.as_https()?; project_repository.git_repository.remote_anonymous(&url) }?; - return Ok(vec![( - https_remote, - vec![Credential::Https(HttpsCredential::GitHubToken( - github_access_token, - ))], - )]); - } - } - - match remote_url.scheme { - super::Scheme::Https => { - let mut flow = vec![]; - - let https_flow = Self::https_flow(project_repository, &remote_url)? + let flow = Self::https_flow(project_repository, &remote_url)? .into_iter() .map(Credential::Https) .collect::>(); - - if !https_flow.is_empty() { - flow.push((remote, https_flow)); - } - - if let Ok(ssh_url) = remote_url.as_ssh() { - let ssh_flow = self - .ssh_flow()? - .into_iter() - .map(Credential::Ssh) - .collect::>(); - if !ssh_flow.is_empty() { - flow.push(( - project_repository - .git_repository - .remote_anonymous(&ssh_url)?, - ssh_flow, - )); - } - } - - Ok(flow) + Ok(vec![(https_remote, flow)]) } - super::Scheme::Ssh => { - let mut flow = vec![]; - - let ssh_flow = self - .ssh_flow()? - .into_iter() - .map(Credential::Ssh) - .collect::>(); - if !ssh_flow.is_empty() { - flow.push((remote, ssh_flow)); - } - - if let Ok(https_url) = remote_url.as_https() { - let https_flow = Self::https_flow(project_repository, &https_url)? - .into_iter() - .map(Credential::Https) - .collect::>(); - if !https_flow.is_empty() { - flow.push(( - project_repository - .git_repository - .remote_anonymous(&https_url)?, - https_flow, - )); + projects::AuthKey::Default => { + // 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 { + Ok(remote) + } else { + let url = remote_url.as_https()?; + project_repository.git_repository.remote_anonymous(&url) + }?; + return Ok(vec![( + https_remote, + vec![Credential::Https(HttpsCredential::GitHubToken( + github_access_token, + ))], + )]); } } - Ok(flow) - } - _ => { - let mut flow = vec![]; + match remote_url.scheme { + super::Scheme::Https => { + let mut flow = vec![]; - if let Ok(https_url) = remote_url.as_https() { - let https_flow = Self::https_flow(project_repository, &https_url)? - .into_iter() - .map(Credential::Https) - .collect::>(); + let https_flow = Self::https_flow(project_repository, &remote_url)? + .into_iter() + .map(Credential::Https) + .collect::>(); - if !https_flow.is_empty() { - flow.push(( - project_repository - .git_repository - .remote_anonymous(&https_url)?, - https_flow, - )); + if !https_flow.is_empty() { + flow.push((remote, https_flow)); + } + + if let Ok(ssh_url) = remote_url.as_ssh() { + let ssh_flow = self + .ssh_flow()? + .into_iter() + .map(Credential::Ssh) + .collect::>(); + if !ssh_flow.is_empty() { + flow.push(( + project_repository + .git_repository + .remote_anonymous(&ssh_url)?, + ssh_flow, + )); + } + } + + Ok(flow) + } + super::Scheme::Ssh => { + let mut flow = vec![]; + + let ssh_flow = self + .ssh_flow()? + .into_iter() + .map(Credential::Ssh) + .collect::>(); + if !ssh_flow.is_empty() { + flow.push((remote, ssh_flow)); + } + + if let Ok(https_url) = remote_url.as_https() { + let https_flow = Self::https_flow(project_repository, &https_url)? + .into_iter() + .map(Credential::Https) + .collect::>(); + if !https_flow.is_empty() { + flow.push(( + project_repository + .git_repository + .remote_anonymous(&https_url)?, + https_flow, + )); + } + } + + Ok(flow) + } + _ => { + let mut flow = vec![]; + + if let Ok(https_url) = remote_url.as_https() { + let https_flow = Self::https_flow(project_repository, &https_url)? + .into_iter() + .map(Credential::Https) + .collect::>(); + + if !https_flow.is_empty() { + flow.push(( + project_repository + .git_repository + .remote_anonymous(&https_url)?, + https_flow, + )); + } + } + + if let Ok(ssh_url) = remote_url.as_ssh() { + let ssh_flow = self + .ssh_flow()? + .into_iter() + .map(Credential::Ssh) + .collect::>(); + if !ssh_flow.is_empty() { + flow.push(( + project_repository + .git_repository + .remote_anonymous(&ssh_url)?, + ssh_flow, + )); + } + } + + Ok(flow) } } - - if let Ok(ssh_url) = remote_url.as_ssh() { - let ssh_flow = self - .ssh_flow()? - .into_iter() - .map(Credential::Ssh) - .collect::>(); - if !ssh_flow.is_empty() { - flow.push(( - project_repository - .git_repository - .remote_anonymous(&ssh_url)?, - ssh_flow, - )); - } - } - - Ok(flow) } } } diff --git a/packages/tauri/src/projects/project.rs b/packages/tauri/src/projects/project.rs index 3716c0d21..4b6a2c44d 100644 --- a/packages/tauri/src/projects/project.rs +++ b/packages/tauri/src/projects/project.rs @@ -8,7 +8,9 @@ use crate::{git, id::Id, types::default_true::DefaultTrue}; #[serde(rename_all = "camelCase")] pub enum AuthKey { #[default] + Default, Generated, + GitCredentialsHelper, Local { private_key_path: path::PathBuf, passphrase: Option, diff --git a/packages/ui/src/lib/backend/projects.ts b/packages/ui/src/lib/backend/projects.ts index 4f072c995..a20c053a3 100644 --- a/packages/ui/src/lib/backend/projects.ts +++ b/packages/ui/src/lib/backend/projects.ts @@ -12,7 +12,9 @@ import { } from 'rxjs'; export type Key = + | 'default' | 'generated' + | 'gitCredentialsHelper' | { local: { private_key_path: string; passphrase?: string }; }; diff --git a/packages/ui/src/routes/[projectId]/settings/KeysForm.svelte b/packages/ui/src/routes/[projectId]/settings/KeysForm.svelte index 4c9cc2285..8ab8024f7 100644 --- a/packages/ui/src/routes/[projectId]/settings/KeysForm.svelte +++ b/packages/ui/src/routes/[projectId]/settings/KeysForm.svelte @@ -24,12 +24,28 @@ sshKey = key; }); - let selectedOption = project.preferred_key === 'generated' ? 'generated' : 'local'; + let selectedOption = + project.preferred_key === 'generated' + ? 'generated' + : project.preferred_key === 'default' + ? 'default' + : project.preferred_key === 'gitCredentialsHelper' + ? 'gitCredentialsHelper' + : 'local'; let privateKeyPath = - project.preferred_key === 'generated' ? '' : project.preferred_key.local.private_key_path; + project.preferred_key === 'generated' || + project.preferred_key === 'default' || + project.preferred_key === 'gitCredentialsHelper' + ? '' + : project.preferred_key.local.private_key_path; let privateKeyPassphrase = - project.preferred_key === 'generated' ? '' : project.preferred_key.local.passphrase; + project.preferred_key === 'generated' || + project.preferred_key === 'default' || + project.preferred_key === 'gitCredentialsHelper' + ? '' + : project.preferred_key.local.passphrase; + function setLocalKey() { dispatch('updated', { preferred_key: { @@ -44,6 +60,18 @@ }); } + function setGitCredentialsHelperKey() { + dispatch('updated', { + preferred_key: 'gitCredentialsHelper' + }); + } + + function setDefaultKey() { + dispatch('updated', { + preferred_key: 'default' + }); + } + function setGeneratedKey() { dispatch('updated', { preferred_key: 'generated' @@ -52,7 +80,7 @@
-

Preferred SSH Key

+

Git Authentication

Select the SSH key that GitButler will use to authenticate with your Git provider. These keys @@ -61,8 +89,44 @@
- + +
+
Default
+ {#if selectedOption === 'default'} +
+
+ We will try all of: your local SHH keys, git credentials helper and local GitButler key. +
+
+ It is recommended to select a specific authenticatation flow for a better expirience. +
+
+ {/if} +
+ + +
+ + {#if selectedOption === 'gitCredentialsHelper'} +
+
+ We will use the system git credentials helper to authenticate with your Git provider. +
+
+ {/if} +
+ +
Use locally generated SSH key
@@ -103,7 +167,7 @@ {/if}
- setLocalKey} /> +
Use existing SSH key
@@ -121,7 +185,7 @@