use std::sync::Arc;
use axum::body::Body;
use axum::extract::{FromRequestParts, State};
use axum::http::request::Parts;
use axum::http::{Request, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum::Router;
use crate::extractors::MountedTenantContext;
use crate::sql::Pool;
use super::auth_backends::{AuthError, AuthUser, BoxedBackend};
use super::permissions;
use super::{Org, TenancyError};
#[derive(Debug, Clone)]
pub struct AuthenticatedUser {
pub id: i64,
pub username: String,
pub is_superuser: bool,
}
impl From<AuthUser> for AuthenticatedUser {
fn from(u: AuthUser) -> Self {
Self {
id: u.id,
username: u.username,
is_superuser: u.is_superuser,
}
}
}
pub struct CurrentUser(pub Option<AuthenticatedUser>);
impl<S: Send + Sync> FromRequestParts<S> for CurrentUser {
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(CurrentUser(
parts.extensions.get::<AuthenticatedUser>().cloned(),
))
}
}
pub(crate) async fn request_org(
parts: &Parts,
ext: &axum::http::Extensions,
) -> Option<Result<Option<Org>, TenancyError>> {
Some(MountedTenantContext::of(ext)?.resolve(parts).await)
}
pub(crate) struct SessionKeys<'a> {
pub session: &'a crate::tenancy::session::SessionSecret,
pub operator: &'a crate::tenancy::session::SessionSecret,
pub registry: Pool,
}
pub(crate) fn session_keys(ext: &axum::http::Extensions) -> Option<SessionKeys<'_>> {
Some(MountedTenantContext::of(ext)?.session_keys())
}
pub(crate) async fn request_pool(
ext: &axum::http::Extensions,
org: &Org,
) -> Option<Result<Pool, TenancyError>> {
Some(MountedTenantContext::of(ext)?.pool_for(org).await)
}
async fn tenant_pool(parts: &Parts, ext: &axum::http::Extensions) -> Result<(Org, Pool), Response> {
let org = match request_org(parts, ext).await {
Some(Ok(Some(org))) => org,
Some(Ok(None)) => return Err((StatusCode::NOT_FOUND, "unknown tenant").into_response()),
Some(Err(e)) => {
tracing::error!(error = %e, "require_auth: tenant resolution failed");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"tenant resolution failed",
)
.into_response());
}
None => {
tracing::error!(
"require_auth ran without a tenant context in request extensions — \
mount the tenancy layer (server::Builder::tenant_pools) ahead of it"
);
return Err(
(StatusCode::INTERNAL_SERVER_ERROR, "tenant context missing").into_response(),
);
}
};
match request_pool(ext, &org).await {
Some(Ok(pool)) => Ok((org, pool)),
Some(Err(e)) => {
tracing::error!(error = %e, slug = %org.slug, "require_auth: tenant pool failed");
Err((StatusCode::INTERNAL_SERVER_ERROR, "tenant pool unavailable").into_response())
}
None => Err((StatusCode::INTERNAL_SERVER_ERROR, "tenant context missing").into_response()),
}
}
#[derive(Clone)]
struct AuthState {
backends: Arc<Vec<BoxedBackend>>,
required: bool, }
#[derive(Clone)]
struct PermState {
codename: &'static str,
}
async fn auth_middleware(
State(state): State<AuthState>,
mut req: Request<Body>,
next: Next,
) -> Response {
let headers = req.headers().clone();
let uri = req.uri().clone();
let method = req.method().clone();
let mut builder = axum::http::Request::builder().method(&method).uri(&uri);
for (k, v) in &headers {
builder = builder.header(k, v);
}
let dummy = builder
.body(())
.unwrap_or_else(|_| axum::http::Request::new(()));
let (mut dummy_parts, _) = dummy.into_parts();
let (org, pool) = match tenant_pool(&dummy_parts, req.extensions()).await {
Ok(t) => t,
Err(resp) => return resp,
};
dummy_parts
.extensions
.insert(super::TenantSlug(org.slug.clone()));
dummy_parts
.extensions
.insert(crate::login_throttle::ClientIp::from_parts(
req.extensions(),
req.headers(),
));
let mut authenticated: Option<AuthUser> = None;
let mut error_response: Option<Response> = None;
for backend in state.backends.iter() {
match backend.authenticate(&dummy_parts, &pool).await {
Ok(Some(user)) => {
authenticated = Some(user);
break;
}
Ok(None) => {}
Err(AuthError::Inactive) => {
error_response = Some((StatusCode::FORBIDDEN, "account inactive").into_response());
break;
}
Err(AuthError::Refused(refused)) => {
error_response = Some(refused.into_response());
break;
}
Err(e) => {
error_response = Some((StatusCode::UNAUTHORIZED, e.to_string()).into_response());
break;
}
}
}
if let Some(resp) = error_response {
return resp;
}
match authenticated {
Some(user) => {
req.extensions_mut().insert(AuthenticatedUser::from(user));
req.extensions_mut().insert(super::TenantSlug(org.slug));
next.run(req).await
}
None if state.required => {
(StatusCode::UNAUTHORIZED, "authentication required").into_response()
}
None => next.run(req).await,
}
}
async fn perm_middleware(
State(state): State<PermState>,
req: Request<Body>,
next: Next,
) -> Response {
let user = req.extensions().get::<AuthenticatedUser>().cloned();
let Some(user) = user else {
return (StatusCode::UNAUTHORIZED, "authentication required").into_response();
};
let (parts, body) = req.into_parts();
let pool = match tenant_pool(&parts, &parts.extensions).await {
Ok((_, p)) => p,
Err(resp) => return resp,
};
let req = Request::from_parts(parts, body);
let ok = permissions::has_perm_pool(user.id, state.codename, &pool)
.await
.unwrap_or(false);
if !ok {
return (
StatusCode::FORBIDDEN,
format!("permission required: {}", state.codename),
)
.into_response();
}
next.run(req).await
}
pub trait RouterAuthExt<S> {
fn require_auth(self, backends: Vec<BoxedBackend>) -> Self;
fn optional_auth(self, backends: Vec<BoxedBackend>) -> Self;
fn require_perm(self, codename: &'static str) -> Self;
}
impl<S: Clone + Send + Sync + 'static> RouterAuthExt<S> for Router<S> {
fn require_auth(self, backends: Vec<BoxedBackend>) -> Self {
let state = AuthState {
backends: Arc::new(backends),
required: true,
};
self.layer(axum::middleware::from_fn_with_state(state, auth_middleware))
}
fn optional_auth(self, backends: Vec<BoxedBackend>) -> Self {
let state = AuthState {
backends: Arc::new(backends),
required: false,
};
self.layer(axum::middleware::from_fn_with_state(state, auth_middleware))
}
fn require_perm(self, codename: &'static str) -> Self {
let state = PermState { codename };
self.layer(axum::middleware::from_fn_with_state(state, perm_middleware))
}
}