fix(auth): validate --subscription in gmail +watch and deduplicate PUBSUB_API_BASE (#441)
* fix(auth): validate --subscription in gmail +watch and deduplicate PUBSUB_API_BASE - Move PUBSUB_API_BASE constant to helpers/mod.rs (shared by events/subscribe and gmail/watch) - Add validate_resource_name on --subscription in gmail +watch parse_watch_args - Replace remaining hardcoded Pub/Sub and Gmail API URLs with constants - Add test for --subscription path traversal rejection Closes #408 * fix: replace remaining hardcoded Pub/Sub URLs in subscribe.rs with PUBSUB_API_BASE --------- Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@googleworkspace/cli": patch
|
||||
---
|
||||
|
||||
Validate `--subscription` resource name in `gmail +watch` and deduplicate `PUBSUB_API_BASE` constant.
|
||||
@@ -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)
|
||||
|
||||
@@ -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<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
|
||||
@@ -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(&[
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user