use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use axum::{
extract::{FromRef, FromRequestParts},
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use crate::state::ChrononState;
pub const REQUIRE_ADMIN_AUTH_ENV: &str = "CHRONON_REQUIRE_ADMIN_AUTH";
#[derive(Debug, Clone)]
pub struct AdminAuthError {
message: String,
}
impl AdminAuthError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
pub trait AdminAuth: Send + Sync {
fn authorize<'a>(
&'a self,
parts: &'a Parts,
) -> Pin<Box<dyn Future<Output = Result<(), AdminAuthError>> + Send + 'a>>;
}
#[must_use]
pub fn require_admin_auth_from_env() -> bool {
match std::env::var(REQUIRE_ADMIN_AUTH_ENV) {
Ok(v) => {
let v = v.trim().to_ascii_lowercase();
matches!(v.as_str(), "1" | "true" | "yes")
}
Err(_) => false,
}
}
fn unauthorized(message: impl Into<String>) -> Response {
(
StatusCode::UNAUTHORIZED,
Json(json!({
"success": false,
"data": null,
"error": message.into(),
})),
)
.into_response()
}
#[derive(Debug)]
pub struct RequireAdmin;
impl<S> FromRequestParts<S> for RequireAdmin
where
S: Send + Sync,
ChrononState: FromRef<S>,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let chronon_state = ChrononState::from_ref(state);
match (&chronon_state.admin_auth, chronon_state.require_admin_auth) {
(Some(auth), _) => match auth.authorize(parts).await {
Ok(()) => Ok(Self),
Err(e) => Err(unauthorized(e.message().to_string())),
},
(None, true) => Err(unauthorized(
"admin auth required but no AdminAuth verifier configured",
)),
(None, false) => Ok(Self),
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AllowAllAdminAuth;
impl AdminAuth for AllowAllAdminAuth {
fn authorize<'a>(
&'a self,
_parts: &'a Parts,
) -> Pin<Box<dyn Future<Output = Result<(), AdminAuthError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
}
#[derive(Debug, Clone)]
pub struct StaticTokenAdminAuth {
token: Arc<str>,
}
impl StaticTokenAdminAuth {
#[must_use]
pub fn new(token: impl Into<String>) -> Self {
Self {
token: Arc::from(token.into()),
}
}
}
impl AdminAuth for StaticTokenAdminAuth {
fn authorize<'a>(
&'a self,
parts: &'a Parts,
) -> Pin<Box<dyn Future<Output = Result<(), AdminAuthError>> + Send + 'a>> {
let expected = Arc::clone(&self.token);
let header = parts
.headers
.get("x-chronon-admin-token")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
Box::pin(async move {
match header {
Some(ref got) if got.as_str() == expected.as_ref() => Ok(()),
_ => Err(AdminAuthError::new(
"missing or invalid x-chronon-admin-token",
)),
}
})
}
}