Hello community, here is the log from the commit of package python-msal-extensions for openSUSE:Factory checked in at 2020-10-02 17:27:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-msal-extensions (Old) and /work/SRC/openSUSE:Factory/.python-msal-extensions.new.4249 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-msal-extensions" Fri Oct 2 17:27:40 2020 rev:2 rq:833122 version:0.3.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-msal-extensions/python-msal-extensions.changes 2020-02-28 15:20:01.321731970 +0100 +++ /work/SRC/openSUSE:Factory/.python-msal-extensions.new.4249/python-msal-extensions.changes 2020-10-02 17:28:28.958422321 +0200 @@ -1,0 +2,36 @@ +Tue Sep 8 19:58:59 UTC 2020 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to version 0.3.0 + + New unified PersistenceNotFound exception is now raised for cases + where the persistence is not found. (#64, #67) + + Bugfix: File not found exception is now handled for Python 2.7 as a no-op (#69) + + Added performance tests for locking behavior (#58) + + A non-exist persistence on Linux platform would previously return a None. + Since this release, it will raise PersistenceNotFound exception which becomes + a consistent behavior on Windows and macOS. + +------------------------------------------------------------------- +Fri Aug 28 13:40:11 UTC 2020 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to version 0.2.2 + + Bugfix: Restored compatibility with upstream package portalocker version + < 1.4.0 when running on non-Windows platform (#50) + + Bugfix: Cache on Windows was not functioning in version 0.2.0 and 0.2.1(#52) + + Enhancement: Improved readme providing installation and usage instructions (#53) +- from version 0.2.1 + + Functionally the same as 0.2.0, but we change the installation-time and import-time + dependency of PyGObject to run-time dependency. This would make the installation + easier for those customers who do not necessarily need to use the Encryption on Linux. (#47) + + The version 1.6.0+ of upstream package portalocker is only required on Windows. + Other platforms remain with portalocker 1.0.0+. (#49) +- from version 0.2.0 + + New feature: Support token cache encryption when running on Linux Desktop (#4, #44) + + Bug fix: The cache lock was not properly removed on Windows 10 (#42, #43) + + Change: A new set of API PersistedTokenCache is provided. Previous API is now deprecated + and will be removed in next major release which will likely come within a month: + WindowsTokenCache, OSXTokenCache, UnencryptedTokenCache, FileTokenCache and TokenCache. + + Since this release, we have a dependency on PyGObject, when running on Linux. + You may need to follow its installation steps, or follow our CI setup. +- Update Requires from setup.py + +------------------------------------------------------------------- Old: ---- msal-extensions-0.1.3.tar.gz New: ---- msal-extensions-0.3.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-msal-extensions.spec ++++++ --- /var/tmp/diff_new_pack.VgofJw/_old 2020-10-02 17:28:31.966424122 +0200 +++ /var/tmp/diff_new_pack.VgofJw/_new 2020-10-02 17:28:31.970424124 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-msal-extensions -Version: 0.1.3 +Version: 0.3.0 Release: 0 Summary: Microsoft Authentication Library (MSAL) for Python Extensions License: MIT @@ -29,16 +29,19 @@ BuildRequires: %{python_module setuptools} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-msal >= 0.4.1 Requires: python-msal < 2.0.0 -Requires: python-portalocker >= 1.0 +Requires: python-msal >= 0.4.1 Requires: python-portalocker < 2.0 +Requires: python-portalocker >= 1.0 +%ifpython2 +Requires: python-pathlib2 +%endif BuildArch: noarch # SECTION test requirements -BuildRequires: %{python_module msal >= 0.4.1} BuildRequires: %{python_module msal < 2.0.0} -BuildRequires: %{python_module portalocker >= 1.0} +BuildRequires: %{python_module msal >= 0.4.1} BuildRequires: %{python_module portalocker < 2.0} +BuildRequires: %{python_module portalocker >= 1.0} BuildRequires: %{python_module pytest} # /SECTION %python_subpackages ++++++ msal-extensions-0.1.3.tar.gz -> msal-extensions-0.3.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/PKG-INFO new/msal-extensions-0.3.0/PKG-INFO --- old/msal-extensions-0.1.3/PKG-INFO 2019-11-02 02:27:37.000000000 +0100 +++ new/msal-extensions-0.3.0/PKG-INFO 2020-09-01 22:43:13.000000000 +0200 @@ -1,11 +1,104 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: msal-extensions -Version: 0.1.3 +Version: 0.3.0 Summary: UNKNOWN Home-page: UNKNOWN -Author: UNKNOWN -Author-email: UNKNOWN License: UNKNOWN -Description: UNKNOWN +Description: + # Microsoft Authentication Extensions for Python + + The Microsoft Authentication Extensions for Python offers secure mechanisms for client applications to perform cross-platform token cache serialization and persistence. It gives additional support to the [Microsoft Authentication Library for Python (MSAL)](https://github.com/AzureAD/microsoft-authentication-library-for-python). + + MSAL Python supports an in-memory cache by default and provides the [SerializableTokenCache](https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache) to perform cache serialization. You can read more about this in the MSAL Python [documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-python-token-cache-serialization). Developers are required to implement their own cache persistance across multiple platforms and Microsoft Authentication Extensions makes this simpler. + + The supported platforms are Windows, Mac and Linux. + - Windows - [DPAPI](https://docs.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection) is used for encryption. + - MAC - The MAC KeyChain is used. + - Linux - [LibSecret](https://wiki.gnome.org/Projects/Libsecret) is used for encryption. + + > Note: It is recommended to use this library for cache persistance support for Public client applications such as Desktop apps only. In web applications, this may lead to scale and performance issues. Web applications are recommended to persist the cache in session. Take a look at this [webapp sample](https://github.com/Azure-Samples/ms-identity-python-webapp). + + ## Installation + + You can find Microsoft Authentication Extensions for Python on [Pypi](https://pypi.org/project/msal-extensions/). + 1. If you haven't already, [install and/or upgrade the pip](https://pip.pypa.io/en/stable/installing/) + of your Python environment to a recent version. We tested with pip 18.1. + 2. Run `pip install msal-extensions`. + + ## Versions + + This library follows [Semantic Versioning](http://semver.org/). + + You can find the changes for each version under + [Releases](https://github.com/AzureAD/microsoft-authentication-extensions-for-python/releases). + + ## Usage + + The Microsoft Authentication Extensions library provides the `PersistedTokenCache` which accepts a platform-dependent persistence instance. This token cache can then be used to instantiate the `PublicClientApplication` in MSAL Python. + + The token cache includes a file lock, and auto-reload behavior under the hood. + + + + Here is an example of this pattern for multiple platforms (taken from the complete [sample here](https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/dev/sample/token_cache_sample.py)): + + ```python + def build_persistence(location, fallback_to_plaintext=False): + """Build a suitable persistence instance based your current OS""" + if sys.platform.startswith('win'): + return FilePersistenceWithDataProtection(location) + if sys.platform.startswith('darwin'): + return KeychainPersistence(location, "my_service_name", "my_account_name") + if sys.platform.startswith('linux'): + try: + return LibsecretPersistence( + location, + schema_name="my_schema_name", + attributes={"my_attr1": "foo", "my_attr2": "bar"}, + ) + except: # pylint: disable=bare-except + if not fallback_to_plaintext: + raise + logging.exception("Encryption unavailable. Opting in to plain text.") + return FilePersistence(location) + + persistence = build_persistence("token_cache.bin") + print("Is this persistence encrypted?", persistence.is_encrypted) + + cache = PersistedTokenCache(persistence) + ``` + Now you can use it in an MSAL application like this: + ```python + app = msal.PublicClientApplication("my_client_id", token_cache=cache) + ``` + + ## Community Help and Support + + We leverage Stack Overflow to work with the community on supporting Azure Active Directory and its SDKs, including this one! + We highly recommend you ask your questions on Stack Overflow (we're all on there!). + Also browse existing issues to see if someone has had your question before. + + We recommend you use the "msal" tag so we can see it! + Here is the latest Q&A on Stack Overflow for MSAL: + [http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal) + + + ## Contributing + + All code is licensed under the MIT license and we triage actively on GitHub. + + This project welcomes contributions and suggestions. Most contributions require you to agree to a + Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us + the rights to use your contribution. For details, visit https://cla.microsoft.com. + + When you submit a pull request, a CLA-bot will automatically determine whether you need to provide + a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions + provided by the bot. You will only need to do this once across all repos using our CLA. + + + ## We value and adhere to the Microsoft Open Source Code of Conduct + + This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [openc...@microsoft.com](mailto:openc...@microsoft.com) with any additional questions or comments. Platform: UNKNOWN Classifier: Development Status :: 2 - Pre-Alpha +Description-Content-Type: text/markdown diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/README.md new/msal-extensions-0.3.0/README.md --- old/msal-extensions-0.1.3/README.md 2019-11-02 02:27:01.000000000 +0100 +++ new/msal-extensions-0.3.0/README.md 2020-09-01 22:42:14.000000000 +0200 @@ -1,5 +1,85 @@ -# Contributing +# Microsoft Authentication Extensions for Python + +The Microsoft Authentication Extensions for Python offers secure mechanisms for client applications to perform cross-platform token cache serialization and persistence. It gives additional support to the [Microsoft Authentication Library for Python (MSAL)](https://github.com/AzureAD/microsoft-authentication-library-for-python). + +MSAL Python supports an in-memory cache by default and provides the [SerializableTokenCache](https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache) to perform cache serialization. You can read more about this in the MSAL Python [documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-python-token-cache-serialization). Developers are required to implement their own cache persistance across multiple platforms and Microsoft Authentication Extensions makes this simpler. + +The supported platforms are Windows, Mac and Linux. +- Windows - [DPAPI](https://docs.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection) is used for encryption. +- MAC - The MAC KeyChain is used. +- Linux - [LibSecret](https://wiki.gnome.org/Projects/Libsecret) is used for encryption. + +> Note: It is recommended to use this library for cache persistance support for Public client applications such as Desktop apps only. In web applications, this may lead to scale and performance issues. Web applications are recommended to persist the cache in session. Take a look at this [webapp sample](https://github.com/Azure-Samples/ms-identity-python-webapp). + +## Installation + +You can find Microsoft Authentication Extensions for Python on [Pypi](https://pypi.org/project/msal-extensions/). +1. If you haven't already, [install and/or upgrade the pip](https://pip.pypa.io/en/stable/installing/) + of your Python environment to a recent version. We tested with pip 18.1. +2. Run `pip install msal-extensions`. + +## Versions + +This library follows [Semantic Versioning](http://semver.org/). + +You can find the changes for each version under +[Releases](https://github.com/AzureAD/microsoft-authentication-extensions-for-python/releases). + +## Usage + +The Microsoft Authentication Extensions library provides the `PersistedTokenCache` which accepts a platform-dependent persistence instance. This token cache can then be used to instantiate the `PublicClientApplication` in MSAL Python. + +The token cache includes a file lock, and auto-reload behavior under the hood. + + + +Here is an example of this pattern for multiple platforms (taken from the complete [sample here](https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/dev/sample/token_cache_sample.py)): + +```python +def build_persistence(location, fallback_to_plaintext=False): + """Build a suitable persistence instance based your current OS""" + if sys.platform.startswith('win'): + return FilePersistenceWithDataProtection(location) + if sys.platform.startswith('darwin'): + return KeychainPersistence(location, "my_service_name", "my_account_name") + if sys.platform.startswith('linux'): + try: + return LibsecretPersistence( + location, + schema_name="my_schema_name", + attributes={"my_attr1": "foo", "my_attr2": "bar"}, + ) + except: # pylint: disable=bare-except + if not fallback_to_plaintext: + raise + logging.exception("Encryption unavailable. Opting in to plain text.") + return FilePersistence(location) + +persistence = build_persistence("token_cache.bin") +print("Is this persistence encrypted?", persistence.is_encrypted) + +cache = PersistedTokenCache(persistence) +``` +Now you can use it in an MSAL application like this: +```python +app = msal.PublicClientApplication("my_client_id", token_cache=cache) +``` + +## Community Help and Support + +We leverage Stack Overflow to work with the community on supporting Azure Active Directory and its SDKs, including this one! +We highly recommend you ask your questions on Stack Overflow (we're all on there!). +Also browse existing issues to see if someone has had your question before. + +We recommend you use the "msal" tag so we can see it! +Here is the latest Q&A on Stack Overflow for MSAL: +[http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal) + + +## Contributing + +All code is licensed under the MIT license and we triage actively on GitHub. This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us @@ -9,6 +89,7 @@ a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [openc...@microsoft.com](mailto:openc...@microsoft.com) with any additional questions or comments. + +## We value and adhere to the Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [openc...@microsoft.com](mailto:openc...@microsoft.com) with any additional questions or comments. \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions/__init__.py new/msal-extensions-0.3.0/msal_extensions/__init__.py --- old/msal-extensions-0.1.3/msal_extensions/__init__.py 2019-11-02 02:27:01.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions/__init__.py 2020-09-01 22:42:14.000000000 +0200 @@ -1,11 +1,20 @@ """Provides auxiliary functionality to the `msal` package.""" -__version__ = "0.1.3" +__version__ = "0.3.0" import sys +from .persistence import ( + FilePersistence, + FilePersistenceWithDataProtection, + KeychainPersistence, + LibsecretPersistence, + ) +from .cache_lock import CrossPlatLock +from .token_cache import PersistedTokenCache + if sys.platform.startswith('win'): from .token_cache import WindowsTokenCache as TokenCache elif sys.platform.startswith('darwin'): from .token_cache import OSXTokenCache as TokenCache else: - from .token_cache import UnencryptedTokenCache as TokenCache + from .token_cache import FileTokenCache as TokenCache diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions/cache_lock.py new/msal-extensions-0.3.0/msal_extensions/cache_lock.py --- old/msal-extensions-0.1.3/msal_extensions/cache_lock.py 2019-11-02 02:27:01.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions/cache_lock.py 2020-09-01 22:42:14.000000000 +0200 @@ -3,6 +3,7 @@ import sys import errno import portalocker +from distutils.version import LooseVersion class CrossPlatLock(object): @@ -12,22 +13,30 @@ """ def __init__(self, lockfile_path): self._lockpath = lockfile_path - self._fh = None + # Support for passing through arguments to the open syscall was added in v1.4.0 + open_kwargs = {'buffering': 0} if LooseVersion(portalocker.__version__) >= LooseVersion("1.4.0") else {} + self._lock = portalocker.Lock( + lockfile_path, + mode='wb+', + # In posix systems, we HAVE to use LOCK_EX(exclusive lock) bitwise ORed + # with LOCK_NB(non-blocking) to avoid blocking on lock acquisition. + # More information here: + # https://docs.python.org/3/library/fcntl.html#fcntl.lockf + flags=portalocker.LOCK_EX | portalocker.LOCK_NB, + **open_kwargs) def __enter__(self): - pid = os.getpid() - - self._fh = open(self._lockpath, 'wb+', buffering=0) - portalocker.lock(self._fh, portalocker.LOCK_EX) - self._fh.write('{} {}'.format(pid, sys.argv[0]).encode('utf-8')) + file_handle = self._lock.__enter__() + file_handle.write('{} {}'.format(os.getpid(), sys.argv[0]).encode('utf-8')) + return file_handle def __exit__(self, *args): - self._fh.close() + self._lock.__exit__(*args) try: # Attempt to delete the lockfile. In either of the failure cases enumerated below, it is # likely that another process has raced this one and ended up clearing or locking the # file for itself. os.remove(self._lockpath) - except OSError as ex: + except OSError as ex: # pylint: disable=invalid-name if ex.errno != errno.ENOENT and ex.errno != errno.EACCES: raise diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions/libsecret.py new/msal-extensions-0.3.0/msal_extensions/libsecret.py --- old/msal-extensions-0.1.3/msal_extensions/libsecret.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions/libsecret.py 2020-09-01 22:42:14.000000000 +0200 @@ -0,0 +1,139 @@ +"""Implements a Linux specific TokenCache, and provides auxiliary helper types. + +This module depends on PyGObject. But `pip install pygobject` would typically fail, +until you install its dependencies first. For example, on a Debian Linux, you need:: + + sudo apt install libgirepository1.0-dev libcairo2-dev python3-dev gir1.2-secret-1 + pip install pygobject + +Alternatively, you could skip Cairo & PyCairo, but you still need to do all these +(derived from https://gitlab.gnome.org/GNOME/pygobject/-/issues/395):: + + sudo apt install libgirepository1.0-dev python3-dev gir1.2-secret-1 + pip install wheel + PYGOBJECT_WITHOUT_PYCAIRO=1 pip install --no-build-isolation pygobject +""" +import logging + +logger = logging.getLogger(__name__) + +try: + import gi # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/Encryption-on-Linux +except ImportError: + logger.exception( + """Runtime dependency of PyGObject is missing. +Depends on your Linux distro, you could install it system-wide by something like: + sudo apt install python3-gi python3-gi-cairo gir1.2-secret-1 +If necessary, please refer to PyGObject's doc: +https://pygobject.readthedocs.io/en/latest/getting_started.html +""") + raise + +try: + # pylint: disable=no-name-in-module + gi.require_version("Secret", "1") # Would require a package gir1.2-secret-1 + # pylint: disable=wrong-import-position + from gi.repository import Secret # Would require a package gir1.2-secret-1 +except (ValueError, ImportError): + logger.exception( + """Require a package "gir1.2-secret-1" which could be installed by: + sudo apt install gir1.2-secret-1 + """) + raise + +class LibSecretAgent(object): + """A loader/saver built on top of low-level libsecret""" + # Inspired by https://developer.gnome.org/libsecret/unstable/py-examples.html + def __init__( # pylint: disable=too-many-arguments + self, + schema_name, + attributes, # {"name": "value", ...} + label="", # Helpful when visualizing secrets by other viewers + attribute_types=None, # {name: SchemaAttributeType, ...} + collection=None, # None means default collection + ): # pylint: disable=bad-continuation + """This agent is built on top of lower level libsecret API. + + Content stored via libsecret is associated with a bunch of attributes. + + :param string schema_name: + Attributes would conceptually follow an existing schema. + But this class will do it in the other way around, + by automatically deriving a schema based on your attributes. + However, you will still need to provide a schema_name. + load() and save() will only operate on data with matching schema_name. + + :param dict attributes: + Attributes are key-value pairs, represented as a Python dict here. + They will be used to filter content during load() and save(). + Their arbitrary keys are strings. + Their arbitrary values can MEAN strings, integers and booleans, + but are always represented as strings, according to upstream sample: + https://developer.gnome.org/libsecret/0.18/py-store-example.html + + :param string label: + It will not be used during data lookup and filtering. + It is only helpful when/if you visualize secrets by other viewers. + + :param dict attribute_types: + Each key is the name of your each attribute. + The corresponding value will be one of the following three: + + * Secret.SchemaAttributeType.STRING + * Secret.SchemaAttributeType.INTEGER + * Secret.SchemaAttributeType.BOOLEAN + + But if all your attributes are Secret.SchemaAttributeType.STRING, + you do not need to provide this types definition at all. + + :param collection: + The default value `None` means default collection. + """ + self._collection = collection + self._attributes = attributes or {} + self._label = label + self._schema = Secret.Schema.new(schema_name, Secret.SchemaFlags.NONE, { + k: (attribute_types or {}).get(k, Secret.SchemaAttributeType.STRING) + for k in self._attributes}) + + def save(self, data): + """Store data. Returns a boolean of whether operation was successful.""" + return Secret.password_store_sync( + self._schema, self._attributes, self._collection, self._label, + data, None) + + def load(self): + """Load a password in the secret service, return None when found nothing""" + return Secret.password_lookup_sync(self._schema, self._attributes, None) + + def clear(self): + """Returns a boolean of whether any passwords were removed""" + return Secret.password_clear_sync(self._schema, self._attributes, None) + + +def trial_run(): + """This trial run will raise an exception if libsecret is not functioning. + + Even after you installed all the dependencies so that your script can start, + or even if your previous run was successful, your script could fail next time, + for example when it will be running inside a headless SSH session. + + You do not have to do trial_run. The exception would also be raised by save(). + """ + try: + agent = LibSecretAgent("Test Schema", {"attr1": "foo", "attr2": "bar"}) + payload = "Test Data" + agent.save(payload) # It would fail when running inside an SSH session + assert agent.load() == payload # This line is probably not reachable + agent.clear() + except (gi.repository.GLib.Error, AssertionError): + message = """libsecret did not perform properly. +* If you encountered error "Remote error from secret service: + org.freedesktop.DBus.Error.ServiceUnknown", + you may need to install gnome-keyring package. +* Headless mode (such as in an ssh session) is not supported. +""" + logger.exception(message) # This log contains trace stack for debugging + logger.warning(message) # This is visible by default + raise + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions/persistence.py new/msal-extensions-0.3.0/msal_extensions/persistence.py --- old/msal-extensions-0.1.3/msal_extensions/persistence.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions/persistence.py 2020-09-01 22:42:14.000000000 +0200 @@ -0,0 +1,284 @@ +"""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 logging +try: + from pathlib import Path # Built-in in Python 3 +except: + 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. + + This implementation based on a Stack Overflow question that can be found here: + https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python + + 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 + try: + os.makedirs(path) + except OSError as exp: + if exp.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + + +# We do not aim to wrap every os-specific exception. +# Here we define only the most common one, +# otherwise caller would need to catch os-specific persistence exceptions. +class PersistenceNotFound(IOError): # Use IOError rather than OSError as base, + # 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=errno.ENOENT, message="Persistence not found", location=None): + super(PersistenceNotFound, self).__init__(err_no, message, location) + + +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 + + +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 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: + 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 + data = self._dp_agent.protect(content) + with 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() + return self._dp_agent.unprotect(data) + 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 + + +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, account_name): + """Initialization could fail due to unsatisfied dependency. + + :param signal_location: See :func:`persistence.LibsecretPersistence.__init__` + """ + if not (service_name and account_name): # It would hang on OSX + raise ValueError("service_name and account_name are required") + 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 + self._service_name = service_name + self._account_name = account_name + + 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: + 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( + 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, attributes, **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, attributes, **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. + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions/token_cache.py new/msal-extensions-0.3.0/msal_extensions/token_cache.py --- old/msal-extensions-0.1.3/msal_extensions/token_cache.py 2019-11-02 02:27:01.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions/token_cache.py 2020-09-01 22:42:14.000000000 +0200 @@ -1,156 +1,88 @@ """Generic functions and types for working with a TokenCache that is not platform specific.""" import os -import sys import warnings import time -import errno +import logging + import msal -from .cache_lock import CrossPlatLock -if sys.platform.startswith('win'): - from .windows import WindowsDataProtectionAgent -elif sys.platform.startswith('darwin'): - from .osx import Keychain - -def _mkdir_p(path): - """Creates a directory, and any necessary parents. - - This implementation based on a Stack Overflow question that can be found here: - https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python - - If the path provided is an existing file, this function raises an exception. - :param path: The directory name that should be created. - """ - try: - os.makedirs(path) - except OSError as exp: - if exp.errno == errno.EEXIST and os.path.isdir(path): - pass - else: - raise +from .cache_lock import CrossPlatLock +from .persistence import ( + _mkdir_p, PersistenceNotFound, FilePersistence, + FilePersistenceWithDataProtection, KeychainPersistence) -class FileTokenCache(msal.SerializableTokenCache): - """Implements basic unprotected SerializableTokenCache to a plain-text file.""" - def __init__(self, - cache_location, - lock_location=None): - super(FileTokenCache, self).__init__() - self._cache_location = cache_location - self._lock_location = lock_location or self._cache_location + '.lockfile' - self._last_sync = 0 # _last_sync is a Unixtime +logger = logging.getLogger(__name__) - self._cache_location = os.path.expanduser(self._cache_location) - self._lock_location = os.path.expanduser(self._lock_location) +class PersistedTokenCache(msal.SerializableTokenCache): + """A token cache using given persistence layer, coordinated by a file lock.""" + def __init__(self, persistence, lock_location=None): + super(PersistedTokenCache, self).__init__() + self._lock_location = ( + os.path.expanduser(lock_location) if lock_location + else persistence.get_location() + ".lockfile") _mkdir_p(os.path.dirname(self._lock_location)) - _mkdir_p(os.path.dirname(self._cache_location)) + self._persistence = persistence + self._last_sync = 0 # _last_sync is a Unixtime + self.is_encrypted = persistence.is_encrypted - def _needs_refresh(self): - # type: () -> Bool - """ - Inspects the file holding the encrypted TokenCache to see if a read is necessary. - :return: True if there are changes not reflected in memory, False otherwise. - """ + def _reload_if_necessary(self): + # type: () -> None + """Reload cache from persistence layer, if necessary""" try: - updated = os.path.getmtime(self._cache_location) - return self._last_sync < updated - except IOError as exp: - if exp.errno != errno.ENOENT: - raise exp - return False - - def _write(self, contents): - # type: (str) -> None - """Handles actually committing the serialized form of this TokenCache to persisted storage. - For types derived of this, class that will be a file, which has the ability to track a last - modified time. - - :param contents: The serialized contents of a TokenCache - """ - with open(self._cache_location, 'w+') as handle: - handle.write(contents) - - def _read(self): - # type: () -> str - """Fetches the contents of a file and invokes deserialization.""" - with open(self._cache_location, 'r') as handle: - return handle.read() + if self._last_sync < self._persistence.time_last_modified(): + self.deserialize(self._persistence.load()) + self._last_sync = time.time() + except PersistenceNotFound: + # From cache's perspective, a nonexistent persistence is a NO-OP. + pass + # However, existing data unable to be decrypted will still be bubbled up. def modify(self, credential_type, old_entry, new_key_value_pairs=None): with CrossPlatLock(self._lock_location): - if self._needs_refresh(): - try: - self.deserialize(self._read()) - except IOError as exp: - if exp.errno != errno.ENOENT: - raise - super(FileTokenCache, self).modify( + self._reload_if_necessary() + super(PersistedTokenCache, self).modify( credential_type, old_entry, new_key_value_pairs=new_key_value_pairs) - self._write(self.serialize()) - self._last_sync = os.path.getmtime(self._cache_location) + self._persistence.save(self.serialize()) + self._last_sync = time.time() def find(self, credential_type, **kwargs): # pylint: disable=arguments-differ with CrossPlatLock(self._lock_location): - if self._needs_refresh(): - try: - self.deserialize(self._read()) - except IOError as exp: - if exp.errno != errno.ENOENT: - raise - self._last_sync = time.time() - return super(FileTokenCache, self).find(credential_type, **kwargs) + self._reload_if_necessary() + return super(PersistedTokenCache, self).find(credential_type, **kwargs) + +class FileTokenCache(PersistedTokenCache): + """A token cache which uses plain text file to store your tokens.""" + def __init__(self, cache_location, **ignored): # pylint: disable=unused-argument + warnings.warn("You are using an unprotected token cache", RuntimeWarning) + warnings.warn("Use PersistedTokenCache(...) instead", DeprecationWarning) + super(FileTokenCache, self).__init__(FilePersistence(cache_location)) -class UnencryptedTokenCache(FileTokenCache): - """An unprotected token cache to default to when no-platform specific option is available.""" - def __init__(self, cache_location, **kwargs): - warnings.warn("You are using an unprotected token cache, " - "because an encrypted option is not available for {}".format(sys.platform), - RuntimeWarning) - super(UnencryptedTokenCache, self).__init__(cache_location, **kwargs) - - -class WindowsTokenCache(FileTokenCache): - """A SerializableTokenCache implementation which uses Win32 encryption APIs to protect your - tokens. - """ - def __init__(self, cache_location, entropy='', **kwargs): - super(WindowsTokenCache, self).__init__(cache_location, **kwargs) - self._dp_agent = WindowsDataProtectionAgent(entropy=entropy) - - def _write(self, contents): - with open(self._cache_location, 'wb') as handle: - handle.write(self._dp_agent.protect(contents)) - - def _read(self): - with open(self._cache_location, 'rb') as handle: - cipher_text = handle.read() - return self._dp_agent.unprotect(cipher_text) - - -class OSXTokenCache(FileTokenCache): - """A SerializableTokenCache implementation which uses native Keychain libraries to protect your - tokens. - """ +UnencryptedTokenCache = FileTokenCache # For backward compatibility + +class WindowsTokenCache(PersistedTokenCache): + """A token cache which uses Windows DPAPI to encrypt your tokens.""" + def __init__( + self, cache_location, entropy='', + **ignored): # pylint: disable=unused-argument + warnings.warn("Use PersistedTokenCache(...) instead", DeprecationWarning) + super(WindowsTokenCache, self).__init__( + FilePersistenceWithDataProtection(cache_location, entropy=entropy)) + + +class OSXTokenCache(PersistedTokenCache): + """A token cache which uses native Keychain libraries to encrypt your tokens.""" def __init__(self, cache_location, service_name='Microsoft.Developer.IdentityService', account_name='MSALCache', - **kwargs): - super(OSXTokenCache, self).__init__(cache_location, **kwargs) - self._service_name = service_name - self._account_name = account_name - - def _read(self): - with Keychain() as locker: - return locker.get_generic_password(self._service_name, self._account_name) - - def _write(self, contents): - with Keychain() as locker: - locker.set_generic_password(self._service_name, self._account_name, contents) - with open(self._cache_location, "w+") as handle: - handle.write('{} {}'.format(os.getpid(), sys.argv[0])) + **ignored): # pylint: disable=unused-argument + warnings.warn("Use PersistedTokenCache(...) instead", DeprecationWarning) + super(OSXTokenCache, self).__init__( + KeychainPersistence(cache_location, service_name, account_name)) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions.egg-info/PKG-INFO new/msal-extensions-0.3.0/msal_extensions.egg-info/PKG-INFO --- old/msal-extensions-0.1.3/msal_extensions.egg-info/PKG-INFO 2019-11-02 02:27:37.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions.egg-info/PKG-INFO 2020-09-01 22:43:13.000000000 +0200 @@ -1,11 +1,104 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: msal-extensions -Version: 0.1.3 +Version: 0.3.0 Summary: UNKNOWN Home-page: UNKNOWN -Author: UNKNOWN -Author-email: UNKNOWN License: UNKNOWN -Description: UNKNOWN +Description: + # Microsoft Authentication Extensions for Python + + The Microsoft Authentication Extensions for Python offers secure mechanisms for client applications to perform cross-platform token cache serialization and persistence. It gives additional support to the [Microsoft Authentication Library for Python (MSAL)](https://github.com/AzureAD/microsoft-authentication-library-for-python). + + MSAL Python supports an in-memory cache by default and provides the [SerializableTokenCache](https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache) to perform cache serialization. You can read more about this in the MSAL Python [documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-python-token-cache-serialization). Developers are required to implement their own cache persistance across multiple platforms and Microsoft Authentication Extensions makes this simpler. + + The supported platforms are Windows, Mac and Linux. + - Windows - [DPAPI](https://docs.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection) is used for encryption. + - MAC - The MAC KeyChain is used. + - Linux - [LibSecret](https://wiki.gnome.org/Projects/Libsecret) is used for encryption. + + > Note: It is recommended to use this library for cache persistance support for Public client applications such as Desktop apps only. In web applications, this may lead to scale and performance issues. Web applications are recommended to persist the cache in session. Take a look at this [webapp sample](https://github.com/Azure-Samples/ms-identity-python-webapp). + + ## Installation + + You can find Microsoft Authentication Extensions for Python on [Pypi](https://pypi.org/project/msal-extensions/). + 1. If you haven't already, [install and/or upgrade the pip](https://pip.pypa.io/en/stable/installing/) + of your Python environment to a recent version. We tested with pip 18.1. + 2. Run `pip install msal-extensions`. + + ## Versions + + This library follows [Semantic Versioning](http://semver.org/). + + You can find the changes for each version under + [Releases](https://github.com/AzureAD/microsoft-authentication-extensions-for-python/releases). + + ## Usage + + The Microsoft Authentication Extensions library provides the `PersistedTokenCache` which accepts a platform-dependent persistence instance. This token cache can then be used to instantiate the `PublicClientApplication` in MSAL Python. + + The token cache includes a file lock, and auto-reload behavior under the hood. + + + + Here is an example of this pattern for multiple platforms (taken from the complete [sample here](https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/dev/sample/token_cache_sample.py)): + + ```python + def build_persistence(location, fallback_to_plaintext=False): + """Build a suitable persistence instance based your current OS""" + if sys.platform.startswith('win'): + return FilePersistenceWithDataProtection(location) + if sys.platform.startswith('darwin'): + return KeychainPersistence(location, "my_service_name", "my_account_name") + if sys.platform.startswith('linux'): + try: + return LibsecretPersistence( + location, + schema_name="my_schema_name", + attributes={"my_attr1": "foo", "my_attr2": "bar"}, + ) + except: # pylint: disable=bare-except + if not fallback_to_plaintext: + raise + logging.exception("Encryption unavailable. Opting in to plain text.") + return FilePersistence(location) + + persistence = build_persistence("token_cache.bin") + print("Is this persistence encrypted?", persistence.is_encrypted) + + cache = PersistedTokenCache(persistence) + ``` + Now you can use it in an MSAL application like this: + ```python + app = msal.PublicClientApplication("my_client_id", token_cache=cache) + ``` + + ## Community Help and Support + + We leverage Stack Overflow to work with the community on supporting Azure Active Directory and its SDKs, including this one! + We highly recommend you ask your questions on Stack Overflow (we're all on there!). + Also browse existing issues to see if someone has had your question before. + + We recommend you use the "msal" tag so we can see it! + Here is the latest Q&A on Stack Overflow for MSAL: + [http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal) + + + ## Contributing + + All code is licensed under the MIT license and we triage actively on GitHub. + + This project welcomes contributions and suggestions. Most contributions require you to agree to a + Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us + the rights to use your contribution. For details, visit https://cla.microsoft.com. + + When you submit a pull request, a CLA-bot will automatically determine whether you need to provide + a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions + provided by the bot. You will only need to do this once across all repos using our CLA. + + + ## We value and adhere to the Microsoft Open Source Code of Conduct + + This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [openc...@microsoft.com](mailto:openc...@microsoft.com) with any additional questions or comments. Platform: UNKNOWN Classifier: Development Status :: 2 - Pre-Alpha +Description-Content-Type: text/markdown diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions.egg-info/SOURCES.txt new/msal-extensions-0.3.0/msal_extensions.egg-info/SOURCES.txt --- old/msal-extensions-0.1.3/msal_extensions.egg-info/SOURCES.txt 2019-11-02 02:27:37.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions.egg-info/SOURCES.txt 2020-09-01 22:43:13.000000000 +0200 @@ -3,7 +3,9 @@ setup.py msal_extensions/__init__.py msal_extensions/cache_lock.py +msal_extensions/libsecret.py msal_extensions/osx.py +msal_extensions/persistence.py msal_extensions/token_cache.py msal_extensions/windows.py msal_extensions.egg-info/PKG-INFO diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/msal_extensions.egg-info/requires.txt new/msal-extensions-0.3.0/msal_extensions.egg-info/requires.txt --- old/msal-extensions-0.1.3/msal_extensions.egg-info/requires.txt 2019-11-02 02:27:37.000000000 +0100 +++ new/msal-extensions-0.3.0/msal_extensions.egg-info/requires.txt 2020-09-01 22:43:13.000000000 +0200 @@ -1,2 +1,10 @@ msal<2.0.0,>=0.4.1 + +[:platform_system != "Windows"] portalocker~=1.0 + +[:platform_system == "Windows"] +portalocker~=1.6 + +[:python_version < "3.0"] +pathlib2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-extensions-0.1.3/setup.py new/msal-extensions-0.3.0/setup.py --- old/msal-extensions-0.1.3/setup.py 2019-11-02 02:27:01.000000000 +0100 +++ new/msal-extensions-0.3.0/setup.py 2020-09-01 22:42:14.000000000 +0200 @@ -8,17 +8,28 @@ io.open('msal_extensions/__init__.py', encoding='utf_8_sig').read() ).group(1) +try: + long_description = open('README.md').read() +except OSError: + long_description = "README.md is not accessible on TRAVIS CI's Python 3.5" + setup( name='msal-extensions', version=__version__, packages=find_packages(), + long_description=long_description, + long_description_content_type="text/markdown", classifiers=[ 'Development Status :: 2 - Pre-Alpha', ], package_data={'': ['LICENSE']}, install_requires=[ 'msal>=0.4.1,<2.0.0', - 'portalocker~=1.0', + "portalocker~=1.6;platform_system=='Windows'", + "portalocker~=1.0;platform_system!='Windows'", + "pathlib2;python_version<'3.0'", + ## We choose to NOT define a hard dependency on this. + # "pygobject>=3,<4;platform_system=='Linux'", ], tests_require=['pytest'], )