Skip to content

Commit a574cad

Browse files
Prep 2.2.3 (#294)
* updated Python Connector reqs * SNOW-102876 secure sso python copy * SNOW-141822 bumped pandas to newest versions * SNOW-141822 bumped pandas to newest versions * SNOW-141932 build manylinux1 wheels * SNOW-118103 fix unclosed file issue * SNOW-144663 added missing test directories to tox commans * SNOW-145906 update python docs * SNOW-143923 tox housekeeping * SNOW-146266 updated Python test * SNOW-146266 fix import ordering * SNOW-145814 wrongly default keyring package * SNOW-146213 Add google storage api url to whitelist for ocsp validation * SNOW-67159 update column size python connector * SNOW-145814 fix mac sso unit test with mock * SNOW-83085 use_openssl_only mode for Python connector * SNOW-147687 in band telemetry update python * SNOW-144043: Add new optional config client_store_temporary_credential into SnowSQL and made it the same in python connector * SNOW-144043 fix lint error * SNOW-148015 Added type checking workaround for Python 3.5.1 * SNOW-121925 Adding a test to verify that the Python connector supports dashed URLs * Revert SNOW-121925 Adding a test to verify that the Python connector supports dashed URLs * python connector version bump to 2.2.3 * skip new sso tests on Travis * reenabled pandas tests
1 parent fbd29c0 commit a574cad

35 files changed

+550
-879
lines changed

DESCRIPTION.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
99
Release Notes
1010
-------------------------------------------------------------------------------
1111

12+
- v2.2.3(March 30,2020)
13+
14+
- Secure SSO ID Token
15+
- Add use_openssl_only connection parameter, which disables the usage of pure Python cryptographic libraries for FIPS
16+
- Add manylinux1 as well as manylinux2010
17+
- Fix a bug where a certificate file was opened and never closed in snowflake-connector-python.
18+
- Fix python connector skips validating GCP URLs
19+
- Adds additional client driver config information to in band telemetry.
20+
1221
- v2.2.2(March 9,2020)
1322

1423
- Fix retry with chunck_downloader.py for stability.

auth.py

Lines changed: 128 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import copy
99
import json
1010
import logging
11-
import platform
1211
import tempfile
1312
import time
1413
import uuid
@@ -18,18 +17,24 @@
1817
from threading import Lock, Thread
1918

2019
from .auth_keypair import AuthByKeyPair
21-
from .compat import IS_LINUX, TO_UNICODE, urlencode
20+
from .compat import IS_LINUX, IS_WINDOWS, IS_MACOS, TO_UNICODE, urlencode
2221
from .constants import (
2322
HTTP_HEADER_ACCEPT,
2423
HTTP_HEADER_CONTENT_TYPE,
2524
HTTP_HEADER_SERVICE_NAME,
2625
HTTP_HEADER_USER_AGENT,
2726
PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL,
28-
PARAMETER_CLIENT_USE_SECURE_STORAGE_FOR_TEMPORARY_CREDENTIAL,
2927
)
3028
from .description import COMPILER, IMPLEMENTATION, OPERATING_SYSTEM, PLATFORM, PYTHON_VERSION
3129
from .errorcode import ER_FAILED_TO_CONNECT_TO_DB
32-
from .errors import BadGatewayError, DatabaseError, Error, ForbiddenError, ServiceUnavailableError
30+
from .errors import (
31+
BadGatewayError,
32+
DatabaseError,
33+
Error,
34+
ForbiddenError,
35+
ServiceUnavailableError,
36+
MissingDependencyError,
37+
)
3338
from .network import (
3439
ACCEPT_TYPE_APPLICATION_SNOWFLAKE,
3540
CONTENT_TYPE_APPLICATION_JSON,
@@ -41,13 +46,19 @@
4146

4247
logger = logging.getLogger(__name__)
4348

49+
try:
50+
import keyring
51+
except ImportError as ie:
52+
keyring = None
53+
logger.debug('Failed to import keyring module. err=[%s]', ie)
54+
4455
# Cache directory
4556
CACHE_ROOT_DIR = getenv('SF_TEMPORARY_CREDENTIAL_CACHE_DIR') or \
4657
expanduser("~") or tempfile.gettempdir()
47-
if platform.system() == 'Windows':
58+
if IS_WINDOWS:
4859
CACHE_DIR = path.join(CACHE_ROOT_DIR, 'AppData', 'Local', 'Snowflake',
4960
'Caches')
50-
elif platform.system() == 'Darwin':
61+
elif IS_MACOS:
5162
CACHE_DIR = path.join(CACHE_ROOT_DIR, 'Library', 'Caches', 'Snowflake')
5263
else:
5364
CACHE_DIR = path.join(CACHE_ROOT_DIR, '.cache', 'snowflake')
@@ -77,6 +88,7 @@
7788
# keyring
7889
KEYRING_SERVICE_NAME = "net.snowflake.temporary_token"
7990
KEYRING_USER = "temp_token"
91+
KEYRING_DRIVER_NAME = "SNOWFLAKE-PYTHON-DRIVER"
8092

8193

8294
class Auth(object):
@@ -91,22 +103,28 @@ def __init__(self, rest):
91103
def base_auth_data(user, account, application,
92104
internal_application_name,
93105
internal_application_version,
94-
ocsp_mode):
106+
ocsp_mode, login_timeout,
107+
network_timeout=None,
108+
store_temp_cred=None):
95109
return {
96-
u'data': {
97-
u"CLIENT_APP_ID": internal_application_name,
98-
u"CLIENT_APP_VERSION": internal_application_version,
99-
u"SVN_REVISION": VERSION[3],
100-
u"ACCOUNT_NAME": account,
101-
u"LOGIN_NAME": user,
102-
u"CLIENT_ENVIRONMENT": {
103-
u"APPLICATION": application,
104-
u"OS": OPERATING_SYSTEM,
105-
u"OS_VERSION": PLATFORM,
106-
u"PYTHON_VERSION": PYTHON_VERSION,
107-
u"PYTHON_RUNTIME": IMPLEMENTATION,
108-
u"PYTHON_COMPILER": COMPILER,
109-
u"OCSP_MODE": ocsp_mode.name,
110+
'data': {
111+
"CLIENT_APP_ID": internal_application_name,
112+
"CLIENT_APP_VERSION": internal_application_version,
113+
"SVN_REVISION": VERSION[3],
114+
"ACCOUNT_NAME": account,
115+
"LOGIN_NAME": user,
116+
"CLIENT_ENVIRONMENT": {
117+
"APPLICATION": application,
118+
"OS": OPERATING_SYSTEM,
119+
"OS_VERSION": PLATFORM,
120+
"PYTHON_VERSION": PYTHON_VERSION,
121+
"PYTHON_RUNTIME": IMPLEMENTATION,
122+
"PYTHON_COMPILER": COMPILER,
123+
"OCSP_MODE": ocsp_mode.name,
124+
"TRACING": logger.getEffectiveLevel(),
125+
"LOGIN_TIMEOUT": login_timeout,
126+
"NETWORK_TIMEOUT": network_timeout,
127+
"CLIENT_STORE_TEMPORARY_CREDENTIAL": store_temp_cred,
110128
}
111129
},
112130
}
@@ -132,11 +150,22 @@ def authenticate(self, auth_instance, account, user,
132150
headers[HTTP_HEADER_SERVICE_NAME] = \
133151
session_parameters[HTTP_HEADER_SERVICE_NAME]
134152
url = u"/session/v1/login-request"
153+
if session_parameters is not None \
154+
and PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL in session_parameters:
155+
store_temp_cred = session_parameters[
156+
PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL]
157+
else:
158+
store_temp_cred = None
159+
135160
body_template = Auth.base_auth_data(
136161
user, account, self._rest._connection.application,
137162
self._rest._connection._internal_application_name,
138163
self._rest._connection._internal_application_version,
139-
self._rest._connection._ocsp_mode())
164+
self._rest._connection._ocsp_mode(),
165+
self._rest._connection._login_timeout,
166+
self._rest._connection._network_timeout,
167+
store_temp_cred,
168+
)
140169

141170
body = copy.deepcopy(body_template)
142171
# updating request body
@@ -317,10 +346,10 @@ def post_request_wrapper(self, url, headers, body):
317346
id_token=ret[u'data'].get(u'idToken')
318347
)
319348
if self._rest._connection.consent_cache_id_token:
320-
write_temporary_credential_file(
321-
account, user, self._rest.id_token,
349+
write_temporary_credential(
350+
self._rest._host, account, user, self._rest.id_token,
322351
session_parameters.get(
323-
PARAMETER_CLIENT_USE_SECURE_STORAGE_FOR_TEMPORARY_CREDENTIAL))
352+
PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL))
324353
if u'sessionId' in ret[u'data']:
325354
self._rest._connection._session_id = ret[u'data'][u'sessionId']
326355
if u'sessionInfo' in ret[u'data']:
@@ -333,17 +362,26 @@ def post_request_wrapper(self, url, headers, body):
333362

334363
return session_parameters
335364

336-
def read_temporary_credential(self, account, user, session_parameters):
337-
if session_parameters.get(PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL):
338-
read_temporary_credential_file(
339-
session_parameters.get(
340-
PARAMETER_CLIENT_USE_SECURE_STORAGE_FOR_TEMPORARY_CREDENTIAL)
341-
)
342-
id_token = TEMPORARY_CREDENTIAL.get(
343-
account.upper(), {}).get(user.upper())
365+
def read_temporary_credential(self, host, account, user, session_parameters):
366+
if session_parameters.get(PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL, False):
367+
id_token = None
368+
if IS_MACOS or IS_WINDOWS:
369+
if not keyring:
370+
# we will leave the exception for write_temporary_credential function to raise
371+
return False
372+
new_target = convert_target(host, user)
373+
try:
374+
id_token = keyring.get_password(new_target, user.upper())
375+
except keyring.errors.KeyringError as ke:
376+
logger.debug("Could not retrieve id_token from secure storage : {}".format(str(ke)))
377+
elif IS_LINUX:
378+
read_temporary_credential_file()
379+
id_token = TEMPORARY_CREDENTIAL.get(
380+
account.upper(), {}).get(user.upper())
381+
else:
382+
logger.debug("connection parameter enable_sso_temporary_credential not set or OS not support")
344383
if id_token:
345384
self._rest.id_token = id_token
346-
if self._rest.id_token:
347385
try:
348386
self._rest._id_token_session()
349387
return True
@@ -354,11 +392,31 @@ def read_temporary_credential(self, account, user, session_parameters):
354392
return False
355393

356394

357-
def write_temporary_credential_file(
358-
account, user, id_token,
359-
use_secure_storage_for_temporary_credential=False):
360-
if not CACHE_DIR or not id_token:
361-
# no cache is enabled or no id_token is given
395+
def write_temporary_credential(host, account, user, id_token, store_temporary_credential=False):
396+
if not id_token:
397+
logger.debug("no ID token is given when try to store temporary credential")
398+
return
399+
if IS_MACOS or IS_WINDOWS:
400+
if not keyring:
401+
raise MissingDependencyError("Please install keyring module to enable SSO token cache feature.")
402+
403+
new_target = convert_target(host, user)
404+
try:
405+
keyring.set_password(new_target, user.upper(), id_token)
406+
except keyring.errors.KeyringError as ke:
407+
logger.debug("Could not store id_token to keyring, %s", str(ke))
408+
elif IS_LINUX and store_temporary_credential:
409+
write_temporary_credential_file(host, account, user, id_token)
410+
else:
411+
logger.debug("connection parameter client_store_temporary_credential not set or OS not support")
412+
413+
414+
def write_temporary_credential_file(host, account, user, id_token):
415+
"""
416+
Write temporary credential file when OS is Linux
417+
"""
418+
if not CACHE_DIR:
419+
# no cache is enabled
362420
return
363421
global TEMPORARY_CREDENTIAL
364422
global TEMPORARY_CREDENTIAL_LOCK
@@ -377,27 +435,19 @@ def write_temporary_credential_file(
377435
"write the temporary credential file: %s",
378436
TEMPORARY_CREDENTIAL_FILE)
379437
try:
380-
if IS_LINUX or not use_secure_storage_for_temporary_credential:
381-
with codecs.open(TEMPORARY_CREDENTIAL_FILE, 'w',
382-
encoding='utf-8', errors='ignore') as f:
383-
json.dump(TEMPORARY_CREDENTIAL, f)
384-
else:
385-
import keyring
386-
keyring.set_password(
387-
KEYRING_SERVICE_NAME, KEYRING_USER,
388-
json.dumps(TEMPORARY_CREDENTIAL))
389-
438+
with codecs.open(TEMPORARY_CREDENTIAL_FILE, 'w',
439+
encoding='utf-8', errors='ignore') as f:
440+
json.dump(TEMPORARY_CREDENTIAL, f)
390441
except Exception as ex:
391442
logger.debug("Failed to write a credential file: "
392443
"file=[%s], err=[%s]", TEMPORARY_CREDENTIAL_FILE, ex)
393444
finally:
394445
unlock_temporary_credential_file()
395446

396447

397-
def read_temporary_credential_file(
398-
use_secure_storage_for_temporary_credential=False):
448+
def read_temporary_credential_file():
399449
"""
400-
Read temporary credential file
450+
Read temporary credential file when OS is Linux
401451
"""
402452
if not CACHE_DIR:
403453
# no cache is enabled
@@ -416,15 +466,9 @@ def read_temporary_credential_file(
416466
"write the temporary credential file: %s",
417467
TEMPORARY_CREDENTIAL_FILE)
418468
try:
419-
if IS_LINUX or not use_secure_storage_for_temporary_credential:
420-
with codecs.open(TEMPORARY_CREDENTIAL_FILE, 'r',
421-
encoding='utf-8', errors='ignore') as f:
422-
TEMPORARY_CREDENTIAL = json.load(f)
423-
else:
424-
import keyring
425-
f = keyring.get_password(
426-
KEYRING_SERVICE_NAME, KEYRING_USER) or "{}"
427-
TEMPORARY_CREDENTIAL = json.loads(f)
469+
with codecs.open(TEMPORARY_CREDENTIAL_FILE, 'r',
470+
encoding='utf-8', errors='ignore') as f:
471+
TEMPORARY_CREDENTIAL = json.load(f)
428472
return TEMPORARY_CREDENTIAL
429473
except Exception as ex:
430474
logger.debug("Failed to read a credential file. The file may not"
@@ -456,26 +500,34 @@ def unlock_temporary_credential_file():
456500
return False
457501

458502

459-
def delete_temporary_credential_file(
460-
use_secure_storage_for_temporary_credential=False):
461-
"""
462-
Delete temporary credential file and its lock file
463-
"""
464-
global TEMPORARY_CREDENTIAL_FILE
465-
if IS_LINUX or not use_secure_storage_for_temporary_credential:
503+
def delete_temporary_credential(host, user, store_temporary_credential=False):
504+
if (IS_MACOS or IS_WINDOWS) and keyring:
505+
new_target = convert_target(host, user)
466506
try:
467-
remove(TEMPORARY_CREDENTIAL_FILE)
468-
except Exception as ex:
469-
logger.debug("Failed to delete a credential file: "
470-
"file=[%s], err=[%s]", TEMPORARY_CREDENTIAL_FILE, ex)
471-
else:
472-
try:
473-
import keyring
474-
keyring.delete_password(KEYRING_SERVICE_NAME, KEYRING_USER)
507+
keyring.delete_password(new_target, user.upper())
475508
except Exception as ex:
476509
logger.debug("Failed to delete credential in the keyring: err=[%s]",
477510
ex)
511+
elif IS_LINUX and store_temporary_credential:
512+
delete_temporary_credential_file()
513+
514+
515+
def delete_temporary_credential_file():
516+
"""
517+
Delete temporary credential file and its lock file
518+
"""
519+
global TEMPORARY_CREDENTIAL_FILE
520+
try:
521+
remove(TEMPORARY_CREDENTIAL_FILE)
522+
except Exception as ex:
523+
logger.debug("Failed to delete a credential file: "
524+
"file=[%s], err=[%s]", TEMPORARY_CREDENTIAL_FILE, ex)
478525
try:
479526
removedirs(TEMPORARY_CREDENTIAL_FILE_LOCK)
480527
except Exception as ex:
481528
logger.debug("Failed to delete credential lock file: err=[%s]", ex)
529+
530+
531+
def convert_target(host, user):
532+
return "{host}:{user}:{driver}".format(
533+
host=host.upper(), user=user.upper(), driver=KEYRING_DRIVER_NAME)

auth_okta.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from .auth import Auth
1010
from .auth_by_plugin import AuthByPlugin
1111
from .compat import unescape, urlencode, urlsplit
12-
from .constants import HTTP_HEADER_ACCEPT, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_SERVICE_NAME, HTTP_HEADER_USER_AGENT
12+
from .constants import HTTP_HEADER_ACCEPT, HTTP_HEADER_CONTENT_TYPE, \
13+
HTTP_HEADER_SERVICE_NAME, HTTP_HEADER_USER_AGENT
1314
from .errorcode import ER_IDP_CONNECTION_ERROR, ER_INCORRECT_DESTINATION
1415
from .errors import DatabaseError, Error
1516
from .network import CONTENT_TYPE_APPLICATION_JSON, PYTHON_CONNECTOR_USER_AGENT
@@ -121,7 +122,10 @@ def _step1(self, authenticator, service_name, account, user):
121122
self._rest._connection.application,
122123
self._rest._connection._internal_application_name,
123124
self._rest._connection._internal_application_version,
124-
self._rest._connection._ocsp_mode())
125+
self._rest._connection._ocsp_mode(),
126+
self._rest._connection._login_timeout,
127+
self._rest._connection._network_timeout,
128+
)
125129

126130
body[u"data"][u"AUTHENTICATOR"] = authenticator
127131
logger.debug(

auth_webbrowser.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from .auth import Auth
1313
from .auth_by_plugin import AuthByPlugin
1414
from .compat import parse_qs, urlparse, urlsplit
15-
from .constants import HTTP_HEADER_ACCEPT, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_SERVICE_NAME, HTTP_HEADER_USER_AGENT
15+
from .constants import HTTP_HEADER_ACCEPT, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_SERVICE_NAME, \
16+
HTTP_HEADER_USER_AGENT
1617
from .errorcode import ER_IDP_CONNECTION_ERROR, ER_NO_HOSTNAME_FOUND, ER_UNABLE_TO_OPEN_BROWSER
1718
from .errors import OperationalError
1819
from .network import CONTENT_TYPE_APPLICATION_JSON, EXTERNAL_BROWSER_AUTHENTICATOR, PYTHON_CONNECTOR_USER_AGENT
@@ -291,7 +292,10 @@ def _get_sso_url(
291292
self._rest._connection.application,
292293
self._rest._connection._internal_application_name,
293294
self._rest._connection._internal_application_version,
294-
self._rest._connection._ocsp_mode())
295+
self._rest._connection._ocsp_mode(),
296+
self._rest._connection._login_timeout,
297+
self._rest._connection._network_timeout,
298+
)
295299

296300
body[u'data'][u'AUTHENTICATOR'] = authenticator
297301
body[u'data'][u"BROWSER_MODE_REDIRECT_PORT"] = str(callback_port)

compat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
IS_LINUX = platform.system() == 'Linux'
1919
IS_WINDOWS = platform.system() == 'Windows'
20+
IS_MACOS = platform.system() == 'Darwin'
2021

2122
NUM_DATA_TYPES = []
2223
try:

0 commit comments

Comments
 (0)