Skip to main content

sqlserver_mcp_catalog/auth/strategies/
sql_server.rs

1// SQL Server 2025 - master/msdb/sandbox combined catalog MCP server.
2//
3// `sqlAuth` (SQL Server Authentication: a SQL login validated by the engine
4// itself — see docs/sqlserver-eda-openapi-pipeline/README.md's
5// `securitySchemes` documentation). Adapted from mcpify's originally
6// generated `strategies::basic` (the OpenAPI `http`/`basic` scheme this
7// maps from), minus the HTTP `Authorization: Basic ...` header encoding
8// that scheme normally implies — this project's transport
9// (`services::api_client`) reads `username`/`password` directly to build a
10// `tiberius::AuthMethod::sql_server(...)`, not an HTTP header.
11
12use async_trait::async_trait;
13
14use super::super::auth_strategy::{AuthConfig, AuthStrategy, Credentials};
15use super::super::errors::AuthError;
16
17#[derive(Debug, Default)]
18pub struct SqlServerAuthStrategy;
19
20#[async_trait]
21impl AuthStrategy for SqlServerAuthStrategy {
22    async fn authenticate(&self, config: &AuthConfig) -> anyhow::Result<Credentials> {
23        let username = config
24            .get("username")
25            .ok_or_else(|| AuthError::MissingCredentials("username, password".to_string()))?;
26        let password = config
27            .get("password")
28            .ok_or_else(|| AuthError::MissingCredentials("username, password".to_string()))?;
29
30        let mut credentials = Credentials::new();
31        credentials.insert("username".to_string(), username.clone());
32        credentials.insert("password".to_string(), password.clone());
33        Ok(credentials)
34    }
35
36    fn validate_credentials(&self, credentials: &Credentials) -> bool {
37        credentials
38            .get("username")
39            .is_some_and(|username| !username.is_empty())
40            && credentials
41                .get("password")
42                .is_some_and(|password| !password.is_empty())
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    fn config(username: &str, password: &str) -> AuthConfig {
51        AuthConfig::from([
52            ("username".to_string(), username.to_string()),
53            ("password".to_string(), password.to_string()),
54        ])
55    }
56
57    #[tokio::test]
58    async fn carries_username_and_password_through_unchanged() {
59        let strategy = SqlServerAuthStrategy;
60        let credentials = strategy
61            .authenticate(&config("sa", "s3cr3t"))
62            .await
63            .unwrap();
64        assert_eq!(credentials.get("username").unwrap(), "sa");
65        assert_eq!(credentials.get("password").unwrap(), "s3cr3t");
66    }
67
68    #[tokio::test]
69    async fn rejects_a_config_missing_either_field() {
70        let strategy = SqlServerAuthStrategy;
71        assert!(strategy.authenticate(&AuthConfig::new()).await.is_err());
72    }
73
74    #[test]
75    fn validates_credentials_carrying_both_fields() {
76        let strategy = SqlServerAuthStrategy;
77        assert!(strategy.validate_credentials(&config("sa", "s3cr3t")));
78        assert!(!strategy.validate_credentials(&Credentials::new()));
79    }
80}