Skip to content

Commit bd29ab3

Browse files
SNOW-1825624: Refactor token cache before applying security changes (#2210)
1 parent 9b5c983 commit bd29ab3

File tree

5 files changed

+389
-340
lines changed

5 files changed

+389
-340
lines changed

src/snowflake/connector/auth/_auth.py

Lines changed: 22 additions & 264 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,12 @@
44

55
from __future__ import annotations
66

7-
import codecs
87
import copy
98
import json
109
import logging
11-
import tempfile
12-
import time
1310
import uuid
1411
from datetime import datetime, timezone
15-
from os import getenv, makedirs, mkdir, path, remove, removedirs, rmdir
16-
from os.path import expanduser
17-
from threading import Lock, Thread
12+
from threading import Thread
1813
from typing import TYPE_CHECKING, Any, Callable
1914

2015
from cryptography.hazmat.backends import default_backend
@@ -26,7 +21,7 @@
2621
load_pem_private_key,
2722
)
2823

29-
from ..compat import IS_LINUX, IS_MACOS, IS_WINDOWS, urlencode
24+
from ..compat import urlencode
3025
from ..constants import (
3126
DAY_IN_SECONDS,
3227
HTTP_HEADER_ACCEPT,
@@ -52,16 +47,15 @@
5247
ProgrammingError,
5348
ServiceUnavailableError,
5449
)
55-
from ..file_util import owner_rw_opener
5650
from ..network import (
5751
ACCEPT_TYPE_APPLICATION_SNOWFLAKE,
5852
CONTENT_TYPE_APPLICATION_JSON,
5953
ID_TOKEN_INVALID_LOGIN_REQUEST_GS_CODE,
6054
PYTHON_CONNECTOR_USER_AGENT,
6155
ReauthenticationRequest,
6256
)
63-
from ..options import installed_keyring, keyring
6457
from ..sqlstate import SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED
58+
from ..token_cache import TokenCache, TokenKey, TokenType
6559
from ..version import VERSION
6660
from .no_auth import AuthNoAuth
6761

@@ -70,42 +64,6 @@
7064

7165
logger = logging.getLogger(__name__)
7266

73-
74-
# Cache directory
75-
CACHE_ROOT_DIR = (
76-
getenv("SF_TEMPORARY_CREDENTIAL_CACHE_DIR")
77-
or expanduser("~")
78-
or tempfile.gettempdir()
79-
)
80-
if IS_WINDOWS:
81-
CACHE_DIR = path.join(CACHE_ROOT_DIR, "AppData", "Local", "Snowflake", "Caches")
82-
elif IS_MACOS:
83-
CACHE_DIR = path.join(CACHE_ROOT_DIR, "Library", "Caches", "Snowflake")
84-
else:
85-
CACHE_DIR = path.join(CACHE_ROOT_DIR, ".cache", "snowflake")
86-
87-
if not path.exists(CACHE_DIR):
88-
try:
89-
makedirs(CACHE_DIR, mode=0o700)
90-
except Exception as ex:
91-
logger.debug("cannot create a cache directory: [%s], err=[%s]", CACHE_DIR, ex)
92-
CACHE_DIR = None
93-
logger.debug("cache directory: %s", CACHE_DIR)
94-
95-
# temporary credential cache
96-
TEMPORARY_CREDENTIAL: dict[str, dict[str, str | None]] = {}
97-
98-
TEMPORARY_CREDENTIAL_LOCK = Lock()
99-
100-
# temporary credential cache file name
101-
TEMPORARY_CREDENTIAL_FILE = "temporary_credential.json"
102-
TEMPORARY_CREDENTIAL_FILE = (
103-
path.join(CACHE_DIR, TEMPORARY_CREDENTIAL_FILE) if CACHE_DIR else ""
104-
)
105-
106-
# temporary credential cache lock directory name
107-
TEMPORARY_CREDENTIAL_FILE_LOCK = TEMPORARY_CREDENTIAL_FILE + ".lck"
108-
10967
# keyring
11068
KEYRING_SERVICE_NAME = "net.snowflake.temporary_token"
11169
KEYRING_USER = "temp_token"
@@ -132,6 +90,7 @@ class Auth:
13290

13391
def __init__(self, rest) -> None:
13492
self._rest = rest
93+
self.token_cache = TokenCache.make()
13594

13695
@staticmethod
13796
def base_auth_data(
@@ -395,7 +354,9 @@ def post_request_wrapper(self, url, headers, body) -> None:
395354
# clear stored id_token if failed to connect because of id_token
396355
# raise an exception for reauth without id_token
397356
self._rest.id_token = None
398-
delete_temporary_credential(self._rest._host, user, ID_TOKEN)
357+
self.delete_temporary_credential(
358+
self._rest._host, user, TokenType.ID_TOKEN
359+
)
399360
raise ReauthenticationRequest(
400361
ProgrammingError(
401362
msg=ret["message"],
@@ -417,7 +378,9 @@ def post_request_wrapper(self, url, headers, body) -> None:
417378
from . import AuthByUsrPwdMfa
418379

419380
if isinstance(auth_instance, AuthByUsrPwdMfa):
420-
delete_temporary_credential(self._rest._host, user, MFA_TOKEN)
381+
self.delete_temporary_credential(
382+
self._rest._host, user, TokenType.MFA_TOKEN
383+
)
421384
Error.errorhandler_wrapper(
422385
self._rest._connection,
423386
None,
@@ -505,36 +468,9 @@ def _read_temporary_credential(
505468
self,
506469
host: str,
507470
user: str,
508-
cred_type: str,
471+
cred_type: TokenType,
509472
) -> str | None:
510-
cred = None
511-
if IS_MACOS or IS_WINDOWS:
512-
if not installed_keyring:
513-
logger.debug(
514-
"Dependency 'keyring' is not installed, cannot cache id token. You might experience "
515-
"multiple authentication pop ups while using ExternalBrowser Authenticator. To avoid "
516-
"this please install keyring module using the following command : pip install "
517-
"snowflake-connector-python[secure-local-storage]"
518-
)
519-
return None
520-
try:
521-
cred = keyring.get_password(
522-
build_temporary_credential_name(host, user, cred_type), user.upper()
523-
)
524-
except keyring.errors.KeyringError as ke:
525-
logger.error(
526-
"Could not retrieve {} from secure storage : {}".format(
527-
cred_type, str(ke)
528-
)
529-
)
530-
elif IS_LINUX:
531-
read_temporary_credential_file()
532-
cred = TEMPORARY_CREDENTIAL.get(host.upper(), {}).get(
533-
build_temporary_credential_name(host, user, cred_type)
534-
)
535-
else:
536-
logger.debug("OS not supported for Local Secure Storage")
537-
return cred
473+
return self.token_cache.retrieve(TokenKey(host, user, cred_type))
538474

539475
def read_temporary_credentials(
540476
self,
@@ -546,51 +482,29 @@ def read_temporary_credentials(
546482
self._rest.id_token = self._read_temporary_credential(
547483
host,
548484
user,
549-
ID_TOKEN,
485+
TokenType.ID_TOKEN,
550486
)
551487

552488
if session_parameters.get(PARAMETER_CLIENT_REQUEST_MFA_TOKEN, False):
553489
self._rest.mfa_token = self._read_temporary_credential(
554490
host,
555491
user,
556-
MFA_TOKEN,
492+
TokenType.MFA_TOKEN,
557493
)
558494

559495
def _write_temporary_credential(
560496
self,
561497
host: str,
562498
user: str,
563-
cred_type: str,
499+
cred_type: TokenType,
564500
cred: str | None,
565501
) -> None:
566502
if not cred:
567503
logger.debug(
568504
"no credential is given when try to store temporary credential"
569505
)
570506
return
571-
if IS_MACOS or IS_WINDOWS:
572-
if not installed_keyring:
573-
logger.debug(
574-
"Dependency 'keyring' is not installed, cannot cache id token. You might experience "
575-
"multiple authentication pop ups while using ExternalBrowser Authenticator. To avoid "
576-
"this please install keyring module using the following command : pip install "
577-
"snowflake-connector-python[secure-local-storage]"
578-
)
579-
return
580-
try:
581-
keyring.set_password(
582-
build_temporary_credential_name(host, user, cred_type),
583-
user.upper(),
584-
cred,
585-
)
586-
except keyring.errors.KeyringError as ke:
587-
logger.error("Could not store id_token to keyring, %s", str(ke))
588-
elif IS_LINUX:
589-
write_temporary_credential_file(
590-
host, build_temporary_credential_name(host, user, cred_type), cred
591-
)
592-
else:
593-
logger.debug("OS not supported for Local Secure Storage")
507+
self.token_cache.store(TokenKey(host, user, cred_type), cred)
594508

595509
def write_temporary_credentials(
596510
self,
@@ -606,174 +520,18 @@ def write_temporary_credentials(
606520
)
607521
):
608522
self._write_temporary_credential(
609-
host, user, ID_TOKEN, response["data"].get("idToken")
523+
host, user, TokenType.ID_TOKEN, response["data"].get("idToken")
610524
)
611525

612526
if session_parameters.get(PARAMETER_CLIENT_REQUEST_MFA_TOKEN, False):
613527
self._write_temporary_credential(
614-
host, user, MFA_TOKEN, response["data"].get("mfaToken")
528+
host, user, TokenType.MFA_TOKEN, response["data"].get("mfaToken")
615529
)
616530

617-
618-
def flush_temporary_credentials() -> None:
619-
"""Flush temporary credentials in memory into disk. Need to hold TEMPORARY_CREDENTIAL_LOCK."""
620-
global TEMPORARY_CREDENTIAL
621-
global TEMPORARY_CREDENTIAL_FILE
622-
for _ in range(10):
623-
if lock_temporary_credential_file():
624-
break
625-
time.sleep(1)
626-
else:
627-
logger.debug(
628-
"The lock file still persists after the maximum wait time."
629-
"Will ignore it and write temporary credential file: %s",
630-
TEMPORARY_CREDENTIAL_FILE,
631-
)
632-
try:
633-
with open(
634-
TEMPORARY_CREDENTIAL_FILE,
635-
"w",
636-
encoding="utf-8",
637-
errors="ignore",
638-
opener=owner_rw_opener,
639-
) as f:
640-
json.dump(TEMPORARY_CREDENTIAL, f)
641-
except Exception as ex:
642-
logger.debug(
643-
"Failed to write a credential file: " "file=[%s], err=[%s]",
644-
TEMPORARY_CREDENTIAL_FILE,
645-
ex,
646-
)
647-
finally:
648-
unlock_temporary_credential_file()
649-
650-
651-
def write_temporary_credential_file(host: str, cred_name: str, cred) -> None:
652-
"""Writes temporary credential file when OS is Linux."""
653-
if not CACHE_DIR:
654-
# no cache is enabled
655-
return
656-
global TEMPORARY_CREDENTIAL
657-
global TEMPORARY_CREDENTIAL_LOCK
658-
with TEMPORARY_CREDENTIAL_LOCK:
659-
# update the cache
660-
host_data = TEMPORARY_CREDENTIAL.get(host.upper(), {})
661-
host_data[cred_name.upper()] = cred
662-
TEMPORARY_CREDENTIAL[host.upper()] = host_data
663-
flush_temporary_credentials()
664-
665-
666-
def read_temporary_credential_file():
667-
"""Reads temporary credential file when OS is Linux."""
668-
if not CACHE_DIR:
669-
# no cache is enabled
670-
return
671-
672-
global TEMPORARY_CREDENTIAL
673-
global TEMPORARY_CREDENTIAL_LOCK
674-
global TEMPORARY_CREDENTIAL_FILE
675-
with TEMPORARY_CREDENTIAL_LOCK:
676-
for _ in range(10):
677-
if lock_temporary_credential_file():
678-
break
679-
time.sleep(1)
680-
else:
681-
logger.debug(
682-
"The lock file still persists. Will ignore and "
683-
"write the temporary credential file: %s",
684-
TEMPORARY_CREDENTIAL_FILE,
685-
)
686-
try:
687-
with codecs.open(
688-
TEMPORARY_CREDENTIAL_FILE, "r", encoding="utf-8", errors="ignore"
689-
) as f:
690-
TEMPORARY_CREDENTIAL = json.load(f)
691-
return TEMPORARY_CREDENTIAL
692-
except Exception as ex:
693-
logger.debug(
694-
"Failed to read a credential file. The file may not"
695-
"exists: file=[%s], err=[%s]",
696-
TEMPORARY_CREDENTIAL_FILE,
697-
ex,
698-
)
699-
finally:
700-
unlock_temporary_credential_file()
701-
702-
703-
def lock_temporary_credential_file() -> bool:
704-
global TEMPORARY_CREDENTIAL_FILE_LOCK
705-
try:
706-
mkdir(TEMPORARY_CREDENTIAL_FILE_LOCK)
707-
return True
708-
except OSError:
709-
logger.debug(
710-
"Temporary cache file lock already exists. Other "
711-
"process may be updating the temporary "
712-
)
713-
return False
714-
715-
716-
def unlock_temporary_credential_file() -> bool:
717-
global TEMPORARY_CREDENTIAL_FILE_LOCK
718-
try:
719-
rmdir(TEMPORARY_CREDENTIAL_FILE_LOCK)
720-
return True
721-
except OSError:
722-
logger.debug("Temporary cache file lock no longer exists.")
723-
return False
724-
725-
726-
def delete_temporary_credential(host, user, cred_type) -> None:
727-
if (IS_MACOS or IS_WINDOWS) and installed_keyring:
728-
try:
729-
keyring.delete_password(
730-
build_temporary_credential_name(host, user, cred_type), user.upper()
731-
)
732-
except Exception as ex:
733-
logger.error("Failed to delete credential in the keyring: err=[%s]", ex)
734-
elif IS_LINUX:
735-
temporary_credential_file_delete_password(host, user, cred_type)
736-
737-
738-
def temporary_credential_file_delete_password(host, user, cred_type) -> None:
739-
"""Remove credential from temporary credential file when OS is Linux."""
740-
if not CACHE_DIR:
741-
# no cache is enabled
742-
return
743-
global TEMPORARY_CREDENTIAL
744-
global TEMPORARY_CREDENTIAL_LOCK
745-
with TEMPORARY_CREDENTIAL_LOCK:
746-
# update the cache
747-
host_data = TEMPORARY_CREDENTIAL.get(host.upper(), {})
748-
host_data.pop(build_temporary_credential_name(host, user, cred_type), None)
749-
if not host_data:
750-
TEMPORARY_CREDENTIAL.pop(host.upper(), None)
751-
else:
752-
TEMPORARY_CREDENTIAL[host.upper()] = host_data
753-
flush_temporary_credentials()
754-
755-
756-
def delete_temporary_credential_file() -> None:
757-
"""Deletes temporary credential file and its lock file."""
758-
global TEMPORARY_CREDENTIAL_FILE
759-
try:
760-
remove(TEMPORARY_CREDENTIAL_FILE)
761-
except Exception as ex:
762-
logger.debug(
763-
"Failed to delete a credential file: " "file=[%s], err=[%s]",
764-
TEMPORARY_CREDENTIAL_FILE,
765-
ex,
766-
)
767-
try:
768-
removedirs(TEMPORARY_CREDENTIAL_FILE_LOCK)
769-
except Exception as ex:
770-
logger.debug("Failed to delete credential lock file: err=[%s]", ex)
771-
772-
773-
def build_temporary_credential_name(host, user, cred_type) -> str:
774-
return "{host}:{user}:{driver}:{cred}".format(
775-
host=host.upper(), user=user.upper(), driver=KEYRING_DRIVER_NAME, cred=cred_type
776-
)
531+
def delete_temporary_credential(
532+
self, host: str, user: str, cred_type: TokenType
533+
) -> None:
534+
self.token_cache.remove(TokenKey(host, user, cred_type))
777535

778536

779537
def get_token_from_private_key(

0 commit comments

Comments
 (0)