Skip to content
Open
Changes from 2 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
100 changes: 98 additions & 2 deletions crates/google-workspace-cli/src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,12 @@ 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 = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_ID")
.ok()
.filter(|value| !value.is_empty());
let env_secret = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET")
.ok()
.filter(|value| !value.is_empty());
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 logic for resolving credentials from environment variables is now duplicated in add_oauth_env_status_fields. To ensure consistency and improve maintainability, consider refactoring this into a shared helper function. This is particularly important for security-sensitive environment variables where the definition of "set" (e.g., handling empty vs. whitespace-only strings) should be uniform across the application.


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 +1264,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 Down Expand Up @@ -1453,6 +1459,25 @@ async fn handle_status() -> Result<(), GwsError> {
Ok(())
}

fn add_oauth_env_status_fields(output: &mut serde_json::Value, has_config: bool) {
let env_client_id = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_ID")
.ok()
.filter(|value| !value.is_empty());
let env_client_secret_set = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET")
.ok()
.filter(|value| !value.is_empty())
.is_some();
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 environment variable check here is inconsistent with the actual credential resolution logic in resolve_client_credentials (lines 718-750). Specifically, resolve_client_credentials does not filter out empty strings, meaning it will attempt to use them if they are set in the environment. By filtering them here, gws auth status will report that no environment overrides are active even when the application is actually using them (albeit likely failing later). To ensure the status output accurately reflects the application's state, these filters should be removed, or resolve_client_credentials should be updated to also ignore empty strings.

    let env_client_id = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_ID").ok();
    let env_client_secret_set = std::env::var("GOOGLE_WORKSPACE_CLI_CLIENT_SECRET").is_ok();


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 +1999,30 @@ mod tests {
assert_eq!(secret, "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 +2031,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