Skip to content
Open
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
124 changes: 119 additions & 5 deletions crates/google-workspace-cli/src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,7 @@ async fn handle_export(unmasked: bool) -> Result<(), GwsError> {
/// Resolve OAuth client credentials from env vars or saved config file.
fn resolve_client_credentials() -> Result<(String, String, Option<String>), GwsError> {
// 1. Try env vars first
let env_id = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_ID").ok();
let env_secret = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET").ok();
let (env_id, env_secret) = oauth_client_env_credentials();

if let (Some(id), Some(secret)) = (env_id, env_secret) {
// Still try to load project_id from config file for the scope picker
Expand Down Expand Up @@ -1260,6 +1259,8 @@ async fn handle_status() -> Result<(), GwsError> {
}
}

add_oauth_env_status_fields(&mut output, has_config);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The addition of add_oauth_env_status_fields introduces a logic inconsistency with the credential_source detection later in this function (lines 1281-1284 in the full file). While the new status fields correctly filter out empty environment variables, the credential_source logic still uses is_ok(), which will incorrectly report environment_variables as the source if the variables are present but empty. This directly contradicts the PR's goal of accurately showing environment overrides.


// Show credential source by attempting actual resolution
let has_token_env = std::env::var("GOOGLE_WORKSPACE_CLI_TOKEN")
.ok()
Expand All @@ -1272,9 +1273,8 @@ async fn handle_status() -> Result<(), GwsError> {
} else {
match resolve_client_credentials() {
Ok((_, _, _)) => {
let has_env_id = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_ID").is_ok();
let has_env_secret = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET").is_ok();
if has_env_id && has_env_secret {
let (env_id, env_secret) = oauth_client_env_credentials();
if env_id.is_some() && env_secret.is_some() {
"environment_variables"
} else {
"client_secret.json"
Expand Down Expand Up @@ -1453,6 +1453,30 @@ async fn handle_status() -> Result<(), GwsError> {
Ok(())
}

fn oauth_client_env_credentials() -> (Option<String>, Option<String>) {
let env_client_id = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_ID")
.ok()
.filter(|value| !value.is_empty());
let env_client_secret = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET")
.ok()
.filter(|value| !value.is_empty());
(env_client_id, env_client_secret)
}

fn add_oauth_env_status_fields(output: &mut serde_json::Value, has_config: bool) {
let (env_client_id, env_client_secret) = oauth_client_env_credentials();
let env_client_secret_set = env_client_secret.is_some();

output["env_client_id_set"] = json!(env_client_id.is_some());
output["env_client_secret_set"] = json!(env_client_secret_set);
output["env_overrides_client_config"] =
json!(has_config && env_client_id.is_some() && env_client_secret_set);

if let Some(client_id) = env_client_id {
output["env_client_id"] = json!(mask_secret(&client_id));
}
}

fn handle_logout() -> Result<(), GwsError> {
let plain_path = plain_credentials_path();
let enc_path = credential_store::encrypted_credentials_path();
Expand Down Expand Up @@ -1974,6 +1998,49 @@ mod tests {
assert_eq!(secret, "test-secret");
}

#[test]
#[serial_test::serial]
fn oauth_client_env_credentials_ignores_empty_values() {
unsafe {
std::env::set_var("GOOGLE_WORKSPACE_CLI_CLIENT_ID", "");
std::env::set_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET", "test-secret");
}

let (client_id, client_secret) = oauth_client_env_credentials();

unsafe {
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_ID");
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET");
}

assert!(client_id.is_none());
assert_eq!(client_secret.as_deref(), Some("test-secret"));
}

#[test]
#[serial_test::serial]
fn resolve_credentials_ignores_empty_env_vars() {
let dir = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR", dir.path());
std::env::set_var("GOOGLE_WORKSPACE_CLI_CLIENT_ID", "");
std::env::set_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET", "test-secret");
}

let result = resolve_client_credentials();

unsafe {
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR");
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_ID");
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET");
}

match result.unwrap_err() {
GwsError::Auth(msg) => assert!(msg.contains("No OAuth client configured")),
other => panic!("Expected Auth error, got: {other:?}"),
}
}

#[tokio::test]
async fn handle_status_succeeds_without_credentials() {
// status should always succeed and report "none"
Expand All @@ -1982,6 +2049,53 @@ mod tests {
assert!(result.is_ok());
}

#[test]
#[serial_test::serial]
fn add_oauth_env_status_fields_marks_env_override() {
unsafe {
std::env::set_var(
"GOOGLE_WORKSPACE_CLI_CLIENT_ID",
"1234567890abcdef.apps.googleusercontent.com",
);
std::env::set_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET", "test-secret");
}

let mut output = json!({});
add_oauth_env_status_fields(&mut output, true);

unsafe {
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_ID");
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET");
}

assert_eq!(output["env_client_id_set"], json!(true));
assert_eq!(output["env_client_secret_set"], json!(true));
assert_eq!(output["env_overrides_client_config"], json!(true));
assert_eq!(output["env_client_id"], json!("1234....com"));
}

#[test]
#[serial_test::serial]
fn add_oauth_env_status_fields_ignores_empty_env_vars() {
unsafe {
std::env::set_var("GOOGLE_WORKSPACE_CLI_CLIENT_ID", "");
std::env::set_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET", "test-secret");
}

let mut output = json!({});
add_oauth_env_status_fields(&mut output, true);

unsafe {
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_ID");
std::env::remove_var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET");
}

assert_eq!(output["env_client_id_set"], json!(false));
assert_eq!(output["env_client_secret_set"], json!(true));
assert_eq!(output["env_overrides_client_config"], json!(false));
assert!(output["env_client_id"].is_null());
}

#[test]
fn credential_store_save_load_round_trip() {
// Use encrypt/decrypt directly to avoid writing to the real config dir
Expand Down
Loading