fraiseql_auth/
error_sanitizer.rs1use std::fmt;
10
11#[derive(Debug, Clone)]
13pub struct SanitizedError {
14 user_message: String,
16 internal_message: String,
18}
19
20impl SanitizedError {
21 pub fn new(user_message: impl Into<String>, internal_message: impl Into<String>) -> Self {
23 Self {
24 user_message: user_message.into(),
25 internal_message: internal_message.into(),
26 }
27 }
28
29 pub fn user_facing(&self) -> &str {
31 &self.user_message
32 }
33
34 pub fn internal(&self) -> &str {
36 &self.internal_message
37 }
38}
39
40impl fmt::Display for SanitizedError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{}", self.user_message)
44 }
45}
46
47impl std::error::Error for SanitizedError {}
48
49pub trait Sanitize {
55 fn sanitized(self, user_message: &str) -> SanitizedError;
57}
58
59impl<E: fmt::Display> Sanitize for E {
60 fn sanitized(self, user_message: &str) -> SanitizedError {
61 SanitizedError::new(user_message, self.to_string())
62 }
63}
64
65pub mod messages {
67 pub const AUTH_FAILED: &str = "Authentication failed";
69
70 pub const PERMISSION_DENIED: &str = "Permission denied";
72
73 pub const SERVICE_UNAVAILABLE: &str = "Service temporarily unavailable";
75
76 pub const REQUEST_FAILED: &str = "Request failed";
78
79 pub const INVALID_STATE: &str = "Authentication failed";
81
82 pub const TOKEN_EXPIRED: &str = "Authentication failed";
84
85 pub const INVALID_SIGNATURE: &str = "Authentication failed";
87
88 pub const SESSION_EXPIRED: &str = "Authentication failed";
90
91 pub const SESSION_REVOKED: &str = "Authentication failed";
93}
94
95pub struct AuthErrorSanitizer;
97
98impl AuthErrorSanitizer {
99 pub fn jwt_validation_error(internal_error: &str) -> SanitizedError {
101 SanitizedError::new(messages::AUTH_FAILED, internal_error)
102 }
103
104 pub fn oidc_provider_error(internal_error: &str) -> SanitizedError {
106 SanitizedError::new(messages::AUTH_FAILED, internal_error)
107 }
108
109 pub fn session_token_error(internal_error: &str) -> SanitizedError {
111 SanitizedError::new(messages::AUTH_FAILED, internal_error)
112 }
113
114 pub fn csrf_state_error(internal_error: &str) -> SanitizedError {
116 SanitizedError::new(messages::INVALID_STATE, internal_error)
117 }
118
119 pub fn permission_error(internal_error: &str) -> SanitizedError {
121 SanitizedError::new(messages::PERMISSION_DENIED, internal_error)
122 }
123
124 pub fn database_error(internal_error: &str) -> SanitizedError {
126 SanitizedError::new(messages::SERVICE_UNAVAILABLE, internal_error)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 #[allow(clippy::wildcard_imports)]
133 use super::*;
135
136 #[test]
137 fn test_sanitized_error_creation() {
138 let error = SanitizedError::new(
139 "Authentication failed",
140 "JWT signature validation failed at cryptographic boundary",
141 );
142
143 assert_eq!(error.user_facing(), "Authentication failed");
144 assert!(error.internal().contains("cryptographic"));
145 }
146
147 #[test]
148 fn test_sanitized_error_display() {
149 let error = SanitizedError::new(
150 "Authentication failed",
151 "Internal database error: constraint violation",
152 );
153
154 assert_eq!(format!("{}", error), "Authentication failed");
156 }
157
158 #[test]
159 fn test_auth_error_sanitizer_jwt() {
160 let error =
161 AuthErrorSanitizer::jwt_validation_error("RS256 signature mismatch at offset 512");
162
163 assert_eq!(error.user_facing(), messages::AUTH_FAILED);
164 assert!(error.internal().contains("RS256"));
165 }
166
167 #[test]
168 fn test_auth_error_sanitizer_permission() {
169 let error = AuthErrorSanitizer::permission_error(
170 "User lacks role=admin for operation write:config",
171 );
172
173 assert_eq!(error.user_facing(), messages::PERMISSION_DENIED);
174 assert!(error.internal().contains("role=admin"));
175 }
176
177 #[test]
178 fn test_sanitizable_trait() {
179 let std_error = "Socket error: Connection refused".to_string();
180 let sanitized = std_error.sanitized("Service temporarily unavailable");
181
182 assert_eq!(sanitized.user_facing(), "Service temporarily unavailable");
183 assert_eq!(sanitized.internal(), "Socket error: Connection refused");
184 }
185}