acme_proxy_admin/webadmin/pages/
auth.rs1use axum::extract::FromRequestParts;
14use axum::http::HeaderMap;
15use axum::http::request::Parts;
16
17use crate::webadmin::AdminState;
18use crate::webadmin::error::AdminError;
19use crate::webadmin::pages::error::PageError;
20use crate::webadmin::session::{
21 AdminRead, AdminWrite, Authenticated, AuthenticatedWrite, EnrolWrite, PendingMfa,
22 PendingMfaSubmit, SelfServiceWrite,
23};
24
25const HX_REQUEST: &str = "hx-request";
27
28#[must_use]
33pub fn is_htmx(headers: &HeaderMap) -> bool {
34 headers
35 .get(HX_REQUEST)
36 .and_then(|value| value.to_str().ok())
37 .is_some_and(|value| value.eq_ignore_ascii_case("true"))
38}
39
40pub struct PageSession {
42 pub auth: Authenticated,
43 pub hx: bool,
46}
47
48pub struct PageSessionWrite {
54 pub auth: Authenticated,
55 pub hx: bool,
56}
57
58impl FromRequestParts<AdminState> for PageSession {
59 type Rejection = PageError;
60
61 async fn from_request_parts(
62 parts: &mut Parts,
63 state: &AdminState,
64 ) -> Result<Self, Self::Rejection> {
65 let hx = is_htmx(&parts.headers);
66 match Authenticated::from_request_parts(parts, state).await {
67 Ok(auth) => Ok(Self { auth, hx }),
68 Err(error) => Err(to_page_error(error, hx)),
69 }
70 }
71}
72
73impl FromRequestParts<AdminState> for PageSessionWrite {
74 type Rejection = PageError;
75
76 async fn from_request_parts(
77 parts: &mut Parts,
78 state: &AdminState,
79 ) -> Result<Self, Self::Rejection> {
80 let hx = is_htmx(&parts.headers);
81 match AuthenticatedWrite::from_request_parts(parts, state).await {
82 Ok(AuthenticatedWrite(auth)) => Ok(Self { auth, hx }),
83 Err(error) => Err(to_page_error(error, hx)),
84 }
85 }
86}
87
88pub struct PageAdminWrite {
91 pub auth: Authenticated,
92 pub hx: bool,
93}
94
95pub struct PageSelfServiceWrite {
98 pub auth: Authenticated,
99 pub hx: bool,
100}
101
102pub struct PageAdminRead {
106 pub auth: Authenticated,
107 pub hx: bool,
108}
109
110pub trait PageAuth {
119 fn auth(&self) -> &Authenticated;
120}
121
122impl PageAuth for PageSession {
123 fn auth(&self) -> &Authenticated {
124 &self.auth
125 }
126}
127
128impl PageAuth for PageAdminRead {
129 fn auth(&self) -> &Authenticated {
130 &self.auth
131 }
132}
133
134impl FromRequestParts<AdminState> for PageAdminWrite {
135 type Rejection = PageError;
136
137 async fn from_request_parts(
138 parts: &mut Parts,
139 state: &AdminState,
140 ) -> Result<Self, Self::Rejection> {
141 let hx = is_htmx(&parts.headers);
142 match AdminWrite::from_request_parts(parts, state).await {
143 Ok(AdminWrite(auth)) => Ok(Self { auth, hx }),
144 Err(error) => Err(to_page_error(error, hx)),
145 }
146 }
147}
148
149impl FromRequestParts<AdminState> for PageAdminRead {
150 type Rejection = PageError;
151
152 async fn from_request_parts(
153 parts: &mut Parts,
154 state: &AdminState,
155 ) -> Result<Self, Self::Rejection> {
156 let hx = is_htmx(&parts.headers);
157 match AdminRead::from_request_parts(parts, state).await {
158 Ok(AdminRead(auth)) => Ok(Self { auth, hx }),
159 Err(error) => Err(to_page_error(error, hx)),
160 }
161 }
162}
163
164impl FromRequestParts<AdminState> for PageSelfServiceWrite {
165 type Rejection = PageError;
166
167 async fn from_request_parts(
168 parts: &mut Parts,
169 state: &AdminState,
170 ) -> Result<Self, Self::Rejection> {
171 let hx = is_htmx(&parts.headers);
172 match SelfServiceWrite::from_request_parts(parts, state).await {
173 Ok(SelfServiceWrite(auth)) => Ok(Self { auth, hx }),
174 Err(error) => Err(to_page_error(error, hx)),
175 }
176 }
177}
178
179pub struct PageMfaPending {
181 pub pending: PendingMfa,
182 pub hx: bool,
183}
184
185pub struct PageMfaSubmit {
190 pub pending: PendingMfa,
191 pub hx: bool,
192}
193
194pub struct PageEnrolWrite {
196 pub enrol: EnrolWrite,
197 pub hx: bool,
198}
199
200impl FromRequestParts<AdminState> for PageMfaPending {
201 type Rejection = PageError;
202
203 async fn from_request_parts(
204 parts: &mut Parts,
205 state: &AdminState,
206 ) -> Result<Self, Self::Rejection> {
207 let hx = is_htmx(&parts.headers);
208 match PendingMfa::from_request_parts(parts, state).await {
209 Ok(pending) => Ok(Self { pending, hx }),
210 Err(error) => Err(to_page_error(error, hx)),
211 }
212 }
213}
214
215impl FromRequestParts<AdminState> for PageMfaSubmit {
216 type Rejection = PageError;
217
218 async fn from_request_parts(
219 parts: &mut Parts,
220 state: &AdminState,
221 ) -> Result<Self, Self::Rejection> {
222 let hx = is_htmx(&parts.headers);
223 match PendingMfaSubmit::from_request_parts(parts, state).await {
224 Ok(PendingMfaSubmit(pending)) => Ok(Self { pending, hx }),
225 Err(error) => Err(to_page_error(error, hx)),
226 }
227 }
228}
229
230impl FromRequestParts<AdminState> for PageEnrolWrite {
231 type Rejection = PageError;
232
233 async fn from_request_parts(
234 parts: &mut Parts,
235 state: &AdminState,
236 ) -> Result<Self, Self::Rejection> {
237 let hx = is_htmx(&parts.headers);
238 match EnrolWrite::from_request_parts(parts, state).await {
239 Ok(enrol) => Ok(Self { enrol, hx }),
240 Err(error) => Err(to_page_error(error, hx)),
241 }
242 }
243}
244
245fn to_page_error(error: AdminError, hx: bool) -> PageError {
251 if error.status == axum::http::StatusCode::UNAUTHORIZED {
252 PageError::login_required(hx)
253 } else {
254 error.into()
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use axum::http::StatusCode;
262
263 fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
264 let mut map = HeaderMap::new();
265 for (name, value) in pairs {
266 map.insert(
267 axum::http::HeaderName::from_bytes(name.as_bytes()).unwrap(),
268 value.parse().unwrap(),
269 );
270 }
271 map
272 }
273
274 #[test]
275 fn is_htmx_reads_the_header_case_insensitively() {
276 assert!(is_htmx(&headers(&[("hx-request", "true")])));
277 assert!(is_htmx(&headers(&[("HX-Request", "True")])));
278 assert!(!is_htmx(&headers(&[("hx-request", "false")])));
279 assert!(!is_htmx(&headers(&[("hx-request", "yes")])));
282 assert!(!is_htmx(&HeaderMap::new()));
283 }
284
285 #[test]
286 fn an_unauthorized_rejection_becomes_a_sign_in_redirect() {
287 assert_eq!(
288 to_page_error(AdminError::session_expired(), false),
289 PageError::login_required(false)
290 );
291 assert_eq!(
292 to_page_error(AdminError::session_invalid(), true),
293 PageError::login_required(true)
294 );
295 }
296
297 #[test]
301 fn a_csrf_failure_keeps_its_own_status() {
302 let error = to_page_error(AdminError::csrf_failed("no token"), true);
303 assert_eq!(error.status(), StatusCode::FORBIDDEN);
304 assert_eq!(
305 error,
306 PageError::Rendered {
307 status: StatusCode::FORBIDDEN,
308 code: "csrf_failed",
309 message: "no token".to_string(),
310 }
311 );
312 }
313}