diff --git a/.changeset/validate-watch-subscription.md b/.changeset/validate-watch-subscription.md new file mode 100644 index 00000000..dd305ee3 --- /dev/null +++ b/.changeset/validate-watch-subscription.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Validate `--subscription` resource name in `gmail +watch` and deduplicate `PUBSUB_API_BASE` constant. diff --git a/src/helpers/events/subscribe.rs b/src/helpers/events/subscribe.rs index c1df3667..edbfb4dc 100644 --- a/src/helpers/events/subscribe.rs +++ b/src/helpers/events/subscribe.rs @@ -1,9 +1,8 @@ use super::*; use crate::auth::AccessTokenProvider; +use crate::helpers::PUBSUB_API_BASE; use std::path::PathBuf; -const PUBSUB_API_BASE: &str = "https://pubsub.googleapis.com/v1"; - #[derive(Debug, Clone, Default, Builder)] #[builder(setter(into))] pub struct SubscribeConfig { @@ -143,7 +142,7 @@ pub(super) async fn handle_subscribe( // 1. Create Pub/Sub topic eprintln!("Creating Pub/Sub topic: {topic}"); let resp = client - .put(format!("https://pubsub.googleapis.com/v1/{topic}")) + .put(format!("{PUBSUB_API_BASE}/{topic}")) .bearer_auth(&pubsub_token) .header("Content-Type", "application/json") .body("{}") @@ -168,7 +167,7 @@ pub(super) async fn handle_subscribe( "ackDeadlineSeconds": 60, }); let resp = client - .put(format!("https://pubsub.googleapis.com/v1/{sub}")) + .put(format!("{PUBSUB_API_BASE}/{sub}")) .bearer_auth(&pubsub_token) .header("Content-Type", "application/json") .json(&sub_body) diff --git a/src/helpers/gmail/watch.rs b/src/helpers/gmail/watch.rs index 4e811952..15bc9889 100644 --- a/src/helpers/gmail/watch.rs +++ b/src/helpers/gmail/watch.rs @@ -1,7 +1,7 @@ use super::*; use crate::auth::AccessTokenProvider; +use crate::helpers::PUBSUB_API_BASE; -const PUBSUB_API_BASE: &str = "https://pubsub.googleapis.com/v1"; const GMAIL_API_BASE: &str = "https://gmail.googleapis.com/gmail/v1"; /// Handles the `+watch` command — Gmail push notifications via Pub/Sub. @@ -50,7 +50,7 @@ pub(super) async fn handle_watch( // Create Pub/Sub topic eprintln!("Creating Pub/Sub topic: {t}"); let resp = client - .put(format!("https://pubsub.googleapis.com/v1/{t}")) + .put(format!("{PUBSUB_API_BASE}/{t}")) .bearer_auth(&pubsub_token) .header("Content-Type", "application/json") .body("{}") @@ -79,7 +79,7 @@ pub(super) async fn handle_watch( } }); let resp = client - .post(format!("https://pubsub.googleapis.com/v1/{t}:setIamPolicy")) + .post(format!("{PUBSUB_API_BASE}/{t}:setIamPolicy")) .bearer_auth(&pubsub_token) .header("Content-Type", "application/json") .json(&iam_body) @@ -115,7 +115,7 @@ pub(super) async fn handle_watch( "ackDeadlineSeconds": 60, }); let resp = client - .put(format!("https://pubsub.googleapis.com/v1/{sub}")) + .put(format!("{PUBSUB_API_BASE}/{sub}")) .bearer_auth(&pubsub_token) .header("Content-Type", "application/json") .json(&sub_body) @@ -144,7 +144,7 @@ pub(super) async fn handle_watch( } let resp = client - .post("https://gmail.googleapis.com/gmail/v1/users/me/watch") + .post(format!("{GMAIL_API_BASE}/users/me/watch")) .bearer_auth(&gmail_token) .header("Content-Type", "application/json") .json(&watch_body) @@ -186,7 +186,7 @@ pub(super) async fn handle_watch( // Get initial historyId for tracking let profile_resp = client - .get("https://gmail.googleapis.com/gmail/v1/users/me/profile") + .get(format!("{GMAIL_API_BASE}/users/me/profile")) .bearer_auth(&gmail_token) .send() .await @@ -570,7 +570,13 @@ fn parse_watch_args(matches: &ArgMatches) -> Result { Ok(WatchConfig { project: matches.get_one::("project").cloned(), - subscription: matches.get_one::("subscription").cloned(), + subscription: matches + .get_one::("subscription") + .map(|s| { + crate::validate::validate_resource_name(s)?; + Ok::<_, GwsError>(s.clone()) + }) + .transpose()?, topic: matches.get_one::("topic").cloned(), label_ids: matches.get_one::("label-ids").cloned(), max_messages: matches @@ -787,6 +793,15 @@ mod tests { assert!(msg.contains("outside the current directory")); } + #[test] + fn test_parse_watch_args_rejects_traversal_subscription() { + let matches = make_matches_watch(&["test", "--subscription", "../../evil"]); + let result = parse_watch_args(&matches); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!(msg.contains("path traversal")); + } + #[test] fn test_parse_watch_args_full() { let matches = make_matches_watch(&[ diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index 378f1ea5..72d31272 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -27,6 +27,12 @@ pub mod script; pub mod sheets; pub mod workflows; +/// Base URL for the Google Cloud Pub/Sub v1 API. +/// +/// Shared across `events::subscribe` and `gmail::watch` so the constant +/// is defined in a single place. +pub(crate) const PUBSUB_API_BASE: &str = "https://pubsub.googleapis.com/v1"; + /// A trait for service-specific CLI helpers that inject custom commands. pub trait Helper: Send + Sync { /// Injects subcommands into the service command.