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 = concat!(
51 include_str!("../../assets/static/admin/tokens/colors.css"),
53 "\n",
54 include_str!("../../assets/static/admin/tokens/spacing.css"),
55 "\n",
56 include_str!("../../assets/static/admin/tokens/radius.css"),
57 "\n",
58 include_str!("../../assets/static/admin/tokens/shadows.css"),
59 "\n",
60 include_str!("../../assets/static/admin/tokens/typography.css"),
61 "\n",
62 include_str!("../../assets/static/admin/themes/dark.css"),
64 "\n",
65 include_str!("../../assets/static/admin/themes/light.css"),
66 "\n",
67 include_str!("../../assets/static/admin/base/reset.css"),
69 "\n",
70 include_str!("../../assets/static/admin/base/base.css"),
71 "\n",
72 include_str!("../../assets/static/admin/base/typography.css"),
73 "\n",
74 include_str!("../../assets/static/admin/base/typography-i18n.css"),
75 "\n",
76 include_str!("../../assets/static/admin/base/utilities.css"),
77 "\n",
78 include_str!("../../assets/static/admin/layout/shell.css"),
80 "\n",
81 include_str!("../../assets/static/admin/layout/topbar.css"),
82 "\n",
83 include_str!("../../assets/static/admin/layout/sidebar.css"),
84 "\n",
85 include_str!("../../assets/static/admin/layout/footer.css"),
86 "\n",
87 include_str!("../../assets/static/admin/components/cards.css"),
89 "\n",
90 include_str!("../../assets/static/admin/components/buttons.css"),
91 "\n",
92 include_str!("../../assets/static/admin/components/forms.css"),
93 "\n",
94 include_str!("../../assets/static/admin/components/tables.css"),
95 "\n",
96 include_str!("../../assets/static/admin/components/filters.css"),
97 "\n",
98 include_str!("../../assets/static/admin/components/dropdowns.css"),
99 "\n",
100 include_str!("../../assets/static/admin/components/pagination.css"),
101 "\n",
102 include_str!("../../assets/static/admin/components/pills.css"),
103 "\n",
104 include_str!("../../assets/static/admin/components/flashes.css"),
105 "\n",
106 include_str!("../../assets/static/admin/components/timeline.css"),
107 "\n",
108 include_str!("../../assets/static/admin/components/tabs.css"),
109 "\n",
110 include_str!("../../assets/static/admin/pages/auth.css"),
112 "\n",
113 include_str!("../../assets/static/admin/pages/dashboard.css"),
114 "\n",
115 include_str!("../../assets/static/admin/pages/permissions.css"),
116 "\n",
117 include_str!("../../assets/static/admin/pages/sessions.css"),
118 "\n",
119 include_str!("../../assets/static/admin/pages/errors.css"),
120 "\n",
121 include_str!("../../assets/static/admin/layout/responsive.css"),
123 "\n",
124 include_str!("../../assets/static/admin/print/print.css"),
126);
127
128const ADMIN_JS: &str = include_str!("../../assets/static/admin.js");
131
132const FONT_GEIST: &[u8] = include_bytes!("../../assets/static/fonts/Geist-Variable.woff2");
146const FONT_GEIST_MONO: &[u8] = include_bytes!("../../assets/static/fonts/GeistMono-Variable.woff2");
147const FONT_INTER: &[u8] = include_bytes!("../../assets/static/fonts/InterVariable.woff2");
148const FONT_TAJAWAL_REG: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Regular.woff2");
149const FONT_TAJAWAL_MED: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Medium.woff2");
150const FONT_TAJAWAL_BOLD: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Bold.woff2");
151const FONT_NOTO_NASKH_AR: &[u8] =
152 include_bytes!("../../assets/static/fonts/NotoNaskhArabic-Variable.woff2");
153const FONT_NOTO_THAI: &[u8] =
154 include_bytes!("../../assets/static/fonts/NotoSansThai-Variable.woff2");
155const FONT_NOTO_DEVA: &[u8] =
156 include_bytes!("../../assets/static/fonts/NotoSansDevanagari-Variable.woff2");
157const FONT_NOTO_JP: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansJP-Regular.woff2");
158const FONT_NOTO_KR: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansKR-Regular.woff2");
159const FONT_NOTO_SC: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansSC-Regular.woff2");
160
161use super::handlers::{self, AdminCtx};
162use super::render;
163use super::types::Admin;
164
165enum Guard {
169 Allow(Identity),
170 Redirect(Response),
171}
172
173const MUST_CHANGE_WHITELIST: &[&str] = &[
183 "/admin/must-change-password",
184 "/admin/logout",
185 "/admin/account/sessions",
186];
187
188fn is_must_change_whitelisted_path(path: &str) -> bool {
192 MUST_CHANGE_WHITELIST.contains(&path)
193}
194
195const MFA_ENROLL_WHITELIST: &[&str] = &[
208 "/admin/account/mfa/enroll",
209 "/admin/logout",
210 "/admin/account/sessions",
211];
212
213fn is_mfa_enroll_whitelisted_path(path: &str) -> bool {
214 MFA_ENROLL_WHITELIST.contains(&path)
215}
216
217const MFA_VERIFY_WHITELIST: &[&str] = &[
227 "/admin/mfa/verify",
228 "/admin/logout",
229 "/admin/account/sessions",
230];
231
232fn is_mfa_verify_whitelisted_path(path: &str) -> bool {
233 MFA_VERIFY_WHITELIST.contains(&path)
234}
235
236fn mfa_required_for_role(policy: crate::auth::MfaPolicy, role: Role) -> bool {
246 use crate::auth::MfaPolicy;
247 match policy {
248 MfaPolicy::Disabled | MfaPolicy::Optional => false,
249 MfaPolicy::Required => true,
250 MfaPolicy::RequiredForRoles(roles) => roles.contains(&role),
251 }
252}
253
254async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
255 let cookie = match req.header("cookie") {
256 Some(c) => c,
257 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
258 };
259 let token = match auth::session_token_from_cookie(cookie) {
260 Some(t) => t,
261 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
262 };
263 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
264 Some(i) => i,
265 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
266 };
267 if !ident.is_active {
268 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
269 }
270
271 if ident.must_change_password && !is_must_change_whitelisted_path(req.path()) {
277 return Ok(Guard::Redirect(Response::redirect(
278 "/admin/must-change-password",
279 )));
280 }
281
282 let policy = ctx.admin.active_mfa_policy();
291 if mfa_required_for_role(policy, ident.role)
292 && !ident.mfa_enabled
293 && !is_mfa_enroll_whitelisted_path(req.path())
294 {
295 return Ok(Guard::Redirect(Response::redirect(
296 "/admin/account/mfa/enroll",
297 )));
298 }
299
300 use crate::auth::SessionTrust;
308 if ident.mfa_enabled
309 && ident.trust_level != SessionTrust::MfaVerified
310 && !is_mfa_verify_whitelisted_path(req.path())
311 {
312 return Ok(Guard::Redirect(Response::redirect("/admin/mfa/verify")));
313 }
314
315 Ok(Guard::Allow(ident))
316}
317
318async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
319 match login_guard(ctx, req).await? {
320 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
321 Guard::Allow(ident) => {
322 if ident.role.includes(min) {
323 Ok(Guard::Allow(ident))
324 } else {
325 let body = render::render_forbidden_body(
326 &ctx.admin,
327 &ctx.templates,
328 &ident,
329 handlers::csrf_token(req),
330 None,
331 Some(min.label()),
332 )?;
333 Ok(Guard::Redirect(
334 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
335 ))
336 }
337 }
338 }
339}
340
341async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
342 match role_guard(ctx, req, Role::Staff).await? {
343 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
344 Guard::Allow(ident) => {
345 if ident.role.bypasses_group_checks() {
346 return Ok(Guard::Allow(ident));
347 }
348 if auth::check_permission(&ctx.db, &ident, perm).await? {
349 Ok(Guard::Allow(ident))
350 } else {
351 let body = render::render_forbidden_body(
352 &ctx.admin,
353 &ctx.templates,
354 &ident,
355 handlers::csrf_token(req),
356 Some(perm.to_string()),
357 None,
358 )?;
359 Ok(Guard::Redirect(
360 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
361 ))
362 }
363 }
364 }
365}
366
367#[cfg(test)]
370fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
371 if !ident.is_active {
372 return false;
373 }
374 if ident.role.bypasses_group_checks() {
375 return true;
376 }
377 perm_held
378}
379
380fn parse_id(raw: Option<&str>) -> Result<i64> {
381 raw.and_then(|s| s.parse().ok())
382 .ok_or_else(|| Error::BadRequest("invalid id".into()))
383}
384
385fn model_name_from_req(req: &Request) -> Result<String> {
386 req.param("admin_name")
387 .map(|s| s.to_string())
388 .ok_or_else(|| Error::BadRequest("missing model".into()))
389}
390
391fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
392 let entry = ctx
393 .admin
394 .find(admin_name)
395 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
396 let singular = entry.singular_name.to_ascii_lowercase();
397 Ok(format!("{admin_name}.{action}_{singular}"))
398}
399
400async fn resolve_identity_for_error_page(db: &Db, cookie_header: &str) -> Option<Identity> {
430 let token = auth::session_token_from_cookie(cookie_header)?;
431 let identity = auth::identity_from_session(db, token.as_str())
432 .await
433 .ok()
434 .flatten()?;
435 if !identity.is_active {
436 return None;
437 }
438 Some(identity)
439}
440
441fn strict_mailer_guard_check(admin: &Admin) -> std::result::Result<(), String> {
442 if admin.active_recovery_policy().strict_mailer_required() && !admin.has_custom_mailer() {
443 Err(
444 "rustio-admin: RecoveryPolicy::strict_mailer_required() = true but no mailer \
445 was registered via Admin::mailer(...).\n\n\
446 The framework's default LogMailer writes recovery emails to log::info! instead \
447 of sending them, which is unsuitable for production. Recovery routes are NOT \
448 registered with this configuration.\n\n\
449 To resolve, choose one:\n\
450 (a) register a real mailer before calling register_admin_routes:\n\
451 Admin::mailer(Arc::new(MyProjectMailer::new(...)))\n\
452 (b) opt the policy out of strict mode (the framework default — dev / CI / \
453 testing baseline):\n\
454 RecoveryPolicy::strict_mailer_required(false)\n\n\
455 See DESIGN_RECOVERY.md §12.1 for the contract."
456 .to_string(),
457 )
458 } else {
459 Ok(())
460 }
461}
462
463pub fn register_admin_routes(
465 router: Router,
466 admin: Admin,
467 db: Db,
468 templates: Arc<Templates>,
469) -> Router {
470 if let Err(msg) = strict_mailer_guard_check(&admin) {
478 panic!("{msg}");
479 }
480
481 let ctx = Arc::new(AdminCtx::new(
482 Arc::new(admin),
483 db.clone(),
484 templates.clone(),
485 ));
486
487 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
490 admin: ctx.admin.clone(),
491 db,
492 templates,
493 });
494
495 let err_admin = ctx.admin.clone();
510 let err_templates = ctx.templates.clone();
511 let err_db = ctx.db.clone();
512 let router = router.middleware(move |req, next| {
513 let admin = err_admin.clone();
514 let templates = err_templates.clone();
515 let db = err_db.clone();
516 Box::pin(async move {
517 let is_admin_path = req.path().starts_with("/admin");
518 let cookie_header = if is_admin_path {
524 req.header("cookie").map(|s| s.to_string())
525 } else {
526 None
527 };
528 let result = next.run(req).await;
529 match result {
530 Ok(resp) => Ok(resp),
531 Err(err) if is_admin_path => {
532 let identity = match cookie_header.as_deref() {
533 Some(cookie) => resolve_identity_for_error_page(&db, cookie).await,
534 None => None,
535 };
536 Ok(render::render_admin_error_response(
537 &admin,
538 &templates,
539 identity.as_ref(),
540 err.status(),
541 err.client_message().to_string(),
542 ))
543 }
544 Err(err) => Err(err),
545 }
546 })
547 });
548
549 let router = router.get("/static/admin.css", |_req| async move {
555 Ok(Response::new(
556 hyper::StatusCode::OK,
557 bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
558 )
559 .with_header("content-type", "text/css; charset=utf-8")
560 .with_header("cache-control", "no-cache, must-revalidate"))
561 });
562 let router = router.get("/static/admin.js", |_req| async move {
563 Ok(Response::new(
564 hyper::StatusCode::OK,
565 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
566 )
567 .with_header("content-type", "application/javascript; charset=utf-8")
568 .with_header("cache-control", "no-cache, must-revalidate"))
569 });
570
571 fn font_response(bytes: &'static [u8]) -> Response {
575 Response::new(hyper::StatusCode::OK, bytes::Bytes::from_static(bytes))
576 .with_header("content-type", "font/woff2")
577 .with_header("cache-control", "public, max-age=31536000, immutable")
578 }
579 let router = router.get("/static/fonts/Geist-Variable.woff2", |_req| async move {
580 Ok(font_response(FONT_GEIST))
581 });
582 let router = router.get(
583 "/static/fonts/GeistMono-Variable.woff2",
584 |_req| async move { Ok(font_response(FONT_GEIST_MONO)) },
585 );
586 let router = router.get("/static/fonts/Tajawal-Regular.woff2", |_req| async move {
587 Ok(font_response(FONT_TAJAWAL_REG))
588 });
589 let router = router.get("/static/fonts/Tajawal-Medium.woff2", |_req| async move {
590 Ok(font_response(FONT_TAJAWAL_MED))
591 });
592 let router = router.get("/static/fonts/Tajawal-Bold.woff2", |_req| async move {
593 Ok(font_response(FONT_TAJAWAL_BOLD))
594 });
595 let router = router.get(
596 "/static/fonts/NotoNaskhArabic-Variable.woff2",
597 |_req| async move { Ok(font_response(FONT_NOTO_NASKH_AR)) },
598 );
599 let router = router.get("/static/fonts/InterVariable.woff2", |_req| async move {
600 Ok(font_response(FONT_INTER))
601 });
602 let router = router.get(
603 "/static/fonts/NotoSansThai-Variable.woff2",
604 |_req| async move { Ok(font_response(FONT_NOTO_THAI)) },
605 );
606 let router = router.get(
607 "/static/fonts/NotoSansDevanagari-Variable.woff2",
608 |_req| async move { Ok(font_response(FONT_NOTO_DEVA)) },
609 );
610 let router = router.get("/static/fonts/NotoSansJP-Regular.woff2", |_req| async move {
611 Ok(font_response(FONT_NOTO_JP))
612 });
613 let router = router.get("/static/fonts/NotoSansKR-Regular.woff2", |_req| async move {
614 Ok(font_response(FONT_NOTO_KR))
615 });
616 let router = router.get("/static/fonts/NotoSansSC-Regular.woff2", |_req| async move {
617 Ok(font_response(FONT_NOTO_SC))
618 });
619
620 let c = ctx.clone();
622 let router = router.get("/admin/login", move |req| {
623 let c = c.clone();
624 async move { handlers::show_login(&c, req).await }
625 });
626
627 let c = ctx.clone();
628 let router = router.post("/admin/login", move |req| {
629 let c = c.clone();
630 async move { handlers::do_login(&c, req).await }
631 });
632
633 let c = ctx.clone();
634 let router = router.post("/admin/logout", move |req| {
635 let c = c.clone();
636 async move { handlers::do_logout(&c, req).await }
637 });
638
639 let recovery_state = Arc::new(super::recovery_handlers::RecoveryState::from_admin(
656 &ctx.admin,
657 ));
658
659 let c = ctx.clone();
660 let router = router.get("/admin/forgot-password", move |req| {
661 let c = c.clone();
662 async move { super::recovery_handlers::show_forgot_password(&c, &req).await }
663 });
664
665 let c = ctx.clone();
666 let rs = recovery_state.clone();
667 let router = router.post("/admin/forgot-password", move |req| {
668 let c = c.clone();
669 let rs = rs.clone();
670 async move { super::recovery_handlers::do_forgot_password(&c, &rs, req).await }
671 });
672
673 let c = ctx.clone();
674 let router = router.get("/admin/forgot-password/sent", move |req| {
675 let c = c.clone();
676 async move { super::recovery_handlers::show_forgot_password_sent(&c, &req).await }
677 });
678
679 let c = ctx.clone();
680 let router = router.get("/admin/reset-password/:token", move |req| {
681 let c = c.clone();
682 async move {
683 let token = req
684 .param("token")
685 .ok_or_else(|| Error::BadRequest("missing token".into()))?
686 .to_string();
687 super::recovery_handlers::show_reset_password(&c, &req, &token).await
688 }
689 });
690
691 let c = ctx.clone();
692 let rs = recovery_state.clone();
693 let router = router.post("/admin/reset-password/:token", move |req| {
694 let c = c.clone();
695 let rs = rs.clone();
696 async move {
697 let token = req
698 .param("token")
699 .ok_or_else(|| Error::BadRequest("missing token".into()))?
700 .to_string();
701 super::recovery_handlers::do_reset_password(&c, &rs, req, &token).await
702 }
703 });
704
705 let c = ctx.clone();
707 let router = router.get("/admin", move |req| {
708 let c = c.clone();
709 async move {
710 match role_guard(&c, &req, Role::Staff).await? {
711 Guard::Redirect(r) => Ok(r),
712 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
713 }
714 }
715 });
716
717 let c = ctx.clone();
719 let router = router.get("/admin/history", move |req| {
720 let c = c.clone();
721 async move {
722 match role_guard(&c, &req, Role::Administrator).await? {
723 Guard::Redirect(r) => Ok(r),
724 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
725 }
726 }
727 });
728
729 let c = ctx.clone();
732 let router = router.get("/admin/account/sessions", move |req| {
733 let c = c.clone();
734 async move {
735 match role_guard(&c, &req, Role::User).await? {
736 Guard::Redirect(r) => Ok(r),
737 Guard::Allow(ident) => handlers::show_account_sessions(&c, ident, &req).await,
738 }
739 }
740 });
741
742 let c = ctx.clone();
750 let router = router.post("/admin/account/sessions/revoke-others", move |req| {
751 let c = c.clone();
752 async move {
753 match role_guard(&c, &req, Role::User).await? {
754 Guard::Redirect(r) => Ok(r),
755 Guard::Allow(ident) => handlers::do_revoke_other_sessions(&c, ident, req).await,
756 }
757 }
758 });
759
760 let c = ctx.clone();
761 let router = router.post("/admin/account/sessions/revoke-all", move |req| {
762 let c = c.clone();
763 async move {
764 match role_guard(&c, &req, Role::User).await? {
765 Guard::Redirect(r) => Ok(r),
766 Guard::Allow(ident) => handlers::do_revoke_all_sessions(&c, ident, req).await,
767 }
768 }
769 });
770
771 let c = ctx.clone();
772 let router = router.post("/admin/account/sessions/:id/revoke", move |req| {
773 let c = c.clone();
774 async move {
775 match role_guard(&c, &req, Role::User).await? {
776 Guard::Redirect(r) => Ok(r),
777 Guard::Allow(ident) => {
778 let id = parse_id(req.param("id"))?;
779 handlers::do_revoke_session(&c, ident, req, id).await
780 }
781 }
782 }
783 });
784
785 let c = ctx.clone();
789 let router = router.get("/admin/password_change", move |req| {
790 let c = c.clone();
791 async move {
792 match role_guard(&c, &req, Role::User).await? {
793 Guard::Redirect(r) => Ok(r),
794 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
795 }
796 }
797 });
798 let c = ctx.clone();
799 let router = router.post("/admin/password_change", move |req| {
800 let c = c.clone();
801 async move {
802 match role_guard(&c, &req, Role::User).await? {
803 Guard::Redirect(r) => Ok(r),
804 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
805 }
806 }
807 });
808
809 let c = ctx.clone();
818 let router = router.get("/admin/reauth", move |req| {
819 let c = c.clone();
820 async move {
821 match role_guard(&c, &req, Role::User).await? {
822 Guard::Redirect(r) => Ok(r),
823 Guard::Allow(ident) => {
824 super::admin_recovery_handlers::show_reauth(&c, ident, &req).await
825 }
826 }
827 }
828 });
829
830 let c = ctx.clone();
831 let router = router.post("/admin/reauth", move |req| {
832 let c = c.clone();
833 async move {
834 match role_guard(&c, &req, Role::User).await? {
835 Guard::Redirect(r) => Ok(r),
836 Guard::Allow(ident) => {
837 super::admin_recovery_handlers::do_reauth(&c, ident, req).await
838 }
839 }
840 }
841 });
842
843 let c = ctx.clone();
853 let router = router.get("/admin/must-change-password", move |req| {
854 let c = c.clone();
855 async move {
856 match role_guard(&c, &req, Role::User).await? {
857 Guard::Redirect(r) => Ok(r),
858 Guard::Allow(ident) => {
859 super::admin_recovery_handlers::show_must_change_password(&c, ident, &req).await
860 }
861 }
862 }
863 });
864
865 let c = ctx.clone();
866 let router = router.post("/admin/must-change-password", move |req| {
867 let c = c.clone();
868 async move {
869 match role_guard(&c, &req, Role::User).await? {
870 Guard::Redirect(r) => Ok(r),
871 Guard::Allow(ident) => {
872 super::admin_recovery_handlers::do_must_change_password(&c, ident, req).await
873 }
874 }
875 }
876 });
877
878 let c = ctx.clone();
896 let router = router.get("/admin/mfa/verify", move |req| {
897 let c = c.clone();
898 async move {
899 match role_guard(&c, &req, Role::User).await? {
900 Guard::Redirect(r) => Ok(r),
901 Guard::Allow(ident) => super::mfa_handlers::show_verify(&c, ident, &req).await,
902 }
903 }
904 });
905
906 let c = ctx.clone();
907 let router = router.post("/admin/mfa/verify", move |req| {
908 let c = c.clone();
909 async move {
910 match role_guard(&c, &req, Role::User).await? {
911 Guard::Redirect(r) => Ok(r),
912 Guard::Allow(ident) => super::mfa_handlers::do_verify(&c, ident, req).await,
913 }
914 }
915 });
916
917 let c = ctx.clone();
919 let router = router.get("/admin/account/mfa/enroll", move |req| {
920 let c = c.clone();
921 async move {
922 match role_guard(&c, &req, Role::User).await? {
923 Guard::Redirect(r) => Ok(r),
924 Guard::Allow(ident) => super::mfa_handlers::show_enroll(&c, ident, &req).await,
925 }
926 }
927 });
928
929 let c = ctx.clone();
930 let router = router.post("/admin/account/mfa/enroll", move |req| {
931 let c = c.clone();
932 async move {
933 match role_guard(&c, &req, Role::User).await? {
934 Guard::Redirect(r) => Ok(r),
935 Guard::Allow(ident) => super::mfa_handlers::do_enroll(&c, ident, req).await,
936 }
937 }
938 });
939
940 let c = ctx.clone();
942 let router = router.get("/admin/account/mfa/regenerate-codes", move |req| {
943 let c = c.clone();
944 async move {
945 match role_guard(&c, &req, Role::User).await? {
946 Guard::Redirect(r) => Ok(r),
947 Guard::Allow(ident) => super::mfa_handlers::show_regenerate(&c, ident, &req).await,
948 }
949 }
950 });
951
952 let c = ctx.clone();
953 let router = router.post("/admin/account/mfa/regenerate-codes", move |req| {
954 let c = c.clone();
955 async move {
956 match role_guard(&c, &req, Role::User).await? {
957 Guard::Redirect(r) => Ok(r),
958 Guard::Allow(ident) => super::mfa_handlers::do_regenerate(&c, ident, req).await,
959 }
960 }
961 });
962
963 let c = ctx.clone();
965 let router = router.get("/admin/account/mfa/disable", move |req| {
966 let c = c.clone();
967 async move {
968 match role_guard(&c, &req, Role::User).await? {
969 Guard::Redirect(r) => Ok(r),
970 Guard::Allow(ident) => super::mfa_handlers::show_disable(&c, ident, &req).await,
971 }
972 }
973 });
974
975 let c = ctx.clone();
976 let router = router.post("/admin/account/mfa/disable", move |req| {
977 let c = c.clone();
978 async move {
979 match role_guard(&c, &req, Role::User).await? {
980 Guard::Redirect(r) => Ok(r),
981 Guard::Allow(ident) => super::mfa_handlers::do_disable(&c, ident, req).await,
982 }
983 }
984 });
985
986 let c = ctx.clone();
988 let ac = auth_ctx.clone();
989 let router = router.get("/admin/users", move |req| {
990 let c = c.clone();
991 let ac = ac.clone();
992 async move {
993 match role_guard(&c, &req, Role::Administrator).await? {
994 Guard::Redirect(r) => Ok(r),
995 Guard::Allow(ident) => {
996 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
997 }
998 }
999 }
1000 });
1001
1002 let c = ctx.clone();
1003 let ac = auth_ctx.clone();
1004 let router = router.get("/admin/users/new", move |req| {
1005 let c = c.clone();
1006 let ac = ac.clone();
1007 async move {
1008 match role_guard(&c, &req, Role::Administrator).await? {
1009 Guard::Redirect(r) => Ok(r),
1010 Guard::Allow(ident) => {
1011 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
1012 }
1013 }
1014 }
1015 });
1016
1017 let c = ctx.clone();
1018 let ac = auth_ctx.clone();
1019 let router = router.post("/admin/users/new", move |req| {
1020 let c = c.clone();
1021 let ac = ac.clone();
1022 async move {
1023 match role_guard(&c, &req, Role::Administrator).await? {
1024 Guard::Redirect(r) => Ok(r),
1025 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
1026 }
1027 }
1028 });
1029
1030 let c = ctx.clone();
1031 let ac = auth_ctx.clone();
1032 let router = router.get("/admin/users/:id/edit", move |req| {
1033 let c = c.clone();
1034 let ac = ac.clone();
1035 async move {
1036 match role_guard(&c, &req, Role::Administrator).await? {
1037 Guard::Redirect(r) => Ok(r),
1038 Guard::Allow(ident) => {
1039 let id = parse_id(req.param("id"))?;
1040 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
1041 }
1042 }
1043 }
1044 });
1045
1046 let c = ctx.clone();
1047 let ac = auth_ctx.clone();
1048 let router = router.post("/admin/users/:id/edit", move |req| {
1049 let c = c.clone();
1050 let ac = ac.clone();
1051 async move {
1052 match role_guard(&c, &req, Role::Administrator).await? {
1053 Guard::Redirect(r) => Ok(r),
1054 Guard::Allow(ident) => {
1055 let id = parse_id(req.param("id"))?;
1056 super::builtin::do_user_edit(&ac, ident, id, req).await
1057 }
1058 }
1059 }
1060 });
1061
1062 let c = ctx.clone();
1063 let ac = auth_ctx.clone();
1064 let router = router.get("/admin/users/:id/delete", move |req| {
1065 let c = c.clone();
1066 let ac = ac.clone();
1067 async move {
1068 match role_guard(&c, &req, Role::Administrator).await? {
1069 Guard::Redirect(r) => Ok(r),
1070 Guard::Allow(ident) => {
1071 let id = parse_id(req.param("id"))?;
1072 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
1073 .await
1074 }
1075 }
1076 }
1077 });
1078
1079 let c = ctx.clone();
1080 let ac = auth_ctx.clone();
1081 let router = router.post("/admin/users/:id/delete", move |req| {
1082 let c = c.clone();
1083 let ac = ac.clone();
1084 async move {
1085 match role_guard(&c, &req, Role::Administrator).await? {
1086 Guard::Redirect(r) => Ok(r),
1087 Guard::Allow(ident) => {
1088 let id = parse_id(req.param("id"))?;
1089 super::builtin::do_user_delete(&ac, ident, id, req).await
1090 }
1091 }
1092 }
1093 });
1094
1095 let c = ctx.clone();
1111 let router = router.get("/admin/users/:id/reset-password", move |req| {
1112 let c = c.clone();
1113 async move {
1114 match role_guard(&c, &req, Role::Administrator).await? {
1115 Guard::Redirect(r) => Ok(r),
1116 Guard::Allow(ident) => {
1117 let id = parse_id(req.param("id"))?;
1118 super::admin_recovery_handlers::show_admin_reset_password(&c, ident, id, &req)
1119 .await
1120 }
1121 }
1122 }
1123 });
1124
1125 let c = ctx.clone();
1127 let router = router.post("/admin/users/:id/reset-password", move |req| {
1128 let c = c.clone();
1129 async move {
1130 match role_guard(&c, &req, Role::Administrator).await? {
1131 Guard::Redirect(r) => Ok(r),
1132 Guard::Allow(ident) => {
1133 let id = parse_id(req.param("id"))?;
1134 super::admin_recovery_handlers::do_admin_reset_password(&c, ident, id, req)
1135 .await
1136 }
1137 }
1138 }
1139 });
1140
1141 let c = ctx.clone();
1143 let router = router.get("/admin/users/:id/lock", move |req| {
1144 let c = c.clone();
1145 async move {
1146 match role_guard(&c, &req, Role::Administrator).await? {
1147 Guard::Redirect(r) => Ok(r),
1148 Guard::Allow(ident) => {
1149 let id = parse_id(req.param("id"))?;
1150 super::admin_recovery_handlers::show_lock_user(&c, ident, id, &req).await
1151 }
1152 }
1153 }
1154 });
1155
1156 let c = ctx.clone();
1158 let router = router.post("/admin/users/:id/lock", move |req| {
1159 let c = c.clone();
1160 async move {
1161 match role_guard(&c, &req, Role::Administrator).await? {
1162 Guard::Redirect(r) => Ok(r),
1163 Guard::Allow(ident) => {
1164 let id = parse_id(req.param("id"))?;
1165 super::admin_recovery_handlers::do_lock_user(&c, ident, id, req).await
1166 }
1167 }
1168 }
1169 });
1170
1171 let c = ctx.clone();
1173 let router = router.get("/admin/users/:id/unlock", move |req| {
1174 let c = c.clone();
1175 async move {
1176 match role_guard(&c, &req, Role::Administrator).await? {
1177 Guard::Redirect(r) => Ok(r),
1178 Guard::Allow(ident) => {
1179 let id = parse_id(req.param("id"))?;
1180 super::admin_recovery_handlers::show_unlock_user(&c, ident, id, &req).await
1181 }
1182 }
1183 }
1184 });
1185
1186 let c = ctx.clone();
1188 let router = router.post("/admin/users/:id/unlock", move |req| {
1189 let c = c.clone();
1190 async move {
1191 match role_guard(&c, &req, Role::Administrator).await? {
1192 Guard::Redirect(r) => Ok(r),
1193 Guard::Allow(ident) => {
1194 let id = parse_id(req.param("id"))?;
1195 super::admin_recovery_handlers::do_unlock_user(&c, ident, id, req).await
1196 }
1197 }
1198 }
1199 });
1200
1201 let c = ctx.clone();
1204 let router = router.get("/admin/users/:id/revoke-sessions", move |req| {
1205 let c = c.clone();
1206 async move {
1207 match role_guard(&c, &req, Role::Administrator).await? {
1208 Guard::Redirect(r) => Ok(r),
1209 Guard::Allow(ident) => {
1210 let id = parse_id(req.param("id"))?;
1211 super::admin_recovery_handlers::show_admin_revoke_sessions(&c, ident, id, &req)
1212 .await
1213 }
1214 }
1215 }
1216 });
1217
1218 let c = ctx.clone();
1220 let router = router.post("/admin/users/:id/revoke-sessions", move |req| {
1221 let c = c.clone();
1222 async move {
1223 match role_guard(&c, &req, Role::Administrator).await? {
1224 Guard::Redirect(r) => Ok(r),
1225 Guard::Allow(ident) => {
1226 let id = parse_id(req.param("id"))?;
1227 super::admin_recovery_handlers::do_admin_revoke_sessions(&c, ident, id, req)
1228 .await
1229 }
1230 }
1231 }
1232 });
1233
1234 let c = ctx.clone();
1241 let ac = auth_ctx.clone();
1242 let router = router.get("/admin/users/:id", move |req| {
1243 let c = c.clone();
1244 let ac = ac.clone();
1245 async move {
1246 match role_guard(&c, &req, Role::Administrator).await? {
1247 Guard::Redirect(r) => Ok(r),
1248 Guard::Allow(ident) => {
1249 let id = parse_id(req.param("id"))?;
1250 let q = req.query();
1251 let tab = q.get("tab").map(|s| s.to_string());
1252 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
1253 super::builtin::show_user_view(
1254 &ac,
1255 ident,
1256 id,
1257 handlers::csrf_token(&req),
1258 tab,
1259 page,
1260 )
1261 .await
1262 }
1263 }
1264 }
1265 });
1266
1267 let c = ctx.clone();
1269 let ac = auth_ctx.clone();
1270 let router = router.get("/admin/groups", move |req| {
1271 let c = c.clone();
1272 let ac = ac.clone();
1273 async move {
1274 match role_guard(&c, &req, Role::Administrator).await? {
1275 Guard::Redirect(r) => Ok(r),
1276 Guard::Allow(ident) => {
1277 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
1278 }
1279 }
1280 }
1281 });
1282
1283 let c = ctx.clone();
1284 let ac = auth_ctx.clone();
1285 let router = router.get("/admin/groups/new", move |req| {
1286 let c = c.clone();
1287 let ac = ac.clone();
1288 async move {
1289 match role_guard(&c, &req, Role::Administrator).await? {
1290 Guard::Redirect(r) => Ok(r),
1291 Guard::Allow(ident) => {
1292 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
1293 }
1294 }
1295 }
1296 });
1297
1298 let c = ctx.clone();
1299 let ac = auth_ctx.clone();
1300 let router = router.post("/admin/groups/new", move |req| {
1301 let c = c.clone();
1302 let ac = ac.clone();
1303 async move {
1304 match role_guard(&c, &req, Role::Administrator).await? {
1305 Guard::Redirect(r) => Ok(r),
1306 Guard::Allow(ident) => super::builtin::do_new_group(&ac, ident, req).await,
1307 }
1308 }
1309 });
1310
1311 let c = ctx.clone();
1312 let ac = auth_ctx.clone();
1313 let router = router.get("/admin/groups/:id/edit", move |req| {
1314 let c = c.clone();
1315 let ac = ac.clone();
1316 async move {
1317 match role_guard(&c, &req, Role::Administrator).await? {
1318 Guard::Redirect(r) => Ok(r),
1319 Guard::Allow(ident) => {
1320 let id = parse_id(req.param("id"))?;
1321 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
1322 .await
1323 }
1324 }
1325 }
1326 });
1327
1328 let c = ctx.clone();
1329 let ac = auth_ctx.clone();
1330 let router = router.post("/admin/groups/:id/edit", move |req| {
1331 let c = c.clone();
1332 let ac = ac.clone();
1333 async move {
1334 match role_guard(&c, &req, Role::Administrator).await? {
1335 Guard::Redirect(r) => Ok(r),
1336 Guard::Allow(ident) => {
1337 let id = parse_id(req.param("id"))?;
1338 super::builtin::do_group_edit(&ac, ident, id, req).await
1339 }
1340 }
1341 }
1342 });
1343
1344 let c = ctx.clone();
1345 let ac = auth_ctx.clone();
1346 let router = router.get("/admin/groups/:id/delete", move |req| {
1347 let c = c.clone();
1348 let ac = ac.clone();
1349 async move {
1350 match role_guard(&c, &req, Role::Administrator).await? {
1351 Guard::Redirect(r) => Ok(r),
1352 Guard::Allow(ident) => {
1353 let id = parse_id(req.param("id"))?;
1354 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
1355 .await
1356 }
1357 }
1358 }
1359 });
1360
1361 let c = ctx.clone();
1362 let ac = auth_ctx.clone();
1363 let router = router.post("/admin/groups/:id/delete", move |req| {
1364 let c = c.clone();
1365 let ac = ac.clone();
1366 async move {
1367 match role_guard(&c, &req, Role::Administrator).await? {
1368 Guard::Redirect(r) => Ok(r),
1369 Guard::Allow(ident) => {
1370 let id = parse_id(req.param("id"))?;
1371 super::builtin::do_group_delete(&ac, ident, id, req).await
1372 }
1373 }
1374 }
1375 });
1376
1377 let c = ctx.clone();
1379 let router = router.get("/admin/:admin_name", move |req| {
1380 let c = c.clone();
1381 async move {
1382 let name = model_name_from_req(&req)?;
1383 let perm = perm_for(&c, &name, "view")?;
1384 match perm_guard(&c, &req, &perm).await? {
1385 Guard::Redirect(r) => Ok(r),
1386 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
1387 }
1388 }
1389 });
1390
1391 let c = ctx.clone();
1393 let router = router.get("/admin/:admin_name/new", move |req| {
1394 let c = c.clone();
1395 async move {
1396 let name = model_name_from_req(&req)?;
1397 let perm = perm_for(&c, &name, "add")?;
1398 match perm_guard(&c, &req, &perm).await? {
1399 Guard::Redirect(r) => Ok(r),
1400 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
1401 }
1402 }
1403 });
1404 let c = ctx.clone();
1405 let router = router.post("/admin/:admin_name/new", move |req| {
1406 let c = c.clone();
1407 async move {
1408 let name = model_name_from_req(&req)?;
1409 let perm = perm_for(&c, &name, "add")?;
1410 match perm_guard(&c, &req, &perm).await? {
1411 Guard::Redirect(r) => Ok(r),
1412 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
1413 }
1414 }
1415 });
1416
1417 let c = ctx.clone();
1419 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
1420 let c = c.clone();
1421 async move {
1422 let name = model_name_from_req(&req)?;
1423 let perm = perm_for(&c, &name, "change")?;
1424 match perm_guard(&c, &req, &perm).await? {
1425 Guard::Redirect(r) => Ok(r),
1426 Guard::Allow(ident) => {
1427 let id = parse_id(req.param("id"))?;
1428 handlers::show_edit_form(&c, ident, &name, id, &req).await
1429 }
1430 }
1431 }
1432 });
1433 let c = ctx.clone();
1434 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
1435 let c = c.clone();
1436 async move {
1437 let name = model_name_from_req(&req)?;
1438 let perm = perm_for(&c, &name, "change")?;
1439 match perm_guard(&c, &req, &perm).await? {
1440 Guard::Redirect(r) => Ok(r),
1441 Guard::Allow(ident) => {
1442 let id = parse_id(req.param("id"))?;
1443 handlers::do_update(&c, ident, &name, id, req).await
1444 }
1445 }
1446 }
1447 });
1448
1449 let c = ctx.clone();
1452 let router = router.get("/admin/:admin_name/:id/history", move |req| {
1453 let c = c.clone();
1454 async move {
1455 let name = model_name_from_req(&req)?;
1456 let perm = perm_for(&c, &name, "view")?;
1457 match perm_guard(&c, &req, &perm).await? {
1458 Guard::Redirect(r) => Ok(r),
1459 Guard::Allow(ident) => {
1460 let id = parse_id(req.param("id"))?;
1461 handlers::show_object_history(&c, ident, &name, id, &req).await
1462 }
1463 }
1464 }
1465 });
1466
1467 let c = ctx.clone();
1469 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
1470 let c = c.clone();
1471 async move {
1472 let name = model_name_from_req(&req)?;
1473 let perm = perm_for(&c, &name, "delete")?;
1474 match perm_guard(&c, &req, &perm).await? {
1475 Guard::Redirect(r) => Ok(r),
1476 Guard::Allow(ident) => {
1477 let id = parse_id(req.param("id"))?;
1478 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
1479 }
1480 }
1481 }
1482 });
1483 let c = ctx.clone();
1484 let router = router.post("/admin/:admin_name/:id/delete", move |req| {
1485 let c = c.clone();
1486 async move {
1487 let name = model_name_from_req(&req)?;
1488 let perm = perm_for(&c, &name, "delete")?;
1489 match perm_guard(&c, &req, &perm).await? {
1490 Guard::Redirect(r) => Ok(r),
1491 Guard::Allow(ident) => {
1492 let id = parse_id(req.param("id"))?;
1493 handlers::do_delete(&c, ident, &name, id).await
1494 }
1495 }
1496 }
1497 });
1498
1499 let c = ctx.clone();
1504 let router = router.post("/admin/:admin_name/bulk_delete", move |req| {
1505 let c = c.clone();
1506 async move {
1507 let name = model_name_from_req(&req)?;
1508 let perm = perm_for(&c, &name, "delete")?;
1509 match perm_guard(&c, &req, &perm).await? {
1510 Guard::Redirect(r) => Ok(r),
1511 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
1512 }
1513 }
1514 });
1515
1516 let c = ctx.clone();
1521 router.post("/admin/:admin_name/bulk/:action", move |req| {
1522 let c = c.clone();
1523 async move {
1524 let name = model_name_from_req(&req)?;
1525 let action = req
1526 .param("action")
1527 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
1528 .to_string();
1529 let perm = perm_for(&c, &name, "change")?;
1530 match perm_guard(&c, &req, &perm).await? {
1531 Guard::Redirect(r) => Ok(r),
1532 Guard::Allow(ident) => {
1533 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
1534 }
1535 }
1536 }
1537 })
1538}
1539
1540#[cfg(test)]
1541mod tests {
1542 use super::*;
1543
1544 fn make_identity(role: Role, is_active: bool) -> Identity {
1545 Identity {
1546 user_id: 42,
1547 email: "test@example.com".into(),
1548 role,
1549 is_active,
1550 is_demo: false,
1551 demo_label: None,
1552 must_change_password: false,
1553 mfa_enabled: false,
1554 trust_level: crate::auth::SessionTrust::Authenticated,
1555 }
1556 }
1557
1558 #[test]
1563 fn role_guard_decision_admin_meets_staff_floor() {
1564 let id = make_identity(Role::Administrator, true);
1565 assert!(id.role.includes(Role::Staff));
1566 }
1567
1568 #[test]
1569 fn role_guard_decision_user_does_not_meet_staff() {
1570 let id = make_identity(Role::User, true);
1571 assert!(!id.role.includes(Role::Staff));
1572 }
1573
1574 #[test]
1575 fn role_guard_decision_administrator_does_not_meet_developer() {
1576 let id = make_identity(Role::Administrator, true);
1577 assert!(!id.role.includes(Role::Developer));
1578 }
1579
1580 #[test]
1581 fn role_guard_decision_developer_meets_everything() {
1582 let id = make_identity(Role::Developer, true);
1583 for &min in &[
1584 Role::User,
1585 Role::Staff,
1586 Role::Supervisor,
1587 Role::Administrator,
1588 Role::Developer,
1589 ] {
1590 assert!(id.role.includes(min), "Developer should meet {min:?}");
1591 }
1592 }
1593
1594 #[test]
1597 fn perm_guard_admin_short_circuits_without_perm() {
1598 let id = make_identity(Role::Administrator, true);
1599 assert!(perm_guard_verdict(&id, false));
1600 }
1601
1602 #[test]
1603 fn perm_guard_developer_short_circuits_without_perm() {
1604 let id = make_identity(Role::Developer, true);
1605 assert!(perm_guard_verdict(&id, false));
1606 }
1607
1608 #[test]
1609 fn perm_guard_staff_with_perm_passes() {
1610 let id = make_identity(Role::Staff, true);
1611 assert!(perm_guard_verdict(&id, true));
1612 }
1613
1614 #[test]
1615 fn perm_guard_staff_without_perm_denies() {
1616 let id = make_identity(Role::Staff, true);
1617 assert!(!perm_guard_verdict(&id, false));
1618 }
1619
1620 #[test]
1621 fn perm_guard_inactive_admin_denies_even_with_bypass() {
1622 let id = make_identity(Role::Administrator, false);
1624 assert!(!perm_guard_verdict(&id, true));
1625 }
1626
1627 #[test]
1628 fn perm_guard_supervisor_without_perm_denies() {
1629 let id = make_identity(Role::Supervisor, true);
1631 assert!(!perm_guard_verdict(&id, false));
1632 }
1633
1634 #[test]
1639 fn strict_mailer_guard_passes_for_default_admin() {
1640 let admin = super::super::types::Admin::new();
1641 assert!(strict_mailer_guard_check(&admin).is_ok());
1642 }
1643
1644 #[test]
1647 fn strict_mailer_guard_fails_when_required_but_default_mailer() {
1648 use crate::auth::DefaultRecoveryPolicy;
1649 let admin = super::super::types::Admin::new().recovery_policy(std::sync::Arc::new(
1650 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
1651 ));
1652 let err = strict_mailer_guard_check(&admin).expect_err("guard should fail");
1653 assert!(
1654 err.contains("strict_mailer_required"),
1655 "error message must name the policy method: {err}"
1656 );
1657 assert!(
1658 err.contains("Admin::mailer"),
1659 "error message must direct the operator to the fix: {err}"
1660 );
1661 }
1662
1663 #[test]
1668 fn strict_mailer_guard_passes_when_mailer_was_explicitly_overridden() {
1669 use crate::auth::DefaultRecoveryPolicy;
1670 use crate::email::LogMailer;
1671 let admin = super::super::types::Admin::new()
1672 .recovery_policy(std::sync::Arc::new(
1673 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
1674 ))
1675 .mailer(std::sync::Arc::new(LogMailer));
1676 assert!(strict_mailer_guard_check(&admin).is_ok());
1677 }
1678
1679 #[test]
1682 fn strict_mailer_guard_passes_when_strict_mode_disabled() {
1683 let admin = super::super::types::Admin::new();
1684 assert!(strict_mailer_guard_check(&admin).is_ok());
1685 }
1686
1687 #[test]
1690 fn whitelist_accepts_the_three_locked_paths() {
1691 assert!(super::is_must_change_whitelisted_path(
1693 "/admin/must-change-password"
1694 ));
1695 assert!(super::is_must_change_whitelisted_path("/admin/logout"));
1696 assert!(super::is_must_change_whitelisted_path(
1697 "/admin/account/sessions"
1698 ));
1699 }
1700
1701 #[test]
1702 fn whitelist_rejects_subpaths_of_account_sessions() {
1703 assert!(!super::is_must_change_whitelisted_path(
1708 "/admin/account/sessions/revoke"
1709 ));
1710 assert!(!super::is_must_change_whitelisted_path(
1711 "/admin/account/sessions/revoke-others"
1712 ));
1713 assert!(!super::is_must_change_whitelisted_path(
1714 "/admin/account/sessions/"
1715 ));
1716 }
1717
1718 #[test]
1719 fn whitelist_rejects_other_admin_paths() {
1720 for path in [
1721 "/admin",
1722 "/admin/",
1723 "/admin/users",
1724 "/admin/users/42",
1725 "/admin/login",
1726 "/admin/password_change",
1727 "/admin/forgot-password",
1728 "/admin/reauth",
1729 "/admin/must-change-password/", ] {
1731 assert!(
1732 !super::is_must_change_whitelisted_path(path),
1733 "expected reject for {path:?}"
1734 );
1735 }
1736 }
1737
1738 #[test]
1739 fn whitelist_rejects_paths_outside_admin_surface() {
1740 for path in ["/", "/login", "/static/admin.css", "/api"] {
1741 assert!(
1742 !super::is_must_change_whitelisted_path(path),
1743 "expected reject for {path:?}"
1744 );
1745 }
1746 }
1747}