1use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13use axum::{
14 extract::{FromRef, FromRequestParts},
15 http::{request::Parts, StatusCode},
16 response::{IntoResponse, Response},
17 Json,
18};
19use serde_json::json;
20
21use crate::state::ChrononState;
22
23pub const REQUIRE_ADMIN_AUTH_ENV: &str = "CHRONON_REQUIRE_ADMIN_AUTH";
25
26#[derive(Debug, Clone)]
28pub struct AdminAuthError {
29 message: String,
30}
31
32impl AdminAuthError {
33 #[must_use]
35 pub fn new(message: impl Into<String>) -> Self {
36 Self {
37 message: message.into(),
38 }
39 }
40
41 #[must_use]
43 pub fn message(&self) -> &str {
44 &self.message
45 }
46}
47
48pub trait AdminAuth: Send + Sync {
50 fn authorize<'a>(
56 &'a self,
57 parts: &'a Parts,
58 ) -> Pin<Box<dyn Future<Output = Result<(), AdminAuthError>> + Send + 'a>>;
59}
60
61#[must_use]
63pub fn require_admin_auth_from_env() -> bool {
64 match std::env::var(REQUIRE_ADMIN_AUTH_ENV) {
65 Ok(v) => {
66 let v = v.trim().to_ascii_lowercase();
67 matches!(v.as_str(), "1" | "true" | "yes")
68 }
69 Err(_) => false,
70 }
71}
72
73fn unauthorized(message: impl Into<String>) -> Response {
74 (
75 StatusCode::UNAUTHORIZED,
76 Json(json!({
77 "success": false,
78 "data": null,
79 "error": message.into(),
80 })),
81 )
82 .into_response()
83}
84
85#[derive(Debug)]
87pub struct RequireAdmin;
88
89impl<S> FromRequestParts<S> for RequireAdmin
90where
91 S: Send + Sync,
92 ChrononState: FromRef<S>,
93{
94 type Rejection = Response;
95
96 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
97 let chronon_state = ChrononState::from_ref(state);
98 match (&chronon_state.admin_auth, chronon_state.require_admin_auth) {
99 (Some(auth), _) => match auth.authorize(parts).await {
100 Ok(()) => Ok(Self),
101 Err(e) => Err(unauthorized(e.message().to_string())),
102 },
103 (None, true) => Err(unauthorized(
104 "admin auth required but no AdminAuth verifier configured",
105 )),
106 (None, false) => Ok(Self),
107 }
108 }
109}
110
111#[derive(Debug, Default, Clone, Copy)]
113pub struct AllowAllAdminAuth;
114
115impl AdminAuth for AllowAllAdminAuth {
116 fn authorize<'a>(
117 &'a self,
118 _parts: &'a Parts,
119 ) -> Pin<Box<dyn Future<Output = Result<(), AdminAuthError>> + Send + 'a>> {
120 Box::pin(async { Ok(()) })
121 }
122}
123
124#[derive(Debug, Clone)]
128pub struct StaticTokenAdminAuth {
129 token: Arc<str>,
130}
131
132impl StaticTokenAdminAuth {
133 #[must_use]
135 pub fn new(token: impl Into<String>) -> Self {
136 Self {
137 token: Arc::from(token.into()),
138 }
139 }
140}
141
142impl AdminAuth for StaticTokenAdminAuth {
143 fn authorize<'a>(
144 &'a self,
145 parts: &'a Parts,
146 ) -> Pin<Box<dyn Future<Output = Result<(), AdminAuthError>> + Send + 'a>> {
147 let expected = Arc::clone(&self.token);
148 let header = parts
149 .headers
150 .get("x-chronon-admin-token")
151 .and_then(|v| v.to_str().ok())
152 .map(str::to_owned);
153 Box::pin(async move {
154 match header {
155 Some(ref got) if got.as_str() == expected.as_ref() => Ok(()),
156 _ => Err(AdminAuthError::new(
157 "missing or invalid x-chronon-admin-token",
158 )),
159 }
160 })
161 }
162}