kotoba_security/
error.rs

1//! Security error types and handling
2
3use totp_rs::SecretParseError;
4
5/// Result type for security operations
6pub type Result<T> = std::result::Result<T, SecurityError>;
7
8impl std::fmt::Display for SecurityError {
9    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10        match self {
11            SecurityError::Configuration(msg) => write!(f, "Configuration error: {}", msg),
12            SecurityError::Authentication(msg) => write!(f, "Authentication failed: {}", msg),
13            SecurityError::Authorization(msg) => write!(f, "Authorization failed: {}", msg),
14            SecurityError::Jwt(e) => write!(f, "JWT error: {}", e),
15            SecurityError::OAuth2(msg) => write!(f, "OAuth2 error: {}", msg),
16            SecurityError::Mfa(msg) => write!(f, "MFA error: {}", msg),
17            SecurityError::Password(msg) => write!(f, "Password hashing error: {}", msg),
18            SecurityError::Session(msg) => write!(f, "Session error: {}", msg),
19            SecurityError::Http(e) => write!(f, "HTTP client error: {}", e),
20            SecurityError::Json(e) => write!(f, "JSON parsing error: {}", e),
21            SecurityError::Url(e) => write!(f, "URL parsing error: {}", e),
22            SecurityError::Io(e) => write!(f, "IO error: {}", e),
23            SecurityError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
24            SecurityError::Time(msg) => write!(f, "Time error: {}", msg),
25            SecurityError::Crypto(msg) => write!(f, "Cryptography error: {}", msg),
26            SecurityError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
27            SecurityError::TokenExpired => write!(f, "Token expired"),
28            SecurityError::TokenInvalid => write!(f, "Token invalid"),
29            SecurityError::UserNotFound => write!(f, "User not found"),
30            SecurityError::UserExists => write!(f, "User already exists"),
31            SecurityError::RateLimitExceeded => write!(f, "Rate limit exceeded"),
32            SecurityError::MfaRequired => write!(f, "MFA required"),
33            SecurityError::MfaSetupRequired => write!(f, "MFA setup required"),
34            SecurityError::AccountLocked => write!(f, "Account locked"),
35            SecurityError::AccountDisabled => write!(f, "Account disabled"),
36            SecurityError::InvalidCredentials => write!(f, "Invalid credentials"),
37            SecurityError::InsufficientPermissions => write!(f, "Insufficient permissions"),
38            SecurityError::ProviderNotSupported => write!(f, "Provider not supported"),
39            SecurityError::StateMismatch => write!(f, "State mismatch"),
40            SecurityError::CsrfTokenInvalid => write!(f, "CSRF token invalid"),
41            SecurityError::SessionExpired => write!(f, "Session expired"),
42            SecurityError::SessionInvalid => write!(f, "Session invalid"),
43            SecurityError::Database(msg) => write!(f, "Database error: {}", msg),
44            SecurityError::Cache(msg) => write!(f, "Cache error: {}", msg),
45            SecurityError::ExternalService(msg) => write!(f, "External service error: {}", msg),
46        }
47    }
48}
49
50impl std::error::Error for SecurityError {}
51
52/// Security error types
53#[derive(Debug)]
54pub enum SecurityError {
55    Configuration(String),
56    Authentication(String),
57    Authorization(String),
58    Jwt(jsonwebtoken::errors::Error),
59    OAuth2(String),
60    Mfa(String),
61    Password(String),
62    Session(String),
63    Http(reqwest::Error),
64    Json(serde_json::Error),
65    Url(url::ParseError),
66    Io(std::io::Error),
67    Utf8(std::string::FromUtf8Error),
68    Time(String),
69    Crypto(String),
70    InvalidInput(String),
71    TokenExpired,
72    TokenInvalid,
73    UserNotFound,
74    UserExists,
75    RateLimitExceeded,
76    MfaRequired,
77    MfaSetupRequired,
78    AccountLocked,
79    AccountDisabled,
80    InvalidCredentials,
81    InsufficientPermissions,
82    ProviderNotSupported,
83    StateMismatch,
84    CsrfTokenInvalid,
85    SessionExpired,
86    SessionInvalid,
87    Database(String),
88    Cache(String),
89    ExternalService(String),
90}
91
92impl SecurityError {
93    /// Check if error is retryable
94    pub fn is_retryable(&self) -> bool {
95        matches!(
96            self,
97            SecurityError::Http(_)
98                | SecurityError::Io(_)
99                | SecurityError::ExternalService(_)
100                | SecurityError::Database(_)
101                | SecurityError::Cache(_)
102        )
103    }
104
105    /// Check if error is client error (4xx)
106    pub fn is_client_error(&self) -> bool {
107        matches!(
108            self,
109            SecurityError::Authentication(_)
110                | SecurityError::Authorization(_)
111                | SecurityError::InvalidInput(_)
112                | SecurityError::TokenExpired
113                | SecurityError::TokenInvalid
114                | SecurityError::UserNotFound
115                | SecurityError::InvalidCredentials
116                | SecurityError::InsufficientPermissions
117                | SecurityError::CsrfTokenInvalid
118                | SecurityError::StateMismatch
119                | SecurityError::RateLimitExceeded
120        )
121    }
122
123    /// Check if error is server error (5xx)
124    pub fn is_server_error(&self) -> bool {
125        matches!(
126            self,
127            SecurityError::Configuration(_)
128                | SecurityError::Jwt(_)
129                | SecurityError::OAuth2(_)
130                | SecurityError::Mfa(_)
131                | SecurityError::Password(_)
132                | SecurityError::Session(_)
133                | SecurityError::Json(_)
134                | SecurityError::Url(_)
135                | SecurityError::Time(_)
136                | SecurityError::Crypto(_)
137                | SecurityError::Database(_)
138                | SecurityError::Cache(_)
139                | SecurityError::ExternalService(_)
140        )
141    }
142
143    /// Get HTTP status code for error
144    pub fn http_status_code(&self) -> u16 {
145        match self {
146            // 4xx Client Errors
147            SecurityError::Authentication(_) => 401,
148            SecurityError::Authorization(_) => 403,
149            SecurityError::InvalidInput(_) => 400,
150            SecurityError::TokenExpired => 401,
151            SecurityError::TokenInvalid => 401,
152            SecurityError::UserNotFound => 404,
153            SecurityError::InvalidCredentials => 401,
154            SecurityError::InsufficientPermissions => 403,
155            SecurityError::CsrfTokenInvalid => 403,
156            SecurityError::StateMismatch => 400,
157            SecurityError::RateLimitExceeded => 429,
158            SecurityError::MfaRequired => 401,
159            SecurityError::MfaSetupRequired => 401,
160            SecurityError::AccountLocked => 423,
161            SecurityError::AccountDisabled => 401,
162
163            // 5xx Server Errors
164            _ => 500,
165        }
166    }
167
168    /// Get user-friendly error message
169    pub fn user_message(&self) -> &'static str {
170        match self {
171            SecurityError::Authentication(_) => "Authentication failed. Please check your credentials.",
172            SecurityError::Authorization(_) => "You don't have permission to access this resource.",
173            SecurityError::InvalidInput(_) => "Invalid input provided.",
174            SecurityError::TokenExpired => "Your session has expired. Please log in again.",
175            SecurityError::TokenInvalid => "Invalid authentication token.",
176            SecurityError::UserNotFound => "User not found.",
177            SecurityError::InvalidCredentials => "Invalid username or password.",
178            SecurityError::InsufficientPermissions => "Insufficient permissions for this action.",
179            SecurityError::CsrfTokenInvalid => "Security token is invalid.",
180            SecurityError::StateMismatch => "Request state mismatch. Please try again.",
181            SecurityError::RateLimitExceeded => "Too many requests. Please try again later.",
182            SecurityError::MfaRequired => "Multi-factor authentication is required.",
183            SecurityError::MfaSetupRequired => "Multi-factor authentication setup is required.",
184            SecurityError::AccountLocked => "Account is locked. Please contact support.",
185            SecurityError::AccountDisabled => "Account is disabled. Please contact support.",
186            _ => "An internal error occurred. Please try again later.",
187        }
188    }
189}
190
191/// Convert from anyhow::Error for compatibility
192impl From<anyhow::Error> for SecurityError {
193    fn from(err: anyhow::Error) -> Self {
194        SecurityError::Configuration(err.to_string())
195    }
196}
197
198/// Convert from chrono errors
199impl From<chrono::ParseError> for SecurityError {
200    fn from(err: chrono::ParseError) -> Self {
201        SecurityError::Time(err.to_string())
202    }
203}
204
205/// Convert from TOTP secret parse errors
206impl From<SecretParseError> for SecurityError {
207    fn from(err: SecretParseError) -> Self {
208        SecurityError::Mfa(format!("TOTP secret parsing failed: {}", err))
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_error_classification() {
218        assert!(SecurityError::InvalidCredentials.is_client_error());
219        assert!(!SecurityError::InvalidCredentials.is_server_error());
220        assert!(!SecurityError::InvalidCredentials.is_retryable());
221
222        assert!(SecurityError::Jwt(jsonwebtoken::errors::Error::from(
223            jsonwebtoken::errors::ErrorKind::InvalidToken
224        )).is_server_error());
225        assert!(!SecurityError::Jwt(jsonwebtoken::errors::Error::from(
226            jsonwebtoken::errors::ErrorKind::InvalidToken
227        )).is_client_error());
228
229        // Test retryable error - skip for now due to reqwest API complexity
230        // TODO: Add proper reqwest error testing when API stabilizes
231        // assert!(SecurityError::Http(some_retryable_error).is_retryable());
232    }
233
234    #[test]
235    fn test_http_status_codes() {
236        assert_eq!(SecurityError::InvalidCredentials.http_status_code(), 401);
237        assert_eq!(SecurityError::Authorization("test".to_string()).http_status_code(), 403);
238        assert_eq!(SecurityError::InvalidInput("test".to_string()).http_status_code(), 400);
239        assert_eq!(SecurityError::Configuration("test".to_string()).http_status_code(), 500);
240    }
241
242    #[test]
243    fn test_user_messages() {
244        assert_eq!(
245            SecurityError::InvalidCredentials.user_message(),
246            "Invalid username or password."
247        );
248        assert_eq!(
249            SecurityError::TokenExpired.user_message(),
250            "Your session has expired. Please log in again."
251        );
252    }
253}