Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ static REFRESH_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

/// Attempt to load stored credentials and refresh if needed.
/// Returns None on any failure (not logged in, expired, refresh failed).
/// Returns None immediately when disable_auth feature flag is enabled.
/// Uses in-process Mutex for thread safety during token refresh.
fn try_load_auth_token() -> Option<String> {
if config::Config::get().get_feature_flags().disable_auth {
return None;
}

let store = CredentialStore::new();

let creds = match store.load() {
Expand Down
20 changes: 18 additions & 2 deletions src/auth/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use crate::auth::types::StoredCredentials;
use crate::config::Config;
use std::path::PathBuf;

#[cfg(not(test))]
const AUTH_DISABLED_MSG: &str =
"Authentication is disabled. The disable_auth feature flag is enabled.";

#[cfg(all(not(test), feature = "keyring"))]
const SERVICE_NAME: &str = "git-ai";
#[cfg(all(not(test), feature = "keyring"))]
Expand Down Expand Up @@ -99,16 +103,28 @@ impl CredentialStore {
))
}

/// Store credentials securely
/// Store credentials securely.
/// Returns an error when the disable_auth feature flag is enabled.
pub fn store(&self, creds: &StoredCredentials) -> Result<(), String> {
#[cfg(not(test))]
if Config::get().get_feature_flags().disable_auth {
return Err(AUTH_DISABLED_MSG.to_string());
}

let json = serde_json::to_string(creds)
.map_err(|e| format!("Failed to serialize credentials: {}", e))?;

self.backend.store(&json)
}

/// Load stored credentials
/// Load stored credentials.
/// Returns Ok(None) when the disable_auth feature flag is enabled.
pub fn load(&self) -> Result<Option<StoredCredentials>, String> {
#[cfg(not(test))]
if Config::get().get_feature_flags().disable_auth {
return Ok(None);
}

let json = self.backend.load()?;

match json {
Expand Down
6 changes: 6 additions & 0 deletions src/commands/exchange_nonce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@

use crate::auth::CredentialStore;
use crate::auth::client::OAuthClient;
use crate::config;

/// Handle the exchange-nonce command (internal - called by install scripts)
///
/// Exits with code 1 on failure (silently) so install script can run `git-ai login`.
/// Exits with code 0 on success.
pub fn handle_exchange_nonce(_args: &[String]) {
if config::Config::get().get_feature_flags().disable_auth {
eprintln!("Error: Authentication is disabled. The disable_auth feature flag is enabled.");
std::process::exit(1);
}

// Read from environment variables (injected by install script)
let nonce = std::env::var("INSTALL_NONCE")
.ok()
Expand Down
6 changes: 6 additions & 0 deletions src/commands/login.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use crate::auth::{CredentialStore, OAuthClient};
use crate::config;

/// Handle the `git-ai login` command
pub fn handle_login(_args: &[String]) {
if config::Config::get().get_feature_flags().disable_auth {
eprintln!("Error: Authentication is disabled. The disable_auth feature flag is enabled.");
std::process::exit(1);
}

let store = CredentialStore::new();

// Check if already logged in
Expand Down
6 changes: 6 additions & 0 deletions src/commands/logout.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use crate::auth::CredentialStore;
use crate::config;

/// Handle the `git-ai logout` command
pub fn handle_logout(_args: &[String]) {
if config::Config::get().get_feature_flags().disable_auth {
eprintln!("Error: Authentication is disabled. The disable_auth feature flag is enabled.");
std::process::exit(1);
}

let store = CredentialStore::new();

// Check if currently logged in
Expand Down
27 changes: 27 additions & 0 deletions src/feature_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ define_feature_flags!(
async_mode: async_mode, debug = false, release = true,
git_hooks_enabled: git_hooks_enabled, debug = false, release = false,
git_hooks_externally_managed: git_hooks_externally_managed, debug = false, release = false,
disable_auth: disable_auth, debug = false, release = false,
);

impl FeatureFlags {
Expand Down Expand Up @@ -133,6 +134,7 @@ mod tests {
assert!(!flags.async_mode);
assert!(!flags.git_hooks_enabled);
assert!(!flags.git_hooks_externally_managed);
assert!(!flags.disable_auth);
}
#[cfg(not(debug_assertions))]
{
Expand All @@ -142,6 +144,7 @@ mod tests {
assert!(flags.async_mode);
assert!(!flags.git_hooks_enabled);
assert!(!flags.git_hooks_externally_managed);
assert!(!flags.disable_auth);
}
}

Expand Down Expand Up @@ -210,13 +213,15 @@ mod tests {
std::env::remove_var("GIT_AI_CHECKPOINT_INTER_COMMIT_MOVE");
std::env::remove_var("GIT_AI_AUTH_KEYRING");
std::env::remove_var("GIT_AI_ASYNC_MODE");
std::env::remove_var("GIT_AI_DISABLE_AUTH");
}

let flags = FeatureFlags::from_env_and_file(None);
let defaults = FeatureFlags::default();
assert_eq!(flags.rewrite_stash, defaults.rewrite_stash);
assert_eq!(flags.inter_commit_move, defaults.inter_commit_move);
assert_eq!(flags.auth_keyring, defaults.auth_keyring);
assert_eq!(flags.disable_auth, defaults.disable_auth);
}

#[test]
Expand All @@ -227,6 +232,7 @@ mod tests {
std::env::remove_var("GIT_AI_CHECKPOINT_INTER_COMMIT_MOVE");
std::env::remove_var("GIT_AI_AUTH_KEYRING");
std::env::remove_var("GIT_AI_ASYNC_MODE");
std::env::remove_var("GIT_AI_DISABLE_AUTH");
}

let file_flags = DeserializableFeatureFlags {
Expand All @@ -251,6 +257,7 @@ mod tests {
async_mode: true,
git_hooks_enabled: false,
git_hooks_externally_managed: false,
disable_auth: false,
};

let serialized = serde_json::to_string(&flags).unwrap();
Expand All @@ -260,6 +267,7 @@ mod tests {
assert!(serialized.contains("async_mode"));
assert!(serialized.contains("git_hooks_enabled"));
assert!(serialized.contains("git_hooks_externally_managed"));
assert!(serialized.contains("disable_auth"));
}

#[test]
Expand All @@ -271,6 +279,7 @@ mod tests {
async_mode: true,
git_hooks_enabled: true,
git_hooks_externally_managed: false,
disable_auth: true,
};
let cloned = flags.clone();
assert_eq!(cloned.rewrite_stash, flags.rewrite_stash);
Expand All @@ -282,6 +291,7 @@ mod tests {
cloned.git_hooks_externally_managed,
flags.git_hooks_externally_managed
);
assert_eq!(cloned.disable_auth, flags.disable_auth);
}

#[test]
Expand All @@ -290,4 +300,21 @@ mod tests {
let debug_str = format!("{:?}", flags);
assert!(debug_str.contains("FeatureFlags"));
}

#[test]
fn test_disable_auth_flag_from_file_config() {
let deserializable = DeserializableFeatureFlags {
disable_auth: Some(true),
..Default::default()
};

let flags = FeatureFlags::from_file_config(Some(deserializable));
assert!(flags.disable_auth);
}

#[test]
fn test_disable_auth_flag_defaults_to_false() {
let flags = FeatureFlags::default();
assert!(!flags.disable_auth);
}
}
1 change: 1 addition & 0 deletions tests/integration/performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fn setup() {
async_mode: false,
git_hooks_enabled: false,
git_hooks_externally_managed: false,
disable_auth: false,
};

git_ai::config::Config::set_test_feature_flags(test_flags.clone());
Expand Down
Loading