Skip to content
Open
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/fix-auth-setup-help-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Handle -h/--help flag in `gws auth setup` to print usage and exit before running setup checks
5 changes: 5 additions & 0 deletions .changeset/fix-gmail-triage-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Fix gmail +triage 403 error by using gmail.readonly scope instead of gmail.modify to avoid conflict with gmail.metadata scope that does not support the q parameter
1 change: 1 addition & 0 deletions src/helpers/gmail/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use std::pin::Pin;
pub struct GmailHelper;

pub(super) const GMAIL_SCOPE: &str = "https://www.googleapis.com/auth/gmail.modify";
pub(super) const GMAIL_READONLY_SCOPE: &str = "https://www.googleapis.com/auth/gmail.readonly";
pub(super) const PUBSUB_SCOPE: &str = "https://www.googleapis.com/auth/pubsub";

impl Helper for GmailHelper {
Expand Down
8 changes: 6 additions & 2 deletions src/helpers/gmail/triage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ pub async fn handle_triage(matches: &ArgMatches) -> Result<(), GwsError> {
.map(|s| crate::formatter::OutputFormat::from_str(s))
.unwrap_or(crate::formatter::OutputFormat::Table);

// Authenticate
let token = auth::get_token(&[GMAIL_SCOPE])
// Authenticate — use gmail.readonly instead of gmail.modify because triage
// is read-only and the `q` query parameter is not supported under the
// gmail.metadata scope. When a token carries both metadata and modify
// scopes the API may resolve to the metadata path and reject `q` with 403.
// gmail.readonly always supports `q`.
let token = auth::get_token(&[GMAIL_READONLY_SCOPE])
.await
.map_err(|e| GwsError::Auth(format!("Gmail auth failed: {e}")))?;

Expand Down
35 changes: 35 additions & 0 deletions src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1431,8 +1431,25 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupStage, Gws
Ok(SetupStage::Finish)
}

const SETUP_USAGE: &str = concat!(
"Usage: gws auth setup [options]\n\n",
"Options:\n",
" --project <PROJECT_ID> Use a specific GCP project\n",
" --dry-run Preview setup actions without making changes\n",
" -h, --help Show this help message\n",
);

fn is_help_requested(args: &[String]) -> bool {
args.iter().any(|arg| arg == "--help" || arg == "-h")
}

/// Run the full setup flow. Orchestrates all steps and outputs JSON summary.
pub async fn run_setup(args: &[String]) -> Result<(), GwsError> {
if is_help_requested(args) {
println!("{SETUP_USAGE}");
return Ok(());
}

let opts = parse_setup_args(args);
Comment on lines +1442 to 1453
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The current implementation with a separate is_help_requested function is too simplistic and can lead to incorrect behavior. For example, a command like gws auth setup --project -h would be misinterpreted as a request for help, when the user likely intends to set the project ID to "-h". The is_help_requested function doesn't consider the argument's context.

To fix this, the argument parsing logic should be consolidated. Instead of a separate help check, the main argument parser should also handle the help flags. This ensures that option values are not mistaken for flags.

I suggest replacing both is_help_requested and the call to parse_setup_args with a single, more robust parsing loop at the beginning of run_setup.

    let mut project = None;
    let mut dry_run = false;
    let mut i = 0;
    while i < args.len() {
        match args[i].as_str() {
            "--help" | "-h" => {
                println!("{SETUP_USAGE}");
                return Ok(());
            }
            "--project" => {
                if i + 1 < args.len() {
                    project = Some(args[i + 1].clone());
                    i += 2;
                } else {
                    i += 1; // Or return an error for missing value
                }
            }
            s if s.starts_with("--project=") => {
                project = Some(s.split_once('=').unwrap().1.to_string());
                i += 1;
            }
            "--dry-run" => {
                dry_run = true;
                i += 1;
            }
            _ => {
                i += 1;
            }
        }
    }
    let opts = SetupOptions { project, dry_run };

let dry_run = opts.dry_run;
let interactive = std::io::IsTerminal::is_terminal(&std::io::stdin()) && !dry_run;
Expand Down Expand Up @@ -1678,6 +1695,24 @@ mod tests {
assert_eq!(opts.project.as_deref(), Some("p"));
}

#[test]
fn test_is_help_requested_true_for_long_flag() {
let args: Vec<String> = vec!["--help".into()];
assert!(is_help_requested(&args));
}

#[test]
fn test_is_help_requested_true_for_short_flag() {
let args: Vec<String> = vec!["--project".into(), "my-project".into(), "-h".into()];
assert!(is_help_requested(&args));
}

#[test]
fn test_is_help_requested_false_without_help_flags() {
let args: Vec<String> = vec!["--project".into(), "my-project".into()];
assert!(!is_help_requested(&args));
}

// ── Account selection → gcloud action ───────────────────────

#[test]
Expand Down