4
4
5
5
from __future__ import annotations
6
6
7
- import codecs
8
7
import copy
9
8
import json
10
9
import logging
11
- import tempfile
12
- import time
13
10
import uuid
14
11
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
18
13
from typing import TYPE_CHECKING , Any , Callable
19
14
20
15
from cryptography .hazmat .backends import default_backend
26
21
load_pem_private_key ,
27
22
)
28
23
29
- from ..compat import IS_LINUX , IS_MACOS , IS_WINDOWS , urlencode
24
+ from ..compat import urlencode
30
25
from ..constants import (
31
26
DAY_IN_SECONDS ,
32
27
HTTP_HEADER_ACCEPT ,
52
47
ProgrammingError ,
53
48
ServiceUnavailableError ,
54
49
)
55
- from ..file_util import owner_rw_opener
56
50
from ..network import (
57
51
ACCEPT_TYPE_APPLICATION_SNOWFLAKE ,
58
52
CONTENT_TYPE_APPLICATION_JSON ,
59
53
ID_TOKEN_INVALID_LOGIN_REQUEST_GS_CODE ,
60
54
PYTHON_CONNECTOR_USER_AGENT ,
61
55
ReauthenticationRequest ,
62
56
)
63
- from ..options import installed_keyring , keyring
64
57
from ..sqlstate import SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED
58
+ from ..token_cache import TokenCache , TokenKey , TokenType
65
59
from ..version import VERSION
66
60
from .no_auth import AuthNoAuth
67
61
70
64
71
65
logger = logging .getLogger (__name__ )
72
66
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
-
109
67
# keyring
110
68
KEYRING_SERVICE_NAME = "net.snowflake.temporary_token"
111
69
KEYRING_USER = "temp_token"
@@ -132,6 +90,7 @@ class Auth:
132
90
133
91
def __init__ (self , rest ) -> None :
134
92
self ._rest = rest
93
+ self .token_cache = TokenCache .make ()
135
94
136
95
@staticmethod
137
96
def base_auth_data (
@@ -395,7 +354,9 @@ def post_request_wrapper(self, url, headers, body) -> None:
395
354
# clear stored id_token if failed to connect because of id_token
396
355
# raise an exception for reauth without id_token
397
356
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
+ )
399
360
raise ReauthenticationRequest (
400
361
ProgrammingError (
401
362
msg = ret ["message" ],
@@ -417,7 +378,9 @@ def post_request_wrapper(self, url, headers, body) -> None:
417
378
from . import AuthByUsrPwdMfa
418
379
419
380
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
+ )
421
384
Error .errorhandler_wrapper (
422
385
self ._rest ._connection ,
423
386
None ,
@@ -505,36 +468,9 @@ def _read_temporary_credential(
505
468
self ,
506
469
host : str ,
507
470
user : str ,
508
- cred_type : str ,
471
+ cred_type : TokenType ,
509
472
) -> 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 ))
538
474
539
475
def read_temporary_credentials (
540
476
self ,
@@ -546,51 +482,29 @@ def read_temporary_credentials(
546
482
self ._rest .id_token = self ._read_temporary_credential (
547
483
host ,
548
484
user ,
549
- ID_TOKEN ,
485
+ TokenType . ID_TOKEN ,
550
486
)
551
487
552
488
if session_parameters .get (PARAMETER_CLIENT_REQUEST_MFA_TOKEN , False ):
553
489
self ._rest .mfa_token = self ._read_temporary_credential (
554
490
host ,
555
491
user ,
556
- MFA_TOKEN ,
492
+ TokenType . MFA_TOKEN ,
557
493
)
558
494
559
495
def _write_temporary_credential (
560
496
self ,
561
497
host : str ,
562
498
user : str ,
563
- cred_type : str ,
499
+ cred_type : TokenType ,
564
500
cred : str | None ,
565
501
) -> None :
566
502
if not cred :
567
503
logger .debug (
568
504
"no credential is given when try to store temporary credential"
569
505
)
570
506
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 )
594
508
595
509
def write_temporary_credentials (
596
510
self ,
@@ -606,174 +520,18 @@ def write_temporary_credentials(
606
520
)
607
521
):
608
522
self ._write_temporary_credential (
609
- host , user , ID_TOKEN , response ["data" ].get ("idToken" )
523
+ host , user , TokenType . ID_TOKEN , response ["data" ].get ("idToken" )
610
524
)
611
525
612
526
if session_parameters .get (PARAMETER_CLIENT_REQUEST_MFA_TOKEN , False ):
613
527
self ._write_temporary_credential (
614
- host , user , MFA_TOKEN , response ["data" ].get ("mfaToken" )
528
+ host , user , TokenType . MFA_TOKEN , response ["data" ].get ("mfaToken" )
615
529
)
616
530
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 ))
777
535
778
536
779
537
def get_token_from_private_key (
0 commit comments