From c8037bbdb2bba965cf68d092c951bf4377008a60 Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Wed, 11 Jun 2025 02:45:38 +0530 Subject: [PATCH 1/9] refactor: extract authentication logic from git_push & git_pull --- src/events/git_pull.rs | 137 +-------------------------------- src/events/git_push.rs | 154 +------------------------------------ src/main.rs | 1 + src/utils/git_auth.rs | 170 +++++++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 1 + 5 files changed, 179 insertions(+), 284 deletions(-) create mode 100644 src/utils/git_auth.rs create mode 100644 src/utils/mod.rs diff --git a/src/events/git_pull.rs b/src/events/git_pull.rs index fe34c95..731464c 100644 --- a/src/events/git_pull.rs +++ b/src/events/git_pull.rs @@ -3,7 +3,8 @@ use std::path::Path; use super::AtomicEvent; use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use git2::{Cred, CredentialType, Repository}; +use crate::utils::git_auth::setup_auth_callbacks; +use git2::Repository; pub struct GitPull { pub pre_check_rules: Vec>, @@ -462,142 +463,10 @@ impl GitPull { Ok(()) } - /// Set up authentication callbacks for git operations - fn setup_auth_callbacks() -> git2::RemoteCallbacks<'static> { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = Arc::new(AtomicUsize::new(0)); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); - // Limit authentication attempts to prevent infinite loops - if current_attempt > 3 { - return Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - "Maximum authentication attempts exceeded", - )); - } - - // If SSH key authentication is allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - // Try SSH agent first (most common and secure) - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH agent failed: {}", e); - } - } - - // Try to find SSH keys in standard locations - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - - let ssh_dir = Path::new(&home_dir).join(".ssh"); - - // Common SSH key file names in order of preference - let key_files = [ - ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - ("id_ecdsa", "id_ecdsa.pub"), - ("id_dsa", "id_dsa.pub"), - ]; - - for (private_name, public_name) in &key_files { - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); - - if private_key.exists() { - // Try with public key if it exists - if public_key.exists() { - match Cred::ssh_key(username, Some(&public_key), &private_key, None) - { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key with public key failed: {}", e); - } - } - } - - // Try without public key - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key without public key failed: {}", e); - } - } - } - } - } else { - eprintln!("No username provided for SSH authentication"); - } - } - - // If username/password authentication is allowed (HTTPS) - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - // Try to get credentials from git config or environment - if let (Ok(username), Ok(password)) = - (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) - { - return Cred::userpass_plaintext(&username, &password); - } - - // For GitHub, you might want to use a personal access token - if url.contains("github.com") { - if let Ok(token) = std::env::var("GITHUB_TOKEN") { - return Cred::userpass_plaintext("git", &token); - } - } - } - - // Default authentication (tries default SSH key) - if allowed_types.contains(CredentialType::DEFAULT) { - match Cred::default() { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("Default authentication failed: {}", e); - } - } - } - - Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - format!( - "Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types - ), - )) - }); - - // Set up certificate check callback for HTTPS - callbacks.certificate_check(|_cert, _host| { - // In production, you should properly validate certificates - // For now, we'll accept all certificates (not recommended for production) - Ok(git2::CertificateCheckStatus::CertificateOk) - }); - - callbacks - } - /// Create fetch options with authentication fn create_fetch_options() -> git2::FetchOptions<'static> { let mut fetch_options = git2::FetchOptions::new(); - fetch_options.remote_callbacks(Self::setup_auth_callbacks()); + fetch_options.remote_callbacks(setup_auth_callbacks()); fetch_options } } diff --git a/src/events/git_push.rs b/src/events/git_push.rs index 23e750e..a15cc6b 100644 --- a/src/events/git_push.rs +++ b/src/events/git_push.rs @@ -1,10 +1,9 @@ -use std::path::Path; - use super::AtomicEvent; use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use git2::{Cred, CredentialType, Repository}; -use log::debug; +use crate::utils::git_auth::setup_auth_callbacks; +use git2::Repository; +use std::path::Path; pub struct GitPush { pub pre_check_rules: Vec>, @@ -316,155 +315,10 @@ impl GitPush { Ok(()) } - /// Set up authentication callbacks for git operations - fn setup_auth_callbacks() -> git2::RemoteCallbacks<'static> { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = Arc::new(AtomicUsize::new(0)); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); - // Limit authentication attempts to prevent infinite loops - if current_attempt > 3 { - return Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - "Maximum authentication attempts exceeded", - )); - } - - // If SSH key authentication is allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - // Try SSH agent first (most common and secure) - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH agent failed: {}", e); - } - } - - // Try to find SSH keys in standard locations - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - - let ssh_dir = Path::new(&home_dir).join(".ssh"); - - // Common SSH key file names in order of preference - let key_files = [ - ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - ("id_ecdsa", "id_ecdsa.pub"), - ("id_dsa", "id_dsa.pub"), - ]; - - for (private_name, public_name) in &key_files { - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); - - if private_key.exists() { - // Try with public key if it exists - if public_key.exists() { - match Cred::ssh_key(username, Some(&public_key), &private_key, None) - { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH key with public key failed: {}", e); - } - } - } - - // Try without public key - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH key without public key failed: {}", e); - } - } - } - } - } else { - println!("No username provided for SSH authentication"); - } - } - - // If username/password authentication is allowed (HTTPS) - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - // Try to get credentials from git config or environment - if let (Ok(username), Ok(password)) = - (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) - { - return Cred::userpass_plaintext(&username, &password); - } - - // For GitHub, you might want to use a personal access token - if url.contains("github.com") { - if let Ok(token) = std::env::var("GITHUB_TOKEN") { - return Cred::userpass_plaintext("git", &token); - } - } - } - - // Default authentication (tries default SSH key) - if allowed_types.contains(CredentialType::DEFAULT) { - match Cred::default() { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("Default authentication failed: {}", e); - } - } - } - - Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - format!( - "Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types - ), - )) - }); - - // Add push update reference callback for better error reporting - callbacks.push_update_reference(|refname, status| match status { - Some(msg) => { - println!("Push failed for {}: {}", refname, msg); - Err(git2::Error::from_str(msg)) - } - None => { - println!("Push successful for {}", refname); - Ok(()) - } - }); - - // Set up certificate check callback for HTTPS - callbacks.certificate_check(|_cert, _host| { - // In production, you should properly validate certificates - // For now, we'll accept all certificates (not recommended for production) - println!("Certificate check for host: {}", _host); - Ok(git2::CertificateCheckStatus::CertificateOk) - }); - - callbacks - } - /// Create push options with authentication fn create_push_options() -> git2::PushOptions<'static> { let mut push_options = git2::PushOptions::new(); - push_options.remote_callbacks(Self::setup_auth_callbacks()); + push_options.remote_callbacks(setup_auth_callbacks()); push_options } } diff --git a/src/main.rs b/src/main.rs index f15d1fd..1a0a525 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod hook_executor; mod rules; mod step; mod util; +mod utils; mod workflow_queue; mod workflows; diff --git a/src/utils/git_auth.rs b/src/utils/git_auth.rs new file mode 100644 index 0000000..07b13d8 --- /dev/null +++ b/src/utils/git_auth.rs @@ -0,0 +1,170 @@ +use git2::{ + CertificateCheckStatus, Cred, CredentialType, Error, ErrorClass, ErrorCode, RemoteCallbacks, +}; +use std::{ + path::Path, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, +}; + +pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { + let mut callbacks = RemoteCallbacks::new(); + let attempt_count = Arc::new(AtomicUsize::new(0)); + + callbacks.credentials(move |url, username_from_url, allowed_types| { + let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); + println!( + "[DEBUG] Authentication attempt #{} for URL: {}", + current_attempt + 1, + url + ); + + // Limit authentication attempts to prevent infinite loops + if current_attempt > 3 { + println!("[DEBUG] Maximum authentication attempts exceeded"); + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Maximum authentication attempts exceeded", + )); + } + + // If SSH key authentication is allowed + if allowed_types.contains(CredentialType::SSH_KEY) { + println!("[DEBUG] SSH_KEY authentication allowed"); + if let Some(username) = username_from_url { + println!("[DEBUG] Username from URL: {}", username); + + // match Cred::ssh_key_from_agent(username) { + // Ok(cred) => { + // return Ok(cred); + // } + // Err(e) => { + // println!("SSH agent failed: {}", e); + // } + // } + + // Try to find SSH keys in standard locations + let home_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + println!("[DEBUG] Home directory resolved to: {}", home_dir); + + let ssh_dir = Path::new(&home_dir).join(".ssh"); + println!("[DEBUG] Checking .ssh directory at: {:?}", ssh_dir); + + // Common SSH key file names in order of preference + let key_files = [ + // ("id_ed25519", "id_ed25519.pub"), + ("id_rsa", "id_rsa.pub"), + // ("id_ecdsa", "id_ecdsa.pub"), + // ("id_dsa", "id_dsa.pub"), + ]; + + for (private_name, public_name) in &key_files { + let private_key = ssh_dir.join(private_name); + let public_key = ssh_dir.join(public_name); + println!( + "[DEBUG] Trying key pair: {:?}, {:?}", + private_key, public_key + ); + + if private_key.exists() { + println!("[DEBUG] Found private key: {:?}", private_key); + + if public_key.exists() { + println!("[DEBUG] Found public key: {:?}", public_key); + match Cred::ssh_key(username, Some(&public_key), &private_key, None) { + Ok(cred) => { + println!("[DEBUG] SSH key auth with public key succeeded"); + return Ok(cred); + } + Err(e) => { + eprintln!("SSH key with public key failed: {}", e); + } + } + } + + println!("[DEBUG] Trying SSH key without public key"); + match Cred::ssh_key(username, None, &private_key, None) { + Ok(cred) => { + println!("[DEBUG] SSH key auth without public key succeeded"); + return Ok(cred); + } + Err(e) => { + eprintln!("SSH key without public key failed: {}", e); + } + } + } else { + println!("[DEBUG] Private key not found: {:?}", private_key); + } + } + } else { + eprintln!("No username provided for SSH authentication"); + } + } + + // If username/password authentication is allowed (HTTPS) + if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { + println!("[DEBUG] USER_PASS_PLAINTEXT authentication allowed"); + + if let (Ok(username), Ok(password)) = + (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) + { + println!("[DEBUG] Using GIT_USERNAME and GIT_PASSWORD from environment"); + return Cred::userpass_plaintext(&username, &password); + } + + if url.contains("github.com") { + println!("[DEBUG] URL contains github.com, checking for GITHUB_TOKEN"); + if let Ok(token) = std::env::var("GITHUB_TOKEN") { + println!("[DEBUG] Using GITHUB_TOKEN from environment"); + return Cred::userpass_plaintext("git", &token); + } else { + println!("[DEBUG] GITHUB_TOKEN not found in environment"); + } + } + } + + // Default authentication (tries default SSH key) + if allowed_types.contains(CredentialType::DEFAULT) { + println!("[DEBUG] Attempting default credentials"); + match Cred::default() { + Ok(cred) => { + println!("[DEBUG] Default credentials succeeded"); + return Ok(cred); + } + Err(e) => { + eprintln!("Default authentication failed: {}", e); + } + } + } + + println!( + "[DEBUG] Authentication failed after {} attempts for {}. Available methods: {:?}", + current_attempt + 1, + url, + allowed_types + ); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!( + "Authentication failed after {} attempts for {}. Available methods: {:?}", + current_attempt + 1, + url, + allowed_types + ), + )) + }); + + // Set up certificate check callback for HTTPS + callbacks.certificate_check(|_cert, _host| { + println!("[DEBUG] Skipping certificate verification (INSECURE)"); + Ok(CertificateCheckStatus::CertificateOk) + }); + + callbacks +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..75d5ade --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod git_auth; From 451a3ae44fdb60d63b1f7677440843d3335acf9b Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Wed, 11 Jun 2025 04:53:07 +0530 Subject: [PATCH 2/9] fix: authentication workflow --- src/utils/git_auth.rs | 330 ++++++++++++++++++++++++------------------ 1 file changed, 190 insertions(+), 140 deletions(-) diff --git a/src/utils/git_auth.rs b/src/utils/git_auth.rs index 07b13d8..734d700 100644 --- a/src/utils/git_auth.rs +++ b/src/utils/git_auth.rs @@ -1,168 +1,218 @@ use git2::{ CertificateCheckStatus, Cred, CredentialType, Error, ErrorClass, ErrorCode, RemoteCallbacks, }; -use std::{ - path::Path, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, -}; - -pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { - let mut callbacks = RemoteCallbacks::new(); - let attempt_count = Arc::new(AtomicUsize::new(0)); +use log::debug; +use std::path::Path; +use std::sync::{Arc, Mutex}; - callbacks.credentials(move |url, username_from_url, allowed_types| { - let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); - println!( - "[DEBUG] Authentication attempt #{} for URL: {}", - current_attempt + 1, - url - ); +fn try_ssh_agent_auth(username: &str) -> Result { + debug!("Attempting SSH agent authentication for user: {}", username); - // Limit authentication attempts to prevent infinite loops - if current_attempt > 3 { - println!("[DEBUG] Maximum authentication attempts exceeded"); - return Err(Error::new( - ErrorCode::Auth, - ErrorClass::Net, - "Maximum authentication attempts exceeded", - )); + if std::env::var("SSH_AUTH_SOCK").is_err() { + debug!("SSH_AUTH_SOCK not set, skipping ssh_key_from_agent"); + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "SSH_AUTH_SOCK not available", + )); + } + + match Cred::ssh_key_from_agent(username) { + Ok(cred) => { + debug!("SSH agent authentication succeeded"); + Ok(cred) } - - // If SSH key authentication is allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - println!("[DEBUG] SSH_KEY authentication allowed"); - if let Some(username) = username_from_url { - println!("[DEBUG] Username from URL: {}", username); - - // match Cred::ssh_key_from_agent(username) { - // Ok(cred) => { - // return Ok(cred); - // } - // Err(e) => { - // println!("SSH agent failed: {}", e); - // } - // } - - // Try to find SSH keys in standard locations - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - println!("[DEBUG] Home directory resolved to: {}", home_dir); - - let ssh_dir = Path::new(&home_dir).join(".ssh"); - println!("[DEBUG] Checking .ssh directory at: {:?}", ssh_dir); - - // Common SSH key file names in order of preference - let key_files = [ - // ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - // ("id_ecdsa", "id_ecdsa.pub"), - // ("id_dsa", "id_dsa.pub"), - ]; - - for (private_name, public_name) in &key_files { - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); - println!( - "[DEBUG] Trying key pair: {:?}, {:?}", - private_key, public_key - ); - - if private_key.exists() { - println!("[DEBUG] Found private key: {:?}", private_key); - - if public_key.exists() { - println!("[DEBUG] Found public key: {:?}", public_key); - match Cred::ssh_key(username, Some(&public_key), &private_key, None) { - Ok(cred) => { - println!("[DEBUG] SSH key auth with public key succeeded"); - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key with public key failed: {}", e); - } - } - } - - println!("[DEBUG] Trying SSH key without public key"); - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - println!("[DEBUG] SSH key auth without public key succeeded"); - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key without public key failed: {}", e); - } - } - } else { - println!("[DEBUG] Private key not found: {:?}", private_key); - } - } - } else { - eprintln!("No username provided for SSH authentication"); - } + Err(e) => { + debug!("SSH agent authentication failed: {}", e); + Err(e) } + } +} - // If username/password authentication is allowed (HTTPS) - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - println!("[DEBUG] USER_PASS_PLAINTEXT authentication allowed"); +fn try_ssh_key_files( + username: &str, + key_index: usize, + use_public_key: bool, +) -> Result { + debug!( + "Attempting SSH key file authentication for user: {}, key_index: {}, use_public_key: {}", + username, key_index, use_public_key + ); + + let home_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + debug!("Home directory resolved to: {}", home_dir); + + let ssh_dir = Path::new(&home_dir).join(".ssh"); + debug!("Checking .ssh directory at: {:?}", ssh_dir); + + // Common SSH key file names in order of preference + let key_files = [ + ("id_ed25519", "id_ed25519.pub"), + ("id_rsa", "id_rsa.pub"), + ("id_ecdsa", "id_ecdsa.pub"), + ("id_dsa", "id_dsa.pub"), + ]; + + if key_index >= key_files.len() { + debug!("Key index {} out of range", key_index); + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "All SSH key files exhausted", + )); + } - if let (Ok(username), Ok(password)) = - (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) - { - println!("[DEBUG] Using GIT_USERNAME and GIT_PASSWORD from environment"); - return Cred::userpass_plaintext(&username, &password); - } + let (private_name, public_name) = key_files[key_index]; + let private_key = ssh_dir.join(private_name); + let public_key = ssh_dir.join(public_name); - if url.contains("github.com") { - println!("[DEBUG] URL contains github.com, checking for GITHUB_TOKEN"); - if let Ok(token) = std::env::var("GITHUB_TOKEN") { - println!("[DEBUG] Using GITHUB_TOKEN from environment"); - return Cred::userpass_plaintext("git", &token); - } else { - println!("[DEBUG] GITHUB_TOKEN not found in environment"); - } - } - } + if !private_key.exists() { + debug!("Private key not found: {:?}", private_key); + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Private key not found: {}", private_name), + )); + } + + debug!("Found private key: {:?}", private_key); - // Default authentication (tries default SSH key) - if allowed_types.contains(CredentialType::DEFAULT) { - println!("[DEBUG] Attempting default credentials"); - match Cred::default() { + if use_public_key { + if public_key.exists() { + debug!("Found public key: {:?}, trying with public key", public_key); + match Cred::ssh_key(username, Some(&public_key), &private_key, None) { Ok(cred) => { - println!("[DEBUG] Default credentials succeeded"); - return Ok(cred); + debug!( + "SSH key auth with public key succeeded for {}", + private_name + ); + Ok(cred) } Err(e) => { - eprintln!("Default authentication failed: {}", e); + debug!("SSH key with public key failed for {}: {}", private_name, e); + Err(e) } } + } else { + debug!( + "Public key not found for {}, skipping this attempt", + private_name + ); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Public key not found for {}", private_name), + )) + } + } else { + debug!("Trying SSH key without public key for {}", private_name); + match Cred::ssh_key(username, None, &private_key, None) { + Ok(cred) => { + debug!( + "SSH key auth without public key succeeded for {}", + private_name + ); + Ok(cred) + } + Err(e) => { + debug!( + "SSH key without public key failed for {}: {}", + private_name, e + ); + Err(e) + } } + } +} - println!( - "[DEBUG] Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types +fn authenticate_git( + url: &str, + username_from_url: Option<&str>, + allowed_types: CredentialType, + attempt_count: usize, +) -> Result { + debug!( + "Git authentication attempt #{} for URL: {}", + attempt_count, url + ); + debug!("Username from URL: {:?}", username_from_url); + debug!("Allowed credential types: {:?}", allowed_types); + + // Prevent infinite loops + if attempt_count > 20 { + debug!( + "Too many authentication attempts ({}), failing to prevent infinite loop", + attempt_count ); - Err(Error::new( + return Err(Error::new( ErrorCode::Auth, ErrorClass::Net, - format!( - "Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types - ), - )) + "Too many authentication attempts", + )); + } + + // Try SSH key authentication if allowed + if allowed_types.contains(CredentialType::SSH_KEY) { + if let Some(username) = username_from_url { + debug!("SSH key authentication is allowed, trying SSH methods"); + + // Try SSH agent first (only on first attempt) + if attempt_count == 1 { + if let Ok(cred) = try_ssh_agent_auth(username) { + return Ok(cred); + } + // If SSH agent fails, fall through to try SSH key files on same attempt + } + + // Try SSH key files with progression + // Attempt 1+: Start with id_ed25519 if SSH agent failed + // Attempt 1: id_ed25519 with public key + // Attempt 2: id_ed25519 without public key + // Attempt 3: id_rsa with public key + // Attempt 4: id_rsa without public key + // etc. + let key_attempt = attempt_count - 1; + let key_index = key_attempt / 2; + let use_public_key = key_attempt % 2 == 0; + + if let Ok(cred) = try_ssh_key_files(username, key_index, use_public_key) { + return Ok(cred); + } + } else { + debug!("No username provided for SSH authentication"); + } + } + + debug!( + "All authentication methods failed for attempt {}", + attempt_count + ); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Authentication failed - attempt {}", attempt_count), + )) +} +pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { + let mut callbacks = RemoteCallbacks::new(); + + // Track attempt count across callback invocations + let attempt_count: Arc> = Arc::new(Mutex::new(0)); + + callbacks.credentials(move |url, username_from_url, allowed_types| { + let mut count = attempt_count.lock().unwrap(); + *count += 1; + let current_attempt = *count; + drop(count); + + authenticate_git(url, username_from_url, allowed_types, current_attempt) }); // Set up certificate check callback for HTTPS callbacks.certificate_check(|_cert, _host| { - println!("[DEBUG] Skipping certificate verification (INSECURE)"); + debug!("Skipping certificate verification (INSECURE)"); Ok(CertificateCheckStatus::CertificateOk) }); From e536707f62b1eb84b8364447b0493c0ce0329b6c Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Fri, 13 Jun 2025 06:46:02 +0530 Subject: [PATCH 3/9] feat: authentication through PAT --- src/utils/git_auth.rs | 61 ++++++++++++++++++- src/workflows/default/action/mod.rs | 1 - src/workflows/default/prompt/mod.rs | 1 + .../default/prompt/pa07_ask_pull_push.rs | 8 +-- .../pa13_pull_push.rs} | 21 ++----- 5 files changed, 69 insertions(+), 23 deletions(-) rename src/workflows/default/{action/ta09_pull_push.rs => prompt/pa13_pull_push.rs} (67%) diff --git a/src/utils/git_auth.rs b/src/utils/git_auth.rs index 734d700..666fb58 100644 --- a/src/utils/git_auth.rs +++ b/src/utils/git_auth.rs @@ -1,3 +1,5 @@ +use dialoguer::theme::ColorfulTheme; +use dialoguer::{Input, Password}; use git2::{ CertificateCheckStatus, Cred, CredentialType, Error, ErrorClass, ErrorCode, RemoteCallbacks, }; @@ -126,8 +128,59 @@ fn try_ssh_key_files( } } } +fn try_userpass_authentication(username_from_url: Option<&str>) -> Result { + debug!("USER_PASS_PLAINTEXT authentication is allowed, prompting for credentials"); -fn authenticate_git( + // Prompt for username if not provided in URL + let username = if let Some(user) = username_from_url { + user.to_string() + } else { + Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your username") + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to read username: {}", e), + ) + })? + }; + + let token = Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your personal access token") + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to read token: {}", e), + ) + })?; + + if !username.is_empty() && !token.is_empty() { + debug!("Creating credentials with username and token"); + match Cred::userpass_plaintext(&username, &token) { + Ok(cred) => { + debug!("Username/token authentication succeeded"); + Ok(cred) + } + Err(e) => { + debug!("Username/token authentication failed: {}", e); + Err(e) + } + } + } else { + debug!("Username or token is empty, skipping userpass authentication"); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Username or token cannot be empty", + )) + } +} + +fn ssh_authenticate_git( url: &str, username_from_url: Option<&str>, allowed_types: CredentialType, @@ -207,7 +260,11 @@ pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { let current_attempt = *count; drop(count); - authenticate_git(url, username_from_url, allowed_types, current_attempt) + if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { + try_userpass_authentication(username_from_url) + } else { + ssh_authenticate_git(url, username_from_url, allowed_types, current_attempt) + } }); // Set up certificate check callback for HTTPS diff --git a/src/workflows/default/action/mod.rs b/src/workflows/default/action/mod.rs index 1c994cc..ee1b038 100644 --- a/src/workflows/default/action/mod.rs +++ b/src/workflows/default/action/mod.rs @@ -4,7 +4,6 @@ pub(crate) mod ta03_pop_stash; pub(crate) mod ta04_has_unstaged; pub(crate) mod ta07_has_uncommitted; pub(crate) mod ta08_is_pulled_pushed; -pub(crate) mod ta09_pull_push; pub(crate) mod ta10_is_branch_main; pub(crate) mod ta11_is_sole_contributor; pub(crate) mod ta12_move_changes; diff --git a/src/workflows/default/prompt/mod.rs b/src/workflows/default/prompt/mod.rs index 1c1cdd4..c35378b 100644 --- a/src/workflows/default/prompt/mod.rs +++ b/src/workflows/default/prompt/mod.rs @@ -11,3 +11,4 @@ pub(crate) mod pa09_ask_branch_name; pub(crate) mod pa10_ask_same_feat; pub(crate) mod pa11_ask_ai_commit_msg; pub(crate) mod pa12_ask_commit_msg; +pub(crate) mod pa13_pull_push; diff --git a/src/workflows/default/prompt/pa07_ask_pull_push.rs b/src/workflows/default/prompt/pa07_ask_pull_push.rs index 72a5455..eca2dd2 100644 --- a/src/workflows/default/prompt/pa07_ask_pull_push.rs +++ b/src/workflows/default/prompt/pa07_ask_pull_push.rs @@ -1,11 +1,11 @@ use crate::config::{StepFlags, WorkflowRules}; -use crate::workflows::default::action::ta09_pull_push::PullAndPush; +use crate::step::Task::PromptStepTask; +use crate::workflows::default::prompt::pa13_pull_push::PullAndPush; use crate::{ bgit_error::{BGitError, BGitErrorWorkflowType, NO_EVENT, NO_RULE}, - step::{ActionStep, PromptStep, Step, Task::ActionStepTask}, + step::{PromptStep, Step}, }; use dialoguer::{Select, theme::ColorfulTheme}; - pub(crate) struct AskPushPull { name: String, } @@ -46,7 +46,7 @@ impl PromptStep for AskPushPull { })?; match selection { - 0 => Ok(Step::Task(ActionStepTask(Box::new(PullAndPush::new())))), + 0 => Ok(Step::Task(PromptStepTask(Box::new(PullAndPush::new())))), 1 => Ok(Step::Stop), _ => Err(Box::new(BGitError::new( "Invalid selection", diff --git a/src/workflows/default/action/ta09_pull_push.rs b/src/workflows/default/prompt/pa13_pull_push.rs similarity index 67% rename from src/workflows/default/action/ta09_pull_push.rs rename to src/workflows/default/prompt/pa13_pull_push.rs index 58290ea..526745b 100644 --- a/src/workflows/default/action/ta09_pull_push.rs +++ b/src/workflows/default/prompt/pa13_pull_push.rs @@ -5,16 +5,14 @@ use crate::events::git_push::GitPush; use crate::rules::Rule; use crate::rules::a14_big_repo_size::IsRepoSizeTooBig; -use crate::{ - bgit_error::BGitError, - step::{ActionStep, Step}, -}; +use crate::step::PromptStep; +use crate::{bgit_error::BGitError, step::Step}; pub(crate) struct PullAndPush { name: String, } -impl ActionStep for PullAndPush { +impl PromptStep for PullAndPush { fn new() -> Self where Self: Sized, @@ -33,27 +31,18 @@ impl ActionStep for PullAndPush { _step_config_flags: Option<&StepFlags>, workflow_rules_config: Option<&WorkflowRules>, ) -> Result> { - // Create GitPull instance with rebase flag enabled let git_pull = GitPull::new().with_rebase(true); - // Execute pull with rebase match git_pull.execute() { Ok(_) => { - // Pull successful, now attempt push let mut git_push = GitPush::new(); git_push.add_pre_check_rule(Box::new(IsRepoSizeTooBig::new(workflow_rules_config))); - // Configure push options - you can customize these as needed - git_push - .with_force_with_lease(false) - .with_upstream_flag(false); + git_push.set_force(false).set_upstream_flag(false); match git_push.execute() { - Ok(_) => { - // Both pull and push successful - Ok(Step::Stop) - } + Ok(_) => Ok(Step::Stop), Err(e) => { // Push failed, return error Err(e) From 21c97601a2cf1b0bb41e54e85241895077a43c5e Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Fri, 13 Jun 2025 07:01:28 +0530 Subject: [PATCH 4/9] refactor(git_clone): use auth logic from utils/git_auth --- src/events/git_clone.rs | 136 +--------------------------------------- 1 file changed, 2 insertions(+), 134 deletions(-) diff --git a/src/events/git_clone.rs b/src/events/git_clone.rs index 4c8357f..5e33999 100644 --- a/src/events/git_clone.rs +++ b/src/events/git_clone.rs @@ -1,7 +1,7 @@ use super::AtomicEvent; use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use git2::{Cred, CredentialType}; +use crate::utils::git_auth::setup_auth_callbacks; use std::env; use std::path::Path; @@ -122,142 +122,10 @@ impl GitClone { } } - /// Set up authentication callbacks for git operations - fn setup_auth_callbacks() -> git2::RemoteCallbacks<'static> { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = Arc::new(AtomicUsize::new(0)); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); - // Limit authentication attempts to prevent infinite loops - if current_attempt > 3 { - return Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - "Maximum authentication attempts exceeded", - )); - } - - // If SSH key authentication is allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - // Try SSH agent first (most common and secure) - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH agent failed: {}", e); - } - } - - // Try to find SSH keys in standard locations - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - - let ssh_dir = Path::new(&home_dir).join(".ssh"); - - // Common SSH key file names in order of preference - let key_files = [ - ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - ("id_ecdsa", "id_ecdsa.pub"), - ("id_dsa", "id_dsa.pub"), - ]; - - for (private_name, public_name) in &key_files { - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); - - if private_key.exists() { - // Try with public key if it exists - if public_key.exists() { - match Cred::ssh_key(username, Some(&public_key), &private_key, None) - { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key with public key failed: {}", e); - } - } - } - - // Try without public key - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key without public key failed: {}", e); - } - } - } - } - } else { - eprintln!("No username provided for SSH authentication"); - } - } - - // If username/password authentication is allowed (HTTPS) - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - // Try to get credentials from git config or environment - if let (Ok(username), Ok(password)) = - (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) - { - return Cred::userpass_plaintext(&username, &password); - } - - // For GitHub, you might want to use a personal access token - if url.contains("github.com") { - if let Ok(token) = std::env::var("GITHUB_TOKEN") { - return Cred::userpass_plaintext("git", &token); - } - } - } - - // Default authentication (tries default SSH key) - if allowed_types.contains(CredentialType::DEFAULT) { - match Cred::default() { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("Default authentication failed: {}", e); - } - } - } - - Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - format!( - "Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types - ), - )) - }); - - // Set up certificate check callback for HTTPS - callbacks.certificate_check(|_cert, _host| { - // In production, you should properly validate certificates - // For now, we'll accept all certificates (not recommended for production) - Ok(git2::CertificateCheckStatus::CertificateOk) - }); - - callbacks - } - /// Create fetch options with authentication fn create_fetch_options() -> git2::FetchOptions<'static> { let mut fetch_options = git2::FetchOptions::new(); - fetch_options.remote_callbacks(Self::setup_auth_callbacks()); + fetch_options.remote_callbacks(setup_auth_callbacks()); fetch_options } } From 02cb8f73d3146fdfa9c992ad99a18222921c00e7 Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Thu, 19 Jun 2025 21:30:05 +0530 Subject: [PATCH 5/9] fix: spawn ssh_agent instead of trying files manually --- src/utils/git_auth.rs | 282 +++++++++++++++++++++++++----------------- 1 file changed, 171 insertions(+), 111 deletions(-) diff --git a/src/utils/git_auth.rs b/src/utils/git_auth.rs index 666fb58..a739948 100644 --- a/src/utils/git_auth.rs +++ b/src/utils/git_auth.rs @@ -4,130 +4,208 @@ use git2::{ CertificateCheckStatus, Cred, CredentialType, Error, ErrorClass, ErrorCode, RemoteCallbacks, }; use log::debug; +use std::collections::HashMap; use std::path::Path; +use std::process::Command; use std::sync::{Arc, Mutex}; -fn try_ssh_agent_auth(username: &str) -> Result { - debug!("Attempting SSH agent authentication for user: {}", username); +fn parse_ssh_agent_output(output: &str) -> HashMap { + let mut env_vars = HashMap::new(); - if std::env::var("SSH_AUTH_SOCK").is_err() { - debug!("SSH_AUTH_SOCK not set, skipping ssh_key_from_agent"); + for line in output.lines() { + if line.contains('=') && (line.contains("SSH_AUTH_SOCK") || line.contains("SSH_AGENT_PID")) + { + if let Some(var_part) = line.split(';').next() { + if let Some((key, value)) = var_part.split_once('=') { + env_vars.insert(key.to_string(), value.to_string()); + } + } + } + } + + env_vars +} + +fn spawn_ssh_agent_and_add_keys() -> Result<(), Error> { + debug!("SSH_AUTH_SOCK not set, spawning ssh-agent"); + + let output = Command::new("ssh-agent").arg("-s").output().map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to spawn ssh-agent: {}", e), + ) + })?; + + if !output.status.success() { return Err(Error::new( ErrorCode::Auth, ErrorClass::Net, - "SSH_AUTH_SOCK not available", + format!("ssh-agent failed with status: {}", output.status), )); } - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - debug!("SSH agent authentication succeeded"); - Ok(cred) - } - Err(e) => { - debug!("SSH agent authentication failed: {}", e); - Err(e) + let agent_output = String::from_utf8_lossy(&output.stdout); + debug!("ssh-agent output: {}", agent_output); + + let env_vars = parse_ssh_agent_output(&agent_output); + + for (key, value) in &env_vars { + unsafe { + std::env::set_var(key, value); } + debug!("Set environment variable: {}={}", key, value); + } + + if env_vars.get("SSH_AUTH_SOCK").is_none() { + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Failed to parse SSH_AUTH_SOCK from ssh-agent output", + )); } + + add_all_ssh_keys()?; + + Ok(()) } -fn try_ssh_key_files( - username: &str, - key_index: usize, - use_public_key: bool, -) -> Result { - debug!( - "Attempting SSH key file authentication for user: {}, key_index: {}, use_public_key: {}", - username, key_index, use_public_key - ); +fn add_all_ssh_keys() -> Result<(), Error> { + debug!("Adding all SSH keys from .ssh folder to ssh-agent"); let home_dir = std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) .unwrap_or_else(|_| ".".to_string()); - debug!("Home directory resolved to: {}", home_dir); let ssh_dir = Path::new(&home_dir).join(".ssh"); - debug!("Checking .ssh directory at: {:?}", ssh_dir); - - // Common SSH key file names in order of preference - let key_files = [ - ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - ("id_ecdsa", "id_ecdsa.pub"), - ("id_dsa", "id_dsa.pub"), - ]; - - if key_index >= key_files.len() { - debug!("Key index {} out of range", key_index); - return Err(Error::new( - ErrorCode::Auth, - ErrorClass::Net, - "All SSH key files exhausted", - )); + + if !ssh_dir.exists() { + debug!("SSH directory {:?} does not exist", ssh_dir); + return Ok(()); // Not an error, just no keys to add } - let (private_name, public_name) = key_files[key_index]; - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); + let key_files = ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]; - if !private_key.exists() { - debug!("Private key not found: {:?}", private_key); - return Err(Error::new( - ErrorCode::Auth, - ErrorClass::Net, - format!("Private key not found: {}", private_name), - )); - } + let mut added_count = 0; - debug!("Found private key: {:?}", private_key); + for key_name in &key_files { + let key_path = ssh_dir.join(key_name); - if use_public_key { - if public_key.exists() { - debug!("Found public key: {:?}, trying with public key", public_key); - match Cred::ssh_key(username, Some(&public_key), &private_key, None) { - Ok(cred) => { - debug!( - "SSH key auth with public key succeeded for {}", - private_name - ); - Ok(cred) + if key_path.exists() { + debug!("Found SSH key: {:?}", key_path); + + let output = Command::new("ssh-add") + .arg(&key_path) + .env( + "SSH_AUTH_SOCK", + std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), + ) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + debug!("Successfully added key: {}", key_name); + added_count += 1; + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + debug!("Failed to add key {}: {}", key_name, stderr); + + // If it's a passphrase-protected key, we might need to handle it differently + if stderr.contains("Bad passphrase") + || stderr.contains("incorrect passphrase") + { + debug!( + "Key {} appears to be passphrase-protected, skipping automatic addition", + key_name + ); + } + } } Err(e) => { - debug!("SSH key with public key failed for {}: {}", private_name, e); - Err(e) + debug!("Error running ssh-add for {}: {}", key_name, e); } } } else { - debug!( - "Public key not found for {}, skipping this attempt", - private_name - ); - Err(Error::new( - ErrorCode::Auth, - ErrorClass::Net, - format!("Public key not found for {}", private_name), - )) + debug!("SSH key not found: {:?}", key_path); } - } else { - debug!("Trying SSH key without public key for {}", private_name); - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - debug!( - "SSH key auth without public key succeeded for {}", - private_name - ); - Ok(cred) - } - Err(e) => { - debug!( - "SSH key without public key failed for {}: {}", - private_name, e - ); - Err(e) + } + + debug!("Added {} SSH keys to ssh-agent", added_count); + + // Don't fail if no keys were added - they might be passphrase-protected + // or the user might authenticate differently + if added_count == 0 { + debug!("No SSH keys were automatically added, but this might be expected"); + } + + Ok(()) +} + +fn try_ssh_key_files_directly(username: &str) -> Result { + debug!("Trying SSH key files directly for user: {}", username); + + let home_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + + let ssh_dir = Path::new(&home_dir).join(".ssh"); + let key_files = ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]; + + for key_name in &key_files { + let private_key_path = ssh_dir.join(key_name); + let public_key_path = ssh_dir.join(format!("{}.pub", key_name)); + + if private_key_path.exists() && public_key_path.exists() { + debug!("Trying SSH key pair: {} / {}.pub", key_name, key_name); + + match Cred::ssh_key( + username, + Some(&public_key_path), + &private_key_path, + None, // No passphrase for now + ) { + Ok(cred) => { + debug!("SSH key authentication succeeded with {}", key_name); + return Ok(cred); + } + Err(e) => { + debug!("SSH key authentication failed with {}: {}", key_name, e); + } } } } + + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "No valid SSH key pairs found or all failed authentication", + )) +} + +fn try_ssh_agent_auth(username: &str) -> Result { + debug!("Attempting SSH agent authentication for user: {}", username); + + if std::env::var("SSH_AUTH_SOCK").is_err() { + debug!("SSH_AUTH_SOCK not set, attempting to spawn ssh-agent and add keys"); + spawn_ssh_agent_and_add_keys()?; + } + + match Cred::ssh_key_from_agent(username) { + Ok(cred) => { + debug!("SSH agent authentication succeeded"); + Ok(cred) + } + Err(e) => { + debug!("SSH agent authentication failed: {}", e); + + // Fallback to trying SSH key files directly + debug!("Falling back to direct SSH key file authentication"); + try_ssh_key_files_directly(username) + } + } } + fn try_userpass_authentication(username_from_url: Option<&str>) -> Result { debug!("USER_PASS_PLAINTEXT authentication is allowed, prompting for credentials"); @@ -194,7 +272,7 @@ fn ssh_authenticate_git( debug!("Allowed credential types: {:?}", allowed_types); // Prevent infinite loops - if attempt_count > 20 { + if attempt_count > 3 { debug!( "Too many authentication attempts ({}), failing to prevent infinite loop", attempt_count @@ -209,28 +287,9 @@ fn ssh_authenticate_git( // Try SSH key authentication if allowed if allowed_types.contains(CredentialType::SSH_KEY) { if let Some(username) = username_from_url { - debug!("SSH key authentication is allowed, trying SSH methods"); - - // Try SSH agent first (only on first attempt) - if attempt_count == 1 { - if let Ok(cred) = try_ssh_agent_auth(username) { - return Ok(cred); - } - // If SSH agent fails, fall through to try SSH key files on same attempt - } + debug!("SSH key authentication is allowed, trying SSH agent"); - // Try SSH key files with progression - // Attempt 1+: Start with id_ed25519 if SSH agent failed - // Attempt 1: id_ed25519 with public key - // Attempt 2: id_ed25519 without public key - // Attempt 3: id_rsa with public key - // Attempt 4: id_rsa without public key - // etc. - let key_attempt = attempt_count - 1; - let key_index = key_attempt / 2; - let use_public_key = key_attempt % 2 == 0; - - if let Ok(cred) = try_ssh_key_files(username, key_index, use_public_key) { + if let Ok(cred) = try_ssh_agent_auth(username) { return Ok(cred); } } else { @@ -248,6 +307,7 @@ fn ssh_authenticate_git( format!("Authentication failed - attempt {}", attempt_count), )) } + pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { let mut callbacks = RemoteCallbacks::new(); From 1d241c4b6bc7b9a45899adbbb954c726bbb4f4ee Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Thu, 19 Jun 2025 22:03:12 +0530 Subject: [PATCH 6/9] feat: passphrase support --- src/events/git_push.rs | 1 + src/utils/git_auth.rs | 121 ++++++++++++++---- .../default/prompt/pa13_pull_push.rs | 4 +- 3 files changed, 100 insertions(+), 26 deletions(-) diff --git a/src/events/git_push.rs b/src/events/git_push.rs index a15cc6b..acbe097 100644 --- a/src/events/git_push.rs +++ b/src/events/git_push.rs @@ -3,6 +3,7 @@ use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; use crate::utils::git_auth::setup_auth_callbacks; use git2::Repository; +use log::debug; use std::path::Path; pub struct GitPush { diff --git a/src/utils/git_auth.rs b/src/utils/git_auth.rs index a739948..b847cdc 100644 --- a/src/utils/git_auth.rs +++ b/src/utils/git_auth.rs @@ -1,12 +1,12 @@ use dialoguer::theme::ColorfulTheme; -use dialoguer::{Input, Password}; +use dialoguer::{Confirm, Input, Password}; use git2::{ CertificateCheckStatus, Cred, CredentialType, Error, ErrorClass, ErrorCode, RemoteCallbacks, }; use log::debug; use std::collections::HashMap; use std::path::Path; -use std::process::Command; +use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; fn parse_ssh_agent_output(output: &str) -> HashMap { @@ -57,7 +57,7 @@ fn spawn_ssh_agent_and_add_keys() -> Result<(), Error> { debug!("Set environment variable: {}={}", key, value); } - if env_vars.get("SSH_AUTH_SOCK").is_none() { + if !env_vars.contains_key("SSH_AUTH_SOCK") { return Err(Error::new( ErrorCode::Auth, ErrorClass::Net, @@ -81,11 +81,10 @@ fn add_all_ssh_keys() -> Result<(), Error> { if !ssh_dir.exists() { debug!("SSH directory {:?} does not exist", ssh_dir); - return Ok(()); // Not an error, just no keys to add + return Ok(()); } let key_files = ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]; - let mut added_count = 0; for key_name in &key_files { @@ -94,31 +93,42 @@ fn add_all_ssh_keys() -> Result<(), Error> { if key_path.exists() { debug!("Found SSH key: {:?}", key_path); - let output = Command::new("ssh-add") + // First try a quick non-interactive add (for keys without passphrase) + let quick_result = Command::new("ssh-add") .arg(&key_path) .env( "SSH_AUTH_SOCK", std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), ) + .stdin(Stdio::null()) // No input for quick try + .stdout(Stdio::null()) // Suppress output for quick try + .stderr(Stdio::piped()) // Capture errors to check if passphrase is needed .output(); - match output { + match quick_result { + Ok(output) if output.status.success() => { + debug!("Successfully added key without interaction: {}", key_name); + added_count += 1; + } Ok(output) => { - if output.status.success() { - debug!("Successfully added key: {}", key_name); - added_count += 1; - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - debug!("Failed to add key {}: {}", key_name, stderr); - - // If it's a passphrase-protected key, we might need to handle it differently - if stderr.contains("Bad passphrase") - || stderr.contains("incorrect passphrase") - { - debug!( - "Key {} appears to be passphrase-protected, skipping automatic addition", - key_name - ); + let stderr = String::from_utf8_lossy(&output.stderr); + debug!("Quick add failed for {}: {}", key_name, stderr); + + debug!( + "Key {} appears to need passphrase, trying interactive add", + key_name + ); + + match add_key_interactive(&key_path, key_name) { + Ok(true) => { + debug!("Successfully added key interactively: {}", key_name); + added_count += 1; + } + Ok(false) => { + debug!("User skipped key: {}", key_name); + } + Err(e) => { + debug!("Interactive add failed for {}: {}", key_name, e); } } } @@ -133,14 +143,75 @@ fn add_all_ssh_keys() -> Result<(), Error> { debug!("Added {} SSH keys to ssh-agent", added_count); - // Don't fail if no keys were added - they might be passphrase-protected - // or the user might authenticate differently if added_count == 0 { - debug!("No SSH keys were automatically added, but this might be expected"); + debug!("No SSH keys were added"); + println!("No SSH keys were added to ssh-agent."); + println!("You may need to generate SSH keys or check your ~/.ssh directory."); + } else { + println!( + "Successfully added {} SSH key(s) to ssh-agent.", + added_count + ); } Ok(()) } +fn add_key_interactive(key_path: &Path, key_name: &str) -> Result { + debug!("Trying interactive ssh-add for key: {}", key_name); + + // Ask user if they want to add this key interactively + let should_add = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "Add SSH key '{}' to ssh-agent? (you may be prompted for passphrase)", + key_name + )) + .default(true) + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to get user confirmation: {}", e), + ) + })?; + + if !should_add { + debug!("User chose not to add key: {}", key_name); + return Ok(false); + } + + println!("Adding SSH key: {}", key_name); + println!("If the key is passphrase-protected, you will be prompted to enter it."); + + // Use interactive ssh-add - this will prompt the user directly in the terminal + let status = Command::new("ssh-add") + .arg(key_path) + .env( + "SSH_AUTH_SOCK", + std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), + ) + .stdin(Stdio::inherit()) // Allow user to input passphrase directly + .stdout(Stdio::inherit()) // Show ssh-add output to user + .stderr(Stdio::inherit()) // Show ssh-add errors to user + .status() // Use status() instead of output() to allow real-time interaction + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to spawn ssh-add: {}", e), + ) + })?; + + if status.success() { + debug!("Successfully added key: {}", key_name); + println!("✓ SSH key '{}' added successfully!", key_name); + Ok(true) + } else { + debug!("Interactive ssh-add failed for key: {}", key_name); + println!("✗ Failed to add SSH key '{}'", key_name); + Ok(false) + } +} fn try_ssh_key_files_directly(username: &str) -> Result { debug!("Trying SSH key files directly for user: {}", username); diff --git a/src/workflows/default/prompt/pa13_pull_push.rs b/src/workflows/default/prompt/pa13_pull_push.rs index 526745b..3d3756f 100644 --- a/src/workflows/default/prompt/pa13_pull_push.rs +++ b/src/workflows/default/prompt/pa13_pull_push.rs @@ -39,7 +39,9 @@ impl PromptStep for PullAndPush { git_push.add_pre_check_rule(Box::new(IsRepoSizeTooBig::new(workflow_rules_config))); - git_push.set_force(false).set_upstream_flag(false); + git_push + .with_force_with_lease(false) + .with_upstream_flag(false); match git_push.execute() { Ok(_) => Ok(Step::Stop), From 5535daaac94c26bf17ffb0b410ad1c2c3c6d307c Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Tue, 24 Jun 2025 00:17:44 +0530 Subject: [PATCH 7/9] refactor --- src/auth/git_auth.rs | 33 +++++ src/auth/git_http.rs | 55 +++++++ src/{utils/git_auth.rs => auth/git_ssh.rs} | 163 +-------------------- src/auth/mod.rs | 4 + src/auth/ssh_utils.rs | 83 +++++++++++ src/events/git_clone.rs | 2 +- src/events/git_pull.rs | 2 +- src/events/git_push.rs | 2 +- src/main.rs | 2 +- src/utils/mod.rs | 1 - 10 files changed, 182 insertions(+), 165 deletions(-) create mode 100644 src/auth/git_auth.rs create mode 100644 src/auth/git_http.rs rename src/{utils/git_auth.rs => auth/git_ssh.rs} (59%) create mode 100644 src/auth/mod.rs create mode 100644 src/auth/ssh_utils.rs delete mode 100644 src/utils/mod.rs diff --git a/src/auth/git_auth.rs b/src/auth/git_auth.rs new file mode 100644 index 0000000..648788c --- /dev/null +++ b/src/auth/git_auth.rs @@ -0,0 +1,33 @@ +use git2::{CertificateCheckStatus, CredentialType, RemoteCallbacks}; +use log::debug; +use std::sync::{Arc, Mutex}; + +use crate::auth::{git_http::try_userpass_authentication, git_ssh::ssh_authenticate_git}; + +pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { + let mut callbacks = RemoteCallbacks::new(); + + // Track attempt count across callback invocations + let attempt_count: Arc> = Arc::new(Mutex::new(0)); + + callbacks.credentials(move |url, username_from_url, allowed_types| { + let mut count = attempt_count.lock().unwrap(); + *count += 1; + let current_attempt = *count; + drop(count); + + if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { + try_userpass_authentication(username_from_url) + } else { + ssh_authenticate_git(url, username_from_url, allowed_types, current_attempt) + } + }); + + // Set up certificate check callback for HTTPS + callbacks.certificate_check(|_cert, _host| { + debug!("Skipping certificate verification (INSECURE)"); + Ok(CertificateCheckStatus::CertificateOk) + }); + + callbacks +} diff --git a/src/auth/git_http.rs b/src/auth/git_http.rs new file mode 100644 index 0000000..be536c0 --- /dev/null +++ b/src/auth/git_http.rs @@ -0,0 +1,55 @@ +use dialoguer::{Input, Password, theme::ColorfulTheme}; +use git2::{Cred, Error, ErrorClass, ErrorCode}; +use log::debug; + +pub fn try_userpass_authentication(username_from_url: Option<&str>) -> Result { + debug!("USER_PASS_PLAINTEXT authentication is allowed, prompting for credentials"); + + // Prompt for username if not provided in URL + let username = if let Some(user) = username_from_url { + user.to_string() + } else { + Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your username") + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to read username: {}", e), + ) + })? + }; + + let token = Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your personal access token") + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to read token: {}", e), + ) + })?; + + if !username.is_empty() && !token.is_empty() { + debug!("Creating credentials with username and token"); + match Cred::userpass_plaintext(&username, &token) { + Ok(cred) => { + debug!("Username/token authentication succeeded"); + Ok(cred) + } + Err(e) => { + debug!("Username/token authentication failed: {}", e); + Err(e) + } + } + } else { + debug!("Username or token is empty, skipping userpass authentication"); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Username or token cannot be empty", + )) + } +} diff --git a/src/utils/git_auth.rs b/src/auth/git_ssh.rs similarity index 59% rename from src/utils/git_auth.rs rename to src/auth/git_ssh.rs index b847cdc..d5d2f8a 100644 --- a/src/utils/git_auth.rs +++ b/src/auth/git_ssh.rs @@ -1,30 +1,9 @@ -use dialoguer::theme::ColorfulTheme; -use dialoguer::{Confirm, Input, Password}; -use git2::{ - CertificateCheckStatus, Cred, CredentialType, Error, ErrorClass, ErrorCode, RemoteCallbacks, -}; +use git2::{Cred, CredentialType, Error, ErrorClass, ErrorCode}; use log::debug; -use std::collections::HashMap; use std::path::Path; use std::process::{Command, Stdio}; -use std::sync::{Arc, Mutex}; -fn parse_ssh_agent_output(output: &str) -> HashMap { - let mut env_vars = HashMap::new(); - - for line in output.lines() { - if line.contains('=') && (line.contains("SSH_AUTH_SOCK") || line.contains("SSH_AGENT_PID")) - { - if let Some(var_part) = line.split(';').next() { - if let Some((key, value)) = var_part.split_once('=') { - env_vars.insert(key.to_string(), value.to_string()); - } - } - } - } - - env_vars -} +use crate::auth::ssh_utils::{add_key_interactive, parse_ssh_agent_output}; fn spawn_ssh_agent_and_add_keys() -> Result<(), Error> { debug!("SSH_AUTH_SOCK not set, spawning ssh-agent"); @@ -156,62 +135,6 @@ fn add_all_ssh_keys() -> Result<(), Error> { Ok(()) } -fn add_key_interactive(key_path: &Path, key_name: &str) -> Result { - debug!("Trying interactive ssh-add for key: {}", key_name); - - // Ask user if they want to add this key interactively - let should_add = Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt(format!( - "Add SSH key '{}' to ssh-agent? (you may be prompted for passphrase)", - key_name - )) - .default(true) - .interact() - .map_err(|e| { - Error::new( - ErrorCode::Auth, - ErrorClass::Net, - format!("Failed to get user confirmation: {}", e), - ) - })?; - - if !should_add { - debug!("User chose not to add key: {}", key_name); - return Ok(false); - } - - println!("Adding SSH key: {}", key_name); - println!("If the key is passphrase-protected, you will be prompted to enter it."); - - // Use interactive ssh-add - this will prompt the user directly in the terminal - let status = Command::new("ssh-add") - .arg(key_path) - .env( - "SSH_AUTH_SOCK", - std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), - ) - .stdin(Stdio::inherit()) // Allow user to input passphrase directly - .stdout(Stdio::inherit()) // Show ssh-add output to user - .stderr(Stdio::inherit()) // Show ssh-add errors to user - .status() // Use status() instead of output() to allow real-time interaction - .map_err(|e| { - Error::new( - ErrorCode::Auth, - ErrorClass::Net, - format!("Failed to spawn ssh-add: {}", e), - ) - })?; - - if status.success() { - debug!("Successfully added key: {}", key_name); - println!("✓ SSH key '{}' added successfully!", key_name); - Ok(true) - } else { - debug!("Interactive ssh-add failed for key: {}", key_name); - println!("✗ Failed to add SSH key '{}'", key_name); - Ok(false) - } -} fn try_ssh_key_files_directly(username: &str) -> Result { debug!("Trying SSH key files directly for user: {}", username); @@ -277,59 +200,7 @@ fn try_ssh_agent_auth(username: &str) -> Result { } } -fn try_userpass_authentication(username_from_url: Option<&str>) -> Result { - debug!("USER_PASS_PLAINTEXT authentication is allowed, prompting for credentials"); - - // Prompt for username if not provided in URL - let username = if let Some(user) = username_from_url { - user.to_string() - } else { - Input::::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your username") - .interact() - .map_err(|e| { - Error::new( - ErrorCode::Auth, - ErrorClass::Net, - format!("Failed to read username: {}", e), - ) - })? - }; - - let token = Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your personal access token") - .interact() - .map_err(|e| { - Error::new( - ErrorCode::Auth, - ErrorClass::Net, - format!("Failed to read token: {}", e), - ) - })?; - - if !username.is_empty() && !token.is_empty() { - debug!("Creating credentials with username and token"); - match Cred::userpass_plaintext(&username, &token) { - Ok(cred) => { - debug!("Username/token authentication succeeded"); - Ok(cred) - } - Err(e) => { - debug!("Username/token authentication failed: {}", e); - Err(e) - } - } - } else { - debug!("Username or token is empty, skipping userpass authentication"); - Err(Error::new( - ErrorCode::Auth, - ErrorClass::Net, - "Username or token cannot be empty", - )) - } -} - -fn ssh_authenticate_git( +pub fn ssh_authenticate_git( url: &str, username_from_url: Option<&str>, allowed_types: CredentialType, @@ -378,31 +249,3 @@ fn ssh_authenticate_git( format!("Authentication failed - attempt {}", attempt_count), )) } - -pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { - let mut callbacks = RemoteCallbacks::new(); - - // Track attempt count across callback invocations - let attempt_count: Arc> = Arc::new(Mutex::new(0)); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let mut count = attempt_count.lock().unwrap(); - *count += 1; - let current_attempt = *count; - drop(count); - - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - try_userpass_authentication(username_from_url) - } else { - ssh_authenticate_git(url, username_from_url, allowed_types, current_attempt) - } - }); - - // Set up certificate check callback for HTTPS - callbacks.certificate_check(|_cert, _host| { - debug!("Skipping certificate verification (INSECURE)"); - Ok(CertificateCheckStatus::CertificateOk) - }); - - callbacks -} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..6ec4ab9 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,4 @@ +pub mod git_auth; +mod git_http; +mod git_ssh; +mod ssh_utils; diff --git a/src/auth/ssh_utils.rs b/src/auth/ssh_utils.rs new file mode 100644 index 0000000..d95573d --- /dev/null +++ b/src/auth/ssh_utils.rs @@ -0,0 +1,83 @@ +use std::{ + collections::HashMap, + path::Path, + process::{Command, Stdio}, +}; + +use dialoguer::{Confirm, theme::ColorfulTheme}; +use git2::{Error, ErrorClass, ErrorCode}; +use log::debug; + +pub fn parse_ssh_agent_output(output: &str) -> HashMap { + let mut env_vars = HashMap::new(); + + for line in output.lines() { + if line.contains('=') && (line.contains("SSH_AUTH_SOCK") || line.contains("SSH_AGENT_PID")) + { + if let Some(var_part) = line.split(';').next() { + if let Some((key, value)) = var_part.split_once('=') { + env_vars.insert(key.to_string(), value.to_string()); + } + } + } + } + + env_vars +} + +pub fn add_key_interactive(key_path: &Path, key_name: &str) -> Result { + debug!("Trying interactive ssh-add for key: {}", key_name); + + // Ask user if they want to add this key interactively + let should_add = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "Add SSH key '{}' to ssh-agent? (you may be prompted for passphrase)", + key_name + )) + .default(true) + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to get user confirmation: {}", e), + ) + })?; + + if !should_add { + debug!("User chose not to add key: {}", key_name); + return Ok(false); + } + + println!("Adding SSH key: {}", key_name); + println!("If the key is passphrase-protected, you will be prompted to enter it."); + + // Use interactive ssh-add - this will prompt the user directly in the terminal + let status = Command::new("ssh-add") + .arg(key_path) + .env( + "SSH_AUTH_SOCK", + std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), + ) + .stdin(Stdio::inherit()) // Allow user to input passphrase directly + .stdout(Stdio::inherit()) // Show ssh-add output to user + .stderr(Stdio::inherit()) // Show ssh-add errors to user + .status() // Use status() instead of output() to allow real-time interaction + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to spawn ssh-add: {}", e), + ) + })?; + + if status.success() { + debug!("Successfully added key: {}", key_name); + println!("✓ SSH key '{}' added successfully!", key_name); + Ok(true) + } else { + debug!("Interactive ssh-add failed for key: {}", key_name); + println!("✗ Failed to add SSH key '{}'", key_name); + Ok(false) + } +} diff --git a/src/events/git_clone.rs b/src/events/git_clone.rs index 5e33999..8ca14ea 100644 --- a/src/events/git_clone.rs +++ b/src/events/git_clone.rs @@ -1,7 +1,7 @@ use super::AtomicEvent; +use crate::auth::git_auth::setup_auth_callbacks; use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use crate::utils::git_auth::setup_auth_callbacks; use std::env; use std::path::Path; diff --git a/src/events/git_pull.rs b/src/events/git_pull.rs index 731464c..7c7025f 100644 --- a/src/events/git_pull.rs +++ b/src/events/git_pull.rs @@ -1,9 +1,9 @@ use std::path::Path; use super::AtomicEvent; +use crate::auth::git_auth::setup_auth_callbacks; use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use crate::utils::git_auth::setup_auth_callbacks; use git2::Repository; pub struct GitPull { diff --git a/src/events/git_push.rs b/src/events/git_push.rs index acbe097..498fa8c 100644 --- a/src/events/git_push.rs +++ b/src/events/git_push.rs @@ -1,7 +1,7 @@ use super::AtomicEvent; +use crate::auth::git_auth::setup_auth_callbacks; use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use crate::utils::git_auth::setup_auth_callbacks; use git2::Repository; use log::debug; use std::path::Path; diff --git a/src/main.rs b/src/main.rs index 1a0a525..8ed3c9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use crate::cmd::log::log; use crate::cmd::{Cli, Commands}; use crate::config::BGitConfig; +mod auth; mod bgit_error; mod cmd; mod config; @@ -15,7 +16,6 @@ mod hook_executor; mod rules; mod step; mod util; -mod utils; mod workflow_queue; mod workflows; diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index 75d5ade..0000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod git_auth; From f5f395cc71bdd19d6b2405cc4459a50d77cd719b Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Tue, 24 Jun 2025 13:52:39 +0530 Subject: [PATCH 8/9] fix: auth fail when ssh-agent empty --- src/auth/git_ssh.rs | 158 ++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 73 deletions(-) diff --git a/src/auth/git_ssh.rs b/src/auth/git_ssh.rs index d5d2f8a..136cb59 100644 --- a/src/auth/git_ssh.rs +++ b/src/auth/git_ssh.rs @@ -5,6 +5,91 @@ use std::process::{Command, Stdio}; use crate::auth::ssh_utils::{add_key_interactive, parse_ssh_agent_output}; +pub fn ssh_authenticate_git( + url: &str, + username_from_url: Option<&str>, + allowed_types: CredentialType, + attempt_count: usize, +) -> Result { + debug!( + "Git authentication attempt #{} for URL: {}", + attempt_count, url + ); + debug!("Username from URL: {:?}", username_from_url); + debug!("Allowed credential types: {:?}", allowed_types); + + // Prevent infinite loops + if attempt_count > 3 { + debug!( + "Too many authentication attempts ({}), failing to prevent infinite loop", + attempt_count + ); + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Too many authentication attempts", + )); + } + + if allowed_types.contains(CredentialType::SSH_KEY) { + if let Some(username) = username_from_url { + debug!("SSH key authentication is allowed, trying SSH agent"); + + // handling the case where ssh-agent is running but empty + if attempt_count == 2 { + debug!("Second attempt: trying to add SSH keys to agent before authentication"); + if std::env::var("SSH_AUTH_SOCK").is_ok() { + if let Err(e) = add_all_ssh_keys() { + debug!("Failed to add keys to ssh-agent on second attempt: {}", e); + } else { + debug!("Keys added to ssh-agent, proceeding with authentication"); + } + } + } + + if let Ok(cred) = try_ssh_agent_auth(username) { + return Ok(cred); + } + } else { + debug!("No username provided for SSH authentication"); + } + } + + debug!( + "All authentication methods failed for attempt {}", + attempt_count + ); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Authentication failed - attempt {}", attempt_count), + )) +} + +fn try_ssh_agent_auth(username: &str) -> Result { + debug!("Attempting SSH agent authentication for user: {}", username); + + if std::env::var("SSH_AUTH_SOCK").is_err() { + debug!("SSH_AUTH_SOCK not set, attempting to spawn ssh-agent and add keys"); + spawn_ssh_agent_and_add_keys()?; + } + + match Cred::ssh_key_from_agent(username) { + Ok(cred) => { + debug!("SSH agent authentication succeeded"); + + Ok(cred) + } + Err(e) => { + debug!("SSH agent authentication failed: {}", e); + + // Fallback to trying SSH key files directly + debug!("Falling back to direct SSH key file authentication"); + try_ssh_key_files_directly(username) + } + } +} + fn spawn_ssh_agent_and_add_keys() -> Result<(), Error> { debug!("SSH_AUTH_SOCK not set, spawning ssh-agent"); @@ -176,76 +261,3 @@ fn try_ssh_key_files_directly(username: &str) -> Result { "No valid SSH key pairs found or all failed authentication", )) } - -fn try_ssh_agent_auth(username: &str) -> Result { - debug!("Attempting SSH agent authentication for user: {}", username); - - if std::env::var("SSH_AUTH_SOCK").is_err() { - debug!("SSH_AUTH_SOCK not set, attempting to spawn ssh-agent and add keys"); - spawn_ssh_agent_and_add_keys()?; - } - - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - debug!("SSH agent authentication succeeded"); - Ok(cred) - } - Err(e) => { - debug!("SSH agent authentication failed: {}", e); - - // Fallback to trying SSH key files directly - debug!("Falling back to direct SSH key file authentication"); - try_ssh_key_files_directly(username) - } - } -} - -pub fn ssh_authenticate_git( - url: &str, - username_from_url: Option<&str>, - allowed_types: CredentialType, - attempt_count: usize, -) -> Result { - debug!( - "Git authentication attempt #{} for URL: {}", - attempt_count, url - ); - debug!("Username from URL: {:?}", username_from_url); - debug!("Allowed credential types: {:?}", allowed_types); - - // Prevent infinite loops - if attempt_count > 3 { - debug!( - "Too many authentication attempts ({}), failing to prevent infinite loop", - attempt_count - ); - return Err(Error::new( - ErrorCode::Auth, - ErrorClass::Net, - "Too many authentication attempts", - )); - } - - // Try SSH key authentication if allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - debug!("SSH key authentication is allowed, trying SSH agent"); - - if let Ok(cred) = try_ssh_agent_auth(username) { - return Ok(cred); - } - } else { - debug!("No username provided for SSH authentication"); - } - } - - debug!( - "All authentication methods failed for attempt {}", - attempt_count - ); - Err(Error::new( - ErrorCode::Auth, - ErrorClass::Net, - format!("Authentication failed - attempt {}", attempt_count), - )) -} From c4a526bbcafff4e891ae90165625bd6517fa94aa Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Thu, 26 Jun 2025 15:07:37 +0530 Subject: [PATCH 9/9] feat: create credentials command --- src/auth/create/mod.rs | 1 + src/auth/create/ssh.rs | 299 ++++++++++++++++++++++++++++++++++++++++ src/auth/mod.rs | 1 + src/cmd.rs | 3 + src/cmd/create_creds.rs | 6 + src/main.rs | 2 + 6 files changed, 312 insertions(+) create mode 100644 src/auth/create/mod.rs create mode 100644 src/auth/create/ssh.rs create mode 100644 src/cmd/create_creds.rs diff --git a/src/auth/create/mod.rs b/src/auth/create/mod.rs new file mode 100644 index 0000000..aa086fd --- /dev/null +++ b/src/auth/create/mod.rs @@ -0,0 +1 @@ +pub mod ssh; diff --git a/src/auth/create/ssh.rs b/src/auth/create/ssh.rs new file mode 100644 index 0000000..4bb0846 --- /dev/null +++ b/src/auth/create/ssh.rs @@ -0,0 +1,299 @@ +use dialoguer::{Confirm, Input, Select}; +use std::fs; +use std::path::Path; +use std::process::Command; + +pub fn setup_ssh_auth() { + println!("🔐 SSH Authentication Setup"); + println!("Setting up SSH authentication for Git operations...\n"); + + let home_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + let ssh_dir = Path::new(&home_dir).join(".ssh"); + + // Create .ssh directory if it doesn't exist + if !ssh_dir.exists() { + println!("📁 Creating .ssh directory..."); + if let Err(e) = fs::create_dir_all(&ssh_dir) { + eprintln!("❌ Failed to create .ssh directory: {}", e); + return; + } + println!("✅ .ssh directory created successfully"); + } + + // Check for existing SSH keys + let key_types = [ + ("id_ed25519", "Ed25519 (recommended)"), + ("id_rsa", "RSA"), + ("id_ecdsa", "ECDSA"), + ]; + + let mut existing_keys = Vec::new(); + for (key_name, key_type) in &key_types { + let private_key = ssh_dir.join(key_name); + let public_key = ssh_dir.join(format!("{}.pub", key_name)); + + if private_key.exists() && public_key.exists() { + // Read the public key to extract identity + let identity = match fs::read_to_string(&public_key) { + Ok(content) => { + // Extract the comment/email part (last part after the key data) + let parts: Vec<&str> = content.split_whitespace().collect(); + if parts.len() >= 3 { + parts[2..].join(" ") + } else { + "No identity found".to_string() + } + } + Err(_) => "Could not read key".to_string(), + }; + + existing_keys.push((*key_name, *key_type, public_key, identity)); + } + } + + if !existing_keys.is_empty() { + println!("🔍 Found existing SSH keys:"); + for (i, (key_name, key_type, _, identity)) in existing_keys.iter().enumerate() { + println!(" {}. {} ({}) - {}", i + 1, key_name, key_type, identity); + } + + let options = vec!["Use existing key", "Generate new key", "Exit"]; + match Select::new() + .with_prompt("Choose an option") + .default(0) + .items(&options) + .interact() + { + Ok(0) => { + if existing_keys.len() == 1 { + display_public_key_and_guide(&existing_keys[0].2); + } else { + select_existing_key(&existing_keys); + } + return; + } + Ok(1) => { + // Continue to generate new key + } + Ok(2) | Err(_) => { + println!("Setup cancelled."); + return; + } + _ => unreachable!(), + } + } + + // Generate new SSH key + generate_new_ssh_key(&ssh_dir); +} + +fn select_existing_key(existing_keys: &[(&str, &str, std::path::PathBuf, String)]) { + let key_options: Vec = existing_keys + .iter() + .map(|(key_name, key_type, _, identity)| { + format!("{} ({}) - {}", key_name, key_type, identity) + }) + .collect(); + + match Select::new() + .with_prompt("Select which key to use") + .default(0) + .items(&key_options) + .interact() + { + Ok(choice) => { + display_public_key_and_guide(&existing_keys[choice].2); + } + Err(_) => { + println!("Selection cancelled."); + } + } +} + +fn generate_new_ssh_key(ssh_dir: &Path) { + println!("\n🔑 Generating new SSH key..."); + + // Get user email + let email = match Input::::new() + .with_prompt("Enter your email address") + .validate_with(|input: &String| -> Result<(), &str> { + if input.trim().is_empty() { + Err("Email cannot be empty") + } else if !input.contains('@') { + Err("Please enter a valid email address") + } else { + Ok(()) + } + }) + .interact() + { + Ok(email) => email.trim().to_string(), + Err(_) => { + println!("Email input cancelled. Exiting."); + return; + } + }; + + // Choose key type + let key_types = vec![ + "Ed25519 (recommended, modern and secure)", + "RSA 4096 (widely compatible)", + ]; + + let key_choice = match Select::new() + .with_prompt("Choose SSH key type") + .default(0) + .items(&key_types) + .interact() + { + Ok(choice) => choice, + Err(_) => { + println!("Key type selection cancelled. Exiting."); + return; + } + }; + + let (key_type, key_name, ssh_keygen_args) = match key_choice { + 1 => ("RSA", "id_rsa", vec!["-t", "rsa", "-b", "4096"]), + _ => ("Ed25519", "id_ed25519", vec!["-t", "ed25519"]), + }; + + println!("\n🔧 Generating {} key...", key_type); + let key_path = ssh_dir.join(key_name); + + // Build ssh-keygen command + let mut cmd = Command::new("ssh-keygen"); + cmd.args(&ssh_keygen_args) + .arg("-C") + .arg(&email) + .arg("-f") + .arg(&key_path) + .arg("-N") + .arg(""); // Empty passphrase for simplicity + + match cmd.status() { + Ok(status) if status.success() => { + println!("✅ SSH key generated successfully!"); + // Add to ssh-agent + add_key_to_agent(&key_path); + // Display public key and guide + let public_key_path = ssh_dir.join(format!("{}.pub", key_name)); + display_public_key_and_guide(&public_key_path); + } + Ok(status) => { + eprintln!("❌ ssh-keygen failed with status: {}", status); + } + Err(e) => { + eprintln!("❌ Failed to run ssh-keygen: {}", e); + eprintln!("Make sure OpenSSH is installed on your system."); + } + } +} + +fn add_key_to_agent(key_path: &Path) { + println!("\n🔧 Adding key to ssh-agent..."); + + // Check if ssh-agent is running + if std::env::var("SSH_AUTH_SOCK").is_err() { + println!("SSH agent is not running. Starting ssh-agent..."); + // Try to start ssh-agent + match Command::new("ssh-agent").arg("-s").output() { + Ok(output) if output.status.success() => { + let agent_output = String::from_utf8_lossy(&output.stdout); + println!("SSH agent started. You may need to run the following commands:"); + println!("{}", agent_output.trim()); + } + _ => { + println!("⚠️ Could not start ssh-agent automatically."); + println!("You may need to start it manually with: eval $(ssh-agent -s)"); + } + } + } + + // Add key to agent + match Command::new("ssh-add").arg(key_path).status() { + Ok(status) if status.success() => { + println!("✅ Key added to ssh-agent successfully!"); + } + Ok(_) => { + println!("⚠️ Failed to add key to ssh-agent"); + println!( + "💡 You can add it manually with: ssh-add {}", + key_path.display() + ); + } + Err(_) => { + println!("⚠️ ssh-add not available"); + println!( + "💡 You can add it manually later with: ssh-add {}", + key_path.display() + ); + } + } +} + +fn display_public_key_and_guide(public_key_path: &Path) { + println!("\n📋 Your SSH Public Key:"); + println!("{}", "─".repeat(60)); + + match fs::read_to_string(public_key_path) { + Ok(public_key) => { + println!("{}", public_key.trim()); + println!("{}", "─".repeat(60)); + + println!("\n🚀 Next Steps:"); + println!("\n1. Copy the public key above (it's already selected for you)"); + println!("\n2. Add it to your GitHub account:"); + println!(" • Go to: https://github.com/settings/ssh/new"); + println!(" • Or navigate to: GitHub → Settings → SSH and GPG keys → New SSH key"); + println!("\n3. Fill in the form:"); + println!(" • Title: Give it a descriptive name (e.g., 'My Laptop - bgit')"); + println!(" • Key type: Authentication Key"); + println!(" • Key: Paste the public key from above"); + println!("\n4. Click 'Add SSH key' and enter your GitHub password if prompted"); + println!("\n5. Test your connection:"); + println!(" ssh -T git@github.com"); + println!( + "\n🎉 You're all set! Your bgit tool can now authenticate with GitHub using SSH." + ); + + // Offer to open GitHub in browser + if Confirm::new() + .with_prompt("Would you like to open GitHub SSH settings in your default browser?") + .default(false) + .interact() + .unwrap_or(false) + { + open_github_ssh_settings(); + } + } + Err(e) => { + eprintln!("❌ Failed to read public key file: {}", e); + } + } +} + +fn open_github_ssh_settings() { + let url = "https://github.com/settings/ssh/new"; + + #[cfg(target_os = "windows")] + let cmd = Command::new("cmd").args(["/c", "start", url]).status(); + + #[cfg(target_os = "macos")] + let cmd = Command::new("open").arg(url).status(); + + #[cfg(target_os = "linux")] + let cmd = Command::new("xdg-open").arg(url).status(); + + match cmd { + Ok(status) if status.success() => { + println!("🌐 Opening GitHub SSH settings in your browser..."); + } + _ => { + println!("⚠️ Could not open browser automatically."); + println!("🔗 Please visit: {}", url); + } + } +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 6ec4ab9..226c0ed 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,3 +1,4 @@ +pub mod create; pub mod git_auth; mod git_http; mod git_ssh; diff --git a/src/cmd.rs b/src/cmd.rs index 9da0a48..89bcdce 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,4 +1,5 @@ pub(crate) mod check; +pub(crate) mod create_creds; pub(crate) mod default; pub(crate) mod init; pub(crate) mod log; @@ -35,6 +36,8 @@ pub enum Commands { /// Do maintenance tasks Check, + #[command(name = "create-creds")] + CreateCreds, } fn print_completions(generator: G, cmd: &mut Command) { diff --git a/src/cmd/create_creds.rs b/src/cmd/create_creds.rs new file mode 100644 index 0000000..b275b73 --- /dev/null +++ b/src/cmd/create_creds.rs @@ -0,0 +1,6 @@ +use crate::auth::create::ssh::setup_ssh_auth; +use crate::config::BGitConfig; + +pub fn create_creds(_config: BGitConfig) { + setup_ssh_auth(); +} diff --git a/src/main.rs b/src/main.rs index 8ed3c9f..765ad4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::cmd::check::check; +use crate::cmd::create_creds::create_creds; use crate::cmd::default::default_cmd_workflow; use crate::cmd::init::init; use crate::cmd::log::log; @@ -43,6 +44,7 @@ fn main() { Some(Commands::Log) => log(bgit_config), Some(Commands::Init) => init(bgit_config), Some(Commands::Check) => check(bgit_config), + Some(Commands::CreateCreds) => create_creds(bgit_config), None => default_cmd_workflow(bgit_config), } }