From d4d85c88a3992d841da2f7def9fd61b28079d377 Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Wed, 11 Jun 2025 02:45:38 +0530 Subject: [PATCH 1/4] refactor: extract authentication logic from git_push & git_pull --- src/events/git_pull.rs | 137 +-------------------------------- src/events/git_push.rs | 153 +------------------------------------ src/main.rs | 1 + src/utils/git_auth.rs | 170 +++++++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 1 + 5 files changed, 179 insertions(+), 283 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 39a1549..fa12627 100644 --- a/src/events/git_push.rs +++ b/src/events/git_push.rs @@ -1,9 +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 crate::utils::git_auth::setup_auth_callbacks; +use git2::Repository; +use std::path::Path; pub struct GitPush { pub pre_check_rules: Vec>, @@ -239,155 +239,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 453653d1173d23a6d05b163aaa52ec19fde56993 Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Wed, 11 Jun 2025 04:53:07 +0530 Subject: [PATCH 2/4] 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 5db5fe70f2c97863268ae45290d9839cbda7cbc2 Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Fri, 13 Jun 2025 06:46:02 +0530 Subject: [PATCH 3/4] 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} | 17 ++---- 5 files changed, 68 insertions(+), 20 deletions(-) rename src/workflows/default/{action/ta09_pull_push.rs => prompt/pa13_pull_push.rs} (73%) 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 73% rename from src/workflows/default/action/ta09_pull_push.rs rename to src/workflows/default/prompt/pa13_pull_push.rs index f480ffd..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,25 +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.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 cdf333dc954e00ee9b7b8fdafb9e1b602f5a2ec3 Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 Date: Fri, 13 Jun 2025 07:01:28 +0530 Subject: [PATCH 4/4] 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 d08efc6..ad95b5e 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 } }