Skip to main content

sqlserver_mcp_catalog/auth/strategies/
windows.rs

1// SQL Server 2025 - master/msdb/sandbox combined catalog MCP server.
2//
3// `windowsAuth` (Windows Authentication / Integrated Security — see
4// docs/sqlserver-eda-openapi-pipeline/README.md's `securitySchemes`
5// documentation). Not discovered by mcpify's own OpenAPI-auth-scheme
6// classifier (it only recognizes `http`/`basic`/`bearer` and `oauth2`
7// shapes; `windowsAuth`'s `http`/`negotiate` scheme isn't one of them —
8// see `core::config_schema::AuthMethod`'s doc comment), so this strategy
9// was added by hand.
10//
11// `tiberius::AuthMethod::windows` is only compiled when the `winauth`
12// feature is enabled *and* the target OS is Windows (native SSPI) — see
13// `tiberius`'s `client::auth` module. On Linux/macOS (this pipeline's
14// primary target — see docs/sqlserver-eda-openapi-pipeline/docker-compose.yml,
15// which runs SQL Server in Linux containers), this strategy still resolves
16// credentials the same way (env vars / OS keychain / prompt), but
17// `services::api_client` cannot actually hand them to `tiberius` on those
18// platforms: there is no non-Windows NTLM/SSPI implementation without also
19// enabling `integrated-auth-gssapi` (Kerberos via `libgssapi`, a system
20// library this project doesn't currently link) — see
21// `auth::auth_manager::AuthManager::resolve_tds_auth`, which surfaces that
22// as a clear runtime error rather than silently falling back to another
23// auth mode.
24
25use async_trait::async_trait;
26
27use super::super::auth_strategy::{AuthConfig, AuthStrategy, Credentials};
28use super::super::errors::AuthError;
29
30#[derive(Debug, Default)]
31pub struct WindowsAuthStrategy;
32
33#[async_trait]
34impl AuthStrategy for WindowsAuthStrategy {
35    async fn authenticate(&self, config: &AuthConfig) -> anyhow::Result<Credentials> {
36        let username = config.get("username").ok_or_else(|| {
37            AuthError::MissingCredentials(
38                "username (optionally DOMAIN\\user), password".to_string(),
39            )
40        })?;
41        let password = config
42            .get("password")
43            .ok_or_else(|| AuthError::MissingCredentials("username, password".to_string()))?;
44
45        let mut credentials = Credentials::new();
46        credentials.insert("username".to_string(), username.clone());
47        credentials.insert("password".to_string(), password.clone());
48        Ok(credentials)
49    }
50
51    fn validate_credentials(&self, credentials: &Credentials) -> bool {
52        credentials
53            .get("username")
54            .is_some_and(|username| !username.is_empty())
55            && credentials
56                .get("password")
57                .is_some_and(|password| !password.is_empty())
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[tokio::test]
66    async fn carries_a_domain_qualified_username_through_unchanged() {
67        let strategy = WindowsAuthStrategy;
68        let config = AuthConfig::from([
69            ("username".to_string(), "CORP\\alice".to_string()),
70            ("password".to_string(), "s3cr3t".to_string()),
71        ]);
72        let credentials = strategy.authenticate(&config).await.unwrap();
73        assert_eq!(credentials.get("username").unwrap(), "CORP\\alice");
74    }
75
76    #[tokio::test]
77    async fn rejects_a_config_missing_either_field() {
78        let strategy = WindowsAuthStrategy;
79        assert!(strategy.authenticate(&AuthConfig::new()).await.is_err());
80    }
81}