From 53397e28fce1a844fb8021ddfe30cc06f07afe54 Mon Sep 17 00:00:00 2001 From: Mubelotix Date: Thu, 31 Jul 2025 11:19:46 +0200 Subject: [PATCH] Replace name by uuid --- crates/meilisearch-types/src/webhooks.rs | 3 +- crates/meilisearch/src/lib.rs | 7 +- crates/meilisearch/src/routes/webhooks.rs | 183 +++++++++++++--------- crates/meilisearch/tests/tasks/webhook.rs | 5 +- 4 files changed, 117 insertions(+), 81 deletions(-) diff --git a/crates/meilisearch-types/src/webhooks.rs b/crates/meilisearch-types/src/webhooks.rs index 8849182ac..0f0741d69 100644 --- a/crates/meilisearch-types/src/webhooks.rs +++ b/crates/meilisearch-types/src/webhooks.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] @@ -14,5 +15,5 @@ pub struct Webhook { #[serde(rename_all = "camelCase")] pub struct Webhooks { #[serde(default)] - pub webhooks: BTreeMap, + pub webhooks: BTreeMap, } diff --git a/crates/meilisearch/src/lib.rs b/crates/meilisearch/src/lib.rs index fcc71f04d..613268936 100644 --- a/crates/meilisearch/src/lib.rs +++ b/crates/meilisearch/src/lib.rs @@ -56,6 +56,7 @@ use option::ScheduleSnapshot; use search_queue::SearchQueue; use tracing::{error, info_span}; use tracing_subscriber::filter::Targets; +use uuid::Uuid; use crate::error::MeilisearchHttpError; @@ -339,13 +340,13 @@ pub fn setup_meilisearch(opt: &Opt) -> anyhow::Result<(Arc, Arc< }, }); let mut webhooks = index_scheduler.webhooks(); - if webhooks.webhooks.get("_cli") != cli_webhook.as_ref() { + if webhooks.webhooks.get(&Uuid::nil()) != cli_webhook.as_ref() { match cli_webhook { Some(webhook) => { - webhooks.webhooks.insert("_cli".to_string(), webhook); + webhooks.webhooks.insert(Uuid::nil(), webhook); } None => { - webhooks.webhooks.remove("_cli"); + webhooks.webhooks.remove(&Uuid::nil()); } } index_scheduler.put_webhooks(webhooks)?; diff --git a/crates/meilisearch/src/routes/webhooks.rs b/crates/meilisearch/src/routes/webhooks.rs index 6157b8efa..a78c36b0c 100644 --- a/crates/meilisearch/src/routes/webhooks.rs +++ b/crates/meilisearch/src/routes/webhooks.rs @@ -16,6 +16,7 @@ use meilisearch_types::webhooks::{Webhook, Webhooks}; use serde::Serialize; use tracing::debug; use utoipa::{OpenApi, ToSchema}; +use uuid::Uuid; use crate::analytics::{Aggregate, Analytics}; use crate::extractors::authentication::policies::ActionPolicy; @@ -37,49 +38,9 @@ pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( web::resource("") .route(web::get().to(get_webhooks)) - .route(web::patch().to(SeqHandler(patch_webhooks))) + .route(web::patch().to(SeqHandler(patch_webhooks))), ) - .service( - web::resource("/{name}") - .route(web::get().to(get_webhook)) - ); -} - -#[utoipa::path( - get, - path = "", - tag = "Webhooks", - security(("Bearer" = ["webhooks.get", "*.get", "*"])), - responses( - (status = OK, description = "Webhooks are returned", body = WebhooksSettings, content_type = "application/json", example = json!({ - "webhooks": { - "name": { - "url": "http://example.com/webhook", - }, - "anotherName": { - "url": "https://your.site/on-tasks-completed", - "headers": { - "Authorization": "Bearer a-secret-token" - } - } - } - })), - (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( - { - "message": "The Authorization header is missing. It must use the bearer authorization method.", - "code": "missing_authorization_header", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing_authorization_header" - } - )), - ) -)] -async fn get_webhooks( - index_scheduler: GuardedData, Data>, -) -> Result { - let webhooks = index_scheduler.webhooks(); - debug!(returns = ?webhooks, "Get webhooks"); - Ok(HttpResponse::Ok().json(webhooks)) + .service(web::resource("/{uuid}").route(web::get().to(get_webhook))); } #[derive(Debug, Deserr, ToSchema)] @@ -105,7 +66,73 @@ struct WebhooksSettings { #[schema(value_type = Option>)] #[deserr(default, error = DeserrJsonError)] #[serde(default)] - webhooks: Setting>>, + webhooks: Setting>>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct WebhookWithMetadata { + uuid: Uuid, + is_editable: bool, + #[serde(flatten)] + webhook: Webhook, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct WebhookResults { + results: Vec, +} + +#[utoipa::path( + get, + path = "", + tag = "Webhooks", + security(("Bearer" = ["webhooks.get", "*.get", "*"])), + responses( + (status = OK, description = "Webhooks are returned", body = WebhooksSettings, content_type = "application/json", example = json!({ + "results": [ + { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://your.site/on-tasks-completed", + "headers": { + "Authorization": "Bearer a-secret-token" + }, + "isEditable": true + }, + { + "uuid": "550e8400-e29b-41d4-a716-446655440001", + "url": "https://another.site/on-tasks-completed", + "isEditable": true + } + ] + })), + (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + )), + ) +)] +async fn get_webhooks( + index_scheduler: GuardedData, Data>, +) -> Result { + let webhooks = index_scheduler.webhooks(); + let results = webhooks + .webhooks + .into_iter() + .map(|(uuid, webhook)| WebhookWithMetadata { + uuid, + is_editable: uuid != Uuid::nil(), + webhook, + }) + .collect::>(); + let results = WebhookResults { results }; + debug!(returns = ?results, "Get webhooks"); + Ok(HttpResponse::Ok().json(results)) } #[derive(Serialize, Default)] @@ -115,10 +142,7 @@ pub struct PatchWebhooksAnalytics { impl PatchWebhooksAnalytics { pub fn patch_webhooks() -> Self { - PatchWebhooksAnalytics { - patch_webhooks_count: 1, - ..Self::default() - } + PatchWebhooksAnalytics { patch_webhooks_count: 1 } } } @@ -141,15 +165,15 @@ impl Aggregate for PatchWebhooksAnalytics { #[derive(Debug, thiserror::Error)] enum WebhooksError { #[error("The URL for the webhook `{0}` is missing.")] - MissingUrl(String), + MissingUrl(Uuid), #[error("Defining too many webhooks would crush the server. Please limit the number of webhooks to 20. You may use a third-party proxy server to dispatch events to more than 20 endpoints.")] TooManyWebhooks, #[error("Too many headers for the webhook `{0}`. Please limit the number of headers to 200.")] - TooManyHeaders(String), + TooManyHeaders(Uuid), #[error("Cannot edit webhook `{0}`. Webhooks prefixed with an underscore are reserved and may not be modified using the API.")] - ReservedWebhook(String), + ReservedWebhook(Uuid), #[error("Webhook `{0}` not found.")] - WebhookNotFound(String), + WebhookNotFound(Uuid), } impl ErrorCode for WebhooksError { @@ -175,10 +199,10 @@ impl ErrorCode for WebhooksError { responses( (status = 200, description = "Returns the updated webhooks", body = WebhooksSettings, content_type = "application/json", example = json!({ "webhooks": { - "name": { + "550e8400-e29b-41d4-a716-446655440000": { "url": "http://example.com/webhook", }, - "anotherName": { + "550e8400-e29b-41d4-a716-446655440001": { "url": "https://your.site/on-tasks-completed", "headers": { "Authorization": "Bearer a-secret-token" @@ -212,7 +236,7 @@ fn patch_webhooks_inner( new_webhooks: WebhooksSettings, ) -> Result { fn merge_webhook( - name: &str, + uuid: &Uuid, old_webhook: Option, new_webhook: WebhookSettings, ) -> Result { @@ -221,8 +245,8 @@ fn patch_webhooks_inner( let url = match new_webhook.url { Setting::Set(url) => url, - Setting::NotSet => old_url.ok_or_else(|| WebhooksError::MissingUrl(name.to_owned()))?, - Setting::Reset => return Err(WebhooksError::MissingUrl(name.to_owned())), + Setting::NotSet => old_url.ok_or_else(|| WebhooksError::MissingUrl(uuid.to_owned()))?, + Setting::Reset => return Err(WebhooksError::MissingUrl(uuid.to_owned())), }; let headers = match new_webhook.headers { @@ -246,7 +270,7 @@ fn patch_webhooks_inner( }; if headers.len() > 200 { - return Err(WebhooksError::TooManyHeaders(name.to_owned())); + return Err(WebhooksError::TooManyHeaders(uuid.to_owned())); } Ok(Webhook { url, headers }) @@ -258,19 +282,19 @@ fn patch_webhooks_inner( match new_webhooks.webhooks { Setting::Set(new_webhooks) => { - for (name, new_webhook) in new_webhooks { - if name.starts_with('_') { - return Err(WebhooksError::ReservedWebhook(name).into()); + for (uuid, new_webhook) in new_webhooks { + if uuid.is_nil() { + return Err(WebhooksError::ReservedWebhook(uuid).into()); } match new_webhook { Setting::Set(new_webhook) => { - let old_webhook = webhooks.remove(&name); - let webhook = merge_webhook(&name, old_webhook, new_webhook)?; - webhooks.insert(name.clone(), webhook); + let old_webhook = webhooks.remove(&uuid); + let webhook = merge_webhook(&uuid, old_webhook, new_webhook)?; + webhooks.insert(uuid, webhook); } Setting::Reset => { - webhooks.remove(&name); + webhooks.remove(&uuid); } Setting::NotSet => (), } @@ -298,25 +322,34 @@ fn patch_webhooks_inner( tag = "Webhooks", security(("Bearer" = ["webhooks.get", "*.get", "*"])), responses( - (status = 200, description = "Webhook found", body = WebhookSettings, content_type = "application/json"), + (status = 200, description = "Webhook found", body = WebhookSettings, content_type = "application/json", example = json!({ + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://your.site/on-tasks-completed", + "headers": { + "Authorization": "Bearer a-secret" + }, + "isEditable": true + })), (status = 404, description = "Webhook not found", body = ResponseError, content_type = "application/json"), (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json"), ), params( - ("name" = String, Path, description = "The name of the webhook") + ("uuid" = Uuid, Path, description = "The universally unique identifier of the webhook") ) )] async fn get_webhook( index_scheduler: GuardedData, Data>, - name: Path, + uuid: Path, ) -> Result { - let webhook_name = name.into_inner(); - let webhooks = index_scheduler.webhooks(); + let uuid = uuid.into_inner(); + let mut webhooks = index_scheduler.webhooks(); - if let Some(webhook) = webhooks.webhooks.get(&webhook_name) { - debug!(returns = ?webhook, "Get webhook {}", webhook_name); - Ok(HttpResponse::Ok().json(webhook)) - } else { - Err(WebhooksError::WebhookNotFound(webhook_name).into()) - } + let webhook = webhooks.webhooks.remove(&uuid).ok_or(WebhooksError::WebhookNotFound(uuid))?; + + debug!(returns = ?webhook, "Get webhook {}", uuid); + Ok(HttpResponse::Ok().json(WebhookWithMetadata { + uuid, + is_editable: uuid != Uuid::nil(), + webhook, + })) } diff --git a/crates/meilisearch/tests/tasks/webhook.rs b/crates/meilisearch/tests/tasks/webhook.rs index 12a8228fa..7fa088eb5 100644 --- a/crates/meilisearch/tests/tasks/webhook.rs +++ b/crates/meilisearch/tests/tasks/webhook.rs @@ -145,13 +145,14 @@ async fn cli_only() { server_handle.abort(); } - #[actix_web::test] async fn cli_with_dumps() { let db_path = tempfile::tempdir().unwrap(); let server = Server::new_with_options(Opt { task_webhook_url: Some(Url::parse("http://defined-in-test-cli.com").unwrap()), - task_webhook_authorization_header: Some(String::from("Bearer a-secret-token-defined-in-test-cli")), + task_webhook_authorization_header: Some(String::from( + "Bearer a-secret-token-defined-in-test-cli", + )), import_dump: Some(PathBuf::from("../dump/tests/assets/v6-with-webhooks.dump")), ..default_settings(db_path.path()) })