use std::fmt;
#[derive(Debug, Clone)]
pub struct SanitizedError {
user_message: String,
internal_message: String,
}
impl SanitizedError {
pub fn new(user_message: impl Into<String>, internal_message: impl Into<String>) -> Self {
Self {
user_message: user_message.into(),
internal_message: internal_message.into(),
}
}
pub fn user_facing(&self) -> &str {
&self.user_message
}
pub fn internal(&self) -> &str {
&self.internal_message
}
}
impl fmt::Display for SanitizedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.user_message)
}
}
impl std::error::Error for SanitizedError {}
pub trait Sanitizable {
fn sanitized(self, user_message: &str) -> SanitizedError;
}
impl<E: fmt::Display> Sanitizable for E {
fn sanitized(self, user_message: &str) -> SanitizedError {
SanitizedError::new(user_message, self.to_string())
}
}
pub mod messages {
pub const AUTH_FAILED: &str = "Authentication failed";
pub const PERMISSION_DENIED: &str = "Permission denied";
pub const SERVICE_UNAVAILABLE: &str = "Service temporarily unavailable";
pub const REQUEST_FAILED: &str = "Request failed";
pub const INVALID_STATE: &str = "Authentication failed";
pub const TOKEN_EXPIRED: &str = "Authentication failed";
pub const INVALID_SIGNATURE: &str = "Authentication failed";
pub const SESSION_EXPIRED: &str = "Authentication failed";
pub const SESSION_REVOKED: &str = "Authentication failed";
}
pub struct AuthErrorSanitizer;
impl AuthErrorSanitizer {
pub fn jwt_validation_error(internal_error: &str) -> SanitizedError {
SanitizedError::new(messages::AUTH_FAILED, internal_error)
}
pub fn oidc_provider_error(internal_error: &str) -> SanitizedError {
SanitizedError::new(messages::AUTH_FAILED, internal_error)
}
pub fn session_token_error(internal_error: &str) -> SanitizedError {
SanitizedError::new(messages::AUTH_FAILED, internal_error)
}
pub fn csrf_state_error(internal_error: &str) -> SanitizedError {
SanitizedError::new(messages::INVALID_STATE, internal_error)
}
pub fn permission_error(internal_error: &str) -> SanitizedError {
SanitizedError::new(messages::PERMISSION_DENIED, internal_error)
}
pub fn database_error(internal_error: &str) -> SanitizedError {
SanitizedError::new(messages::SERVICE_UNAVAILABLE, internal_error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitized_error_creation() {
let error = SanitizedError::new(
"Authentication failed",
"JWT signature validation failed at cryptographic boundary",
);
assert_eq!(error.user_facing(), "Authentication failed");
assert!(error.internal().contains("cryptographic"));
}
#[test]
fn test_sanitized_error_display() {
let error = SanitizedError::new(
"Authentication failed",
"Internal database error: constraint violation",
);
assert_eq!(format!("{}", error), "Authentication failed");
}
#[test]
fn test_auth_error_sanitizer_jwt() {
let error =
AuthErrorSanitizer::jwt_validation_error("RS256 signature mismatch at offset 512");
assert_eq!(error.user_facing(), messages::AUTH_FAILED);
assert!(error.internal().contains("RS256"));
}
#[test]
fn test_auth_error_sanitizer_permission() {
let error = AuthErrorSanitizer::permission_error(
"User lacks role=admin for operation write:config",
);
assert_eq!(error.user_facing(), messages::PERMISSION_DENIED);
assert!(error.internal().contains("role=admin"));
}
#[test]
fn test_sanitizable_trait() {
let std_error = "Socket error: Connection refused".to_string();
let sanitized = std_error.sanitized("Service temporarily unavailable");
assert_eq!(sanitized.user_facing(), "Service temporarily unavailable");
assert_eq!(sanitized.internal(), "Socket error: Connection refused");
}
}