feat: add support for default and git credentials helper as preferred SSH key options in the UI

This commit is contained in:
Nikita Galaiko 2023-12-14 11:18:13 +01:00 committed by GitButler
parent 9b00b5fe7d
commit 4ddc970747
6 changed files with 224 additions and 273 deletions

View File

@ -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,

View File

@ -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<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

@ -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<Credential> 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<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.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,12 +153,11 @@ impl Helper {
return Ok(vec![(remote, vec![Credential::Noop])]);
}
// if prefernce set, only try that.
if let projects::AuthKey::Local {
match &project_repository.project().preferred_key {
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 {
@ -254,15 +165,42 @@ impl Helper {
project_repository.git_repository.remote_anonymous(&ssh_url)
}?;
return Ok(vec![(
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)
}?;
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)
}?;
let flow = Self::https_flow(project_repository, &remote_url)?
.into_iter()
.map(Credential::Https)
.collect::<Vec<_>>();
Ok(vec![(https_remote, flow)])
}
projects::AuthKey::Default => {
// is github is authenticated, only try github.
if remote_url.is_github() {
if let Some(github_access_token) = self
@ -384,6 +322,8 @@ impl Helper {
}
}
}
}
}
fn https_flow(
project_repository: &project_repository::Repository,

View File

@ -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<String>,

View File

@ -12,7 +12,9 @@ import {
} from 'rxjs';
export type Key =
| 'default'
| 'generated'
| 'gitCredentialsHelper'
| {
local: { private_key_path: string; passphrase?: string };
};

View File

@ -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 @@
</script>
<div class="flex flex-col gap-1">
<p>Preferred SSH Key</p>
<p>Git Authentication</p>
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
<div>
Select the SSH key that GitButler will use to authenticate with your Git provider. These keys
@ -61,8 +89,44 @@
</div>
<div class="grid grid-cols-2 gap-2" style="grid-template-columns: max-content 1fr;">
<input type="radio" bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
<input type="radio" bind:group={selectedOption} value="default" on:input={setDefaultKey} />
<div class="flex flex-col space-y-2">
<div>Default</div>
{#if selectedOption === 'default'}
<div class="pr-8">
<div>
We will try all of: your local SHH keys, git credentials helper and local GitButler key.
</div>
<div>
It is recommended to select a specific authenticatation flow for a better expirience.
</div>
</div>
{/if}
</div>
<input
type="radio"
bind:group={selectedOption}
value="gitCredentialsHelper"
on:input={setGitCredentialsHelperKey}
/>
<div class="flex flex-col space-y-2">
<div class="pr-8">
<div>
Use <a href="https://git-scm.com/doc/credential-helpers">git credentials helper</a>
</div>
</div>
{#if selectedOption === 'gitCredentialsHelper'}
<div>
<div>
We will use the system git credentials helper to authenticate with your Git provider.
</div>
</div>
{/if}
</div>
<input type="radio" bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
<div class="flex flex-col space-y-2">
<div class="pr-8">
<div>Use locally generated SSH key</div>
@ -103,7 +167,7 @@
{/if}
</div>
<input type="radio" bind:group={selectedOption} value="local" on:input={() => setLocalKey} />
<input type="radio" bind:group={selectedOption} value="local" on:input={setLocalKey} />
<div class="flex flex-col space-y-2">
<div>Use existing SSH key</div>
@ -121,7 +185,7 @@
<label for="path">Path to private key</label>
<TextBox
placeholder="/absolute/path/id_rsa"
placeholder="~/.ssh/id_rsa"
bind:value={privateKeyPath}
on:change={setLocalKey}
/>