1use crate::types::AdminError;
7use reinhardt_http::AuthState;
8use reinhardt_pages::server_fn::{ServerFnError, ServerFnRequest};
9use std::sync::Arc;
10
11pub trait IntoServerFnError {
13 fn into_server_fn_error(self) -> ServerFnError;
15}
16
17impl IntoServerFnError for AdminError {
18 fn into_server_fn_error(self) -> ServerFnError {
19 match self {
20 AdminError::ModelNotRegistered(msg) => ServerFnError::server(404, msg),
21 AdminError::PermissionDenied(msg) => ServerFnError::server(403, msg),
22 AdminError::InvalidAction(msg) | AdminError::ValidationError(msg) => {
23 ServerFnError::application(msg)
24 }
25 AdminError::DatabaseError(_) => {
26 ServerFnError::server(500, "Database operation failed")
28 }
29 AdminError::TemplateError(_) => {
30 ServerFnError::server(500, "Template rendering failed")
32 }
33 }
34 }
35}
36
37pub trait MapServerFnError<T> {
39 fn map_server_fn_error(self) -> Result<T, ServerFnError>;
41}
42
43impl<T> MapServerFnError<T> for Result<T, AdminError> {
44 fn map_server_fn_error(self) -> Result<T, ServerFnError> {
45 self.map_err(|e| e.into_server_fn_error())
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ModelPermission {
55 View,
57 Add,
59 Change,
61 Delete,
63}
64
65pub fn require_object_filters(
67 model_admin: &dyn crate::core::ModelAdmin,
68 user: &dyn crate::core::AdminUser,
69) -> Result<Vec<reinhardt_db::orm::Filter>, ServerFnError> {
70 if user.is_superuser() {
71 return Ok(Vec::new());
72 }
73 let filters = model_admin
74 .object_filters(user)
75 .ok_or_else(|| ServerFnError::server(403, "Object permission denied"))?;
76 crate::core::database::build_object_scope_condition(&filters)
77 .map_err(|_| ServerFnError::server(403, "Object permission denied"))?;
78 Ok(filters)
79}
80
81pub struct AdminAuth {
86 auth_state: Option<AuthState>,
88}
89
90impl AdminAuth {
91 pub fn from_request(request: &ServerFnRequest) -> Self {
101 let auth_state = request.inner().extensions.get::<AuthState>();
102 Self { auth_state }
103 }
104
105 pub fn from_arc_request(request: &Arc<reinhardt_http::Request>) -> Self {
115 let auth_state = request.extensions.get::<AuthState>();
116 Self { auth_state }
117 }
118
119 pub fn auth_state(&self) -> Option<&AuthState> {
121 self.auth_state.as_ref()
122 }
123
124 pub fn is_authenticated(&self) -> bool {
130 self.auth_state
131 .as_ref()
132 .is_some_and(|s| s.is_authenticated())
133 }
134
135 pub fn is_staff(&self) -> bool {
141 self.auth_state.as_ref().is_some_and(|s| s.is_admin())
142 }
143
144 pub fn is_active(&self) -> bool {
150 self.auth_state.as_ref().is_some_and(|s| s.is_active())
151 }
152
153 pub fn user_id(&self) -> Option<&str> {
155 self.auth_state.as_ref().map(|s| s.user_id())
156 }
157
158 pub fn require_authenticated(&self) -> Result<(), ServerFnError> {
164 if !self.is_authenticated() {
165 return Err(ServerFnError::server(
166 401,
167 "Authentication required to access admin panel",
168 ));
169 }
170 Ok(())
171 }
172
173 pub fn require_staff(&self) -> Result<(), ServerFnError> {
179 self.require_authenticated()?;
180 if !self.is_staff() {
181 return Err(ServerFnError::server(
182 403,
183 "Staff access required for admin panel",
184 ));
185 }
186 Ok(())
187 }
188
189 pub async fn require_model_permission(
212 &self,
213 model_admin: &dyn crate::core::ModelAdmin,
214 user: &dyn crate::core::AdminUser,
215 permission: ModelPermission,
216 ) -> Result<(), ServerFnError> {
217 self.require_staff()?;
218
219 let has_permission = match permission {
222 ModelPermission::View => model_admin.has_view_permission(user).await,
223 ModelPermission::Add => model_admin.has_add_permission(user).await,
224 ModelPermission::Change => model_admin.has_change_permission(user).await,
225 ModelPermission::Delete => model_admin.has_delete_permission(user).await,
226 };
227
228 if !has_permission {
229 return Err(ServerFnError::server(403, "Permission denied"));
230 }
231
232 Ok(())
233 }
234}
235
236#[cfg(all(test, server))]
237mod tests {
238 use super::*;
239 use async_trait::async_trait;
240 use rstest::rstest;
241 use std::sync::Arc;
242
243 struct TestUser;
247
248 impl crate::core::AdminUser for TestUser {
249 fn is_active(&self) -> bool {
250 true
251 }
252 fn is_staff(&self) -> bool {
253 true
254 }
255 fn is_superuser(&self) -> bool {
256 false
257 }
258 fn get_username(&self) -> &str {
259 "test_user"
260 }
261 }
262
263 struct DenyAllAdmin;
265
266 #[async_trait]
267 impl crate::core::ModelAdmin for DenyAllAdmin {
268 fn model_name(&self) -> &str {
269 "DenyModel"
270 }
271 }
272
273 struct AllowAllAdmin;
275
276 #[async_trait]
277 impl crate::core::ModelAdmin for AllowAllAdmin {
278 fn model_name(&self) -> &str {
279 "AllowModel"
280 }
281
282 async fn has_view_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
283 true
284 }
285 async fn has_add_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
286 true
287 }
288 async fn has_change_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
289 true
290 }
291 async fn has_delete_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
292 true
293 }
294 }
295
296 struct AllowAllScopedAdmin;
298
299 #[async_trait]
300 impl crate::core::ModelAdmin for AllowAllScopedAdmin {
301 fn model_name(&self) -> &str {
302 "AllowScopedModel"
303 }
304
305 async fn has_view_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
306 true
307 }
308 async fn has_add_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
309 true
310 }
311 async fn has_change_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
312 true
313 }
314 async fn has_delete_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
315 true
316 }
317
318 fn object_filters(
319 &self,
320 _: &dyn crate::core::AdminUser,
321 ) -> Option<Vec<reinhardt_db::orm::Filter>> {
322 Some(Vec::new())
323 }
324 }
325
326 struct SelectiveAdmin {
328 allowed: ModelPermission,
329 }
330
331 #[async_trait]
332 impl crate::core::ModelAdmin for SelectiveAdmin {
333 fn model_name(&self) -> &str {
334 "SelectiveModel"
335 }
336
337 async fn has_view_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
338 self.allowed == ModelPermission::View
339 }
340 async fn has_add_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
341 self.allowed == ModelPermission::Add
342 }
343 async fn has_change_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
344 self.allowed == ModelPermission::Change
345 }
346 async fn has_delete_permission(&self, _: &dyn crate::core::AdminUser) -> bool {
347 self.allowed == ModelPermission::Delete
348 }
349 }
350
351 fn make_admin_auth(auth_state: Option<AuthState>) -> AdminAuth {
353 let request = reinhardt_http::Request::builder()
354 .uri("/admin/test")
355 .build()
356 .expect("Failed to build test request");
357 if let Some(state) = auth_state {
358 request.extensions.insert(state);
359 }
360 AdminAuth::from_arc_request(&Arc::new(request))
361 }
362
363 #[rstest]
366 #[tokio::test]
367 async fn test_require_model_permission_staff_with_permission() {
368 let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
370 let admin = AllowAllAdmin;
371 let user_obj = TestUser;
372
373 let result = auth
375 .require_model_permission(
376 &admin,
377 &user_obj as &dyn crate::core::AdminUser,
378 ModelPermission::View,
379 )
380 .await;
381
382 assert!(result.is_ok());
384 }
385
386 #[rstest]
387 #[tokio::test]
388 async fn test_require_model_permission_staff_denied_by_model() {
389 let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
391 let admin = DenyAllAdmin;
392 let user_obj = TestUser;
393
394 let result = auth
396 .require_model_permission(
397 &admin,
398 &user_obj as &dyn crate::core::AdminUser,
399 ModelPermission::View,
400 )
401 .await;
402
403 assert!(result.is_err());
405 match result.unwrap_err() {
406 ServerFnError::Server { status, message } => {
407 assert_eq!(status, 403);
408 assert_eq!(message, "Permission denied");
409 }
410 other => panic!("Expected Server error with 403, got: {other:?}"),
411 }
412 }
413
414 #[rstest]
415 #[tokio::test]
416 async fn test_require_model_permission_non_staff_denied() {
417 let auth = make_admin_auth(Some(AuthState::authenticated("user1", false, true)));
419 let admin = AllowAllAdmin;
420 let user_obj = TestUser;
421
422 let result = auth
424 .require_model_permission(
425 &admin,
426 &user_obj as &dyn crate::core::AdminUser,
427 ModelPermission::View,
428 )
429 .await;
430
431 assert!(result.is_err());
433 match result.unwrap_err() {
434 ServerFnError::Server { status, message } => {
435 assert_eq!(status, 403);
436 assert_eq!(message, "Staff access required for admin panel");
437 }
438 other => panic!("Expected Server error with 403, got: {other:?}"),
439 }
440 }
441
442 #[rstest]
443 #[tokio::test]
444 async fn test_require_model_permission_unauthenticated() {
445 let auth = make_admin_auth(None);
447 let admin = AllowAllAdmin;
448 let user_obj = TestUser;
449
450 let result = auth
452 .require_model_permission(
453 &admin,
454 &user_obj as &dyn crate::core::AdminUser,
455 ModelPermission::View,
456 )
457 .await;
458
459 assert!(result.is_err());
461 match result.unwrap_err() {
462 ServerFnError::Server { status, message } => {
463 assert_eq!(status, 401);
464 assert_eq!(message, "Authentication required to access admin panel");
465 }
466 other => panic!("Expected Server error with 401, got: {other:?}"),
467 }
468 }
469
470 #[rstest]
471 #[case::view_matches_view(ModelPermission::View, ModelPermission::View, true)]
472 #[case::view_does_not_match_add(ModelPermission::View, ModelPermission::Add, false)]
473 #[case::add_matches_add(ModelPermission::Add, ModelPermission::Add, true)]
474 #[case::change_does_not_match_delete(ModelPermission::Change, ModelPermission::Delete, false)]
475 #[tokio::test]
476 async fn test_require_model_permission_selective_permissions(
477 #[case] granted: ModelPermission,
478 #[case] requested: ModelPermission,
479 #[case] expected_ok: bool,
480 ) {
481 let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
483 let admin = SelectiveAdmin { allowed: granted };
484 let user_obj = TestUser;
485
486 let result = auth
488 .require_model_permission(&admin, &user_obj as &dyn crate::core::AdminUser, requested)
489 .await;
490
491 assert_eq!(
493 result.is_ok(),
494 expected_ok,
495 "granted={granted:?}, requested={requested:?}: expected is_ok()={expected_ok}"
496 );
497 }
498
499 #[test]
500 fn object_filters_deny_custom_admin_without_scope() {
501 let result = require_object_filters(&AllowAllAdmin, &TestUser);
502
503 assert!(matches!(
504 result,
505 Err(ServerFnError::Server { status: 403, .. })
506 ));
507 }
508
509 #[test]
510 fn object_filters_allow_custom_admin_with_empty_scope() {
511 let filters = require_object_filters(&AllowAllScopedAdmin, &TestUser)
512 .expect("custom admin with Some(vec![]) should allow objects");
513
514 assert_eq!(filters.len(), 0);
515 }
516
517 #[test]
518 fn configured_admin_explicitly_allows_unscoped_objects() {
519 let admin = crate::core::ModelAdminConfig::builder()
520 .model_name("Record")
521 .allow_all(true)
522 .build()
523 .expect("test admin should build");
524
525 assert_eq!(
526 require_object_filters(&admin, &TestUser)
527 .expect("configured admin should allow objects")
528 .len(),
529 0
530 );
531 }
532
533 #[rstest]
536 #[test]
537 fn test_model_not_registered_converts_to_404() {
538 let admin_err = AdminError::ModelNotRegistered("User".into());
539 let server_err = admin_err.into_server_fn_error();
540
541 match server_err {
542 ServerFnError::Server { status, message } => {
543 assert_eq!(status, 404);
544 assert_eq!(message, "User");
545 }
546 _ => panic!("Expected Server error"),
547 }
548 }
549
550 #[rstest]
551 #[test]
552 fn test_permission_denied_converts_to_403() {
553 let admin_err = AdminError::PermissionDenied("Access denied".into());
554 let server_err = admin_err.into_server_fn_error();
555
556 match server_err {
557 ServerFnError::Server { status, message } => {
558 assert_eq!(status, 403);
559 assert_eq!(message, "Access denied");
560 }
561 _ => panic!("Expected Server error"),
562 }
563 }
564
565 #[rstest]
566 #[test]
567 fn test_validation_error_converts_to_application() {
568 let admin_err = AdminError::ValidationError("Invalid input".into());
569 let server_err = admin_err.into_server_fn_error();
570
571 match server_err {
572 ServerFnError::Application(msg) => {
573 assert_eq!(msg, "Invalid input");
574 }
575 _ => panic!("Expected Application error"),
576 }
577 }
578
579 #[rstest]
580 #[test]
581 fn test_database_error_hides_details() {
582 let admin_err = AdminError::DatabaseError("SQL syntax error at line 42".into());
583 let server_err = admin_err.into_server_fn_error();
584
585 match server_err {
586 ServerFnError::Server { status, message } => {
587 assert_eq!(status, 500);
588 assert_eq!(message, "Database operation failed");
589 assert!(!message.contains("SQL"));
591 assert!(!message.contains("42"));
592 }
593 _ => panic!("Expected Server error"),
594 }
595 }
596
597 #[rstest]
598 #[test]
599 fn test_result_conversion() {
600 let result: Result<String, AdminError> = Err(AdminError::ModelNotRegistered("Post".into()));
601 let server_result = result.map_server_fn_error();
602
603 assert!(server_result.is_err());
604 match server_result.unwrap_err() {
605 ServerFnError::Server { status, .. } => assert_eq!(status, 404),
606 _ => panic!("Expected Server error"),
607 }
608 }
609}