omni_dev/snowflake/
client.rs1pub mod config;
20pub mod error;
21pub mod row;
22
23mod auth;
24mod jwt;
25mod session;
26mod transport;
27
28use std::sync::Arc;
29
30pub use config::{AuthMethod, BrowserConfig, BrowserLaunch, KeyPairConfig, SnowflakeClientConfig};
31pub use error::{Error, Result};
32pub use row::{rows_to_payload, value_to_json, Column, Row};
33pub use session::SnowflakeSession;
34
35use transport::Transport;
36
37pub struct SnowflakeClient {
39 transport: Arc<Transport>,
40 config: SnowflakeClientConfig,
41}
42
43impl SnowflakeClient {
44 pub fn new(config: SnowflakeClientConfig) -> Result<Self> {
51 let transport = Arc::new(Transport::new(&config.api_host(), config.http_timeout)?);
52 Ok(Self { transport, config })
53 }
54
55 #[must_use]
57 pub fn config(&self) -> &SnowflakeClientConfig {
58 &self.config
59 }
60
61 pub async fn create_session(&self) -> Result<SnowflakeSession> {
68 let tokens = match &self.config.auth {
69 AuthMethod::ExternalBrowser(browser) => {
70 auth::external_browser_login(&self.transport, &self.config, browser).await?
71 }
72 AuthMethod::ProgrammaticAccessToken { token } => {
73 auth::pat_login(&self.transport, &self.config, token).await?
74 }
75 AuthMethod::KeyPairJwt(keypair) => {
76 auth::keypair_jwt_login(&self.transport, &self.config, keypair).await?
77 }
78 };
79 Ok(SnowflakeSession::new(
80 Arc::clone(&self.transport),
81 tokens,
82 self.config.query_timeout,
83 ))
84 }
85}
86
87#[cfg(test)]
91#[allow(clippy::unwrap_used, clippy::expect_used)]
92pub(crate) fn test_session(base_url: &str, query_timeout: std::time::Duration) -> SnowflakeSession {
93 use std::time::Duration;
94 let url = url::Url::parse(base_url).unwrap().join("/").unwrap();
95 let transport =
96 Arc::new(transport::Transport::with_base_url(url, Duration::from_secs(5)).unwrap());
97 SnowflakeSession::new(
98 transport,
99 session::LoginTokens {
100 session_token: "test-sess".into(),
101 master_token: "test-mast".into(),
102 session_validity_secs: 3600,
103 master_validity_secs: 14_400,
104 },
105 query_timeout,
106 )
107}
108
109#[cfg(test)]
113#[allow(clippy::unwrap_used, clippy::expect_used)]
114pub(crate) fn test_client(base_url: &str, auth: AuthMethod) -> SnowflakeClient {
115 use std::time::Duration;
116 let url = url::Url::parse(base_url).unwrap().join("/").unwrap();
117 let transport =
118 Arc::new(transport::Transport::with_base_url(url, Duration::from_secs(5)).unwrap());
119 let mut config = SnowflakeClientConfig::external_browser("TESTACCT", "tester");
120 config.auth = auth;
121 SnowflakeClient { transport, config }
122}
123
124#[cfg(test)]
125#[allow(clippy::unwrap_used, clippy::expect_used)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn new_builds_a_client_and_exposes_its_config() {
131 let client = SnowflakeClient::new(SnowflakeClientConfig::external_browser("MyAcct", "me"))
132 .expect("a valid account resolves to a valid host");
133 assert_eq!(client.config().account, "MyAcct");
134 assert_eq!(client.config().user, "me");
135 assert_eq!(client.config().api_host(), "myacct.snowflakecomputing.com");
136 }
137
138 #[test]
139 fn new_rejects_an_invalid_host_override() {
140 let config = SnowflakeClientConfig {
141 host: Some("[".to_string()),
143 ..SnowflakeClientConfig::external_browser("acct", "me")
144 };
145 assert!(matches!(
146 SnowflakeClient::new(config),
147 Err(Error::Protocol(_))
148 ));
149 }
150
151 #[tokio::test]
152 async fn pat_create_session_sends_the_token_and_captures_login_tokens() {
153 use wiremock::matchers::{method, path};
154 use wiremock::{Mock, MockServer, ResponseTemplate};
155
156 let server = MockServer::start().await;
157 Mock::given(method("POST"))
158 .and(path("/session/v1/login-request"))
159 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
160 "success": true,
161 "data": {
162 "token": "sess-tok",
163 "masterToken": "mast-tok",
164 "validityInSeconds": 3600,
165 "masterValidityInSeconds": 14_400,
166 },
167 })))
168 .expect(1)
169 .mount(&server)
170 .await;
171
172 let client = test_client(
173 &server.uri(),
174 AuthMethod::ProgrammaticAccessToken {
175 token: "pat-xyz".into(),
176 },
177 );
178 client
180 .create_session()
181 .await
182 .expect("PAT login should succeed against the mock");
183
184 let requests = server.received_requests().await.expect("recorded requests");
186 let body: serde_json::Value = serde_json::from_slice(&requests[0].body).unwrap();
187 assert_eq!(body["data"]["AUTHENTICATOR"], "PROGRAMMATIC_ACCESS_TOKEN");
188 assert_eq!(body["data"]["TOKEN"], "pat-xyz");
189 assert_eq!(body["data"]["LOGIN_NAME"], "tester");
190 assert_eq!(body["data"]["ACCOUNT_NAME"], "TESTACCT");
191 }
192
193 #[tokio::test]
194 async fn keypair_jwt_create_session_sends_a_jwt_and_captures_login_tokens() {
195 use aws_lc_rs::encoding::AsDer as _;
196 use base64::Engine as _;
197 use wiremock::matchers::{method, path};
198 use wiremock::{Mock, MockServer, ResponseTemplate};
199
200 let server = MockServer::start().await;
201 Mock::given(method("POST"))
202 .and(path("/session/v1/login-request"))
203 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
204 "success": true,
205 "data": {
206 "token": "sess-tok",
207 "masterToken": "mast-tok",
208 "validityInSeconds": 3600,
209 "masterValidityInSeconds": 14_400,
210 },
211 })))
212 .expect(1)
213 .mount(&server)
214 .await;
215
216 let key_pair = aws_lc_rs::rsa::KeyPair::generate(aws_lc_rs::rsa::KeySize::Rsa2048).unwrap();
218 let der = key_pair.as_der().unwrap();
219 let pem = format!(
220 "-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
221 base64::engine::general_purpose::STANDARD.encode(der.as_ref())
222 );
223
224 let client = test_client(
225 &server.uri(),
226 AuthMethod::KeyPairJwt(KeyPairConfig {
227 private_key_pem: pem.into(),
228 }),
229 );
230 client
231 .create_session()
232 .await
233 .expect("key-pair JWT login should succeed against the mock");
234
235 let requests = server.received_requests().await.expect("recorded requests");
236 let body: serde_json::Value = serde_json::from_slice(&requests[0].body).unwrap();
237 assert_eq!(body["data"]["AUTHENTICATOR"], "SNOWFLAKE_JWT");
238 let jwt = body["data"]["TOKEN"].as_str().expect("a JWT string");
239 assert_eq!(jwt.split('.').count(), 3, "TOKEN must be a compact JWS");
240 }
241}