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
43const FONT_GEIST: &[u8] = include_bytes!("../../assets/static/fonts/Geist-Variable.woff2");
53const FONT_GEIST_MONO: &[u8] = include_bytes!("../../assets/static/fonts/GeistMono-Variable.woff2");
54const FONT_TAJAWAL_REG: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Regular.woff2");
55const FONT_TAJAWAL_MED: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Medium.woff2");
56const FONT_TAJAWAL_BOLD: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Bold.woff2");
57const FONT_NOTO_NASKH_AR: &[u8] =
58 include_bytes!("../../assets/static/fonts/NotoNaskhArabic-Variable.woff2");
59
60use super::handlers::{self, AdminCtx};
61use super::render;
62use super::types::Admin;
63
64enum Guard {
68 Allow(Identity),
69 Redirect(Response),
70}
71
72async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
73 let cookie = match req.header("cookie") {
74 Some(c) => c,
75 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
76 };
77 let token = match auth::session_token_from_cookie(cookie) {
78 Some(t) => t,
79 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
80 };
81 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
82 Some(i) => i,
83 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
84 };
85 if !ident.is_active {
86 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
87 }
88 Ok(Guard::Allow(ident))
89}
90
91async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
92 match login_guard(ctx, req).await? {
93 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
94 Guard::Allow(ident) => {
95 if ident.role.includes(min) {
96 Ok(Guard::Allow(ident))
97 } else {
98 let body = render::render_forbidden_body(
99 &ctx.admin,
100 &ctx.templates,
101 &ident,
102 handlers::csrf_token(req),
103 None,
104 Some(min.label()),
105 )?;
106 Ok(Guard::Redirect(
107 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
108 ))
109 }
110 }
111 }
112}
113
114async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
115 match role_guard(ctx, req, Role::Staff).await? {
116 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
117 Guard::Allow(ident) => {
118 if ident.role.bypasses_group_checks() {
119 return Ok(Guard::Allow(ident));
120 }
121 if auth::check_permission(&ctx.db, &ident, perm).await? {
122 Ok(Guard::Allow(ident))
123 } else {
124 let body = render::render_forbidden_body(
125 &ctx.admin,
126 &ctx.templates,
127 &ident,
128 handlers::csrf_token(req),
129 Some(perm.to_string()),
130 None,
131 )?;
132 Ok(Guard::Redirect(
133 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
134 ))
135 }
136 }
137 }
138}
139
140#[cfg(test)]
143fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
144 if !ident.is_active {
145 return false;
146 }
147 if ident.role.bypasses_group_checks() {
148 return true;
149 }
150 perm_held
151}
152
153fn parse_id(raw: Option<&str>) -> Result<i64> {
154 raw.and_then(|s| s.parse().ok())
155 .ok_or_else(|| Error::BadRequest("invalid id".into()))
156}
157
158fn model_name_from_req(req: &Request) -> Result<String> {
159 req.param("admin_name")
160 .map(|s| s.to_string())
161 .ok_or_else(|| Error::BadRequest("missing model".into()))
162}
163
164fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
165 let entry = ctx
166 .admin
167 .find(admin_name)
168 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
169 let singular = entry.singular_name.to_ascii_lowercase();
170 Ok(format!("{admin_name}.{action}_{singular}"))
171}
172
173pub fn register_admin_routes(
174 router: Router,
175 admin: Admin,
176 db: Db,
177 templates: Arc<Templates>,
178) -> Router {
179 let ctx = Arc::new(AdminCtx::new(
180 Arc::new(admin),
181 db.clone(),
182 templates.clone(),
183 ));
184
185 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
188 admin: ctx.admin.clone(),
189 db,
190 templates,
191 });
192
193 let err_admin = ctx.admin.clone();
200 let err_templates = ctx.templates.clone();
201 let router = router.middleware(move |req, next| {
202 let admin = err_admin.clone();
203 let templates = err_templates.clone();
204 Box::pin(async move {
205 let is_admin_path = req.path().starts_with("/admin");
206 let result = next.run(req).await;
207 match result {
208 Ok(resp) => Ok(resp),
209 Err(err) if is_admin_path => Ok(render::render_admin_error_response(
210 &admin,
211 &templates,
212 None,
213 err.status(),
214 err.client_message().to_string(),
215 )),
216 Err(err) => Err(err),
217 }
218 })
219 });
220
221 let router = router.get("/static/admin.css", |_req| async move {
227 Ok(Response::new(
228 hyper::StatusCode::OK,
229 bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
230 )
231 .with_header("content-type", "text/css; charset=utf-8")
232 .with_header("cache-control", "no-cache, must-revalidate"))
233 });
234 let router = router.get("/static/admin.js", |_req| async move {
235 Ok(Response::new(
236 hyper::StatusCode::OK,
237 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
238 )
239 .with_header("content-type", "application/javascript; charset=utf-8")
240 .with_header("cache-control", "no-cache, must-revalidate"))
241 });
242
243 fn font_response(bytes: &'static [u8]) -> Response {
247 Response::new(hyper::StatusCode::OK, bytes::Bytes::from_static(bytes))
248 .with_header("content-type", "font/woff2")
249 .with_header("cache-control", "public, max-age=31536000, immutable")
250 }
251 let router = router.get("/static/fonts/Geist-Variable.woff2", |_req| async move {
252 Ok(font_response(FONT_GEIST))
253 });
254 let router = router.get(
255 "/static/fonts/GeistMono-Variable.woff2",
256 |_req| async move { Ok(font_response(FONT_GEIST_MONO)) },
257 );
258 let router = router.get("/static/fonts/Tajawal-Regular.woff2", |_req| async move {
259 Ok(font_response(FONT_TAJAWAL_REG))
260 });
261 let router = router.get("/static/fonts/Tajawal-Medium.woff2", |_req| async move {
262 Ok(font_response(FONT_TAJAWAL_MED))
263 });
264 let router = router.get("/static/fonts/Tajawal-Bold.woff2", |_req| async move {
265 Ok(font_response(FONT_TAJAWAL_BOLD))
266 });
267 let router = router.get(
268 "/static/fonts/NotoNaskhArabic-Variable.woff2",
269 |_req| async move { Ok(font_response(FONT_NOTO_NASKH_AR)) },
270 );
271
272 let c = ctx.clone();
274 let router = router.get("/admin/login", move |req| {
275 let c = c.clone();
276 async move { handlers::show_login(&c, req).await }
277 });
278
279 let c = ctx.clone();
280 let router = router.post("/admin/login", move |req| {
281 let c = c.clone();
282 async move { handlers::do_login(&c, req).await }
283 });
284
285 let c = ctx.clone();
286 let router = router.post("/admin/logout", move |req| {
287 let c = c.clone();
288 async move { handlers::do_logout(&c, req).await }
289 });
290
291 let c = ctx.clone();
293 let router = router.get("/admin", move |req| {
294 let c = c.clone();
295 async move {
296 match role_guard(&c, &req, Role::Staff).await? {
297 Guard::Redirect(r) => Ok(r),
298 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
299 }
300 }
301 });
302
303 let c = ctx.clone();
305 let router = router.get("/admin/history", move |req| {
306 let c = c.clone();
307 async move {
308 match role_guard(&c, &req, Role::Administrator).await? {
309 Guard::Redirect(r) => Ok(r),
310 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
311 }
312 }
313 });
314
315 let c = ctx.clone();
320 let router = router.get("/admin/account/sessions", move |req| {
321 let c = c.clone();
322 async move {
323 match role_guard(&c, &req, Role::User).await? {
324 Guard::Redirect(r) => Ok(r),
325 Guard::Allow(ident) => handlers::show_account_sessions(&c, ident, &req).await,
326 }
327 }
328 });
329
330 let c = ctx.clone();
334 let router = router.get("/admin/password_change", move |req| {
335 let c = c.clone();
336 async move {
337 match role_guard(&c, &req, Role::User).await? {
338 Guard::Redirect(r) => Ok(r),
339 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
340 }
341 }
342 });
343 let c = ctx.clone();
344 let router = router.post("/admin/password_change", move |req| {
345 let c = c.clone();
346 async move {
347 match role_guard(&c, &req, Role::User).await? {
348 Guard::Redirect(r) => Ok(r),
349 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
350 }
351 }
352 });
353
354 let c = ctx.clone();
356 let ac = auth_ctx.clone();
357 let router = router.get("/admin/users", move |req| {
358 let c = c.clone();
359 let ac = ac.clone();
360 async move {
361 match role_guard(&c, &req, Role::Administrator).await? {
362 Guard::Redirect(r) => Ok(r),
363 Guard::Allow(ident) => {
364 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
365 }
366 }
367 }
368 });
369
370 let c = ctx.clone();
371 let ac = auth_ctx.clone();
372 let router = router.get("/admin/users/new", move |req| {
373 let c = c.clone();
374 let ac = ac.clone();
375 async move {
376 match role_guard(&c, &req, Role::Administrator).await? {
377 Guard::Redirect(r) => Ok(r),
378 Guard::Allow(ident) => {
379 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
380 }
381 }
382 }
383 });
384
385 let c = ctx.clone();
386 let ac = auth_ctx.clone();
387 let router = router.post("/admin/users/new", move |req| {
388 let c = c.clone();
389 let ac = ac.clone();
390 async move {
391 match role_guard(&c, &req, Role::Administrator).await? {
392 Guard::Redirect(r) => Ok(r),
393 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
394 }
395 }
396 });
397
398 let c = ctx.clone();
399 let ac = auth_ctx.clone();
400 let router = router.get("/admin/users/:id/edit", move |req| {
401 let c = c.clone();
402 let ac = ac.clone();
403 async move {
404 match role_guard(&c, &req, Role::Administrator).await? {
405 Guard::Redirect(r) => Ok(r),
406 Guard::Allow(ident) => {
407 let id = parse_id(req.param("id"))?;
408 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
409 }
410 }
411 }
412 });
413
414 let c = ctx.clone();
415 let ac = auth_ctx.clone();
416 let router = router.post("/admin/users/:id/edit", move |req| {
417 let c = c.clone();
418 let ac = ac.clone();
419 async move {
420 match role_guard(&c, &req, Role::Administrator).await? {
421 Guard::Redirect(r) => Ok(r),
422 Guard::Allow(ident) => {
423 let id = parse_id(req.param("id"))?;
424 super::builtin::do_user_edit(&ac, ident, id, req).await
425 }
426 }
427 }
428 });
429
430 let c = ctx.clone();
431 let ac = auth_ctx.clone();
432 let router = router.get("/admin/users/:id/delete", move |req| {
433 let c = c.clone();
434 let ac = ac.clone();
435 async move {
436 match role_guard(&c, &req, Role::Administrator).await? {
437 Guard::Redirect(r) => Ok(r),
438 Guard::Allow(ident) => {
439 let id = parse_id(req.param("id"))?;
440 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
441 .await
442 }
443 }
444 }
445 });
446
447 let c = ctx.clone();
448 let ac = auth_ctx.clone();
449 let router = router.post("/admin/users/:id/delete", move |req| {
450 let c = c.clone();
451 let ac = ac.clone();
452 async move {
453 match role_guard(&c, &req, Role::Administrator).await? {
454 Guard::Redirect(r) => Ok(r),
455 Guard::Allow(ident) => {
456 let id = parse_id(req.param("id"))?;
457 super::builtin::do_user_delete(&ac, ident, id, req).await
458 }
459 }
460 }
461 });
462
463 let c = ctx.clone();
470 let ac = auth_ctx.clone();
471 let router = router.get("/admin/users/:id", move |req| {
472 let c = c.clone();
473 let ac = ac.clone();
474 async move {
475 match role_guard(&c, &req, Role::Administrator).await? {
476 Guard::Redirect(r) => Ok(r),
477 Guard::Allow(ident) => {
478 let id = parse_id(req.param("id"))?;
479 let q = req.query();
480 let tab = q.get("tab").map(|s| s.to_string());
481 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
482 super::builtin::show_user_view(
483 &ac,
484 ident,
485 id,
486 handlers::csrf_token(&req),
487 tab,
488 page,
489 )
490 .await
491 }
492 }
493 }
494 });
495
496 let c = ctx.clone();
498 let ac = auth_ctx.clone();
499 let router = router.get("/admin/groups", move |req| {
500 let c = c.clone();
501 let ac = ac.clone();
502 async move {
503 match role_guard(&c, &req, Role::Administrator).await? {
504 Guard::Redirect(r) => Ok(r),
505 Guard::Allow(ident) => {
506 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
507 }
508 }
509 }
510 });
511
512 let c = ctx.clone();
513 let ac = auth_ctx.clone();
514 let router = router.get("/admin/groups/new", move |req| {
515 let c = c.clone();
516 let ac = ac.clone();
517 async move {
518 match role_guard(&c, &req, Role::Administrator).await? {
519 Guard::Redirect(r) => Ok(r),
520 Guard::Allow(ident) => {
521 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
522 }
523 }
524 }
525 });
526
527 let c = ctx.clone();
528 let ac = auth_ctx.clone();
529 let router = router.post("/admin/groups/new", 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) => super::builtin::do_new_group(&ac, ident, req).await,
536 }
537 }
538 });
539
540 let c = ctx.clone();
541 let ac = auth_ctx.clone();
542 let router = router.get("/admin/groups/:id/edit", move |req| {
543 let c = c.clone();
544 let ac = ac.clone();
545 async move {
546 match role_guard(&c, &req, Role::Administrator).await? {
547 Guard::Redirect(r) => Ok(r),
548 Guard::Allow(ident) => {
549 let id = parse_id(req.param("id"))?;
550 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
551 .await
552 }
553 }
554 }
555 });
556
557 let c = ctx.clone();
558 let ac = auth_ctx.clone();
559 let router = router.post("/admin/groups/:id/edit", move |req| {
560 let c = c.clone();
561 let ac = ac.clone();
562 async move {
563 match role_guard(&c, &req, Role::Administrator).await? {
564 Guard::Redirect(r) => Ok(r),
565 Guard::Allow(ident) => {
566 let id = parse_id(req.param("id"))?;
567 super::builtin::do_group_edit(&ac, ident, id, req).await
568 }
569 }
570 }
571 });
572
573 let c = ctx.clone();
574 let ac = auth_ctx.clone();
575 let router = router.get("/admin/groups/:id/delete", move |req| {
576 let c = c.clone();
577 let ac = ac.clone();
578 async move {
579 match role_guard(&c, &req, Role::Administrator).await? {
580 Guard::Redirect(r) => Ok(r),
581 Guard::Allow(ident) => {
582 let id = parse_id(req.param("id"))?;
583 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
584 .await
585 }
586 }
587 }
588 });
589
590 let c = ctx.clone();
591 let ac = auth_ctx.clone();
592 let router = router.post("/admin/groups/:id/delete", move |req| {
593 let c = c.clone();
594 let ac = ac.clone();
595 async move {
596 match role_guard(&c, &req, Role::Administrator).await? {
597 Guard::Redirect(r) => Ok(r),
598 Guard::Allow(ident) => {
599 let id = parse_id(req.param("id"))?;
600 super::builtin::do_group_delete(&ac, ident, id, req).await
601 }
602 }
603 }
604 });
605
606 let c = ctx.clone();
608 let router = router.get("/admin/:admin_name", move |req| {
609 let c = c.clone();
610 async move {
611 let name = model_name_from_req(&req)?;
612 let perm = perm_for(&c, &name, "view")?;
613 match perm_guard(&c, &req, &perm).await? {
614 Guard::Redirect(r) => Ok(r),
615 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
616 }
617 }
618 });
619
620 let c = ctx.clone();
622 let router = router.get("/admin/:admin_name/new", move |req| {
623 let c = c.clone();
624 async move {
625 let name = model_name_from_req(&req)?;
626 let perm = perm_for(&c, &name, "add")?;
627 match perm_guard(&c, &req, &perm).await? {
628 Guard::Redirect(r) => Ok(r),
629 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
630 }
631 }
632 });
633 let c = ctx.clone();
634 let router = router.post("/admin/:admin_name/new", move |req| {
635 let c = c.clone();
636 async move {
637 let name = model_name_from_req(&req)?;
638 let perm = perm_for(&c, &name, "add")?;
639 match perm_guard(&c, &req, &perm).await? {
640 Guard::Redirect(r) => Ok(r),
641 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
642 }
643 }
644 });
645
646 let c = ctx.clone();
648 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
649 let c = c.clone();
650 async move {
651 let name = model_name_from_req(&req)?;
652 let perm = perm_for(&c, &name, "change")?;
653 match perm_guard(&c, &req, &perm).await? {
654 Guard::Redirect(r) => Ok(r),
655 Guard::Allow(ident) => {
656 let id = parse_id(req.param("id"))?;
657 handlers::show_edit_form(&c, ident, &name, id, &req).await
658 }
659 }
660 }
661 });
662 let c = ctx.clone();
663 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
664 let c = c.clone();
665 async move {
666 let name = model_name_from_req(&req)?;
667 let perm = perm_for(&c, &name, "change")?;
668 match perm_guard(&c, &req, &perm).await? {
669 Guard::Redirect(r) => Ok(r),
670 Guard::Allow(ident) => {
671 let id = parse_id(req.param("id"))?;
672 handlers::do_update(&c, ident, &name, id, req).await
673 }
674 }
675 }
676 });
677
678 let c = ctx.clone();
681 let router = router.get("/admin/:admin_name/:id/history", move |req| {
682 let c = c.clone();
683 async move {
684 let name = model_name_from_req(&req)?;
685 let perm = perm_for(&c, &name, "view")?;
686 match perm_guard(&c, &req, &perm).await? {
687 Guard::Redirect(r) => Ok(r),
688 Guard::Allow(ident) => {
689 let id = parse_id(req.param("id"))?;
690 handlers::show_object_history(&c, ident, &name, id, &req).await
691 }
692 }
693 }
694 });
695
696 let c = ctx.clone();
698 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
699 let c = c.clone();
700 async move {
701 let name = model_name_from_req(&req)?;
702 let perm = perm_for(&c, &name, "delete")?;
703 match perm_guard(&c, &req, &perm).await? {
704 Guard::Redirect(r) => Ok(r),
705 Guard::Allow(ident) => {
706 let id = parse_id(req.param("id"))?;
707 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
708 }
709 }
710 }
711 });
712 let c = ctx.clone();
713 let router = router.post("/admin/:admin_name/:id/delete", move |req| {
714 let c = c.clone();
715 async move {
716 let name = model_name_from_req(&req)?;
717 let perm = perm_for(&c, &name, "delete")?;
718 match perm_guard(&c, &req, &perm).await? {
719 Guard::Redirect(r) => Ok(r),
720 Guard::Allow(ident) => {
721 let id = parse_id(req.param("id"))?;
722 handlers::do_delete(&c, ident, &name, id).await
723 }
724 }
725 }
726 });
727
728 let c = ctx.clone();
733 let router = router.post("/admin/:admin_name/bulk_delete", move |req| {
734 let c = c.clone();
735 async move {
736 let name = model_name_from_req(&req)?;
737 let perm = perm_for(&c, &name, "delete")?;
738 match perm_guard(&c, &req, &perm).await? {
739 Guard::Redirect(r) => Ok(r),
740 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
741 }
742 }
743 });
744
745 let c = ctx.clone();
750 router.post("/admin/:admin_name/bulk/:action", move |req| {
751 let c = c.clone();
752 async move {
753 let name = model_name_from_req(&req)?;
754 let action = req
755 .param("action")
756 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
757 .to_string();
758 let perm = perm_for(&c, &name, "change")?;
759 match perm_guard(&c, &req, &perm).await? {
760 Guard::Redirect(r) => Ok(r),
761 Guard::Allow(ident) => {
762 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
763 }
764 }
765 }
766 })
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772
773 fn make_identity(role: Role, is_active: bool) -> Identity {
774 Identity {
775 user_id: 42,
776 email: "test@example.com".into(),
777 role,
778 is_active,
779 is_demo: false,
780 demo_label: None,
781 }
782 }
783
784 #[test]
789 fn role_guard_decision_admin_meets_staff_floor() {
790 let id = make_identity(Role::Administrator, true);
791 assert!(id.role.includes(Role::Staff));
792 }
793
794 #[test]
795 fn role_guard_decision_user_does_not_meet_staff() {
796 let id = make_identity(Role::User, true);
797 assert!(!id.role.includes(Role::Staff));
798 }
799
800 #[test]
801 fn role_guard_decision_administrator_does_not_meet_developer() {
802 let id = make_identity(Role::Administrator, true);
803 assert!(!id.role.includes(Role::Developer));
804 }
805
806 #[test]
807 fn role_guard_decision_developer_meets_everything() {
808 let id = make_identity(Role::Developer, true);
809 for &min in &[
810 Role::User,
811 Role::Staff,
812 Role::Supervisor,
813 Role::Administrator,
814 Role::Developer,
815 ] {
816 assert!(id.role.includes(min), "Developer should meet {min:?}");
817 }
818 }
819
820 #[test]
823 fn perm_guard_admin_short_circuits_without_perm() {
824 let id = make_identity(Role::Administrator, true);
825 assert!(perm_guard_verdict(&id, false));
826 }
827
828 #[test]
829 fn perm_guard_developer_short_circuits_without_perm() {
830 let id = make_identity(Role::Developer, true);
831 assert!(perm_guard_verdict(&id, false));
832 }
833
834 #[test]
835 fn perm_guard_staff_with_perm_passes() {
836 let id = make_identity(Role::Staff, true);
837 assert!(perm_guard_verdict(&id, true));
838 }
839
840 #[test]
841 fn perm_guard_staff_without_perm_denies() {
842 let id = make_identity(Role::Staff, true);
843 assert!(!perm_guard_verdict(&id, false));
844 }
845
846 #[test]
847 fn perm_guard_inactive_admin_denies_even_with_bypass() {
848 let id = make_identity(Role::Administrator, false);
850 assert!(!perm_guard_verdict(&id, true));
851 }
852
853 #[test]
854 fn perm_guard_supervisor_without_perm_denies() {
855 let id = make_identity(Role::Supervisor, true);
857 assert!(!perm_guard_verdict(&id, false));
858 }
859}