Skip to main content

fraiseql_auth/
error_sanitizer.rs

1// Error sanitization layer
2// Separates user-facing messages from internal details
3
4use std::fmt;
5
6/// A sanitizable error that separates user-facing and internal messages
7#[derive(Debug, Clone)]
8pub struct SanitizedError {
9    /// User-facing message (safe for API responses)
10    user_message:     String,
11    /// Internal message (for logging only)
12    internal_message: String,
13}
14
15impl SanitizedError {
16    /// Create a new sanitized error
17    pub fn new(user_message: impl Into<String>, internal_message: impl Into<String>) -> Self {
18        Self {
19            user_message:     user_message.into(),
20            internal_message: internal_message.into(),
21        }
22    }
23
24    /// Get the user-facing message (safe for API responses)
25    pub fn user_facing(&self) -> &str {
26        &self.user_message
27    }
28
29    /// Get the internal message (for logging only)
30    pub fn internal(&self) -> &str {
31        &self.internal_message
32    }
33}
34
35impl fmt::Display for SanitizedError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        // Display uses user-facing message (safe for logs)
38        write!(f, "{}", self.user_message)
39    }
40}
41
42impl std::error::Error for SanitizedError {}
43
44/// Helper trait for creating sanitized errors from standard error types
45pub trait Sanitizable {
46    /// Convert to a sanitized error
47    fn sanitized(self, user_message: &str) -> SanitizedError;
48}
49
50impl<E: fmt::Display> Sanitizable for E {
51    fn sanitized(self, user_message: &str) -> SanitizedError {
52        SanitizedError::new(user_message, self.to_string())
53    }
54}
55
56/// Pre-defined error messages for common authentication scenarios
57pub mod messages {
58    /// Generic authentication failure message
59    pub const AUTH_FAILED: &str = "Authentication failed";
60
61    /// Generic permission denied message
62    pub const PERMISSION_DENIED: &str = "Permission denied";
63
64    /// Generic service error message
65    pub const SERVICE_UNAVAILABLE: &str = "Service temporarily unavailable";
66
67    /// Generic request error message
68    pub const REQUEST_FAILED: &str = "Request failed";
69
70    /// Invalid state (CSRF token)
71    pub const INVALID_STATE: &str = "Authentication failed";
72
73    /// Token expired
74    pub const TOKEN_EXPIRED: &str = "Authentication failed";
75
76    /// Invalid signature
77    pub const INVALID_SIGNATURE: &str = "Authentication failed";
78
79    /// Session expired
80    pub const SESSION_EXPIRED: &str = "Authentication failed";
81
82    /// Session revoked
83    pub const SESSION_REVOKED: &str = "Authentication failed";
84}
85
86/// Error sanitization for authentication errors
87pub struct AuthErrorSanitizer;
88
89impl AuthErrorSanitizer {
90    /// Sanitize JWT validation error
91    pub fn jwt_validation_error(internal_error: &str) -> SanitizedError {
92        SanitizedError::new(messages::AUTH_FAILED, internal_error)
93    }
94
95    /// Sanitize OIDC provider error
96    pub fn oidc_provider_error(internal_error: &str) -> SanitizedError {
97        SanitizedError::new(messages::AUTH_FAILED, internal_error)
98    }
99
100    /// Sanitize session token error
101    pub fn session_token_error(internal_error: &str) -> SanitizedError {
102        SanitizedError::new(messages::AUTH_FAILED, internal_error)
103    }
104
105    /// Sanitize CSRF state error
106    pub fn csrf_state_error(internal_error: &str) -> SanitizedError {
107        SanitizedError::new(messages::INVALID_STATE, internal_error)
108    }
109
110    /// Sanitize permission/authorization error
111    pub fn permission_error(internal_error: &str) -> SanitizedError {
112        SanitizedError::new(messages::PERMISSION_DENIED, internal_error)
113    }
114
115    /// Sanitize database error
116    pub fn database_error(internal_error: &str) -> SanitizedError {
117        SanitizedError::new(messages::SERVICE_UNAVAILABLE, internal_error)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_sanitized_error_creation() {
127        let error = SanitizedError::new(
128            "Authentication failed",
129            "JWT signature validation failed at cryptographic boundary",
130        );
131
132        assert_eq!(error.user_facing(), "Authentication failed");
133        assert!(error.internal().contains("cryptographic"));
134    }
135
136    #[test]
137    fn test_sanitized_error_display() {
138        let error = SanitizedError::new(
139            "Authentication failed",
140            "Internal database error: constraint violation",
141        );
142
143        // Display should show user message
144        assert_eq!(format!("{}", error), "Authentication failed");
145    }
146
147    #[test]
148    fn test_auth_error_sanitizer_jwt() {
149        let error =
150            AuthErrorSanitizer::jwt_validation_error("RS256 signature mismatch at offset 512");
151
152        assert_eq!(error.user_facing(), messages::AUTH_FAILED);
153        assert!(error.internal().contains("RS256"));
154    }
155
156    #[test]
157    fn test_auth_error_sanitizer_permission() {
158        let error = AuthErrorSanitizer::permission_error(
159            "User lacks role=admin for operation write:config",
160        );
161
162        assert_eq!(error.user_facing(), messages::PERMISSION_DENIED);
163        assert!(error.internal().contains("role=admin"));
164    }
165
166    #[test]
167    fn test_sanitizable_trait() {
168        let std_error = "Socket error: Connection refused".to_string();
169        let sanitized = std_error.sanitized("Service temporarily unavailable");
170
171        assert_eq!(sanitized.user_facing(), "Service temporarily unavailable");
172        assert_eq!(sanitized.internal(), "Socket error: Connection refused");
173    }
174}