fix(server): invalid webhook authorisations
Some checks failed
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline failed
ci/woodpecker/push/tag-created Pipeline was successful
Rust / build (push) Successful in 1m4s

Parameters had been passed in wrong order. Added strong types to prevent
a repeat.
This commit is contained in:
Paul Campbell 2024-05-20 20:41:01 +01:00
parent eabeeeda47
commit 5176e3e8c7
5 changed files with 91 additions and 72 deletions

View file

@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/cli", "crates/server", "crates/config", "crates/git"] members = ["crates/cli", "crates/server", "crates/config", "crates/git"]
[workspace.package] [workspace.package]
version = "0.5.0" version = "0.5.1"
edition = "2021" edition = "2021"
[workspace.lints.clippy] [workspace.lints.clippy]

View file

@ -24,8 +24,9 @@ pub struct WebhookId(String);
pub struct WebhookAuth(ulid::Ulid); pub struct WebhookAuth(ulid::Ulid);
impl WebhookAuth { impl WebhookAuth {
pub fn from_str(authorisation: &str) -> Result<Self, DecodeError> { pub fn from_str(authorisation: &str) -> Result<Self, DecodeError> {
let id = ulid::Ulid::from_str(authorisation); let id = ulid::Ulid::from_str(authorisation)?;
Ok(Self(id?)) info!("Parse auth token: {}", id);
Ok(Self(id))
} }
fn generate() -> Self { fn generate() -> Self {
@ -186,23 +187,16 @@ impl Handler<WebhookMessage> for RepoActor {
warn!("Don't know what authorization to expect"); warn!("Don't know what authorization to expect");
return; return;
}; };
let Some(received_authorization) = &msg.authorisation() else { if msg.authorisation() != expected_authorization {
warn!("Missing authorization token");
return;
};
if received_authorization != expected_authorization {
warn!( warn!(
"Invalid authorization - expected {}", "Invalid authorization - expected {}",
expected_authorization expected_authorization
); );
return; return;
} }
let id = msg.id();
let span = tracing::info_span!("handle", %id);
let _guard = span.enter();
let body = msg.body(); let body = msg.body();
match serde_json::from_str::<Push>(body) { match serde_json::from_str::<Push>(body.as_str()) {
Err(err) => warn!(?err, %body, "Not a 'push'"), Err(err) => warn!(?err, ?body, "Not a 'push'"),
Ok(push) => { Ok(push) => {
if let Some(config) = &self.details.repo_config { if let Some(config) = &self.details.repo_config {
match push.branch(config.branches()) { match push.branch(config.branches()) {
@ -262,6 +256,10 @@ impl Handler<WebhookMessage> for RepoActor {
} }
} }
pub fn split_ref(reference: &str) -> (&str, &str) {
reference.split_at(11)
}
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
struct Push { struct Push {
#[serde(rename = "ref")] #[serde(rename = "ref")]
@ -298,12 +296,8 @@ impl Push {
} }
} }
pub fn split_ref(reference: &str) -> (&str, &str) {
reference.split_at(11)
}
#[derive(Debug)] #[derive(Debug)]
enum Branch { pub enum Branch {
Main, Main,
Next, Next,
Dev, Dev,

View file

@ -1,27 +1,33 @@
// //
use actix::prelude::*; use actix::prelude::*;
use git_next_config::RepoAlias;
use crate::actors::repo::webhook::WebhookAuth; use crate::actors::repo::webhook::WebhookAuth;
#[derive(Message, Debug, Clone, derive_more::Constructor)] #[derive(Message, Debug, Clone, derive_more::Constructor)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct WebhookMessage { pub struct WebhookMessage {
id: String, // forge // TODO: differentiate between multiple forges
path: String, repo_alias: RepoAlias,
authorisation: String, authorisation: WebhookAuth,
body: String, body: Body,
} }
impl WebhookMessage { impl WebhookMessage {
pub const fn id(&self) -> &String { pub const fn repo_alias(&self) -> &RepoAlias {
&self.id &self.repo_alias
} }
pub const fn path(&self) -> &String { pub const fn body(&self) -> &Body {
&self.path
}
pub const fn body(&self) -> &String {
&self.body &self.body
} }
pub fn authorisation(&self) -> Option<WebhookAuth> { pub const fn authorisation(&self) -> &WebhookAuth {
WebhookAuth::from_str(&self.authorisation).ok() &self.authorisation
}
}
#[derive(Clone, Debug, derive_more::Constructor)]
pub struct Body(String);
impl Body {
pub fn as_str(&self) -> &str {
self.0.as_str()
} }
} }

View file

@ -28,9 +28,9 @@ impl Handler<WebhookMessage> for WebhookRouter {
fn handle(&mut self, msg: WebhookMessage, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: WebhookMessage, _ctx: &mut Self::Context) -> Self::Result {
let _gaurd = self.span.enter(); let _gaurd = self.span.enter();
let repo_alias = RepoAlias::new(msg.path()); let repo_alias = msg.repo_alias();
debug!(repo = %repo_alias, "Router..."); debug!(repo = %repo_alias, "Router...");
if let Some(recipient) = self.repos.get(&repo_alias) { if let Some(recipient) = self.repos.get(repo_alias) {
info!(repo = %repo_alias, "Sending to Recipient"); info!(repo = %repo_alias, "Sending to Recipient");
recipient.do_send(msg); recipient.do_send(msg);
} }

View file

@ -2,13 +2,17 @@ use std::net::SocketAddr;
use actix::prelude::*; use actix::prelude::*;
use git_next_config::RepoAlias;
use tracing::{info, warn}; use tracing::{info, warn};
use warp::reject::Rejection;
use crate::actors::webhook::message::WebhookMessage; use crate::actors::{repo::webhook::WebhookAuth, webhook::message::WebhookMessage};
use super::message;
pub async fn start( pub async fn start(
socket_addr: SocketAddr, socket_addr: SocketAddr,
address: actix::prelude::Recipient<super::message::WebhookMessage>, address: actix::prelude::Recipient<message::WebhookMessage>,
) { ) {
// start webhook server // start webhook server
use warp::Filter; use warp::Filter;
@ -21,51 +25,42 @@ pub async fn start(
.and(warp::body::bytes()) .and(warp::body::bytes())
.and_then( .and_then(
|recipient: Recipient<WebhookMessage>, |recipient: Recipient<WebhookMessage>,
path, path: String,
// query: String, // query: String,
headers: warp::http::HeaderMap, headers: warp::http::HeaderMap,
body: bytes::Bytes| async move { body: bytes::Bytes| async move {
info!("POST received"); info!("POST received");
let repo_alias = RepoAlias::new(path);
let bytes = body.to_vec(); let bytes = body.to_vec();
let request_data = String::from_utf8_lossy(&bytes).to_string(); let body = message::Body::new(String::from_utf8_lossy(&bytes).to_string());
let id = ulid::Ulid::new().to_string(); headers.get("Authorization").map_or_else(
match headers.get("Authorization") { || {
Some(auhorisation) => {
info!(id, path, "Received webhook");
let authorisation = auhorisation
.to_str()
.map_err(|e| {
warn!("Invalid value in authorization: {:?}", e);
warp::reject()
})? // valid characters
.strip_prefix("Basic ")
.ok_or_else(|| {
warn!("Authorization must be 'Basic'");
warp::reject()
})? // must start with "Basic "
.to_string();
let message = WebhookMessage::new(
id,
path,
/* query, headers, */ request_data,
authorisation,
);
recipient
.try_send(message)
.map(|_| {
info!("Message sent ok");
warp::reply::with_status("OK", warp::http::StatusCode::OK)
})
.map_err(|e| {
warn!("Unknown error: {:?}", e);
warp::reject()
})
}
_ => {
warn!("No Authorization header"); warn!("No Authorization header");
Err(warp::reject()) Err(warp::reject())
} },
} |authorisation_header| {
info!(?repo_alias, ?authorisation_header, "Received webhook",);
match parse_auth(authorisation_header) {
Ok(authorisation) => {
let message = WebhookMessage::new(repo_alias, authorisation, body);
recipient
.try_send(message)
.map(|_| {
info!("Message sent ok");
warp::reply::with_status("OK", warp::http::StatusCode::OK)
})
.map_err(|e| {
warn!("Unknown error: {:?}", e);
warp::reject()
})
}
Err(e) => {
warn!(?e, "Failed to decode authorization header");
Err(warp::reject())
}
}
},
)
}, },
); );
@ -73,3 +68,27 @@ pub async fn start(
info!("Starting webhook server: {}", socket_addr); info!("Starting webhook server: {}", socket_addr);
warp::serve(route).run(socket_addr).await; warp::serve(route).run(socket_addr).await;
} }
fn parse_auth(authorization_header: &warp::http::HeaderValue) -> Result<WebhookAuth, Rejection> {
WebhookAuth::from_str(
authorization_header
.to_str()
.map_err(|e| {
warn!("Invalid non-ascii value in authorization: {:?}", e);
warp::reject()
}) // valid characters
.map(|v| {
info!("raw auth header: {}", v);
v
})?
.strip_prefix("Basic ")
.ok_or_else(|| {
warn!("Authorization must be 'Basic'");
warp::reject()
})?, // must start with "Basic "
)
.map_err(|e| {
warn!(?e, "decode error");
warp::reject()
})
}