1use std::sync::Arc;
25
26use crate::auth::{self, Identity, Role};
27use crate::error::{Error, Result};
28use crate::http::{Request, Response};
29use crate::orm::Db;
30use crate::router::Router;
31use crate::templates::Templates;
32
33const ADMIN_CSS: &str = include_str!("../../assets/static/admin.css");
38
39const ADMIN_JS: &str = include_str!("../../assets/static/admin.js");
42
43use super::handlers::{self, AdminCtx};
44use super::render;
45use super::types::Admin;
46
47enum Guard {
51 Allow(Identity),
52 Redirect(Response),
53}
54
55async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
56 let cookie = match req.header("cookie") {
57 Some(c) => c,
58 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
59 };
60 let token = match auth::session_token_from_cookie(cookie) {
61 Some(t) => t,
62 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
63 };
64 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
65 Some(i) => i,
66 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
67 };
68 if !ident.is_active {
69 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
70 }
71 Ok(Guard::Allow(ident))
72}
73
74async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
75 match login_guard(ctx, req).await? {
76 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
77 Guard::Allow(ident) => {
78 if ident.role.includes(min) {
79 Ok(Guard::Allow(ident))
80 } else {
81 let body = render::render_forbidden_body(
82 &ctx.admin,
83 &ctx.templates,
84 &ident,
85 handlers::csrf_token(req),
86 None,
87 Some(min.label()),
88 )?;
89 Ok(Guard::Redirect(
90 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
91 ))
92 }
93 }
94 }
95}
96
97async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
98 match role_guard(ctx, req, Role::Staff).await? {
99 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
100 Guard::Allow(ident) => {
101 if ident.role.bypasses_group_checks() {
102 return Ok(Guard::Allow(ident));
103 }
104 if auth::check_permission(&ctx.db, &ident, perm).await? {
105 Ok(Guard::Allow(ident))
106 } else {
107 let body = render::render_forbidden_body(
108 &ctx.admin,
109 &ctx.templates,
110 &ident,
111 handlers::csrf_token(req),
112 Some(perm.to_string()),
113 None,
114 )?;
115 Ok(Guard::Redirect(
116 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
117 ))
118 }
119 }
120 }
121}
122
123#[cfg(test)]
126fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
127 if !ident.is_active {
128 return false;
129 }
130 if ident.role.bypasses_group_checks() {
131 return true;
132 }
133 perm_held
134}
135
136fn parse_id(raw: Option<&str>) -> Result<i64> {
137 raw.and_then(|s| s.parse().ok())
138 .ok_or_else(|| Error::BadRequest("invalid id".into()))
139}
140
141fn model_name_from_req(req: &Request) -> Result<String> {
142 req.param("admin_name")
143 .map(|s| s.to_string())
144 .ok_or_else(|| Error::BadRequest("missing model".into()))
145}
146
147fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
148 let entry = ctx
149 .admin
150 .find(admin_name)
151 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
152 let singular = entry.singular_name.to_ascii_lowercase();
153 Ok(format!("{admin_name}.{action}_{singular}"))
154}
155
156pub fn register_admin_routes(
157 router: Router,
158 admin: Admin,
159 db: Db,
160 templates: Arc<Templates>,
161) -> Router {
162 let ctx = Arc::new(AdminCtx::new(
163 Arc::new(admin),
164 db.clone(),
165 templates.clone(),
166 ));
167
168 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
171 admin: ctx.admin.clone(),
172 db,
173 templates,
174 });
175
176 let err_admin = ctx.admin.clone();
183 let err_templates = ctx.templates.clone();
184 let router = router.middleware(move |req, next| {
185 let admin = err_admin.clone();
186 let templates = err_templates.clone();
187 Box::pin(async move {
188 let is_admin_path = req.path().starts_with("/admin");
189 let result = next.run(req).await;
190 match result {
191 Ok(resp) => Ok(resp),
192 Err(err) if is_admin_path => Ok(render::render_admin_error_response(
193 &admin,
194 &templates,
195 None,
196 err.status(),
197 err.client_message().to_string(),
198 )),
199 Err(err) => Err(err),
200 }
201 })
202 });
203
204 let router = router.get("/static/admin.css", |_req| async move {
208 Ok(Response::new(
209 hyper::StatusCode::OK,
210 bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
211 )
212 .with_header("content-type", "text/css; charset=utf-8")
213 .with_header("cache-control", "public, max-age=3600"))
214 });
215 let router = router.get("/static/admin.js", |_req| async move {
216 Ok(Response::new(
217 hyper::StatusCode::OK,
218 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
219 )
220 .with_header("content-type", "application/javascript; charset=utf-8")
221 .with_header("cache-control", "public, max-age=3600"))
222 });
223
224 let c = ctx.clone();
226 let router = router.get("/admin/login", move |req| {
227 let c = c.clone();
228 async move { handlers::show_login(&c, req).await }
229 });
230
231 let c = ctx.clone();
232 let router = router.post("/admin/login", move |req| {
233 let c = c.clone();
234 async move { handlers::do_login(&c, req).await }
235 });
236
237 let c = ctx.clone();
238 let router = router.post("/admin/logout", move |req| {
239 let c = c.clone();
240 async move { handlers::do_logout(&c, req).await }
241 });
242
243 let c = ctx.clone();
245 let router = router.get("/admin", move |req| {
246 let c = c.clone();
247 async move {
248 match role_guard(&c, &req, Role::Staff).await? {
249 Guard::Redirect(r) => Ok(r),
250 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
251 }
252 }
253 });
254
255 let c = ctx.clone();
257 let router = router.get("/admin/history", move |req| {
258 let c = c.clone();
259 async move {
260 match role_guard(&c, &req, Role::Administrator).await? {
261 Guard::Redirect(r) => Ok(r),
262 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
263 }
264 }
265 });
266
267 let c = ctx.clone();
271 let router = router.get("/admin/password_change", move |req| {
272 let c = c.clone();
273 async move {
274 match role_guard(&c, &req, Role::User).await? {
275 Guard::Redirect(r) => Ok(r),
276 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
277 }
278 }
279 });
280 let c = ctx.clone();
281 let router = router.post("/admin/password_change", move |req| {
282 let c = c.clone();
283 async move {
284 match role_guard(&c, &req, Role::User).await? {
285 Guard::Redirect(r) => Ok(r),
286 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
287 }
288 }
289 });
290
291 let c = ctx.clone();
293 let ac = auth_ctx.clone();
294 let router = router.get("/admin/users", move |req| {
295 let c = c.clone();
296 let ac = ac.clone();
297 async move {
298 match role_guard(&c, &req, Role::Administrator).await? {
299 Guard::Redirect(r) => Ok(r),
300 Guard::Allow(ident) => {
301 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
302 }
303 }
304 }
305 });
306
307 let c = ctx.clone();
308 let ac = auth_ctx.clone();
309 let router = router.get("/admin/users/new", move |req| {
310 let c = c.clone();
311 let ac = ac.clone();
312 async move {
313 match role_guard(&c, &req, Role::Administrator).await? {
314 Guard::Redirect(r) => Ok(r),
315 Guard::Allow(ident) => {
316 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
317 }
318 }
319 }
320 });
321
322 let c = ctx.clone();
323 let ac = auth_ctx.clone();
324 let router = router.post("/admin/users/new", move |req| {
325 let c = c.clone();
326 let ac = ac.clone();
327 async move {
328 match role_guard(&c, &req, Role::Administrator).await? {
329 Guard::Redirect(r) => Ok(r),
330 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
331 }
332 }
333 });
334
335 let c = ctx.clone();
336 let ac = auth_ctx.clone();
337 let router = router.get("/admin/users/:id/edit", move |req| {
338 let c = c.clone();
339 let ac = ac.clone();
340 async move {
341 match role_guard(&c, &req, Role::Administrator).await? {
342 Guard::Redirect(r) => Ok(r),
343 Guard::Allow(ident) => {
344 let id = parse_id(req.param("id"))?;
345 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
346 }
347 }
348 }
349 });
350
351 let c = ctx.clone();
352 let ac = auth_ctx.clone();
353 let router = router.post("/admin/users/:id/edit", move |req| {
354 let c = c.clone();
355 let ac = ac.clone();
356 async move {
357 match role_guard(&c, &req, Role::Administrator).await? {
358 Guard::Redirect(r) => Ok(r),
359 Guard::Allow(ident) => {
360 let id = parse_id(req.param("id"))?;
361 super::builtin::do_user_edit(&ac, ident, id, req).await
362 }
363 }
364 }
365 });
366
367 let c = ctx.clone();
368 let ac = auth_ctx.clone();
369 let router = router.get("/admin/users/:id/delete", move |req| {
370 let c = c.clone();
371 let ac = ac.clone();
372 async move {
373 match role_guard(&c, &req, Role::Administrator).await? {
374 Guard::Redirect(r) => Ok(r),
375 Guard::Allow(ident) => {
376 let id = parse_id(req.param("id"))?;
377 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
378 .await
379 }
380 }
381 }
382 });
383
384 let c = ctx.clone();
385 let ac = auth_ctx.clone();
386 let router = router.post("/admin/users/:id/delete", move |req| {
387 let c = c.clone();
388 let ac = ac.clone();
389 async move {
390 match role_guard(&c, &req, Role::Administrator).await? {
391 Guard::Redirect(r) => Ok(r),
392 Guard::Allow(ident) => {
393 let id = parse_id(req.param("id"))?;
394 super::builtin::do_user_delete(&ac, ident, id, req).await
395 }
396 }
397 }
398 });
399
400 let c = ctx.clone();
407 let ac = auth_ctx.clone();
408 let router = router.get("/admin/users/:id", move |req| {
409 let c = c.clone();
410 let ac = ac.clone();
411 async move {
412 match role_guard(&c, &req, Role::Administrator).await? {
413 Guard::Redirect(r) => Ok(r),
414 Guard::Allow(ident) => {
415 let id = parse_id(req.param("id"))?;
416 let q = req.query();
417 let tab = q.get("tab").map(|s| s.to_string());
418 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
419 super::builtin::show_user_view(
420 &ac,
421 ident,
422 id,
423 handlers::csrf_token(&req),
424 tab,
425 page,
426 )
427 .await
428 }
429 }
430 }
431 });
432
433 let c = ctx.clone();
435 let ac = auth_ctx.clone();
436 let router = router.get("/admin/groups", move |req| {
437 let c = c.clone();
438 let ac = ac.clone();
439 async move {
440 match role_guard(&c, &req, Role::Administrator).await? {
441 Guard::Redirect(r) => Ok(r),
442 Guard::Allow(ident) => {
443 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
444 }
445 }
446 }
447 });
448
449 let c = ctx.clone();
450 let ac = auth_ctx.clone();
451 let router = router.get("/admin/groups/new", move |req| {
452 let c = c.clone();
453 let ac = ac.clone();
454 async move {
455 match role_guard(&c, &req, Role::Administrator).await? {
456 Guard::Redirect(r) => Ok(r),
457 Guard::Allow(ident) => {
458 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
459 }
460 }
461 }
462 });
463
464 let c = ctx.clone();
465 let ac = auth_ctx.clone();
466 let router = router.post("/admin/groups/new", move |req| {
467 let c = c.clone();
468 let ac = ac.clone();
469 async move {
470 match role_guard(&c, &req, Role::Administrator).await? {
471 Guard::Redirect(r) => Ok(r),
472 Guard::Allow(ident) => super::builtin::do_new_group(&ac, ident, req).await,
473 }
474 }
475 });
476
477 let c = ctx.clone();
478 let ac = auth_ctx.clone();
479 let router = router.get("/admin/groups/:id/edit", move |req| {
480 let c = c.clone();
481 let ac = ac.clone();
482 async move {
483 match role_guard(&c, &req, Role::Administrator).await? {
484 Guard::Redirect(r) => Ok(r),
485 Guard::Allow(ident) => {
486 let id = parse_id(req.param("id"))?;
487 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
488 .await
489 }
490 }
491 }
492 });
493
494 let c = ctx.clone();
495 let ac = auth_ctx.clone();
496 let router = router.post("/admin/groups/:id/edit", move |req| {
497 let c = c.clone();
498 let ac = ac.clone();
499 async move {
500 match role_guard(&c, &req, Role::Administrator).await? {
501 Guard::Redirect(r) => Ok(r),
502 Guard::Allow(ident) => {
503 let id = parse_id(req.param("id"))?;
504 super::builtin::do_group_edit(&ac, ident, id, req).await
505 }
506 }
507 }
508 });
509
510 let c = ctx.clone();
511 let ac = auth_ctx.clone();
512 let router = router.get("/admin/groups/:id/delete", move |req| {
513 let c = c.clone();
514 let ac = ac.clone();
515 async move {
516 match role_guard(&c, &req, Role::Administrator).await? {
517 Guard::Redirect(r) => Ok(r),
518 Guard::Allow(ident) => {
519 let id = parse_id(req.param("id"))?;
520 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
521 .await
522 }
523 }
524 }
525 });
526
527 let c = ctx.clone();
528 let ac = auth_ctx.clone();
529 let router = router.post("/admin/groups/:id/delete", move |req| {
530 let c = c.clone();
531 let ac = ac.clone();
532 async move {
533 match role_guard(&c, &req, Role::Administrator).await? {
534 Guard::Redirect(r) => Ok(r),
535 Guard::Allow(ident) => {
536 let id = parse_id(req.param("id"))?;
537 super::builtin::do_group_delete(&ac, ident, id, req).await
538 }
539 }
540 }
541 });
542
543 let c = ctx.clone();
545 let router = router.get("/admin/:admin_name", move |req| {
546 let c = c.clone();
547 async move {
548 let name = model_name_from_req(&req)?;
549 let perm = perm_for(&c, &name, "view")?;
550 match perm_guard(&c, &req, &perm).await? {
551 Guard::Redirect(r) => Ok(r),
552 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
553 }
554 }
555 });
556
557 let c = ctx.clone();
559 let router = router.get("/admin/:admin_name/new", move |req| {
560 let c = c.clone();
561 async move {
562 let name = model_name_from_req(&req)?;
563 let perm = perm_for(&c, &name, "add")?;
564 match perm_guard(&c, &req, &perm).await? {
565 Guard::Redirect(r) => Ok(r),
566 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
567 }
568 }
569 });
570 let c = ctx.clone();
571 let router = router.post("/admin/:admin_name/new", move |req| {
572 let c = c.clone();
573 async move {
574 let name = model_name_from_req(&req)?;
575 let perm = perm_for(&c, &name, "add")?;
576 match perm_guard(&c, &req, &perm).await? {
577 Guard::Redirect(r) => Ok(r),
578 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
579 }
580 }
581 });
582
583 let c = ctx.clone();
585 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
586 let c = c.clone();
587 async move {
588 let name = model_name_from_req(&req)?;
589 let perm = perm_for(&c, &name, "change")?;
590 match perm_guard(&c, &req, &perm).await? {
591 Guard::Redirect(r) => Ok(r),
592 Guard::Allow(ident) => {
593 let id = parse_id(req.param("id"))?;
594 handlers::show_edit_form(&c, ident, &name, id, &req).await
595 }
596 }
597 }
598 });
599 let c = ctx.clone();
600 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
601 let c = c.clone();
602 async move {
603 let name = model_name_from_req(&req)?;
604 let perm = perm_for(&c, &name, "change")?;
605 match perm_guard(&c, &req, &perm).await? {
606 Guard::Redirect(r) => Ok(r),
607 Guard::Allow(ident) => {
608 let id = parse_id(req.param("id"))?;
609 handlers::do_update(&c, ident, &name, id, req).await
610 }
611 }
612 }
613 });
614
615 let c = ctx.clone();
618 let router = router.get("/admin/:admin_name/:id/history", move |req| {
619 let c = c.clone();
620 async move {
621 let name = model_name_from_req(&req)?;
622 let perm = perm_for(&c, &name, "view")?;
623 match perm_guard(&c, &req, &perm).await? {
624 Guard::Redirect(r) => Ok(r),
625 Guard::Allow(ident) => {
626 let id = parse_id(req.param("id"))?;
627 handlers::show_object_history(&c, ident, &name, id, &req).await
628 }
629 }
630 }
631 });
632
633 let c = ctx.clone();
635 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
636 let c = c.clone();
637 async move {
638 let name = model_name_from_req(&req)?;
639 let perm = perm_for(&c, &name, "delete")?;
640 match perm_guard(&c, &req, &perm).await? {
641 Guard::Redirect(r) => Ok(r),
642 Guard::Allow(ident) => {
643 let id = parse_id(req.param("id"))?;
644 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
645 }
646 }
647 }
648 });
649 let c = ctx.clone();
650 router.post("/admin/:admin_name/:id/delete", move |req| {
651 let c = c.clone();
652 async move {
653 let name = model_name_from_req(&req)?;
654 let perm = perm_for(&c, &name, "delete")?;
655 match perm_guard(&c, &req, &perm).await? {
656 Guard::Redirect(r) => Ok(r),
657 Guard::Allow(ident) => {
658 let id = parse_id(req.param("id"))?;
659 handlers::do_delete(&c, ident, &name, id).await
660 }
661 }
662 }
663 })
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 fn make_identity(role: Role, is_active: bool) -> Identity {
671 Identity {
672 user_id: 42,
673 email: "test@example.com".into(),
674 role,
675 is_active,
676 is_demo: false,
677 demo_label: None,
678 }
679 }
680
681 #[test]
686 fn role_guard_decision_admin_meets_staff_floor() {
687 let id = make_identity(Role::Administrator, true);
688 assert!(id.role.includes(Role::Staff));
689 }
690
691 #[test]
692 fn role_guard_decision_user_does_not_meet_staff() {
693 let id = make_identity(Role::User, true);
694 assert!(!id.role.includes(Role::Staff));
695 }
696
697 #[test]
698 fn role_guard_decision_administrator_does_not_meet_developer() {
699 let id = make_identity(Role::Administrator, true);
700 assert!(!id.role.includes(Role::Developer));
701 }
702
703 #[test]
704 fn role_guard_decision_developer_meets_everything() {
705 let id = make_identity(Role::Developer, true);
706 for &min in &[
707 Role::User,
708 Role::Staff,
709 Role::Supervisor,
710 Role::Administrator,
711 Role::Developer,
712 ] {
713 assert!(id.role.includes(min), "Developer should meet {min:?}");
714 }
715 }
716
717 #[test]
720 fn perm_guard_admin_short_circuits_without_perm() {
721 let id = make_identity(Role::Administrator, true);
722 assert!(perm_guard_verdict(&id, false));
723 }
724
725 #[test]
726 fn perm_guard_developer_short_circuits_without_perm() {
727 let id = make_identity(Role::Developer, true);
728 assert!(perm_guard_verdict(&id, false));
729 }
730
731 #[test]
732 fn perm_guard_staff_with_perm_passes() {
733 let id = make_identity(Role::Staff, true);
734 assert!(perm_guard_verdict(&id, true));
735 }
736
737 #[test]
738 fn perm_guard_staff_without_perm_denies() {
739 let id = make_identity(Role::Staff, true);
740 assert!(!perm_guard_verdict(&id, false));
741 }
742
743 #[test]
744 fn perm_guard_inactive_admin_denies_even_with_bypass() {
745 let id = make_identity(Role::Administrator, false);
747 assert!(!perm_guard_verdict(&id, true));
748 }
749
750 #[test]
751 fn perm_guard_supervisor_without_perm_denies() {
752 let id = make_identity(Role::Supervisor, true);
754 assert!(!perm_guard_verdict(&id, false));
755 }
756}