Skip to content

Commit e2fd4a4

Browse files
sfc-gh-mkellersfc-gh-mhofmansfc-gh-pbulawasfc-gh-mmishchenkosfc-gh-mkubik
committed
SNOW-1825495 OAuth flows implementation (#2135)
Co-authored-by: Michał Hofman <[email protected]> Co-authored-by: Piotr Bulawa <[email protected]> Co-authored-by: Maxim Mishchenko <[email protected]> Co-authored-by: Mikołaj Kubik <[email protected]> Co-authored-by: Yijun Xie <[email protected]> Co-authored-by: Zexin Yao <[email protected]> Co-authored-by: Jakub Szczerbiński <[email protected]> Co-authored-by: Patryk Cyrek <[email protected]> # Conflicts: # src/snowflake/connector/connection.py
1 parent bdf851a commit e2fd4a4

File tree

62 files changed

+4395
-275
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+4395
-275
lines changed
Binary file not shown.
Binary file not shown.
Binary file not shown.

DESCRIPTION.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1313
- Dropped support for Python 3.8.
1414
- Basic decimal floating-point type support.
1515
- Added handling of PAT provided in `password` field.
16+
- Added experimental support for OAuth authorization code and client credentials flows.
1617
- Improved error message for client-side query cancellations due to timeouts.
1718
- Added support of GCS regional endpoints.
1819
- Added `gcs_use_virtual_endpoints` connection property that forces the usage of the virtual GCS usage. Thanks to this it should be possible to set up private DNS entry for the GCS endpoint. See more: https://cloud.google.com/storage/docs/request-endpoints#xml-api

Jenkinsfile

+37-20
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,46 @@ timestamps {
3535
string(name: 'parent_job', value: env.JOB_NAME),
3636
string(name: 'parent_build_number', value: env.BUILD_NUMBER)
3737
]
38-
stage('Test') {
39-
try {
40-
def commit_hash = "main" // default which we want to override
41-
def bptp_tag = "bptp-stable"
42-
def response = authenticatedGithubCall("https://api.github.com/repos/snowflakedb/snowflake/git/ref/tags/${bptp_tag}")
43-
commit_hash = response.object.sha
44-
// Append the bptp-stable commit sha to params
45-
params += [string(name: 'svn_revision', value: commit_hash)]
46-
} catch(Exception e) {
47-
println("Exception computing commit hash from: ${response}")
38+
parallel(
39+
'Test': {
40+
stage('Test') {
41+
try {
42+
def commit_hash = "main" // default which we want to override
43+
def bptp_tag = "bptp-stable"
44+
def response = authenticatedGithubCall("https://api.github.com/repos/snowflakedb/snowflake/git/ref/tags/${bptp_tag}")
45+
commit_hash = response.object.sha
46+
// Append the bptp-stable commit sha to params
47+
params += [string(name: 'svn_revision', value: commit_hash)]
48+
} catch(Exception e) {
49+
println("Exception computing commit hash from: ${response}")
50+
}
51+
parallel (
52+
'Test Python 39': { build job: 'RT-PyConnector39-PC',parameters: params},
53+
'Test Python 310': { build job: 'RT-PyConnector310-PC',parameters: params},
54+
'Test Python 311': { build job: 'RT-PyConnector311-PC',parameters: params},
55+
'Test Python 312': { build job: 'RT-PyConnector312-PC',parameters: params},
56+
'Test Python 313': { build job: 'RT-PyConnector313-PC',parameters: params},
57+
'Test Python 39 OldDriver': { build job: 'RT-PyConnector39-OldDriver-PC',parameters: params},
58+
'Test Python 39 FIPS': { build job: 'RT-FIPS-PyConnector39',parameters: params},
59+
)
60+
}
61+
},
62+
'Test Authentication': {
63+
stage('Test Authentication') {
64+
withCredentials([
65+
string(credentialsId: 'a791118f-a1ea-46cd-b876-56da1b9bc71c', variable: 'NEXUS_PASSWORD'),
66+
string(credentialsId: 'sfctest0-parameters-secret', variable: 'PARAMETERS_SECRET')
67+
]) {
68+
sh '''\
69+
|#!/bin/bash -e
70+
|$WORKSPACE/ci/test_authentication.sh
71+
'''.stripMargin()
4872
}
49-
parallel (
50-
'Test Python 39': { build job: 'RT-PyConnector39-PC',parameters: params},
51-
'Test Python 310': { build job: 'RT-PyConnector310-PC',parameters: params},
52-
'Test Python 311': { build job: 'RT-PyConnector311-PC',parameters: params},
53-
'Test Python 312': { build job: 'RT-PyConnector312-PC',parameters: params},
54-
'Test Python 313': { build job: 'RT-PyConnector313-PC',parameters: params},
55-
'Test Python 39 OldDriver': { build job: 'RT-PyConnector39-OldDriver-PC',parameters: params},
56-
'Test Python 39 FIPS': { build job: 'RT-FIPS-PyConnector39',parameters: params},
57-
)
5873
}
5974
}
60-
}
75+
)
76+
}
77+
}
6178

6279

6380
pipeline {

ci/container/test_authentication.sh

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash -e
2+
3+
set -o pipefail
4+
5+
6+
export WORKSPACE=${WORKSPACE:-/mnt/workspace}
7+
export SOURCE_ROOT=${SOURCE_ROOT:-/mnt/host}
8+
9+
MVNW_EXE=$SOURCE_ROOT/mvnw
10+
AUTH_PARAMETER_FILE=./.github/workflows/parameters/private/parameters_aws_auth_tests.json
11+
eval $(jq -r '.authtestparams | to_entries | map("export \(.key)=\(.value|tostring)")|.[]' $AUTH_PARAMETER_FILE)
12+
13+
export SNOWFLAKE_AUTH_TEST_PRIVATE_KEY_PATH=./.github/workflows/parameters/private/rsa_keys/rsa_key.p8
14+
export SNOWFLAKE_AUTH_TEST_INVALID_PRIVATE_KEY_PATH=./.github/workflows/parameters/private/rsa_keys/rsa_key_invalid.p8
15+
16+
export SF_OCSP_TEST_MODE=true
17+
export SF_ENABLE_EXPERIMENTAL_AUTHENTICATION=true
18+
export RUN_AUTH_TESTS=true
19+
export AUTHENTICATION_TESTS_ENV="docker"
20+
export PYTHONPATH=$SOURCE_ROOT
21+
22+
python3 -m pip install --break-system-packages -e .
23+
24+
python3 -m pytest test/auth/*

ci/test_authentication.sh

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash -e
2+
3+
set -o pipefail
4+
5+
6+
export THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
7+
export WORKSPACE=${WORKSPACE:-/tmp}
8+
9+
CI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
10+
if [[ -n "$JENKINS_HOME" ]]; then
11+
ROOT_DIR="$(cd "${CI_DIR}/.." && pwd)"
12+
export WORKSPACE=${WORKSPACE:-/tmp}
13+
echo "Use /sbin/ip"
14+
IP_ADDR=$(/sbin/ip -4 addr show scope global dev eth0 | grep inet | awk '{print $2}' | cut -d / -f 1)
15+
16+
fi
17+
18+
gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/parameters/private/parameters_aws_auth_tests.json "$THIS_DIR/../.github/workflows/parameters/private/parameters_aws_auth_tests.json.gpg"
19+
gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/parameters/private/rsa_keys/rsa_key.p8 "$THIS_DIR/../.github/workflows/parameters/private/rsa_keys/rsa_key.p8.gpg"
20+
gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/parameters/private/rsa_keys/rsa_key_invalid.p8 "$THIS_DIR/../.github/workflows/parameters/private/rsa_keys/rsa_key_invalid.p8.gpg"
21+
22+
docker run \
23+
-v $(cd $THIS_DIR/.. && pwd):/mnt/host \
24+
-v $WORKSPACE:/mnt/workspace \
25+
--rm \
26+
nexus.int.snowflakecomputing.com:8086/docker/snowdrivers-test-external-browser-python:1 \
27+
"/mnt/host/ci/container/test_authentication.sh"

src/snowflake/connector/auth/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from .keypair import AuthByKeyPair
88
from .no_auth import AuthNoAuth
99
from .oauth import AuthByOAuth
10+
from .oauth_code import AuthByOauthCode
11+
from .oauth_credentials import AuthByOauthCredentials
1012
from .okta import AuthByOkta
1113
from .pat import AuthByPAT
1214
from .usrpwdmfa import AuthByUsrPwdMfa
@@ -18,6 +20,8 @@
1820
AuthByDefault,
1921
AuthByKeyPair,
2022
AuthByOAuth,
23+
AuthByOauthCode,
24+
AuthByOauthCredentials,
2125
AuthByOkta,
2226
AuthByUsrPwdMfa,
2327
AuthByWebBrowser,
@@ -34,6 +38,8 @@
3438
"AuthByKeyPair",
3539
"AuthByPAT",
3640
"AuthByOAuth",
41+
"AuthByOauthCode",
42+
"AuthByOauthCredentials",
3743
"AuthByOkta",
3844
"AuthByUsrPwdMfa",
3945
"AuthByWebBrowser",

src/snowflake/connector/auth/_auth.py

+21-7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
ACCEPT_TYPE_APPLICATION_SNOWFLAKE,
4848
CONTENT_TYPE_APPLICATION_JSON,
4949
ID_TOKEN_INVALID_LOGIN_REQUEST_GS_CODE,
50+
OAUTH_ACCESS_TOKEN_EXPIRED_GS_CODE,
5051
PYTHON_CONNECTOR_USER_AGENT,
5152
ReauthenticationRequest,
5253
)
@@ -86,7 +87,7 @@ class Auth:
8687

8788
def __init__(self, rest) -> None:
8889
self._rest = rest
89-
self.token_cache = TokenCache.make()
90+
self._token_cache: TokenCache | None = None
9091

9192
@staticmethod
9293
def base_auth_data(
@@ -350,7 +351,7 @@ def post_request_wrapper(self, url, headers, body) -> None:
350351
# clear stored id_token if failed to connect because of id_token
351352
# raise an exception for reauth without id_token
352353
self._rest.id_token = None
353-
self.delete_temporary_credential(
354+
self._delete_temporary_credential(
354355
self._rest._host, user, TokenType.ID_TOKEN
355356
)
356357
raise ReauthenticationRequest(
@@ -360,6 +361,14 @@ def post_request_wrapper(self, url, headers, body) -> None:
360361
sqlstate=SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED,
361362
)
362363
)
364+
elif errno == OAUTH_ACCESS_TOKEN_EXPIRED_GS_CODE:
365+
raise ReauthenticationRequest(
366+
ProgrammingError(
367+
msg=ret["message"],
368+
errno=int(errno),
369+
sqlstate=SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED,
370+
)
371+
)
363372

364373
from . import AuthByKeyPair
365374

@@ -374,7 +383,7 @@ def post_request_wrapper(self, url, headers, body) -> None:
374383
from . import AuthByUsrPwdMfa
375384

376385
if isinstance(auth_instance, AuthByUsrPwdMfa):
377-
self.delete_temporary_credential(
386+
self._delete_temporary_credential(
378387
self._rest._host, user, TokenType.MFA_TOKEN
379388
)
380389
Error.errorhandler_wrapper(
@@ -466,7 +475,7 @@ def _read_temporary_credential(
466475
user: str,
467476
cred_type: TokenType,
468477
) -> str | None:
469-
return self.token_cache.retrieve(TokenKey(host, user, cred_type))
478+
return self.get_token_cache().retrieve(TokenKey(host, user, cred_type))
470479

471480
def read_temporary_credentials(
472481
self,
@@ -500,7 +509,7 @@ def _write_temporary_credential(
500509
"no credential is given when try to store temporary credential"
501510
)
502511
return
503-
self.token_cache.store(TokenKey(host, user, cred_type), cred)
512+
self.get_token_cache().store(TokenKey(host, user, cred_type), cred)
504513

505514
def write_temporary_credentials(
506515
self,
@@ -524,10 +533,15 @@ def write_temporary_credentials(
524533
host, user, TokenType.MFA_TOKEN, response["data"].get("mfaToken")
525534
)
526535

527-
def delete_temporary_credential(
536+
def _delete_temporary_credential(
528537
self, host: str, user: str, cred_type: TokenType
529538
) -> None:
530-
self.token_cache.remove(TokenKey(host, user, cred_type))
539+
self.get_token_cache().remove(TokenKey(host, user, cred_type))
540+
541+
def get_token_cache(self) -> TokenCache:
542+
if self._token_cache is None:
543+
self._token_cache = TokenCache.make()
544+
return self._token_cache
531545

532546

533547
def get_token_from_private_key(

0 commit comments

Comments
 (0)