Skip to main content

reinhardt_admin/server/
error.rs

1//! Error conversion for Server Functions
2//!
3//! This module provides error conversion from AdminError to ServerFnError
4//! and authentication/authorization helpers for admin panel endpoints.
5
6use crate::types::AdminError;
7use reinhardt_http::AuthState;
8use reinhardt_pages::server_fn::{ServerFnError, ServerFnRequest};
9use std::sync::Arc;
10
11/// Extension trait for converting AdminError to ServerFnError
12pub trait IntoServerFnError {
13	/// Convert AdminError to ServerFnError
14	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				// Hide internal database error details from clients
27				ServerFnError::server(500, "Database operation failed")
28			}
29			AdminError::TemplateError(_) => {
30				// Hide internal template error details from clients
31				ServerFnError::server(500, "Template rendering failed")
32			}
33		}
34	}
35}
36
37/// Convert `Result<T, AdminError>` to `Result<T, ServerFnError>`
38pub trait MapServerFnError<T> {
39	/// Map AdminError to ServerFnError
40	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/// Permission types for model-level access control.
50///
51/// Used with [`AdminAuth::require_model_permission`] to specify which
52/// permission to check against the `ModelAdmin`.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ModelPermission {
55	/// Permission to view model instances
56	View,
57	/// Permission to add (create) model instances
58	Add,
59	/// Permission to change (update) model instances
60	Change,
61	/// Permission to delete model instances
62	Delete,
63}
64
65/// Resolve the filters that restrict object-level access for an admin request.
66pub 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
81/// Authentication and authorization checker for admin panel.
82///
83/// This struct extracts authentication state from the HTTP request
84/// and provides methods to check authentication and permissions.
85pub struct AdminAuth {
86	/// The authentication state from the request
87	auth_state: Option<AuthState>,
88}
89
90impl AdminAuth {
91	/// Creates a new AdminAuth from a ServerFnRequest.
92	///
93	/// # Arguments
94	///
95	/// * `request` - The server function request wrapper
96	///
97	/// # Returns
98	///
99	/// A new AdminAuth instance
100	pub fn from_request(request: &ServerFnRequest) -> Self {
101		let auth_state = request.inner().extensions.get::<AuthState>();
102		Self { auth_state }
103	}
104
105	/// Creates a new AdminAuth from an `Arc<Request>`.
106	///
107	/// # Arguments
108	///
109	/// * `request` - The HTTP request
110	///
111	/// # Returns
112	///
113	/// A new AdminAuth instance
114	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	/// Returns the AuthState if available.
120	pub fn auth_state(&self) -> Option<&AuthState> {
121		self.auth_state.as_ref()
122	}
123
124	/// Checks if the user is authenticated.
125	///
126	/// # Returns
127	///
128	/// `true` if the user is authenticated, `false` otherwise
129	pub fn is_authenticated(&self) -> bool {
130		self.auth_state
131			.as_ref()
132			.is_some_and(|s| s.is_authenticated())
133	}
134
135	/// Checks if the user is a staff member (admin access).
136	///
137	/// # Returns
138	///
139	/// `true` if the user is staff/admin, `false` otherwise
140	pub fn is_staff(&self) -> bool {
141		self.auth_state.as_ref().is_some_and(|s| s.is_admin())
142	}
143
144	/// Checks if the user is active.
145	///
146	/// # Returns
147	///
148	/// `true` if the user is active, `false` otherwise
149	pub fn is_active(&self) -> bool {
150		self.auth_state.as_ref().is_some_and(|s| s.is_active())
151	}
152
153	/// Returns the user ID if authenticated.
154	pub fn user_id(&self) -> Option<&str> {
155		self.auth_state.as_ref().map(|s| s.user_id())
156	}
157
158	/// Requires authentication, returning an error if not authenticated.
159	///
160	/// # Errors
161	///
162	/// Returns `ServerFnError` with status 401 if not authenticated
163	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	/// Requires staff (admin) status, returning an error if not staff.
174	///
175	/// # Errors
176	///
177	/// Returns `ServerFnError` with status 403 if not staff
178	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	/// Checks model-level permission using `ModelAdmin`, returning an error if denied.
190	///
191	/// This method first verifies staff status, then delegates to the
192	/// `ModelAdmin`'s permission method for the specified permission type.
193	///
194	/// The caller is responsible for providing the authenticated user object
195	/// extracted from the DI context via [`AdminAuthenticatedUser`]. The user
196	/// is passed as a `&dyn AdminUser` trait object, which is produced by the
197	/// type-erased user loader registered during admin route setup.
198	///
199	/// [`AdminAuthenticatedUser`]: crate::server::admin_auth::AdminAuthenticatedUser
200	///
201	/// # Arguments
202	///
203	/// * `model_admin` - The model admin to check permissions against
204	/// * `user` - The authenticated user object as a trait object
205	/// * `permission` - The type of permission to check
206	///
207	/// # Errors
208	///
209	/// Returns `ServerFnError` with status 401 if not authenticated,
210	/// 403 if not staff or if model-level permission is denied
211	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		// require_staff() already guarantees auth_state is Some and authenticated,
220		// so we can proceed directly to the permission check.
221		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	// --- Helper structs for require_model_permission tests ---
244
245	/// Test user implementing AdminUser for permission tests
246	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	/// Always denies all permissions (uses default trait behavior)
264	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	/// Always grants all permissions
274	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	/// Grants all permissions and explicitly allows every object.
297	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	/// Grants only a specific permission type
327	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	/// Create AdminAuth from an optional AuthState
352	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	// --- require_model_permission tests ---
364
365	#[rstest]
366	#[tokio::test]
367	async fn test_require_model_permission_staff_with_permission() {
368		// Arrange
369		let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
370		let admin = AllowAllAdmin;
371		let user_obj = TestUser;
372
373		// Act
374		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
383		assert!(result.is_ok());
384	}
385
386	#[rstest]
387	#[tokio::test]
388	async fn test_require_model_permission_staff_denied_by_model() {
389		// Arrange
390		let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
391		let admin = DenyAllAdmin;
392		let user_obj = TestUser;
393
394		// Act
395		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
404		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		// Arrange
418		let auth = make_admin_auth(Some(AuthState::authenticated("user1", false, true)));
419		let admin = AllowAllAdmin;
420		let user_obj = TestUser;
421
422		// Act
423		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
432		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		// Arrange
446		let auth = make_admin_auth(None);
447		let admin = AllowAllAdmin;
448		let user_obj = TestUser;
449
450		// Act
451		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
460		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		// Arrange
482		let auth = make_admin_auth(Some(AuthState::authenticated("user1", true, true)));
483		let admin = SelectiveAdmin { allowed: granted };
484		let user_obj = TestUser;
485
486		// Act
487		let result = auth
488			.require_model_permission(&admin, &user_obj as &dyn crate::core::AdminUser, requested)
489			.await;
490
491		// Assert
492		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	// --- Error conversion tests ---
534
535	#[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				// Verify that the original error details are hidden
590				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}