fraiseql_auth/
error_sanitizer.rs1use std::fmt;
5
6#[derive(Debug, Clone)]
8pub struct SanitizedError {
9 user_message: String,
11 internal_message: String,
13}
14
15impl SanitizedError {
16 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 pub fn user_facing(&self) -> &str {
26 &self.user_message
27 }
28
29 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 write!(f, "{}", self.user_message)
39 }
40}
41
42impl std::error::Error for SanitizedError {}
43
44pub trait Sanitizable {
46 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
56pub mod messages {
58 pub const AUTH_FAILED: &str = "Authentication failed";
60
61 pub const PERMISSION_DENIED: &str = "Permission denied";
63
64 pub const SERVICE_UNAVAILABLE: &str = "Service temporarily unavailable";
66
67 pub const REQUEST_FAILED: &str = "Request failed";
69
70 pub const INVALID_STATE: &str = "Authentication failed";
72
73 pub const TOKEN_EXPIRED: &str = "Authentication failed";
75
76 pub const INVALID_SIGNATURE: &str = "Authentication failed";
78
79 pub const SESSION_EXPIRED: &str = "Authentication failed";
81
82 pub const SESSION_REVOKED: &str = "Authentication failed";
84}
85
86pub struct AuthErrorSanitizer;
88
89impl AuthErrorSanitizer {
90 pub fn jwt_validation_error(internal_error: &str) -> SanitizedError {
92 SanitizedError::new(messages::AUTH_FAILED, internal_error)
93 }
94
95 pub fn oidc_provider_error(internal_error: &str) -> SanitizedError {
97 SanitizedError::new(messages::AUTH_FAILED, internal_error)
98 }
99
100 pub fn session_token_error(internal_error: &str) -> SanitizedError {
102 SanitizedError::new(messages::AUTH_FAILED, internal_error)
103 }
104
105 pub fn csrf_state_error(internal_error: &str) -> SanitizedError {
107 SanitizedError::new(messages::INVALID_STATE, internal_error)
108 }
109
110 pub fn permission_error(internal_error: &str) -> SanitizedError {
112 SanitizedError::new(messages::PERMISSION_DENIED, internal_error)
113 }
114
115 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 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}