From cbe26c4619507eb679175637924ee38ceb576241 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 6 Aug 2014 15:31:20 -0400 Subject: [PATCH] Upgrade keyring to 4.0 --- awx/lib/site-packages/README | 2 +- .../site-packages/keyring/backends/Windows.py | 36 ++++++- .../site-packages/keyring/backends/kwallet.py | 2 + awx/lib/site-packages/keyring/cli.py | 5 +- awx/lib/site-packages/keyring/core.py | 60 ++++-------- .../site-packages/keyring/tests/test_core.py | 95 +++++++------------ 6 files changed, 93 insertions(+), 107 deletions(-) diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index f48447b272..56a7ea9595 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -30,7 +30,7 @@ gevent-websocket==0.9.3 (geventwebsocket/*) httplib2==0.9 (httplib2/*) importlib==1.0.3 (importlib/*, needed for Python 2.6 support) iso8601==0.1.10 (iso8601/*) -keyring==3.7 (keyring/*, excluded bin/keyring) +keyring==4.0 (keyring/*, excluded bin/keyring) kombu==3.0.14 (kombu/*) Markdown==2.4 (markdown/*, excluded bin/markdown_py) mock==1.0.1 (mock.py) diff --git a/awx/lib/site-packages/keyring/backends/Windows.py b/awx/lib/site-packages/keyring/backends/Windows.py index 9627633051..d2c74ffe90 100644 --- a/awx/lib/site-packages/keyring/backends/Windows.py +++ b/awx/lib/site-packages/keyring/backends/Windows.py @@ -10,10 +10,18 @@ from ..errors import PasswordDeleteError, ExceptionRaisedContext from . import file try: - import pywintypes - import win32cred + # prefer pywin32-ctypes + from win32ctypes import pywintypes + from win32ctypes import win32cred + # force demand import to raise ImportError + win32cred.__name__ except ImportError: - pass + # fallback to pywin32 + try: + import pywintypes + import win32cred + except ImportError: + pass try: import winreg @@ -229,9 +237,31 @@ class RegistryKeyring(KeyringBackend): hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_name, 0, winreg.KEY_ALL_ACCESS) winreg.DeleteValue(hkey, username) + winreg.CloseKey(hkey) except WindowsError: e = sys.exc_info()[1] raise PasswordDeleteError(e) + self._delete_key_if_empty(service) + + def _delete_key_if_empty(self, service): + key_name = r'Software\%s\Keyring' % service + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_name, 0, + winreg.KEY_ALL_ACCESS) + try: + winreg.EnumValue(key, 0) + return + except WindowsError: + pass + winreg.CloseKey(key) + + # it's empty; delete everything + while key_name != 'Software': + parent, sep, base = key_name.rpartition('\\') + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, parent, 0, + winreg.KEY_ALL_ACCESS) + winreg.DeleteKey(key, base) + winreg.CloseKey(key) + key_name = parent class OldPywinError(object): diff --git a/awx/lib/site-packages/keyring/backends/kwallet.py b/awx/lib/site-packages/keyring/backends/kwallet.py index 1747701713..209b75d818 100644 --- a/awx/lib/site-packages/keyring/backends/kwallet.py +++ b/awx/lib/site-packages/keyring/backends/kwallet.py @@ -62,6 +62,8 @@ class Keyring(KeyringBackend): KWallet.__name__ if exc: raise RuntimeError("KDE libraries not available") + if "DISPLAY" not in os.environ: + raise RuntimeError("cannot connect to X server") # Infer if KDE environment is active based on environment vars. # TODO: Does PyKDE provide a better indicator? kde_session_keys = ( diff --git a/awx/lib/site-packages/keyring/cli.py b/awx/lib/site-packages/keyring/cli.py index 7645870574..4fae8211a6 100644 --- a/awx/lib/site-packages/keyring/cli.py +++ b/awx/lib/site-packages/keyring/cli.py @@ -37,8 +37,9 @@ class CommandLineTool(object): if opts.keyring_backend is not None: try: - backend = core.load_keyring(opts.keyring_path, - opts.keyring_backend) + if opts.keyring_path: + sys.path.insert(0, opts.keyring_path) + backend = core.load_keyring(opts.keyring_backend) set_keyring(backend) except (Exception,): # Tons of things can go wrong here: diff --git a/awx/lib/site-packages/keyring/core.py b/awx/lib/site-packages/keyring/core.py index 3b50cdc06a..6d193c9050 100644 --- a/awx/lib/site-packages/keyring/core.py +++ b/awx/lib/site-packages/keyring/core.py @@ -5,7 +5,6 @@ Created by Kang Zhang on 2009-07-09 """ import os import sys -import warnings import logging from .py27compat import configparser @@ -71,19 +70,12 @@ def _get_best_keyring(): return keyrings[0] -def load_keyring(keyring_path, keyring_name): +def load_keyring(keyring_name): """ Load the specified keyring by name (a fully-qualified name to the keyring, such as 'keyring.backends.file.PlaintextKeyring') - - `keyring_path` is an additional, optional search path and may be None. - **deprecated** In the future, keyring_path must be None. """ module_name, sep, class_name = keyring_name.rpartition('.') - if keyring_path is not None and keyring_path not in sys.path: - warnings.warn("keyring_path is deprecated and should always be None", - DeprecationWarning) - sys.path.insert(0, keyring_path) __import__(module_name) module = sys.modules[module_name] class_ = getattr(module, class_name) @@ -93,46 +85,32 @@ def load_keyring(keyring_path, keyring_name): def load_config(): - """Load a keyring using the config file. - - The config file can be in the current working directory, or in the user's - home directory. - """ - keyring = None + """Load a keyring using the config file in the config root.""" filename = 'keyringrc.cfg' - local_path = os.path.join(os.getcwd(), filename) - config_path = os.path.join(platform.config_root(), filename) + keyring_cfg = os.path.join(platform.config_root(), filename) - # search from current working directory and the data root - keyring_cfg_candidates = [local_path, config_path] + if not os.path.exists(keyring_cfg): + return - # initialize the keyring_config with the first detected config file - keyring_cfg = None - for path in keyring_cfg_candidates: - keyring_cfg = path - if os.path.exists(path): - break + config = configparser.RawConfigParser() + config.read(keyring_cfg) + _load_keyring_path(config) - if os.path.exists(keyring_cfg): - config = configparser.RawConfigParser() - config.read(keyring_cfg) - _load_keyring_path(config) + # load the keyring class name, and then load this keyring + try: + if config.has_section("backend"): + keyring_name = config.get("backend", "default-keyring").strip() + else: + raise configparser.NoOptionError('backend', 'default-keyring') - # load the keyring class name, and then load this keyring - try: - if config.has_section("backend"): - keyring_name = config.get("backend", "default-keyring").strip() - else: - raise configparser.NoOptionError('backend', 'default-keyring') + except (configparser.NoOptionError, ImportError): + logger.warning("Keyring config file contains incorrect values.\n" + + "Config file: %s" % keyring_cfg) + return - keyring = load_keyring(None, keyring_name) - except (configparser.NoOptionError, ImportError): - logger.warning("Keyring config file contains incorrect values.\n" + - "Config file: %s" % keyring_cfg) - - return keyring + return load_keyring(keyring_name) def _load_keyring_path(config): "load the keyring-path option (if present)" diff --git a/awx/lib/site-packages/keyring/tests/test_core.py b/awx/lib/site-packages/keyring/tests/test_core.py index 2e54167686..3328ce02ce 100644 --- a/awx/lib/site-packages/keyring/tests/test_core.py +++ b/awx/lib/site-packages/keyring/tests/test_core.py @@ -10,17 +10,23 @@ import os import tempfile import shutil -from keyring.tests.py30compat import unittest - import mock +import pytest import keyring.backend import keyring.core +import keyring.util.platform_ from keyring import errors PASSWORD_TEXT = "This is password" PASSWORD_TEXT_2 = "This is password2" -KEYRINGRC = "keyringrc.cfg" + + +@pytest.yield_fixture() +def config_filename(tmpdir): + filename = tmpdir / 'keyringrc.cfg' + with mock.patch('keyring.util.platform_.config_root', lambda: str(tmpdir)): + yield str(filename) class TestKeyring(keyring.backend.KeyringBackend): @@ -53,7 +59,7 @@ class TestKeyring2(TestKeyring): return PASSWORD_TEXT_2 -class CoreTestCase(unittest.TestCase): +class TestCore: mock_global_backend = mock.patch('keyring.core._keyring_backend') @mock_global_backend @@ -72,7 +78,7 @@ class CoreTestCase(unittest.TestCase): """ result = keyring.core.get_password("test", "user") backend.get_password.assert_called_once_with('test', 'user') - self.assertIsNotNone(result) + assert result is not None @mock_global_backend def test_delete_password(self, backend): @@ -85,71 +91,40 @@ class CoreTestCase(unittest.TestCase): keyring.core.set_keyring(TestKeyring()) keyring.core.set_password("test", "user", "password") - self.assertEqual(keyring.core.get_password("test", "user"), - PASSWORD_TEXT) + assert keyring.core.get_password("test", "user") == PASSWORD_TEXT - def test_set_keyring_in_config(self): + def test_set_keyring_in_config(self, config_filename): """Test setting the keyring by config file. """ # create the config file - config_file = open(KEYRINGRC, 'w') - config_file.writelines([ - "[backend]\n", - # the path for the user created keyring - "keyring-path= %s\n" % os.path.dirname(os.path.abspath(__file__)), - # the name of the keyring class - "default-keyring=test_core.TestKeyring2\n", - ]) - config_file.close() + with open(config_filename, 'w') as config_file: + config_file.writelines([ + "[backend]\n", + # the path for the user created keyring + "keyring-path= %s\n" % os.path.dirname(os.path.abspath(__file__)), + # the name of the keyring class + "default-keyring=test_core.TestKeyring2\n", + ]) # init the keyring lib, the lib will automaticlly load the # config file and load the user defined module keyring.core.init_backend() keyring.core.set_password("test", "user", "password") - self.assertEqual(keyring.core.get_password("test", "user"), - PASSWORD_TEXT_2) + assert keyring.core.get_password("test", "user") == PASSWORD_TEXT_2 - os.remove(KEYRINGRC) + def test_load_config_empty(self, config_filename): + "A non-existent or empty config should load" + assert keyring.core.load_config() is None - def test_load_config(self): - tempdir = tempfile.mkdtemp() - old_location = os.getcwd() - os.chdir(tempdir) - personal_cfg = os.path.join(os.path.expanduser("~"), "keyringrc.cfg") - if os.path.exists(personal_cfg): - os.rename(personal_cfg, personal_cfg + '.old') - personal_renamed = True - else: - personal_renamed = False + def test_load_config_degenerate(self, config_filename): + "load_config should succeed in the absence of a backend section" + with open(config_filename, 'w') as config_file: + config_file.write('[keyring]') + assert keyring.core.load_config() is None - # loading with an empty environment - keyring.core.load_config() - - # loading with a file that doesn't have a backend section - cfg = os.path.join(tempdir, "keyringrc.cfg") - f = open(cfg, 'w') - f.write('[keyring]') - f.close() - keyring.core.load_config() - - # loading with a file that doesn't have a default-keyring value - cfg = os.path.join(tempdir, "keyringrc.cfg") - f = open(cfg, 'w') - f.write('[backend]') - f.close() - keyring.core.load_config() - - os.chdir(old_location) - shutil.rmtree(tempdir) - if personal_renamed: - os.rename(personal_cfg + '.old', personal_cfg) - - -def test_suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CoreTestCase)) - return suite - -if __name__ == "__main__": - unittest.main(defaultTest="test_suite") + def test_load_config_blank_backend(self, config_filename): + "load_config should succeed with an empty [backend] section" + with open(config_filename, 'w') as config_file: + config_file.write('[backend]') + assert keyring.core.load_config() is None