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();
319 let router = router.get("/admin/password_change", move |req| {
320 let c = c.clone();
321 async move {
322 match role_guard(&c, &req, Role::User).await? {
323 Guard::Redirect(r) => Ok(r),
324 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
325 }
326 }
327 });
328 let c = ctx.clone();
329 let router = router.post("/admin/password_change", move |req| {
330 let c = c.clone();
331 async move {
332 match role_guard(&c, &req, Role::User).await? {
333 Guard::Redirect(r) => Ok(r),
334 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
335 }
336 }
337 });
338
339 let c = ctx.clone();
341 let ac = auth_ctx.clone();
342 let router = router.get("/admin/users", move |req| {
343 let c = c.clone();
344 let ac = ac.clone();
345 async move {
346 match role_guard(&c, &req, Role::Administrator).await? {
347 Guard::Redirect(r) => Ok(r),
348 Guard::Allow(ident) => {
349 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
350 }
351 }
352 }
353 });
354
355 let c = ctx.clone();
356 let ac = auth_ctx.clone();
357 let router = router.get("/admin/users/new", 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::show_new_user(&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.post("/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) => super::builtin::do_new_user(&ac, ident, req).await,
379 }
380 }
381 });
382
383 let c = ctx.clone();
384 let ac = auth_ctx.clone();
385 let router = router.get("/admin/users/:id/edit", move |req| {
386 let c = c.clone();
387 let ac = ac.clone();
388 async move {
389 match role_guard(&c, &req, Role::Administrator).await? {
390 Guard::Redirect(r) => Ok(r),
391 Guard::Allow(ident) => {
392 let id = parse_id(req.param("id"))?;
393 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
394 }
395 }
396 }
397 });
398
399 let c = ctx.clone();
400 let ac = auth_ctx.clone();
401 let router = router.post("/admin/users/:id/edit", move |req| {
402 let c = c.clone();
403 let ac = ac.clone();
404 async move {
405 match role_guard(&c, &req, Role::Administrator).await? {
406 Guard::Redirect(r) => Ok(r),
407 Guard::Allow(ident) => {
408 let id = parse_id(req.param("id"))?;
409 super::builtin::do_user_edit(&ac, ident, id, req).await
410 }
411 }
412 }
413 });
414
415 let c = ctx.clone();
416 let ac = auth_ctx.clone();
417 let router = router.get("/admin/users/:id/delete", move |req| {
418 let c = c.clone();
419 let ac = ac.clone();
420 async move {
421 match role_guard(&c, &req, Role::Administrator).await? {
422 Guard::Redirect(r) => Ok(r),
423 Guard::Allow(ident) => {
424 let id = parse_id(req.param("id"))?;
425 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
426 .await
427 }
428 }
429 }
430 });
431
432 let c = ctx.clone();
433 let ac = auth_ctx.clone();
434 let router = router.post("/admin/users/:id/delete", move |req| {
435 let c = c.clone();
436 let ac = ac.clone();
437 async move {
438 match role_guard(&c, &req, Role::Administrator).await? {
439 Guard::Redirect(r) => Ok(r),
440 Guard::Allow(ident) => {
441 let id = parse_id(req.param("id"))?;
442 super::builtin::do_user_delete(&ac, ident, id, req).await
443 }
444 }
445 }
446 });
447
448 let c = ctx.clone();
455 let ac = auth_ctx.clone();
456 let router = router.get("/admin/users/:id", move |req| {
457 let c = c.clone();
458 let ac = ac.clone();
459 async move {
460 match role_guard(&c, &req, Role::Administrator).await? {
461 Guard::Redirect(r) => Ok(r),
462 Guard::Allow(ident) => {
463 let id = parse_id(req.param("id"))?;
464 let q = req.query();
465 let tab = q.get("tab").map(|s| s.to_string());
466 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
467 super::builtin::show_user_view(
468 &ac,
469 ident,
470 id,
471 handlers::csrf_token(&req),
472 tab,
473 page,
474 )
475 .await
476 }
477 }
478 }
479 });
480
481 let c = ctx.clone();
483 let ac = auth_ctx.clone();
484 let router = router.get("/admin/groups", move |req| {
485 let c = c.clone();
486 let ac = ac.clone();
487 async move {
488 match role_guard(&c, &req, Role::Administrator).await? {
489 Guard::Redirect(r) => Ok(r),
490 Guard::Allow(ident) => {
491 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
492 }
493 }
494 }
495 });
496
497 let c = ctx.clone();
498 let ac = auth_ctx.clone();
499 let router = router.get("/admin/groups/new", 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::show_new_group(&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.post("/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) => super::builtin::do_new_group(&ac, ident, req).await,
521 }
522 }
523 });
524
525 let c = ctx.clone();
526 let ac = auth_ctx.clone();
527 let router = router.get("/admin/groups/:id/edit", move |req| {
528 let c = c.clone();
529 let ac = ac.clone();
530 async move {
531 match role_guard(&c, &req, Role::Administrator).await? {
532 Guard::Redirect(r) => Ok(r),
533 Guard::Allow(ident) => {
534 let id = parse_id(req.param("id"))?;
535 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
536 .await
537 }
538 }
539 }
540 });
541
542 let c = ctx.clone();
543 let ac = auth_ctx.clone();
544 let router = router.post("/admin/groups/:id/edit", move |req| {
545 let c = c.clone();
546 let ac = ac.clone();
547 async move {
548 match role_guard(&c, &req, Role::Administrator).await? {
549 Guard::Redirect(r) => Ok(r),
550 Guard::Allow(ident) => {
551 let id = parse_id(req.param("id"))?;
552 super::builtin::do_group_edit(&ac, ident, id, req).await
553 }
554 }
555 }
556 });
557
558 let c = ctx.clone();
559 let ac = auth_ctx.clone();
560 let router = router.get("/admin/groups/:id/delete", move |req| {
561 let c = c.clone();
562 let ac = ac.clone();
563 async move {
564 match role_guard(&c, &req, Role::Administrator).await? {
565 Guard::Redirect(r) => Ok(r),
566 Guard::Allow(ident) => {
567 let id = parse_id(req.param("id"))?;
568 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
569 .await
570 }
571 }
572 }
573 });
574
575 let c = ctx.clone();
576 let ac = auth_ctx.clone();
577 let router = router.post("/admin/groups/:id/delete", move |req| {
578 let c = c.clone();
579 let ac = ac.clone();
580 async move {
581 match role_guard(&c, &req, Role::Administrator).await? {
582 Guard::Redirect(r) => Ok(r),
583 Guard::Allow(ident) => {
584 let id = parse_id(req.param("id"))?;
585 super::builtin::do_group_delete(&ac, ident, id, req).await
586 }
587 }
588 }
589 });
590
591 let c = ctx.clone();
593 let router = router.get("/admin/:admin_name", move |req| {
594 let c = c.clone();
595 async move {
596 let name = model_name_from_req(&req)?;
597 let perm = perm_for(&c, &name, "view")?;
598 match perm_guard(&c, &req, &perm).await? {
599 Guard::Redirect(r) => Ok(r),
600 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
601 }
602 }
603 });
604
605 let c = ctx.clone();
607 let router = router.get("/admin/:admin_name/new", move |req| {
608 let c = c.clone();
609 async move {
610 let name = model_name_from_req(&req)?;
611 let perm = perm_for(&c, &name, "add")?;
612 match perm_guard(&c, &req, &perm).await? {
613 Guard::Redirect(r) => Ok(r),
614 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
615 }
616 }
617 });
618 let c = ctx.clone();
619 let router = router.post("/admin/:admin_name/new", move |req| {
620 let c = c.clone();
621 async move {
622 let name = model_name_from_req(&req)?;
623 let perm = perm_for(&c, &name, "add")?;
624 match perm_guard(&c, &req, &perm).await? {
625 Guard::Redirect(r) => Ok(r),
626 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
627 }
628 }
629 });
630
631 let c = ctx.clone();
633 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
634 let c = c.clone();
635 async move {
636 let name = model_name_from_req(&req)?;
637 let perm = perm_for(&c, &name, "change")?;
638 match perm_guard(&c, &req, &perm).await? {
639 Guard::Redirect(r) => Ok(r),
640 Guard::Allow(ident) => {
641 let id = parse_id(req.param("id"))?;
642 handlers::show_edit_form(&c, ident, &name, id, &req).await
643 }
644 }
645 }
646 });
647 let c = ctx.clone();
648 let router = router.post("/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::do_update(&c, ident, &name, id, req).await
658 }
659 }
660 }
661 });
662
663 let c = ctx.clone();
666 let router = router.get("/admin/:admin_name/:id/history", move |req| {
667 let c = c.clone();
668 async move {
669 let name = model_name_from_req(&req)?;
670 let perm = perm_for(&c, &name, "view")?;
671 match perm_guard(&c, &req, &perm).await? {
672 Guard::Redirect(r) => Ok(r),
673 Guard::Allow(ident) => {
674 let id = parse_id(req.param("id"))?;
675 handlers::show_object_history(&c, ident, &name, id, &req).await
676 }
677 }
678 }
679 });
680
681 let c = ctx.clone();
683 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
684 let c = c.clone();
685 async move {
686 let name = model_name_from_req(&req)?;
687 let perm = perm_for(&c, &name, "delete")?;
688 match perm_guard(&c, &req, &perm).await? {
689 Guard::Redirect(r) => Ok(r),
690 Guard::Allow(ident) => {
691 let id = parse_id(req.param("id"))?;
692 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
693 }
694 }
695 }
696 });
697 let c = ctx.clone();
698 let router = router.post("/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::do_delete(&c, ident, &name, id).await
708 }
709 }
710 }
711 });
712
713 let c = ctx.clone();
718 let router = router.post("/admin/:admin_name/bulk_delete", move |req| {
719 let c = c.clone();
720 async move {
721 let name = model_name_from_req(&req)?;
722 let perm = perm_for(&c, &name, "delete")?;
723 match perm_guard(&c, &req, &perm).await? {
724 Guard::Redirect(r) => Ok(r),
725 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
726 }
727 }
728 });
729
730 let c = ctx.clone();
735 router.post("/admin/:admin_name/bulk/:action", move |req| {
736 let c = c.clone();
737 async move {
738 let name = model_name_from_req(&req)?;
739 let action = req
740 .param("action")
741 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
742 .to_string();
743 let perm = perm_for(&c, &name, "change")?;
744 match perm_guard(&c, &req, &perm).await? {
745 Guard::Redirect(r) => Ok(r),
746 Guard::Allow(ident) => {
747 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
748 }
749 }
750 }
751 })
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 fn make_identity(role: Role, is_active: bool) -> Identity {
759 Identity {
760 user_id: 42,
761 email: "test@example.com".into(),
762 role,
763 is_active,
764 is_demo: false,
765 demo_label: None,
766 }
767 }
768
769 #[test]
774 fn role_guard_decision_admin_meets_staff_floor() {
775 let id = make_identity(Role::Administrator, true);
776 assert!(id.role.includes(Role::Staff));
777 }
778
779 #[test]
780 fn role_guard_decision_user_does_not_meet_staff() {
781 let id = make_identity(Role::User, true);
782 assert!(!id.role.includes(Role::Staff));
783 }
784
785 #[test]
786 fn role_guard_decision_administrator_does_not_meet_developer() {
787 let id = make_identity(Role::Administrator, true);
788 assert!(!id.role.includes(Role::Developer));
789 }
790
791 #[test]
792 fn role_guard_decision_developer_meets_everything() {
793 let id = make_identity(Role::Developer, true);
794 for &min in &[
795 Role::User,
796 Role::Staff,
797 Role::Supervisor,
798 Role::Administrator,
799 Role::Developer,
800 ] {
801 assert!(id.role.includes(min), "Developer should meet {min:?}");
802 }
803 }
804
805 #[test]
808 fn perm_guard_admin_short_circuits_without_perm() {
809 let id = make_identity(Role::Administrator, true);
810 assert!(perm_guard_verdict(&id, false));
811 }
812
813 #[test]
814 fn perm_guard_developer_short_circuits_without_perm() {
815 let id = make_identity(Role::Developer, true);
816 assert!(perm_guard_verdict(&id, false));
817 }
818
819 #[test]
820 fn perm_guard_staff_with_perm_passes() {
821 let id = make_identity(Role::Staff, true);
822 assert!(perm_guard_verdict(&id, true));
823 }
824
825 #[test]
826 fn perm_guard_staff_without_perm_denies() {
827 let id = make_identity(Role::Staff, true);
828 assert!(!perm_guard_verdict(&id, false));
829 }
830
831 #[test]
832 fn perm_guard_inactive_admin_denies_even_with_bypass() {
833 let id = make_identity(Role::Administrator, false);
835 assert!(!perm_guard_verdict(&id, true));
836 }
837
838 #[test]
839 fn perm_guard_supervisor_without_perm_denies() {
840 let id = make_identity(Role::Supervisor, true);
842 assert!(!perm_guard_verdict(&id, false));
843 }
844}