Skip to content

Commit 68f5961

Browse files
SNOW-2454885: Add auth tests for aio
SNOW-2266953: Add aio tests to jenkins builds
1 parent a02a3a5 commit 68f5961

19 files changed

+1169
-9
lines changed

ci/container/test_authentication.sh

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/bash -e
22

3-
set -o pipefail
3+
set -ox pipefail
44

55

66
export WORKSPACE=${WORKSPACE:-/mnt/workspace}
@@ -17,6 +17,9 @@ export RUN_AUTH_TESTS=true
1717
export AUTHENTICATION_TESTS_ENV="docker"
1818
export PYTHONPATH=$SOURCE_ROOT
1919

20-
python3 -m pip install --break-system-packages -e .
20+
python3 -m pip install --break-system-packages -e ".[development]"
2121

22-
python3 -m pytest test/auth/*
22+
python3 -m pytest test/auth/ --ignore=test/auth/aio
23+
24+
python3 -m pip install --break-system-packages -e ".[development,aio,aioboto]"
25+
python3 -m pytest test/auth/aio/

ci/test_darwin.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
3737
SHORT_VERSION=$(python3 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
3838
CONNECTOR_WHL=$(ls ${CONNECTOR_DIR}/dist/snowflake_connector_python*cp${SHORT_VERSION}*.whl)
3939
# pandas not tested here because of macos issue: SNOW-1660226
40-
TEST_ENVLIST=$(python3 -c "print('fix_lint,' + ','.join('py${SHORT_VERSION}-' + e + '-ci' for e in ['unit','integ','sso']) + ',py${SHORT_VERSION}-coverage')")
40+
TEST_ENVLIST=$(python3 -c "print('fix_lint,' + ','.join('py${SHORT_VERSION}-' + e + '-ci' for e in ['unit','integ','sso','aio']) + ',py${SHORT_VERSION}-coverage')")
4141
echo "[Info] Running tox for ${TEST_ENVLIST}"
4242
python3.12 -m tox run -e ${TEST_ENVLIST} --installpkg ${CONNECTOR_WHL}
4343
done

ci/test_fips.sh

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Test Snowflake Connector (FIPS)
44
# Note this is the script that test_fips_docker.sh runs inside of the docker container
55
#
6+
set -x
67

78
# Export USE_PASSWORD only on Jenkins (not on GitHub Actions)
89
# Jenkins FIPS tests run against mocked Snowflake with password auth
@@ -41,6 +42,14 @@ pip freeze
4142
cd $CONNECTOR_DIR
4243

4344
# Run tests in parallel using pytest-xdist
44-
pytest -n auto -vvv --cov=snowflake.connector --cov-report=xml:coverage.xml test --ignore=test/integ/aio_it --ignore=test/unit/aio --ignore=test/wif/test_wif_async.py
45-
45+
pytest -n auto -vvv --cov=snowflake.connector --cov-report=xml:coverage.xml test \
46+
--ignore=test/integ/aio_it \
47+
--ignore=test/unit/aio \
48+
--ignore=test/auth/aio \
49+
--ignore=test/wif/test_wif_async.py
50+
51+
pip install "${CONNECTOR_WHL}[aio,aioboto]"
52+
# Run aio tests separately
53+
pytest -n auto -vvv --cov=snowflake.connector --cov-append --cov-report=xml:coverage.xml -m "aio and unit" test
54+
pytest -n auto -vvv --cov=snowflake.connector --cov-append --cov-report=xml:coverage.xml -m "aio and integ" test
4655
deactivate

ci/test_linux.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ else
4141
echo "[Info] Testing with ${PYTHON_VERSION}"
4242
SHORT_VERSION=$(python3.10 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
4343
CONNECTOR_WHL=$(ls $CONNECTOR_DIR/dist/snowflake_connector_python*cp${SHORT_VERSION}*manylinux2014*.whl | sort -r | head -n 1)
44-
TEST_LIST=`echo py${PYTHON_VERSION/\./}-{unit-parallel,integ,pandas-parallel,sso}-ci | sed 's/ /,/g'`
44+
TEST_LIST=`echo py${PYTHON_VERSION/\./}-{unit-parallel,integ,pandas-parallel,sso,aio}-ci | sed 's/ /,/g'`
4545
TEST_ENVLIST=fix_lint,$TEST_LIST,py${PYTHON_VERSION/\./}-coverage
4646
echo "[Info] Running tox for ${TEST_ENVLIST}"
4747

ci/test_windows.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ curl https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.11.0/wire
4949
set JUNIT_REPORT_DIR=%workspace%
5050
set COV_REPORT_DIR=%workspace%
5151

52-
set TEST_ENVLIST=fix_lint,py%pv%-unit-ci,py%pv%-integ-ci,py%pv%-pandas-ci,py%pv%-sso-ci,py%pv%-coverage
52+
set TEST_ENVLIST=fix_lint,py%pv%-unit-ci,py%pv%-integ-ci,py%pv%-pandas-ci,py%pv%-sso-ci,py%pv%-aio-ci,py%pv%-coverage
5353
tox -e %TEST_ENVLIST% --installpkg %connector_whl%
5454
if %errorlevel% neq 0 goto :error
5555

test/auth/aio/__init__.py

Whitespace-only changes.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import logging.config
2+
import os
3+
import subprocess
4+
import threading
5+
import webbrowser
6+
from enum import Enum
7+
from typing import Union
8+
9+
import requests
10+
11+
import snowflake.connector.aio
12+
13+
try:
14+
from src.snowflake.connector.vendored.requests.auth import HTTPBasicAuth
15+
except ImportError:
16+
pass
17+
18+
logger = logging.getLogger(__name__)
19+
20+
logger.setLevel(logging.INFO)
21+
22+
23+
class Scenario(Enum):
24+
SUCCESS = "success"
25+
FAIL = "fail"
26+
TIMEOUT = "timeout"
27+
EXTERNAL_OAUTH_OKTA_SUCCESS = "externalOauthOktaSuccess"
28+
INTERNAL_OAUTH_SNOWFLAKE_SUCCESS = "internalOauthSnowflakeSuccess"
29+
30+
31+
def get_access_token_oauth(cfg):
32+
auth_url = cfg["auth_url"]
33+
34+
data = {
35+
"username": cfg["okta_user"],
36+
"password": cfg["okta_pass"],
37+
"grant_type": "password",
38+
"scope": f"session:role:{cfg['role']}",
39+
}
40+
41+
headers = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}
42+
43+
auth_credentials = HTTPBasicAuth(cfg["oauth_client_id"], cfg["oauth_client_secret"])
44+
try:
45+
response = requests.post(
46+
url=auth_url, data=data, headers=headers, auth=auth_credentials
47+
)
48+
response.raise_for_status()
49+
return response.json()["access_token"]
50+
51+
except requests.exceptions.HTTPError as http_err:
52+
logger.error(f"HTTP error occurred: {http_err}")
53+
raise
54+
55+
56+
def clean_browser_processes():
57+
if os.getenv("AUTHENTICATION_TESTS_ENV") == "docker":
58+
try:
59+
clean_browser_processes_path = "/externalbrowser/cleanBrowserProcesses.js"
60+
process = subprocess.run(["node", clean_browser_processes_path], timeout=15)
61+
logger.debug(f"OUTPUT: {process.stdout}, ERRORS: {process.stderr}")
62+
except Exception as e:
63+
raise RuntimeError(e)
64+
65+
66+
class AuthorizationTestHelper:
67+
def __init__(self, configuration: dict):
68+
self.auth_test_env = os.getenv("AUTHENTICATION_TESTS_ENV")
69+
self.configuration = configuration
70+
self.error_msg = ""
71+
72+
def update_config(self, configuration):
73+
self.configuration = configuration
74+
75+
async def connect_and_provide_credentials(
76+
self, scenario: Scenario, login: str, password: str
77+
):
78+
import asyncio
79+
80+
try:
81+
# Use asyncio task for connection instead of thread
82+
connect_task = asyncio.create_task(self.connect_and_execute_simple_query())
83+
84+
if self.auth_test_env == "docker":
85+
# Browser credentials still needs to run in thread since it's sync
86+
browser = threading.Thread(
87+
target=self._provide_credentials, args=(scenario, login, password)
88+
)
89+
browser.start()
90+
# Wait for browser thread to complete
91+
await asyncio.get_event_loop().run_in_executor(None, browser.join)
92+
93+
# Wait for connection task to complete
94+
await connect_task
95+
96+
except Exception as e:
97+
self.error_msg = e
98+
logger.error(e)
99+
100+
def get_error_msg(self) -> str:
101+
return str(self.error_msg)
102+
103+
async def connect_and_execute_simple_query(self):
104+
try:
105+
logger.info("Trying to connect to Snowflake")
106+
async with snowflake.connector.aio.SnowflakeConnection(
107+
**self.configuration
108+
) as con:
109+
result = await con.cursor().execute("select 1;")
110+
logger.debug(await result.fetchall())
111+
logger.info("Successfully connected to Snowflake")
112+
return True
113+
except Exception as e:
114+
self.error_msg = e
115+
logger.error(e)
116+
return False
117+
118+
async def connect_and_execute_set_session_state(self, key: str, value: str):
119+
try:
120+
logger.info("Trying to connect to Snowflake")
121+
async with snowflake.connector.aio.SnowflakeConnection(
122+
**self.configuration
123+
) as con:
124+
result = await con.cursor().execute(f"SET {key} = '{value}'")
125+
logger.debug(await result.fetchall())
126+
logger.info("Successfully SET session variable")
127+
return True
128+
except Exception as e:
129+
self.error_msg = e
130+
logger.error(e)
131+
return False
132+
133+
async def connect_and_execute_check_session_state(self, key: str):
134+
try:
135+
logger.info("Trying to connect to Snowflake")
136+
async with snowflake.connector.aio.SnowflakeConnection(
137+
**self.configuration
138+
) as con:
139+
result = await con.cursor().execute(f"SELECT 1, ${key}")
140+
value = (await result.fetchone())[1]
141+
logger.debug(value)
142+
logger.info("Successfully READ session variable")
143+
return value
144+
except Exception as e:
145+
self.error_msg = e
146+
logger.error(e)
147+
return False
148+
149+
def _provide_credentials(self, scenario: Scenario, login: str, password: str):
150+
try:
151+
webbrowser.register("xdg-open", None, webbrowser.GenericBrowser("xdg-open"))
152+
provide_browser_credentials_path = (
153+
"/externalbrowser/provideBrowserCredentials.js"
154+
)
155+
process = subprocess.run(
156+
[
157+
"node",
158+
provide_browser_credentials_path,
159+
scenario.value,
160+
login,
161+
password,
162+
],
163+
timeout=15,
164+
)
165+
logger.debug(f"OUTPUT: {process.stdout}, ERRORS: {process.stderr}")
166+
except Exception as e:
167+
self.error_msg = e
168+
raise RuntimeError(e)
169+
170+
def get_totp(self, seed: str = "") -> []:
171+
if self.auth_test_env == "docker":
172+
try:
173+
provide_totp_generator_path = "/externalbrowser/totpGenerator.js"
174+
process = subprocess.run(
175+
["node", provide_totp_generator_path, seed],
176+
timeout=40,
177+
capture_output=True,
178+
text=True,
179+
)
180+
logger.debug(f"OUTPUT: {process.stdout}, ERRORS: {process.stderr}")
181+
return process.stdout.strip().split()
182+
except Exception as e:
183+
self.error_msg = e
184+
raise RuntimeError(e)
185+
else:
186+
logger.info("TOTP generation is not supported in this environment")
187+
return ""
188+
189+
async def connect_using_okta_connection_and_execute_custom_command(
190+
self, command: str, return_token: bool = False
191+
) -> Union[bool, str]:
192+
try:
193+
logger.info("Setup PAT")
194+
async with snowflake.connector.aio.SnowflakeConnection(
195+
**self.configuration
196+
) as con:
197+
result = await con.cursor().execute(command)
198+
token = (await result.fetchall())[0][1]
199+
except Exception as e:
200+
self.error_msg = e
201+
logger.error(e)
202+
return False
203+
if return_token:
204+
return token
205+
return False
206+
207+
async def connect_and_execute_simple_query_with_mfa_token(self, totp_codes):
208+
# Try each TOTP code until one works
209+
for i, totp_code in enumerate(totp_codes):
210+
logging.info(f"Trying TOTP code {i + 1}/{len(totp_codes)}")
211+
212+
self.configuration["passcode"] = totp_code
213+
self.error_msg = ""
214+
215+
connection_success = await self.connect_and_execute_simple_query()
216+
217+
if connection_success:
218+
logging.info(f"Successfully connected with TOTP code {i + 1}")
219+
return True
220+
else:
221+
last_error = str(self.error_msg)
222+
logging.warning(f"TOTP code {i + 1} failed: {last_error}")
223+
if "TOTP Invalid" in last_error:
224+
logging.info("TOTP/MFA error detected.")
225+
continue
226+
else:
227+
logging.error(f"Non-TOTP error detected: {last_error}")
228+
break
229+
return False

0 commit comments

Comments
 (0)