From d2f08e7d66274718c69e829a8d78630bf8d8f800 Mon Sep 17 00:00:00 2001 From: Varun Nuthalapati Date: Sun, 31 May 2026 20:14:05 -0700 Subject: [PATCH 1/3] fix(services): support : syntax for unlisted Discovery APIs When resolve_service fails but a version override is present (via colon syntax or --api-version flag), fall through to use the raw service arg as the API name with the provided version instead of returning an error. Fixes #670 --- .changeset/fix-api-version-syntax.md | 5 +++++ crates/google-workspace-cli/src/main.rs | 25 +++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-api-version-syntax.md diff --git a/.changeset/fix-api-version-syntax.md b/.changeset/fix-api-version-syntax.md new file mode 100644 index 00000000..58e7df6c --- /dev/null +++ b/.changeset/fix-api-version-syntax.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix `:` syntax so unlisted Discovery APIs can be called directly diff --git a/crates/google-workspace-cli/src/main.rs b/crates/google-workspace-cli/src/main.rs index 41dcc1e1..ed54ebec 100644 --- a/crates/google-workspace-cli/src/main.rs +++ b/crates/google-workspace-cli/src/main.rs @@ -340,8 +340,16 @@ pub fn parse_service_and_version( } } - let (api_name, default_version) = services::resolve_service(service_arg)?; - let version = version_override.unwrap_or(default_version); + let (api_name, version) = match services::resolve_service(service_arg) { + Ok((name, default_ver)) => (name, version_override.unwrap_or(default_ver)), + Err(e) => { + if let Some(ver) = version_override { + (service_arg.to_string(), ver) + } else { + return Err(e); + } + } + }; Ok((api_name, version)) } @@ -735,4 +743,17 @@ mod tests { let scopes: Vec = vec![]; assert_eq!(select_scope(&scopes), None); } + + #[test] + fn test_parse_service_and_version_unlisted_with_colon() { + // admob is not in the known services list, but admob:v1 should work + let args = vec![ + "gws".to_string(), + "admob:v1".to_string(), + "accounts".to_string(), + "list".to_string(), + ]; + let result = parse_service_and_version(&args, "admob:v1"); + assert_eq!(result.unwrap(), ("admob".to_string(), "v1".to_string())); + } } From 5b7452c278afe639f823de248d9b19407d77bcf9 Mon Sep 17 00:00:00 2001 From: Varun Nuthalapati Date: Sun, 31 May 2026 21:22:06 -0700 Subject: [PATCH 2/3] fix(services): handle --api-version=VER equals-sign syntax for unlisted APIs The version-override loop only matched the space-separated form (--api-version VER). Add a strip_prefix branch to also capture the equals-sign form (--api-version=VER), which filter_args_for_subcommand already stripped but parse_service_and_version silently ignored. Add a test to cover the equals-sign path with an unlisted service. --- crates/google-workspace-cli/src/main.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/google-workspace-cli/src/main.rs b/crates/google-workspace-cli/src/main.rs index ed54ebec..3633213a 100644 --- a/crates/google-workspace-cli/src/main.rs +++ b/crates/google-workspace-cli/src/main.rs @@ -325,10 +325,12 @@ pub fn parse_service_and_version( let mut service_arg = first_arg; let mut version_override: Option = None; - // Check for --api-version flag anywhere in args + // Check for --api-version flag anywhere in args (space-separated or = form) for i in 0..args.len() { if args[i] == "--api-version" && i + 1 < args.len() { version_override = Some(args[i + 1].clone()); + } else if let Some(ver) = args[i].strip_prefix("--api-version=") { + version_override = Some(ver.to_string()); } } @@ -756,4 +758,18 @@ mod tests { let result = parse_service_and_version(&args, "admob:v1"); assert_eq!(result.unwrap(), ("admob".to_string(), "v1".to_string())); } + + #[test] + fn test_parse_service_and_version_unlisted_with_equals_flag() { + // admob is not in the known services list; --api-version=v1 (equals form) should work + let args = vec![ + "gws".to_string(), + "admob".to_string(), + "--api-version=v1".to_string(), + "accounts".to_string(), + "list".to_string(), + ]; + let result = parse_service_and_version(&args, "admob"); + assert_eq!(result.unwrap(), ("admob".to_string(), "v1".to_string())); + } } From 34edcb08597776ece81ab8bce46c470653b58b16 Mon Sep 17 00:00:00 2001 From: Varun Nuthalapati Date: Mon, 1 Jun 2026 09:23:26 -0700 Subject: [PATCH 3/3] fix(services): validate chars in unlisted service/version to prevent path traversal Add alphanumeric + _-. allowlist check when falling back to a raw service name and version for unlisted Discovery APIs. Rejects inputs containing ../ traversal sequences or special URL characters before they can reach the Discovery URL or filesystem cache. Resolves Gemini r2 comment on PR #826. --- crates/google-workspace-cli/src/main.rs | 41 ++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/google-workspace-cli/src/main.rs b/crates/google-workspace-cli/src/main.rs index 3633213a..77588f40 100644 --- a/crates/google-workspace-cli/src/main.rs +++ b/crates/google-workspace-cli/src/main.rs @@ -346,7 +346,18 @@ pub fn parse_service_and_version( Ok((name, default_ver)) => (name, version_override.unwrap_or(default_ver)), Err(e) => { if let Some(ver) = version_override { - (service_arg.to_string(), ver) + let is_valid = |s: &str| { + !s.is_empty() + && s.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.') + }; + if is_valid(service_arg) && is_valid(&ver) { + (service_arg.to_string(), ver) + } else { + return Err(GwsError::Validation( + "Invalid service name or version: only alphanumeric characters, underscores, hyphens, and dots are allowed.".to_string() + )); + } } else { return Err(e); } @@ -772,4 +783,32 @@ mod tests { let result = parse_service_and_version(&args, "admob"); assert_eq!(result.unwrap(), ("admob".to_string(), "v1".to_string())); } + + #[test] + fn test_parse_service_and_version_rejects_path_traversal_in_service() { + let args = vec![ + "gws".to_string(), + "../evil:v1".to_string(), + ]; + let result = parse_service_and_version(&args, "../evil:v1"); + assert!(result.is_err(), "path traversal in service name must be rejected"); + } + + #[test] + fn test_parse_service_and_version_rejects_path_traversal_in_version() { + let args = vec![ + "gws".to_string(), + "admob".to_string(), + "--api-version=../evil".to_string(), + ]; + let result = parse_service_and_version(&args, "admob"); + assert!(result.is_err(), "path traversal in version must be rejected"); + } + + #[test] + fn test_parse_service_and_version_rejects_special_chars_in_service() { + let args = vec!["gws".to_string(), "svc?foo:v1".to_string()]; + let result = parse_service_and_version(&args, "svc?foo:v1"); + assert!(result.is_err(), "special chars in service name must be rejected"); + } }