Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne

# Release Notes
- v4.2.0(TBD)
- Added support for async I/O. Asynchronous version of connector is available via `snowflake.connector.aio` module.
- Added PREVIEW support for async I/O. Asynchronous version of connector is available via `snowflake.connector.aio` module.
This is preview feature and should not be used in production code. To use this feature contact your Snowflake Sales
Representative ( Snowflake Support cannot help with this feature in the current stage, while its in preview).
- v4.1.1(TBD)
- Relaxed pandas dependency requirements for Python below 3.12.
- Changed CRL cache cleanup background task to daemon to avoid blocking main thread.
Expand Down
9 changes: 6 additions & 3 deletions ci/container/test_authentication.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash -e

set -o pipefail
set -ox pipefail


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

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

python3 -m pytest test/auth/*
python3 -m pytest test/auth/ --ignore=test/auth/aio

python3 -m pip install --break-system-packages -e ".[development,aio,aioboto]"
python3 -m pytest test/auth/aio/
2 changes: 1 addition & 1 deletion ci/test_darwin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
SHORT_VERSION=$(python3 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
CONNECTOR_WHL=$(ls ${CONNECTOR_DIR}/dist/snowflake_connector_python*cp${SHORT_VERSION}*.whl)
# pandas not tested here because of macos issue: SNOW-1660226
TEST_ENVLIST=$(python3 -c "print('fix_lint,' + ','.join('py${SHORT_VERSION}-' + e + '-ci' for e in ['unit','integ','sso']) + ',py${SHORT_VERSION}-coverage')")
TEST_ENVLIST=$(python3 -c "print('fix_lint,' + ','.join('py${SHORT_VERSION}-' + e + '-ci' for e in ['unit','integ','sso','aio']) + ',py${SHORT_VERSION}-coverage')")
echo "[Info] Running tox for ${TEST_ENVLIST}"
python3.12 -m tox run -e ${TEST_ENVLIST} --installpkg ${CONNECTOR_WHL}
done
Expand Down
13 changes: 11 additions & 2 deletions ci/test_fips.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Test Snowflake Connector (FIPS)
# Note this is the script that test_fips_docker.sh runs inside of the docker container
#
set -x

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

# Run tests in parallel using pytest-xdist
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

pytest -n auto -vvv --cov=snowflake.connector --cov-report=xml:coverage.xml test \
--ignore=test/integ/aio_it \
--ignore=test/unit/aio \
--ignore=test/auth/aio \
--ignore=test/wif/test_wif_async.py

pip install "${CONNECTOR_WHL}[aio,aioboto]"
# Run aio tests separately
pytest -n auto -vvv --cov=snowflake.connector --cov-append --cov-report=xml:coverage.xml -m "aio and unit" test
pytest -n auto -vvv --cov=snowflake.connector --cov-append --cov-report=xml:coverage.xml -m "aio and integ" test
deactivate
2 changes: 1 addition & 1 deletion ci/test_linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ else
echo "[Info] Testing with ${PYTHON_VERSION}"
SHORT_VERSION=$(python3.10 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
CONNECTOR_WHL=$(ls $CONNECTOR_DIR/dist/snowflake_connector_python*cp${SHORT_VERSION}*manylinux2014*.whl | sort -r | head -n 1)
TEST_LIST=`echo py${PYTHON_VERSION/\./}-{unit-parallel,integ,pandas-parallel,sso}-ci | sed 's/ /,/g'`
TEST_LIST=`echo py${PYTHON_VERSION/\./}-{unit-parallel,integ,pandas-parallel,sso,aio}-ci | sed 's/ /,/g'`
TEST_ENVLIST=fix_lint,$TEST_LIST,py${PYTHON_VERSION/\./}-coverage
echo "[Info] Running tox for ${TEST_ENVLIST}"

Expand Down
2 changes: 1 addition & 1 deletion ci/test_rockylinux9.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ else
continue
fi

TEST_LIST=`echo py${PYTHON_VERSION/\./}-{extras,unit-parallel,integ-parallel,pandas-parallel,sso}-ci | sed 's/ /,/g'`
TEST_LIST=`echo py${PYTHON_VERSION/\./}-{extras,unit-parallel,integ-parallel,pandas-parallel,aio}-ci | sed 's/ /,/g'`
TEST_ENVLIST=fix_lint,$TEST_LIST,py${PYTHON_VERSION/\./}-coverage
echo "[Info] Running tox for ${TEST_ENVLIST}"

Expand Down
2 changes: 1 addition & 1 deletion ci/test_windows.bat
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ curl https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.11.0/wire
set JUNIT_REPORT_DIR=%workspace%
set COV_REPORT_DIR=%workspace%

set TEST_ENVLIST=fix_lint,py%pv%-unit-ci,py%pv%-integ-ci,py%pv%-pandas-ci,py%pv%-sso-ci,py%pv%-coverage
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
tox -e %TEST_ENVLIST% --installpkg %connector_whl%
if %errorlevel% neq 0 goto :error

Expand Down
Empty file added test/auth/aio/__init__.py
Empty file.
229 changes: 229 additions & 0 deletions test/auth/aio/authorization_test_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import logging.config
import os
import subprocess
import threading
import webbrowser
from enum import Enum
from typing import Union

import requests

import snowflake.connector.aio

try:
from src.snowflake.connector.vendored.requests.auth import HTTPBasicAuth
except ImportError:
pass

logger = logging.getLogger(__name__)

logger.setLevel(logging.INFO)


class Scenario(Enum):
SUCCESS = "success"
FAIL = "fail"
TIMEOUT = "timeout"
EXTERNAL_OAUTH_OKTA_SUCCESS = "externalOauthOktaSuccess"
INTERNAL_OAUTH_SNOWFLAKE_SUCCESS = "internalOauthSnowflakeSuccess"


def get_access_token_oauth(cfg):
auth_url = cfg["auth_url"]

data = {
"username": cfg["okta_user"],
"password": cfg["okta_pass"],
"grant_type": "password",
"scope": f"session:role:{cfg['role']}",
}

headers = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}

auth_credentials = HTTPBasicAuth(cfg["oauth_client_id"], cfg["oauth_client_secret"])
try:
response = requests.post(
url=auth_url, data=data, headers=headers, auth=auth_credentials
)
response.raise_for_status()
return response.json()["access_token"]

except requests.exceptions.HTTPError as http_err:
logger.error(f"HTTP error occurred: {http_err}")
raise


def clean_browser_processes():
if os.getenv("AUTHENTICATION_TESTS_ENV") == "docker":
try:
clean_browser_processes_path = "/externalbrowser/cleanBrowserProcesses.js"
process = subprocess.run(["node", clean_browser_processes_path], timeout=15)
logger.debug(f"OUTPUT: {process.stdout}, ERRORS: {process.stderr}")
except Exception as e:
raise RuntimeError(e)


class AuthorizationTestHelper:
def __init__(self, configuration: dict):
self.auth_test_env = os.getenv("AUTHENTICATION_TESTS_ENV")
self.configuration = configuration
self.error_msg = ""

def update_config(self, configuration):
self.configuration = configuration

async def connect_and_provide_credentials(
self, scenario: Scenario, login: str, password: str
):
import asyncio

try:
# Use asyncio task for connection instead of thread
connect_task = asyncio.create_task(self.connect_and_execute_simple_query())

if self.auth_test_env == "docker":
# Browser credentials still needs to run in thread since it's sync
browser = threading.Thread(
target=self._provide_credentials, args=(scenario, login, password)
)
browser.start()
# Wait for browser thread to complete
await asyncio.get_event_loop().run_in_executor(None, browser.join)

# Wait for connection task to complete
await connect_task

except Exception as e:
self.error_msg = e
logger.error(e)

def get_error_msg(self) -> str:
return str(self.error_msg)

async def connect_and_execute_simple_query(self):
try:
logger.info("Trying to connect to Snowflake")
async with snowflake.connector.aio.SnowflakeConnection(
**self.configuration
) as con:
result = await con.cursor().execute("select 1;")
logger.debug(await result.fetchall())
logger.info("Successfully connected to Snowflake")
return True
except Exception as e:
self.error_msg = e
logger.error(e)
return False

async def connect_and_execute_set_session_state(self, key: str, value: str):
try:
logger.info("Trying to connect to Snowflake")
async with snowflake.connector.aio.SnowflakeConnection(
**self.configuration
) as con:
result = await con.cursor().execute(f"SET {key} = '{value}'")
logger.debug(await result.fetchall())
logger.info("Successfully SET session variable")
return True
except Exception as e:
self.error_msg = e
logger.error(e)
return False

async def connect_and_execute_check_session_state(self, key: str):
try:
logger.info("Trying to connect to Snowflake")
async with snowflake.connector.aio.SnowflakeConnection(
**self.configuration
) as con:
result = await con.cursor().execute(f"SELECT 1, ${key}")
value = (await result.fetchone())[1]
logger.debug(value)
logger.info("Successfully READ session variable")
return value
except Exception as e:
self.error_msg = e
logger.error(e)
return False

def _provide_credentials(self, scenario: Scenario, login: str, password: str):
try:
webbrowser.register("xdg-open", None, webbrowser.GenericBrowser("xdg-open"))
provide_browser_credentials_path = (
"/externalbrowser/provideBrowserCredentials.js"
)
process = subprocess.run(
[
"node",
provide_browser_credentials_path,
scenario.value,
login,
password,
],
timeout=15,
)
logger.debug(f"OUTPUT: {process.stdout}, ERRORS: {process.stderr}")
except Exception as e:
self.error_msg = e
raise RuntimeError(e)

def get_totp(self, seed: str = "") -> []:
if self.auth_test_env == "docker":
try:
provide_totp_generator_path = "/externalbrowser/totpGenerator.js"
process = subprocess.run(
["node", provide_totp_generator_path, seed],
timeout=40,
capture_output=True,
text=True,
)
logger.debug(f"OUTPUT: {process.stdout}, ERRORS: {process.stderr}")
return process.stdout.strip().split()
except Exception as e:
self.error_msg = e
raise RuntimeError(e)
else:
logger.info("TOTP generation is not supported in this environment")
return ""

async def connect_using_okta_connection_and_execute_custom_command(
self, command: str, return_token: bool = False
) -> Union[bool, str]:
try:
logger.info("Setup PAT")
async with snowflake.connector.aio.SnowflakeConnection(
**self.configuration
) as con:
result = await con.cursor().execute(command)
token = (await result.fetchall())[0][1]
except Exception as e:
self.error_msg = e
logger.error(e)
return False
if return_token:
return token
return False

async def connect_and_execute_simple_query_with_mfa_token(self, totp_codes):
# Try each TOTP code until one works
for i, totp_code in enumerate(totp_codes):
logging.info(f"Trying TOTP code {i + 1}/{len(totp_codes)}")

self.configuration["passcode"] = totp_code
self.error_msg = ""

connection_success = await self.connect_and_execute_simple_query()

if connection_success:
logging.info(f"Successfully connected with TOTP code {i + 1}")
return True
else:
last_error = str(self.error_msg)
logging.warning(f"TOTP code {i + 1} failed: {last_error}")
if "TOTP Invalid" in last_error:
logging.info("TOTP/MFA error detected.")
continue
else:
logging.error(f"Non-TOTP error detected: {last_error}")
break
return False
Loading
Loading