Skip to content
Merged
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/validate-watch-subscription.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Validate `--subscription` resource name in `gmail +watch` and deduplicate `PUBSUB_API_BASE` constant.
7 changes: 3 additions & 4 deletions src/helpers/events/subscribe.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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("{}")
Expand All @@ -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)
Expand Down
29 changes: 22 additions & 7 deletions src/helpers/gmail/watch.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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("{}")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -570,7 +570,13 @@ fn parse_watch_args(matches: &ArgMatches) -> Result<WatchConfig, GwsError> {

Ok(WatchConfig {
project: matches.get_one::<String>("project").cloned(),
subscription: matches.get_one::<String>("subscription").cloned(),
subscription: matches
.get_one::<String>("subscription")
.map(|s| {
crate::validate::validate_resource_name(s)?;
Ok::<_, GwsError>(s.clone())
})
.transpose()?,
topic: matches.get_one::<String>("topic").cloned(),
label_ids: matches.get_one::<String>("label-ids").cloned(),
max_messages: matches
Expand Down Expand Up @@ -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(&[
Expand Down
6 changes: 6 additions & 0 deletions src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading