????
Current Path : C:/opt/pgsql/pgAdmin 4/python/Lib/site-packages/msal_extensions/ |
Current File : C:/opt/pgsql/pgAdmin 4/python/Lib/site-packages/msal_extensions/persistence.py |
"""A generic persistence layer, optionally encrypted on Windows, OSX, and Linux. Should a certain encryption is unavailable, exception will be raised at run-time, rather than at import time. By successfully creating and using a certain persistence object, app developer would naturally know whether the data are protected by encryption. """ import abc import os import errno import hashlib import logging import sys try: from pathlib import Path # Built-in in Python 3 except ImportError: from pathlib2 import Path # An extra lib for Python 2 try: ABC = abc.ABC except AttributeError: # Python 2.7, abc exists, but not ABC ABC = abc.ABCMeta("ABC", (object,), {"__slots__": ()}) # type: ignore logger = logging.getLogger(__name__) def _mkdir_p(path): """Creates a directory, and any necessary parents. If the path provided is an existing file, this function raises an exception. :param path: The directory name that should be created. """ if not path: return # NO-OP if sys.version_info >= (3, 2): os.makedirs(path, exist_ok=True) return # This fallback implementation is based on a Stack Overflow question: # https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python # Known issue: it won't work when the path is a root folder like "C:\\" try: os.makedirs(path) except OSError as exp: if exp.errno == errno.EEXIST and os.path.isdir(path): pass else: raise def _auto_hash(input_string): return hashlib.sha256(input_string.encode('utf-8')).hexdigest() # We do not aim to wrap every os-specific exception. # Here we standardize only the most common ones, # otherwise caller would need to catch os-specific underlying exceptions. class PersistenceError(IOError): # Use IOError rather than OSError as base, """The base exception for persistence.""" # because historically an IOError was bubbled up and expected. # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/0.2.2/msal_extensions/token_cache.py#L38 # Now we want to maintain backward compatibility even when using Python 2.x # It makes no difference in Python 3.3+ where IOError is an alias of OSError. def __init__(self, err_no=None, message=None, location=None): # pylint: disable=useless-super-delegation super(PersistenceError, self).__init__(err_no, message, location) class PersistenceNotFound(PersistenceError): """This happens when attempting BasePersistence.load() on a non-existent persistence instance""" def __init__(self, err_no=None, message=None, location=None): super(PersistenceNotFound, self).__init__( err_no=errno.ENOENT, message=message or "Persistence not found", location=location) class PersistenceEncryptionError(PersistenceError): """This could be raised by persistence.save()""" class PersistenceDecryptionError(PersistenceError): """This could be raised by persistence.load()""" def build_encrypted_persistence(location): """Build a suitable encrypted persistence instance based your current OS. If you do not need encryption, then simply use ``FilePersistence`` constructor. """ # Does not (yet?) support fallback_to_plaintext flag, # because the persistence on Windows and macOS do not support built-in trial_run(). if sys.platform.startswith('win'): return FilePersistenceWithDataProtection(location) if sys.platform.startswith('darwin'): return KeychainPersistence(location) if sys.platform.startswith('linux'): return LibsecretPersistence(location) raise RuntimeError("Unsupported platform: {}".format(sys.platform)) # pylint: disable=consider-using-f-string class BasePersistence(ABC): """An abstract persistence defining the common interface of this family""" is_encrypted = False # Default to False. To be overridden by sub-classes. @abc.abstractmethod def save(self, content): # type: (str) -> None """Save the content into this persistence""" raise NotImplementedError @abc.abstractmethod def load(self): # type: () -> str """Load content from this persistence. Could raise PersistenceNotFound if no save() was called before. """ raise NotImplementedError @abc.abstractmethod def time_last_modified(self): """Get the last time when this persistence has been modified. Could raise PersistenceNotFound if no save() was called before. """ raise NotImplementedError @abc.abstractmethod def get_location(self): """Return the file path which this persistence stores (meta)data into""" raise NotImplementedError def _open(location): return os.open(location, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600) # The 600 seems no-op on NTFS/Windows, and that is fine class FilePersistence(BasePersistence): """A generic persistence, storing data in a plain-text file""" def __init__(self, location): if not location: raise ValueError("Requires a file path") self._location = os.path.expanduser(location) _mkdir_p(os.path.dirname(self._location)) def save(self, content): # type: (str) -> None """Save the content into this persistence""" with os.fdopen(_open(self._location), 'w+') as handle: handle.write(content) def load(self): # type: () -> str """Load content from this persistence""" try: with open(self._location, 'r') as handle: # pylint: disable=unspecified-encoding return handle.read() except EnvironmentError as exp: # EnvironmentError in Py 2.7 works across platform if exp.errno == errno.ENOENT: raise PersistenceNotFound( message=( "Persistence not initialized. " "You can recover by calling a save() first."), location=self._location, ) raise def time_last_modified(self): try: return os.path.getmtime(self._location) except EnvironmentError as exp: # EnvironmentError in Py 2.7 works across platform if exp.errno == errno.ENOENT: raise PersistenceNotFound( message=( "Persistence not initialized. " "You can recover by calling a save() first."), location=self._location, ) raise def touch(self): """To touch this file-based persistence without writing content into it""" Path(self._location).touch() # For os.path.getmtime() to work def get_location(self): return self._location class FilePersistenceWithDataProtection(FilePersistence): """A generic persistence with data stored in a file, protected by Win32 encryption APIs on Windows""" is_encrypted = True def __init__(self, location, entropy=''): """Initialization could fail due to unsatisfied dependency""" # pylint: disable=import-outside-toplevel from .windows import WindowsDataProtectionAgent self._dp_agent = WindowsDataProtectionAgent(entropy=entropy) super(FilePersistenceWithDataProtection, self).__init__(location) def save(self, content): # type: (str) -> None try: data = self._dp_agent.protect(content) except OSError as exception: raise PersistenceEncryptionError( err_no=getattr(exception, "winerror", None), # Exists in Python 3 on Windows message="Encryption failed: {} Consider disable encryption.".format(exception), ) with os.fdopen(_open(self._location), 'wb+') as handle: handle.write(data) def load(self): # type: () -> str try: with open(self._location, 'rb') as handle: data = handle.read() except EnvironmentError as exp: # EnvironmentError in Py 2.7 works across platform if exp.errno == errno.ENOENT: raise PersistenceNotFound( message=( "Persistence not initialized. " "You can recover by calling a save() first."), location=self._location, ) logger.exception( "DPAPI error likely caused by file content not previously encrypted. " "App developer should migrate by calling save(plaintext) first.") raise try: return self._dp_agent.unprotect(data) except OSError as exception: raise PersistenceDecryptionError( err_no=getattr(exception, "winerror", None), # Exists in Python 3 on Windows message="Decryption failed: {} " "App developer may consider this guidance: " "https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/PersistenceDecryptionError" # pylint: disable=line-too-long .format(exception), location=self._location, ) class KeychainPersistence(BasePersistence): """A generic persistence with data stored in, and protected by native Keychain libraries on OSX""" is_encrypted = True def __init__(self, signal_location, service_name=None, account_name=None): """Initialization could fail due to unsatisfied dependency. :param signal_location: See :func:`persistence.LibsecretPersistence.__init__` """ from .osx import Keychain, KeychainError # pylint: disable=import-outside-toplevel self._file_persistence = FilePersistence(signal_location) # Favor composition self._Keychain = Keychain # pylint: disable=invalid-name self._KeychainError = KeychainError # pylint: disable=invalid-name default_service_name = "msal-extensions" # This is also our package name self._service_name = service_name or default_service_name self._account_name = account_name or _auto_hash(signal_location) def save(self, content): with self._Keychain() as locker: locker.set_generic_password( self._service_name, self._account_name, content) self._file_persistence.touch() # For time_last_modified() def load(self): with self._Keychain() as locker: try: return locker.get_generic_password( self._service_name, self._account_name) except self._KeychainError as ex: # pylint: disable=invalid-name if ex.exit_status == self._KeychainError.ITEM_NOT_FOUND: # This happens when a load() is called before a save(). # We map it into cross-platform error for unified catching. raise PersistenceNotFound( location="Service:{} Account:{}".format( # pylint: disable=consider-using-f-string self._service_name, self._account_name), message=( "Keychain persistence not initialized. " "You can recover by call a save() first."), ) raise # We do not intend to hide any other underlying exceptions def time_last_modified(self): return self._file_persistence.time_last_modified() def get_location(self): return self._file_persistence.get_location() class LibsecretPersistence(BasePersistence): """A generic persistence with data stored in, and protected by native libsecret libraries on Linux""" is_encrypted = True def __init__(self, signal_location, schema_name=None, attributes=None, **kwargs): """Initialization could fail due to unsatisfied dependency. :param string signal_location: Besides saving the real payload into encrypted storage, this class will also touch this signal file. Applications may listen a FileSystemWatcher.Changed event for reload. https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.changed?view=netframework-4.8#remarks :param string schema_name: See :func:`libsecret.LibSecretAgent.__init__` :param dict attributes: See :func:`libsecret.LibSecretAgent.__init__` """ # pylint: disable=import-outside-toplevel from .libsecret import ( # This uncertain import is deferred till runtime LibSecretAgent, trial_run) trial_run() self._agent = LibSecretAgent( schema_name or _auto_hash(signal_location), attributes or {}, **kwargs) self._file_persistence = FilePersistence(signal_location) # Favor composition def save(self, content): if self._agent.save(content): self._file_persistence.touch() # For time_last_modified() def load(self): data = self._agent.load() if data is None: # Lower level libsecret would return None when found nothing. Here # in persistence layer, we convert it to a unified error for consistence. raise PersistenceNotFound(message=( "Keyring persistence not initialized. " "You can recover by call a save() first.")) return data def time_last_modified(self): return self._file_persistence.time_last_modified() def get_location(self): return self._file_persistence.get_location() # We could also have a KeyringPersistence() which can then be used together # with a FilePersistence to achieve # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/issues/12 # But this idea is not pursued at this time.