hdbconnect-mcp 0.3.6

MCP server for SAP HANA database
Documentation
//! Authentication error types

use thiserror::Error;

#[derive(Debug, Error)]
pub enum AuthError {
    #[error("authentication required")]
    NotAuthenticated,

    #[error("invalid token")]
    InvalidToken,

    #[error("token expired")]
    TokenExpired,

    #[error("invalid issuer")]
    InvalidIssuer,

    #[error("invalid audience")]
    InvalidAudience,

    #[error("invalid signature")]
    InvalidSignature,

    #[error("key not found: {0}")]
    KeyNotFound(String),

    #[error("no matching key for algorithm")]
    NoMatchingKey,

    #[error("missing tenant claim")]
    MissingTenantClaim,

    #[error("insufficient permissions")]
    InsufficientPermissions,

    #[error("OIDC discovery failed: {0}")]
    DiscoveryFailed(String),

    #[error("JWKS fetch failed: {0}")]
    JwksFetch(#[from] reqwest::Error),

    #[error("JWKS parse failed: {0}")]
    JwksParse(String),

    #[error("token validation failed: {0}")]
    ValidationFailed(String),

    #[error("configuration error: {0}")]
    Config(String),
}

impl From<jsonwebtoken::errors::Error> for AuthError {
    fn from(err: jsonwebtoken::errors::Error) -> Self {
        use jsonwebtoken::errors::ErrorKind;
        match err.kind() {
            ErrorKind::ExpiredSignature => Self::TokenExpired,
            ErrorKind::InvalidIssuer => Self::InvalidIssuer,
            ErrorKind::InvalidAudience => Self::InvalidAudience,
            ErrorKind::InvalidSignature => Self::InvalidSignature,
            _ => Self::InvalidToken,
        }
    }
}

impl From<AuthError> for rmcp::ErrorData {
    fn from(err: AuthError) -> Self {
        match &err {
            AuthError::NotAuthenticated
            | AuthError::InvalidToken
            | AuthError::TokenExpired
            | AuthError::InvalidIssuer
            | AuthError::InvalidAudience
            | AuthError::InvalidSignature
            | AuthError::KeyNotFound(_)
            | AuthError::NoMatchingKey => Self::invalid_params(err.to_string(), None),

            AuthError::InsufficientPermissions => {
                Self::invalid_params(format!("Authorization failed: {err}"), None)
            }

            AuthError::MissingTenantClaim => {
                Self::invalid_params(format!("Tenant resolution failed: {err}"), None)
            }

            AuthError::DiscoveryFailed(_)
            | AuthError::JwksFetch(_)
            | AuthError::JwksParse(_)
            | AuthError::ValidationFailed(_)
            | AuthError::Config(_) => Self::internal_error(err.to_string(), None),
        }
    }
}

pub type Result<T> = std::result::Result<T, AuthError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_auth_error_display() {
        assert_eq!(
            AuthError::NotAuthenticated.to_string(),
            "authentication required"
        );
        assert_eq!(AuthError::InvalidToken.to_string(), "invalid token");
        assert_eq!(AuthError::TokenExpired.to_string(), "token expired");
    }

    #[test]
    fn test_key_not_found_error() {
        let err = AuthError::KeyNotFound("kid123".to_string());
        assert_eq!(err.to_string(), "key not found: kid123");
    }

    #[test]
    fn test_no_matching_key_error() {
        let err = AuthError::NoMatchingKey;
        assert_eq!(err.to_string(), "no matching key for algorithm");
    }

    #[test]
    fn test_jwks_parse_error() {
        let err = AuthError::JwksParse("invalid JSON".to_string());
        assert_eq!(err.to_string(), "JWKS parse failed: invalid JSON");
    }

    #[test]
    fn test_auth_error_to_error_data() {
        let err: rmcp::ErrorData = AuthError::NotAuthenticated.into();
        assert!(err.message.contains("authentication required"));
    }

    #[test]
    fn test_insufficient_permissions_error() {
        let err: rmcp::ErrorData = AuthError::InsufficientPermissions.into();
        assert!(err.message.contains("Authorization failed"));
    }

    #[test]
    fn test_missing_tenant_claim_error() {
        let err: rmcp::ErrorData = AuthError::MissingTenantClaim.into();
        assert!(err.message.contains("Tenant resolution failed"));
    }

    #[test]
    fn test_internal_error_conversion() {
        let err: rmcp::ErrorData = AuthError::DiscoveryFailed("failed".into()).into();
        assert!(err.message.contains("OIDC discovery failed"));

        let err: rmcp::ErrorData = AuthError::Config("bad config".into()).into();
        assert!(err.message.contains("configuration error"));
    }

    #[test]
    fn test_key_errors_conversion() {
        let err: rmcp::ErrorData = AuthError::KeyNotFound("kid".into()).into();
        assert!(err.message.contains("key not found"));

        let err: rmcp::ErrorData = AuthError::NoMatchingKey.into();
        assert!(err.message.contains("no matching key"));
    }
}