From f0e7bea5c8038e41f2272b21db52e7b1084e0f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:34:01 +0200 Subject: [PATCH] auth etc --- rust/Cargo.lock | 1 + rust/Cargo.toml | 13 +++- rust/src/auth.rs | 100 ++++++++++++++++++------------ rust/src/{cmd.rs => cli.rs} | 20 ++++-- rust/src/error.rs | 82 +++++++++++++++++++----- rust/src/lib.rs | 4 +- rust/src/main.rs | 19 +++--- rust/src/telemetry/metrics.rs | 6 ++ rust/src/telemetry/tracing/mod.rs | 43 +++++++++---- 9 files changed, 202 insertions(+), 86 deletions(-) rename rust/src/{cmd.rs => cli.rs} (86%) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f896ae6..6d9efa8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1436,6 +1436,7 @@ dependencies = [ "hyper", "log", "maud", + "metrics", "opentelemetry", "opentelemetry-jaeger", "serde", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c0b943d..bae2511 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,6 +7,13 @@ edition = "2021" name = "packager" path = "src/main.rs" +[features] +jaeger = [] +prometheus = [] +tokio-console = [] + +default = ["jaeger", "prometheus", "tokio-console"] + [profile.dev] opt-level = 0 lto = "off" @@ -105,6 +112,8 @@ features = ["derive"] [dependencies.serde_variant] version = "0.1" -[dependencies] -axum-prometheus = "0.4" +[dependencies.axum-prometheus] +version = "0.4" +[dependencies.metrics] +version = "0.21" diff --git a/rust/src/auth.rs b/rust/src/auth.rs index d4e8f44..f2aef9d 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,10 +1,13 @@ use axum::{extract::State, middleware::Next, response::IntoResponse}; +use futures::FutureExt; use tracing::Instrument; use hyper::Request; +use crate::models::user::User; + use super::models; -use super::{AppState, Error, RequestError}; +use super::{AppState, AuthError, Error}; #[derive(Clone, Debug)] pub enum Config { @@ -18,56 +21,73 @@ pub async fn authorize( mut request: Request, next: Next, ) -> Result { - let current_user = async { - let user = match state.auth_config { + let user = async { + let auth: Result, Error> = match state.auth_config { Config::Disabled { assume_user } => { let user = match models::user::User::find_by_name(&state.database_pool, &assume_user) .await? { - Some(user) => user, - None => { - return Err(Error::Request(RequestError::AuthenticationUserNotFound { - username: assume_user, - })) - } + Some(user) => Ok(user), + None => Err(AuthError::AuthenticationUserNotFound { + username: assume_user, + }), }; - tracing::info!(?user, "auth disabled, requested user exists"); - user + Ok(user) } - Config::Enabled => { - let Some(username) = request.headers().get("x-auth-username") else { - return Err(Error::Request(RequestError::AuthenticationHeaderMissing)); - }; - let username = username - .to_str() - .map_err(|error| { - Error::Request(RequestError::AuthenticationHeaderInvalid { - message: error.to_string(), - }) - })? - .to_string(); - - let user = match models::user::User::find_by_name(&state.database_pool, &username) - .await? - { - Some(user) => user, - None => { - tracing::warn!(username, "auth rejected, user not found"); - return Err(Error::Request(RequestError::AuthenticationUserNotFound { - username, - })); + Config::Enabled => match request.headers().get("x-auth-username") { + None => Ok(Err(AuthError::AuthenticationHeaderMissing)), + Some(username) => match username.to_str() { + Err(e) => Ok(Err(AuthError::AuthenticationHeaderInvalid { + message: e.to_string(), + })), + Ok(username) => { + match models::user::User::find_by_name(&state.database_pool, &username) + .await? + { + Some(user) => Ok(Ok(user)), + None => Ok(Err(AuthError::AuthenticationUserNotFound { + username: username.to_string(), + })), + } } - }; - tracing::info!(?user, "auth successful"); - user - } + }, + }, }; - Ok(user) + + auth } .instrument(tracing::debug_span!("authorize")) - .await?; + .inspect(|r| { + if let Ok(auth) = r { + match auth { + Ok(user) => tracing::debug!(?user, "auth successful"), + Err(e) => e.trace(), + } + } + }) + .map(|r| { + r.map(|auth| { + metrics::counter!( + format!("packager_auth_{}_total", { + match auth { + Ok(_) => "success".to_string(), + Err(ref e) => format!("failure_{}", e.to_prom_metric_name()), + } + }), + 1, + &match &auth { + Ok(user) => vec![("username", user.username.clone())], + Err(e) => e.to_prom_labels(), + } + ); + auth + }) + }) + // outer result: failure of the process, e.g. database connection failed + // inner result: auth rejected, with AuthError + .await??; - request.extensions_mut().insert(current_user); + request.extensions_mut().insert(user); Ok(next.run(request).await) } diff --git a/rust/src/cmd.rs b/rust/src/cli.rs similarity index 86% rename from rust/src/cmd.rs rename to rust/src/cli.rs index 8279cf1..3ea0ea5 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cli.rs @@ -1,4 +1,8 @@ -use crate::{Error, StartError}; +use crate::Error; + +#[cfg(feature = "prometheus")] +use crate::StartError; + use clap::{Parser, Subcommand, ValueEnum}; #[derive(ValueEnum, Clone, Copy, Debug)] @@ -39,18 +43,23 @@ pub struct Args { #[arg(long)] pub database_url: String, + #[cfg(feature = "jaeger")] #[arg(long, value_enum, default_value_t = BoolArg::False)] pub enable_opentelemetry: BoolArg, + #[cfg(feature = "tokio-console")] #[arg(long, value_enum, default_value_t = BoolArg::False)] pub enable_tokio_console: BoolArg, + #[cfg(feature = "prometheus")] #[arg(long, value_enum, default_value_t = BoolArg::False)] pub enable_prometheus: BoolArg, + #[cfg(feature = "prometheus")] #[arg(long, value_enum, required_if_eq("enable_prometheus", BoolArg::True))] pub prometheus_port: Option, + #[cfg(feature = "prometheus")] #[arg(long, value_enum, required_if_eq("enable_prometheus", BoolArg::True))] pub prometheus_bind: Option, @@ -99,14 +108,15 @@ impl Args { pub fn get() -> Result { let args = Args::parse(); + #[cfg(feature = "prometheus")] if !args.enable_prometheus.bool() && (args.prometheus_port.is_some() || args.prometheus_bind.is_some()) { - Err(Error::Start(StartError::CallError { + return Err(Error::Start(StartError::CallError { message: "do not set prometheus options when prometheus is not enabled".to_string(), - })) - } else { - Ok(args) + })); } + + Ok(args) } } diff --git a/rust/src/error.rs b/rust/src/error.rs index b786f16..7fb52be 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -15,9 +15,7 @@ pub enum RequestError { RefererNotFound, RefererInvalid { message: String }, NotFound { message: String }, - AuthenticationUserNotFound { username: String }, - AuthenticationHeaderMissing, - AuthenticationHeaderInvalid { message: String }, + Auth { inner: AuthError }, Transport { inner: hyper::Error }, } @@ -30,12 +28,8 @@ impl fmt::Display for RequestError { Self::RefererNotFound => write!(f, "Referer header not found"), Self::RefererInvalid { message } => write!(f, "Referer header invalid: {message}"), Self::NotFound { message } => write!(f, "Not found: {message}"), - Self::AuthenticationUserNotFound { username } => { - write!(f, "User \"{username}\" not found") - } - Self::AuthenticationHeaderMissing => write!(f, "Authentication header not found"), - Self::AuthenticationHeaderInvalid { message } => { - write!(f, "Authentication header invalid: {message}") + Self::Auth { inner } => { + write!(f, "Authentication failed: {inner}") } Self::Transport { inner } => { write!(f, "HTTP error: {inner}") @@ -44,6 +38,67 @@ impl fmt::Display for RequestError { } } +#[derive(Debug)] +pub enum AuthError { + AuthenticationUserNotFound { username: String }, + AuthenticationHeaderMissing, + AuthenticationHeaderInvalid { message: String }, +} + +impl AuthError { + pub fn to_prom_metric_name(&self) -> &'static str { + match self { + Self::AuthenticationUserNotFound { username: _ } => "user_not_found", + Self::AuthenticationHeaderMissing => "header_missing", + Self::AuthenticationHeaderInvalid { message: _ } => "header_invalid", + } + } + + pub fn trace(&self) { + match self { + Self::AuthenticationUserNotFound { username } => { + tracing::info!(username, "auth failed, user not found") + } + Self::AuthenticationHeaderMissing => tracing::info!("auth failed, auth header missing"), + Self::AuthenticationHeaderInvalid { message } => { + tracing::info!(message, "auth failed, auth header invalid") + } + } + } +} + +impl<'a> AuthError { + pub fn to_prom_labels(&'a self) -> Vec<(&'static str, String)> { + match self { + Self::AuthenticationUserNotFound { username } => vec![("username", username.clone())], + Self::AuthenticationHeaderMissing => vec![], + Self::AuthenticationHeaderInvalid { message: _ } => vec![], + } + } +} + +impl fmt::Display for AuthError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::AuthenticationUserNotFound { username } => { + write!(f, "User \"{username}\" not found") + } + Self::AuthenticationHeaderMissing => write!(f, "Authentication header not found"), + Self::AuthenticationHeaderInvalid { message } => { + write!(f, "Authentication header invalid: {message}") + } + } + } +} + +impl From for Error { + fn from(e: AuthError) -> Self { + Self::Request(RequestError::Auth { inner: e }) + } +} + +impl std::error::Error for AuthError {} + #[derive(Debug)] pub enum Error { Model(models::Error), @@ -136,14 +191,9 @@ impl IntoResponse for Error { StatusCode::NOT_FOUND, view::ErrorPage::build(&format!("not found: {message}")), ), - RequestError::AuthenticationUserNotFound { username: _ } => ( - StatusCode::BAD_REQUEST, - view::ErrorPage::build(&request_error.to_string()), - ), - RequestError::AuthenticationHeaderMissing - | RequestError::AuthenticationHeaderInvalid { message: _ } => ( + RequestError::Auth { inner: e } => ( StatusCode::UNAUTHORIZED, - view::ErrorPage::build(&request_error.to_string()), + view::ErrorPage::build(&format!("authentication failed: {e}")), ), RequestError::Transport { inner } => ( StatusCode::INTERNAL_SERVER_ERROR, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9fbae7d..4072b4d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,7 +3,7 @@ use uuid::Uuid; use std::fmt; pub mod auth; -pub mod cmd; +pub mod cli; pub mod error; pub mod htmx; pub mod models; @@ -13,7 +13,7 @@ pub mod telemetry; mod view; -pub use error::{CommandError, Error, RequestError, StartError}; +pub use error::{AuthError, CommandError, Error, RequestError, StartError}; #[derive(Clone, Debug)] pub struct AppState { diff --git a/rust/src/main.rs b/rust/src/main.rs index 57d623e..8f4bb5e 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -4,7 +4,7 @@ use std::process::ExitCode; use std::str::FromStr; use packager::{ - auth, cmd, models, routing, sqlite, telemetry, AppState, ClientState, Error, StartError, + auth, cli, models, routing, sqlite, telemetry, AppState, ClientState, Error, StartError, }; struct MainResult(Result<(), Error>); @@ -35,17 +35,19 @@ impl From for MainResult { #[tokio::main] async fn main() -> MainResult { - let args = match cmd::Args::get() { + let args = match cli::Args::get() { Ok(args) => args, Err(e) => return e.into(), }; - telemetry::tracing::init_tracing( + telemetry::tracing::init( + #[cfg(feature = "jaeger")] if args.enable_opentelemetry.into() { telemetry::tracing::OpenTelemetryConfig::Enabled } else { telemetry::tracing::OpenTelemetryConfig::Disabled }, + #[cfg(feature = "tokio-console")] if args.enable_tokio_console.into() { telemetry::tracing::TokioConsoleConfig::Enabled } else { @@ -55,7 +57,7 @@ async fn main() -> MainResult { |args| -> Pin>> { Box::pin(async move { match args.command { - cmd::Command::Serve(serve_args) => { + cli::Command::Serve(serve_args) => { if let Err(e) = sqlite::migrate(&args.database_url).await { return <_ as Into>::into(e).into(); } @@ -84,6 +86,7 @@ async fn main() -> MainResult { let mut join_set = tokio::task::JoinSet::new(); + #[cfg(feature = "prometheus")] let app = if args.enable_prometheus.into() { // we `require_if()` prometheus port & bind when `enable_prometheus` is set, so // this cannot fail @@ -145,9 +148,9 @@ async fn main() -> MainResult { return result.into(); } - cmd::Command::Admin(admin_command) => match admin_command { - cmd::Admin::User(cmd) => match cmd { - cmd::UserCommand::Create(user) => { + cli::Command::Admin(admin_command) => match admin_command { + cli::Admin::User(cmd) => match cmd { + cli::UserCommand::Create(user) => { let database_pool = match sqlite::init_database_pool(&args.database_url).await { Ok(pool) => pool, @@ -183,7 +186,7 @@ async fn main() -> MainResult { } }, }, - cmd::Command::Migrate => { + cli::Command::Migrate => { if let Err(e) = sqlite::migrate(&args.database_url).await { return <_ as Into>::into(e).into(); } diff --git a/rust/src/telemetry/metrics.rs b/rust/src/telemetry/metrics.rs index 20f28d0..dd643cc 100644 --- a/rust/src/telemetry/metrics.rs +++ b/rust/src/telemetry/metrics.rs @@ -7,6 +7,12 @@ use axum_prometheus::{Handle, MakeDefaultHandle, PrometheusMetricLayerBuilder}; use crate::{Error, StartError}; +pub struct LabelBool(bool); + +/// Serves metrics on the specified `addr`. +/// +/// You will get two outputs back: Another router, and a task that you have +/// to run to actually spawn the metrics server endpoint pub fn prometheus_server( router: Router, addr: std::net::SocketAddr, diff --git a/rust/src/telemetry/tracing/mod.rs b/rust/src/telemetry/tracing/mod.rs index 4496ced..5c70188 100644 --- a/rust/src/telemetry/tracing/mod.rs +++ b/rust/src/telemetry/tracing/mod.rs @@ -22,6 +22,7 @@ use tracing::Instrument; use uuid::Uuid; +#[cfg(feature = "jaeger")] use opentelemetry::{global, runtime::Tokio}; pub enum OpenTelemetryConfig { @@ -72,6 +73,7 @@ trait Forwarder { ) -> Option>>; } +#[cfg(feature = "jaeger")] fn get_jaeger_layer< T: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, >( @@ -118,34 +120,49 @@ fn get_jaeger_layer< opentelemetry_layer } -pub async fn init_tracing( - opentelemetry_config: OpenTelemetryConfig, - tokio_console_config: TokioConsoleConfig, - args: crate::cmd::Args, +pub async fn init( + #[cfg(feature = "jaeger")] opentelemetry_config: OpenTelemetryConfig, + #[cfg(feature = "tokio-console")] tokio_console_config: TokioConsoleConfig, + args: crate::cli::Args, f: Func, ) -> T where - Func: FnOnce(crate::cmd::Args) -> Pin>>, + Func: FnOnce(crate::cli::Args) -> Pin>>, T: std::process::Termination, { - let mut shutdown_functions: Vec Result<(), Box>>> = + // mut is dependent on features (it's only required when jaeger is set), so + // let's just disable the lint + #[cfg(feature = "jaeger")] + let mut shutdown_functions: Vec< + Box Result<(), Box>>, + > = vec![]; + + #[cfg(not(feature = "jaeger"))] + let shutdown_functions: Vec Result<(), Box>>> = vec![]; + #[cfg(feature = "tokio-console")] let console_layer = match tokio_console_config { TokioConsoleConfig::Enabled => Some(console_subscriber::Builder::default().spawn()), TokioConsoleConfig::Disabled => None, }; let stdout_layer = get_stdout_layer(); + + #[cfg(feature = "jaeger")] let jaeger_layer = get_jaeger_layer(opentelemetry_config, &mut shutdown_functions); - let registry = Registry::default() - .with(console_layer) - .with(jaeger_layer) - // just an example, you can actuall pass Options here for layers that might be - // set/unset at runtime - .with(stdout_layer) - .with(None::>); + let registry = Registry::default(); + + #[cfg(feature = "tokio-console")] + let registry = registry.with(console_layer); + + #[cfg(feature = "jaeger")] + let registry = registry.with(jaeger_layer); + // just an example, you can actuall pass Options here for layers that might be + // set/unset at runtime + + let registry = registry.with(stdout_layer).with(None::>); tracing::subscriber::set_global_default(registry).unwrap();