diff --git a/src/outpost/proxy/application.rs b/src/outpost/proxy/application.rs deleted file mode 100644 index 71726be519..0000000000 --- a/src/outpost/proxy/application.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::sync::Arc; - -use ak_client::models::ProxyOutpostConfig; -use ak_common::tls::store::Certificate; -use axum::Router; -use eyre::{Result, eyre}; -use tracing::instrument; -use url::Url; - -use crate::outpost::proxy::ProxyOutpost; - -const _REDIRECT_PARAM: &str = "rd"; -const CALLBACK_SIGNATURE: &str = "X-authentik-auth-callback"; -const _LOGOUT_SIGNATURE: &str = "X-authentik-logout"; - -#[derive(Debug)] -pub(super) struct Application { - pub(super) host: String, - pub(super) provider: ProxyOutpostConfig, - pub(super) router: Router, - pub(super) cert: Option>, -} - -impl Application { - #[instrument(skip_all)] - pub(super) async fn new(outpost: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result { - let external_url = Url::parse(&provider.external_host)?; - if !external_url.has_authority() { - return Err(eyre!("no host in external host")); - } - let external_host = external_url.authority(); - - let _old_app = outpost.apps.load().get(external_host); - - let cert = if let Some(Some(kp_uuid)) = provider.certificate { - Some( - outpost - .certificate_store - .ensure_keypair(&outpost.controller.api_config, kp_uuid) - .await?, - ) - } else { - None - }; - - let _redirect_url = { - let mut redirect_url = external_url.join("outpost.goauthentik.io/callback")?; - redirect_url.set_query(Some(&format!("{CALLBACK_SIGNATURE}=true"))); - redirect_url - }; - - let router = Router::new(); - - Ok(Self { - host: external_host.to_owned(), - provider, - router, - cert, - }) - } -} diff --git a/src/outpost/proxy/application/handlers/forward.rs b/src/outpost/proxy/application/handlers/forward.rs new file mode 100644 index 0000000000..e2888d518f --- /dev/null +++ b/src/outpost/proxy/application/handlers/forward.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use ak_axum::error::Result; +use axum::{ + extract::{Request, State}, + response::Response, +}; +use tracing::instrument; + +use crate::outpost::proxy::application::Application; + +#[instrument(skip_all)] +pub(crate) async fn handle_caddy( + State(_app): State>, + _request: Request, +) -> Result { + todo!() +} + +#[instrument(skip_all)] +pub(crate) async fn handle_envoy( + State(_app): State>, + _request: Request, +) -> Result { + todo!() +} + +#[instrument(skip_all)] +pub(crate) async fn handle_nginx( + State(_app): State>, + _request: Request, +) -> Result { + todo!() +} + +#[instrument(skip_all)] +pub(crate) async fn handle_traefik( + State(_app): State>, + _request: Request, +) -> Result { + todo!() +} diff --git a/src/outpost/proxy/application/handlers/mod.rs b/src/outpost/proxy/application/handlers/mod.rs new file mode 100644 index 0000000000..5063766b25 --- /dev/null +++ b/src/outpost/proxy/application/handlers/mod.rs @@ -0,0 +1,82 @@ +use std::str::FromStr; +use std::{fmt, sync::Arc}; + +use ak_axum::error::Result; +use axum::{ + extract::{Query, Request, State}, + response::Response, +}; +use serde::{Deserialize, Deserializer}; +use tower::util::ServiceExt as _; +use tracing::{debug, instrument}; + +use crate::outpost::proxy::application::Application; + +pub(super) mod forward; +pub(super) mod proxy; + +// TODO: move this to ak-common +fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + T::Err: fmt::Display, +{ + let opt = Option::::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => FromStr::from_str(s) + .map_err(serde::de::Error::custom) + .map(Some), + } +} + +#[derive(Deserialize)] +struct Parameters { + // #[serde(rename = "rd", default, deserialize_with = "empty_string_as_none")] + // redirect: Option, + #[serde( + rename = "X-authentik-auth-callback", + default, + deserialize_with = "empty_string_as_none" + )] + callback_signature: Option, + #[serde( + rename = "X-authentik-logout", + default, + deserialize_with = "empty_string_as_none" + )] + logout_signature: Option, +} + +#[instrument(skip_all)] +pub(crate) async fn handle(app: Arc, request: Request) -> Result { + if let Ok(query) = Query::::try_from_uri(request.uri()) { + if query.callback_signature == Some(true) { + debug!("handling OAuth Callback from querystring signature"); + return handle_auth_callback(State(app), request).await; + } + if query.logout_signature == Some(true) { + debug!("handling OAuth Logout from querystring signature"); + return handle_sign_out(State(app), request).await; + } + } + + Ok(app.router.clone().with_state(app).oneshot(request).await?) +} + +#[instrument(skip_all)] +pub(super) async fn handle_auth_callback( + State(_app): State>, + _request: Request, +) -> Result { + todo!() +} + +#[instrument(skip_all)] +pub(super) async fn handle_sign_out( + State(_app): State>, + _request: Request, +) -> Result { + todo!() +} diff --git a/src/outpost/proxy/application/handlers/proxy.rs b/src/outpost/proxy/application/handlers/proxy.rs new file mode 100644 index 0000000000..3e9a349a1e --- /dev/null +++ b/src/outpost/proxy/application/handlers/proxy.rs @@ -0,0 +1,18 @@ +use std::sync::Arc; + +use ak_axum::error::Result; +use axum::{ + extract::{Request, State}, + response::Response, +}; +use tracing::instrument; + +use crate::outpost::proxy::application::Application; + +#[instrument(skip_all)] +pub(crate) async fn handle( + State(_app): State>, + _request: Request, +) -> Result { + todo!() +} diff --git a/src/outpost/proxy/application/mod.rs b/src/outpost/proxy/application/mod.rs new file mode 100644 index 0000000000..669421cec1 --- /dev/null +++ b/src/outpost/proxy/application/mod.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; + +use ak_client::models::{ProxyMode, ProxyOutpostConfig}; +use ak_common::tls::store::Certificate; +use axum::{Router, routing::any}; +use eyre::{Result, eyre}; +use tracing::instrument; +use url::Url; + +use crate::outpost::proxy::ProxyOutpost; + +pub(super) mod handlers; + +#[derive(Debug)] +pub(super) struct Application { + pub(super) host: String, + pub(super) provider: ProxyOutpostConfig, + pub(super) router: Router>, + pub(super) cert: Option>, +} + +impl Application { + #[instrument(skip_all)] + pub(super) async fn new(outpost: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result { + let external_url = Url::parse(&provider.external_host)?; + if !external_url.has_authority() { + return Err(eyre!("no host in external host")); + } + let external_host = external_url.authority(); + + let _old_app = outpost.apps.load().get(external_host); + + let cert = if let Some(Some(kp_uuid)) = provider.certificate { + Some( + outpost + .certificate_store + .ensure_keypair(&outpost.controller.api_config, kp_uuid) + .await?, + ) + } else { + None + }; + + let router = Router::new() + // TODO: /start + .route( + "/outpost.goauthentik.io/callback", + any(handlers::handle_auth_callback), + ) + .route( + "/outpost.goauthentik.io/sign_out", + any(handlers::handle_sign_out), + ); + + let router = match provider.mode { + Some(ProxyMode::ForwardSingle | ProxyMode::ForwardDomain) => router + .route( + "/outpost.goauthentik.io/auth/caddy", + any(handlers::forward::handle_caddy), + ) + .route( + "/outpost.goauthentik.io/auth/envoy", + any(handlers::forward::handle_envoy), + ) + .route( + "/outpost.goauthentik.io/auth/nginx", + any(handlers::forward::handle_nginx), + ) + .route( + "/outpost.goauthentik.io/auth/traefik", + any(handlers::forward::handle_traefik), + ), + Some(ProxyMode::Proxy) => router.fallback(handlers::proxy::handle), + None => return Err(eyre!("no provider mode set")), + }; + + Ok(Self { + host: external_host.to_owned(), + provider, + router, + cert, + }) + } +} diff --git a/src/outpost/proxy/handlers.rs b/src/outpost/proxy/handlers.rs index a450f12980..49faca9168 100644 --- a/src/outpost/proxy/handlers.rs +++ b/src/outpost/proxy/handlers.rs @@ -9,10 +9,9 @@ use axum::{ use metrics::histogram; use serde_json::json; use tokio::time::Instant; -use tower::util::ServiceExt as _; use tracing::{Instrument as _, debug, field, info_span, instrument, trace, warn}; -use crate::outpost::proxy::ProxyOutpost; +use crate::outpost::proxy::{ProxyOutpost, application}; #[instrument(skip_all)] pub(super) async fn handle_ping( @@ -72,7 +71,9 @@ pub(super) async fn default( }; trace!("passing to application"); - let response = app.router.clone().oneshot(request).instrument(span).await?; + let response = application::handlers::handle(app, request) + .instrument(span) + .await?; histogram!( "authentik_outpost_proxy_request_duration_seconds",