diff --git a/.changeset/auth-legacy-credentials-fallback.md b/.changeset/auth-legacy-credentials-fallback.md new file mode 100644 index 0000000..32b8a6c --- /dev/null +++ b/.changeset/auth-legacy-credentials-fallback.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix auth deadlock when legacy credentials.enc exists without accounts.json: resolve_account now falls back gracefully instead of bailing, and login warns when email fetch fails diff --git a/src/auth.rs b/src/auth.rs index d895f26..579b39a 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -94,7 +94,8 @@ pub async fn get_token(scopes: &[&str], account: Option<&str>) -> anyhow::Result /// Resolve which account to use: /// 1. Explicit `account` parameter takes priority. /// 2. Fall back to `accounts.json` default. -/// 3. If no registry exists but legacy `credentials.enc` exists, fail with upgrade message. +/// 3. If no registry exists but legacy `credentials.enc` exists, return None with a note +/// (caller will use the legacy file directly). /// 4. If nothing exists, return None (will fall through to standard error). fn resolve_account(account: Option<&str>) -> anyhow::Result> { let registry = crate::accounts::load_accounts()?; @@ -135,14 +136,13 @@ fn resolve_account(account: Option<&str>) -> anyhow::Result> { (None, None) => { let legacy_path = credential_store::encrypted_credentials_path(); if legacy_path.exists() { - anyhow::bail!( - "Legacy credentials found at {}. \ - gws now supports multiple accounts. \ - Please run 'gws auth login' to upgrade your credentials.", + eprintln!( + "[gws] Note: Using legacy credentials from {}. \ + Run 'gws auth login' to upgrade to multi-account format.", legacy_path.display() ); } - // No registry, no legacy — fall through to standard credential loading + // No registry — fall through to standard credential loading Ok(None) } } @@ -425,6 +425,36 @@ mod tests { assert_eq!(result.unwrap(), "my-test-token"); } + #[test] + #[serial_test::serial] + fn test_resolve_account_legacy_credentials_returns_none() { + // Save the old value + let old_config_dir = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR").ok(); + + // Setup: create credentials.enc but no accounts.json in a temp config dir + let dir = tempfile::tempdir().unwrap(); + unsafe { + std::env::set_var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR", dir.path().as_os_str()); + } + + // Create legacy credentials.enc + let creds_enc = dir.path().join("credentials.enc"); + std::fs::write(&creds_enc, b"dummy-encrypted-data").unwrap(); + + // No accounts.json exists — resolve_account(None) should return Ok(None), not Err + let result = resolve_account(None); + assert!(result.is_ok(), "Expected Ok, got: {:?}", result); + assert!(result.unwrap().is_none()); + + unsafe { + if let Some(v) = old_config_dir { + std::env::set_var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR", v); + } else { + std::env::remove_var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR"); + } + } + } + #[tokio::test] #[serial_test::serial] async fn test_get_token_env_var_empty_falls_through() { diff --git a/src/auth_commands.rs b/src/auth_commands.rs index d559d94..21ed79a 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -365,6 +365,11 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { credential_store::encrypted_credentials_path_for(email) } else { // Legacy single-account save (no email available) + eprintln!( + "[gws] Warning: Could not determine account email. \ + Credentials saved in legacy format. \ + Re-run with 'gws auth login --account ' for multi-account support." + ); credential_store::save_encrypted(&creds_str) .map_err(|e| GwsError::Auth(format!("Failed to encrypt credentials: {e}")))? }; @@ -406,7 +411,10 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { async fn fetch_userinfo_email(access_token: &str) -> Option { let client = match crate::client::build_client() { Ok(c) => c, - Err(_) => return None, + Err(e) => { + eprintln!("[gws] Warning: Failed to build HTTP client for userinfo: {e}"); + return None; + } }; let resp = client .get("https://www.googleapis.com/oauth2/v2/userinfo") @@ -415,6 +423,11 @@ async fn fetch_userinfo_email(access_token: &str) -> Option { .await .ok()?; if !resp.status().is_success() { + eprintln!( + "[gws] Warning: userinfo request failed with HTTP {}. \ + Account email could not be determined.", + resp.status() + ); return None; } let body: serde_json::Value = resp.json().await.ok()?;