diff --git a/bin/core/src/api/listener/integrations/github.rs b/bin/core/src/api/listener/integrations/github.rs index b93a9eea4..9bdd63c40 100644 --- a/bin/core/src/api/listener/integrations/github.rs +++ b/bin/core/src/api/listener/integrations/github.rs @@ -17,6 +17,7 @@ pub struct Github; impl VerifySecret for Github { #[instrument("VerifyGithubSecret", skip_all)] fn verify_secret( + _query: &std::collections::HashMap, headers: &HeaderMap, body: &str, custom_secret: &str, @@ -53,7 +54,7 @@ struct GithubWebhookBody { } impl ExtractBranch for Github { - fn extract_branch(body: &str) -> anyhow::Result { + fn extract_branch(_query: &std::collections::HashMap, body: &str) -> anyhow::Result { let branch = serde_json::from_str::(body) .context("Failed to parse github request body")? .branch diff --git a/bin/core/src/api/listener/integrations/gitlab.rs b/bin/core/src/api/listener/integrations/gitlab.rs index 13b6cc79a..53b52541a 100644 --- a/bin/core/src/api/listener/integrations/gitlab.rs +++ b/bin/core/src/api/listener/integrations/gitlab.rs @@ -12,6 +12,7 @@ pub struct Gitlab; impl VerifySecret for Gitlab { #[instrument("VerifyGitlabSecret", skip_all)] fn verify_secret( + _query: &std::collections::HashMap, headers: &HeaderMap, _body: &str, custom_secret: &str, @@ -41,7 +42,7 @@ struct GitlabWebhookBody { } impl ExtractBranch for Gitlab { - fn extract_branch(body: &str) -> anyhow::Result { + fn extract_branch(_query: &std::collections::HashMap, body: &str) -> anyhow::Result { let branch = serde_json::from_str::(body) .context("Failed to parse gitlab request body")? .branch diff --git a/bin/core/src/api/listener/integrations/mod.rs b/bin/core/src/api/listener/integrations/mod.rs index c072b9dbf..9fa254560 100644 --- a/bin/core/src/api/listener/integrations/mod.rs +++ b/bin/core/src/api/listener/integrations/mod.rs @@ -1,4 +1,5 @@ pub mod github; pub mod gitlab; +pub mod query; use super::{ExtractBranch, VerifySecret}; diff --git a/bin/core/src/api/listener/integrations/query.rs b/bin/core/src/api/listener/integrations/query.rs new file mode 100644 index 000000000..54f1b52d3 --- /dev/null +++ b/bin/core/src/api/listener/integrations/query.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; +use axum::http::HeaderMap; +use anyhow::anyhow; + +use crate::api::listener::{ExtractBranch, VerifySecret}; + +pub struct QueryAuth; + +impl VerifySecret for QueryAuth { + fn verify_secret( + query: &HashMap, + _headers: &HeaderMap, + _body: &str, + custom_secret: &str, + ) -> anyhow::Result<()> { + if query.get("secret").map(|s| s.as_str()) == Some(custom_secret) { + Ok(()) + } else { + Err(anyhow!("Invalid or missing 'secret' query parameter")) + } + } +} + +impl ExtractBranch for QueryAuth { + fn extract_branch(query: &std::collections::HashMap, _body: &str) -> anyhow::Result { + if let Some(branch) = query.get("branch") { + Ok(branch.to_string()) + } else { + Ok("main".to_string()) + } + } +} diff --git a/bin/core/src/api/listener/mod.rs b/bin/core/src/api/listener/mod.rs index e90aeea9f..548aff558 100644 --- a/bin/core/src/api/listener/mod.rs +++ b/bin/core/src/api/listener/mod.rs @@ -18,6 +18,7 @@ pub fn router() -> Router { Router::new() .nest("/github", router::router::()) .nest("/gitlab", router::router::()) + .nest("/query", router::router::()) } type ListenerLockCache = CloneCache>>; @@ -32,6 +33,7 @@ trait CustomSecret: KomodoResource { /// Implemented on the integration struct, eg [integrations::github::Github] trait VerifySecret { fn verify_secret( + query: &std::collections::HashMap, headers: &HeaderMap, body: &str, custom_secret: &str, @@ -40,9 +42,9 @@ trait VerifySecret { /// Implemented on the integration struct, eg [integrations::github::Github] trait ExtractBranch { - fn extract_branch(body: &str) -> anyhow::Result; - fn verify_branch(body: &str, expected: &str) -> anyhow::Result<()> { - let branch = Self::extract_branch(body)?; + fn extract_branch(query: &std::collections::HashMap, body: &str) -> anyhow::Result; + fn verify_branch(query: &std::collections::HashMap, body: &str, expected: &str) -> anyhow::Result<()> { + let branch = Self::extract_branch(query, body)?; if branch == expected { Ok(()) } else { diff --git a/bin/core/src/api/listener/resources.rs b/bin/core/src/api/listener/resources.rs index 2f1be4964..fbb94ed10 100644 --- a/bin/core/src/api/listener/resources.rs +++ b/bin/core/src/api/listener/resources.rs @@ -43,6 +43,7 @@ fn build_locks() -> &'static ListenerLockCache { } pub async fn handle_build_webhook( + query: &std::collections::HashMap, build: Build, body: String, ) -> anyhow::Result<()> { @@ -67,7 +68,7 @@ pub async fn handle_build_webhook( .branch }; - B::verify_branch(&body, &branch)?; + B::verify_branch(query, &body, &branch)?; let user = git_webhook_user().to_owned(); let req = ExecuteRequest::RunBuild(RunBuild { build: build.id }); @@ -186,19 +187,20 @@ pub enum RepoWebhookOption { } pub async fn handle_repo_webhook( + query: &std::collections::HashMap, option: RepoWebhookOption, repo: Repo, body: String, ) -> anyhow::Result<()> { match option { RepoWebhookOption::Clone => { - handle_repo_webhook_inner::(repo, body).await + handle_repo_webhook_inner::(query, repo, body).await } RepoWebhookOption::Pull => { - handle_repo_webhook_inner::(repo, body).await + handle_repo_webhook_inner::(query, repo, body).await } RepoWebhookOption::Build => { - handle_repo_webhook_inner::(repo, body).await + handle_repo_webhook_inner::(query, repo, body).await } } } @@ -207,6 +209,7 @@ async fn handle_repo_webhook_inner< B: super::ExtractBranch, E: RepoExecution, >( + query: &std::collections::HashMap, repo: Repo, body: String, ) -> anyhow::Result<()> { @@ -220,7 +223,7 @@ async fn handle_repo_webhook_inner< let lock = repo_locks().get_or_insert_default(&repo.id).await; let _lock = lock.lock().await; - B::verify_branch(&body, &repo.config.branch)?; + B::verify_branch(query, &body, &repo.config.branch)?; E::resolve(repo).await } @@ -308,17 +311,18 @@ pub enum StackWebhookOption { } pub async fn handle_stack_webhook( + query: &std::collections::HashMap, option: StackWebhookOption, stack: Stack, body: String, ) -> anyhow::Result<()> { match option { StackWebhookOption::Refresh => { - handle_stack_webhook_inner::(stack, body) + handle_stack_webhook_inner::(query, stack, body) .await } StackWebhookOption::Deploy => { - handle_stack_webhook_inner::(stack, body).await + handle_stack_webhook_inner::(query, stack, body).await } } } @@ -327,6 +331,7 @@ pub async fn handle_stack_webhook_inner< B: super::ExtractBranch, E: StackExecution, >( + query: &std::collections::HashMap, stack: Stack, body: String, ) -> anyhow::Result<()> { @@ -351,7 +356,7 @@ pub async fn handle_stack_webhook_inner< .branch }; - B::verify_branch(&body, &branch)?; + B::verify_branch(query, &body, &branch)?; E::resolve(stack).await.map_err(|e| e.error) } @@ -419,6 +424,7 @@ pub enum SyncWebhookOption { } pub async fn handle_sync_webhook( + query: &std::collections::HashMap, option: SyncWebhookOption, sync: ResourceSync, body: String, @@ -426,12 +432,12 @@ pub async fn handle_sync_webhook( match option { SyncWebhookOption::Refresh => { handle_sync_webhook_inner::( - sync, body, + query, sync, body, ) .await } SyncWebhookOption::Sync => { - handle_sync_webhook_inner::(sync, body).await + handle_sync_webhook_inner::(query, sync, body).await } } } @@ -440,6 +446,7 @@ async fn handle_sync_webhook_inner< B: super::ExtractBranch, E: SyncExecution, >( + query: &std::collections::HashMap, sync: ResourceSync, body: String, ) -> anyhow::Result<()> { @@ -464,7 +471,7 @@ async fn handle_sync_webhook_inner< .branch }; - B::verify_branch(&body, &branch)?; + B::verify_branch(query, &body, &branch)?; E::resolve(sync).await } @@ -486,6 +493,7 @@ fn procedure_locks() -> &'static ListenerLockCache { } pub async fn handle_procedure_webhook( + query: &std::collections::HashMap, procedure: Procedure, target_branch: &str, body: String, @@ -502,7 +510,7 @@ pub async fn handle_procedure_webhook( let _lock = lock.lock().await; if target_branch != ANY_BRANCH { - B::verify_branch(&body, target_branch)?; + B::verify_branch(query, &body, target_branch)?; } let user = git_webhook_user().to_owned(); @@ -540,6 +548,7 @@ fn action_locks() -> &'static ListenerLockCache { } pub async fn handle_action_webhook( + query: &std::collections::HashMap, action: Action, target_branch: &str, body: String, @@ -554,7 +563,7 @@ pub async fn handle_action_webhook( let lock = action_locks().get_or_insert_default(&action.id).await; let _lock = lock.lock().await; - let branch = B::extract_branch(&body)?; + let branch = B::extract_branch(query, &body)?; if target_branch != ANY_BRANCH && branch != target_branch { return Err(anyhow!("request branch does not match expected")); @@ -562,8 +571,12 @@ pub async fn handle_action_webhook( let user = git_webhook_user().to_owned(); - let body = serde_json::Value::from_str(&body) - .context("Failed to deserialize webhook body")?; + let body = if body.trim().is_empty() { + serde_json::Value::Null + } else { + serde_json::Value::from_str(&body) + .context("Failed to deserialize webhook body")? + }; let serde_json::Value::Object(args) = json!({ "WEBHOOK_BRANCH": branch, "WEBHOOK_BODY": body, diff --git a/bin/core/src/api/listener/router.rs b/bin/core/src/api/listener/router.rs index 350573015..ef45338d5 100644 --- a/bin/core/src/api/listener/router.rs +++ b/bin/core/src/api/listener/router.rs @@ -51,14 +51,14 @@ pub fn router() -> Router { .route( "/build/{id}", post( - |Path(Id { id }), RequestIp(ip), headers: HeaderMap, body: String| async move { + |Path(Id { id }), axum::extract::Query(query): axum::extract::Query>, RequestIp(ip), headers: HeaderMap, body: String| async move { let build = - auth_webhook::(&id, &headers, ip, &body).await?; + auth_webhook::(&id, &query, &headers, ip, &body).await?; tokio::spawn(async move { let span = info_span!("BuildWebhook", id); async { let res = handle_build_webhook::

( - build, body, + &query, build, body, ) .await; if let Err(e) = res { @@ -77,14 +77,14 @@ pub fn router() -> Router { .route( "/repo/{id}/{option}", post( - |Path(IdAndOption:: { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move { + |Path(IdAndOption:: { id, option }), axum::extract::Query(query): axum::extract::Query>, RequestIp(ip), headers: HeaderMap, body: String| async move { let repo = - auth_webhook::(&id, &headers, ip, &body).await?; + auth_webhook::(&id, &query, &headers, ip, &body).await?; tokio::spawn(async move { let span = info_span!("RepoWebhook", id); async { let res = handle_repo_webhook::

( - option, repo, body, + &query, option, repo, body, ) .await; if let Err(e) = res { @@ -103,14 +103,14 @@ pub fn router() -> Router { .route( "/stack/{id}/{option}", post( - |Path(IdAndOption:: { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move { + |Path(IdAndOption:: { id, option }), axum::extract::Query(query): axum::extract::Query>, RequestIp(ip), headers: HeaderMap, body: String| async move { let stack = - auth_webhook::(&id, &headers, ip, &body).await?; + auth_webhook::(&id, &query, &headers, ip, &body).await?; tokio::spawn(async move { let span = info_span!("StackWebhook", id); async { let res = handle_stack_webhook::

( - option, stack, body, + &query, option, stack, body, ) .await; if let Err(e) = res { @@ -129,14 +129,14 @@ pub fn router() -> Router { .route( "/sync/{id}/{option}", post( - |Path(IdAndOption:: { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move { + |Path(IdAndOption:: { id, option }), axum::extract::Query(query): axum::extract::Query>, RequestIp(ip), headers: HeaderMap, body: String| async move { let sync = - auth_webhook::(&id, &headers, ip, &body).await?; + auth_webhook::(&id, &query, &headers, ip, &body).await?; tokio::spawn(async move { let span = info_span!("ResourceSyncWebhook", id); async { let res = handle_sync_webhook::

( - option, sync, body, + &query, option, sync, body, ) .await; if let Err(e) = res { @@ -155,14 +155,14 @@ pub fn router() -> Router { .route( "/procedure/{id}/{branch}", post( - |Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move { + |Path(IdAndBranch { id, branch }), axum::extract::Query(query): axum::extract::Query>, RequestIp(ip), headers: HeaderMap, body: String| async move { let procedure = - auth_webhook::(&id, &headers, ip, &body).await?; + auth_webhook::(&id, &query, &headers, ip, &body).await?; tokio::spawn(async move { let span = info_span!("ProcedureWebhook", id); async { let res = handle_procedure_webhook::

( - procedure, &branch, body, + &query, procedure, &branch, body, ) .await; if let Err(e) = res { @@ -181,14 +181,14 @@ pub fn router() -> Router { .route( "/action/{id}/{branch}", post( - |Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move { + |Path(IdAndBranch { id, branch }), axum::extract::Query(query): axum::extract::Query>, RequestIp(ip), headers: HeaderMap, body: String| async move { let action = - auth_webhook::(&id, &headers, ip, &body).await?; + auth_webhook::(&id, &query, &headers, ip, &body).await?; tokio::spawn(async move { let span = info_span!("ActionWebhook", id); async { let res = handle_action_webhook::

( - action, &branch, body, + &query, action, &branch, body, ) .await; if let Err(e) = res { @@ -208,6 +208,7 @@ pub fn router() -> Router { async fn auth_webhook( id: &str, + query: &std::collections::HashMap, headers: &HeaderMap, ip: IpAddr, body: &str, @@ -220,7 +221,7 @@ where let resource = crate::resource::get::(id) .await .status_code(StatusCode::BAD_REQUEST)?; - P::verify_secret(headers, body, R::custom_secret(&resource)) + P::verify_secret(query, headers, body, R::custom_secret(&resource)) .status_code(StatusCode::UNAUTHORIZED)?; info!( diff --git a/ui/src/components/webhook/builder.tsx b/ui/src/components/webhook/builder.tsx index ebd83ce29..c042f53f3 100644 --- a/ui/src/components/webhook/builder.tsx +++ b/ui/src/components/webhook/builder.tsx @@ -30,7 +30,7 @@ export default function WebhookBuilder({ integration && setIntegration(gitProvider, integration as WebhookIntegration) } - data={["Github", "Gitlab"]} + data={["Github", "Gitlab", "Query"]} w={{ base: "100%", sm: 200 }} />