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 .changeset/auth-legacy-credentials-fallback.md
Original file line number Diff line number Diff line change
@@ -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
42 changes: 36 additions & 6 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<String>> {
let registry = crate::accounts::load_accounts()?;
Expand Down Expand Up @@ -135,14 +136,13 @@ fn resolve_account(account: Option<&str>) -> anyhow::Result<Option<String>> {
(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)
}
}
Expand Down Expand Up @@ -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() {
Expand Down
15 changes: 14 additions & 1 deletion src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <email>' for multi-account support."
);
credential_store::save_encrypted(&creds_str)
.map_err(|e| GwsError::Auth(format!("Failed to encrypt credentials: {e}")))?
};
Expand Down Expand Up @@ -406,7 +411,10 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> {
async fn fetch_userinfo_email(access_token: &str) -> Option<String> {
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")
Expand All @@ -415,6 +423,11 @@ async fn fetch_userinfo_email(access_token: &str) -> Option<String> {
.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()?;
Expand Down