diff -Nru python-launchpadlib-1.10.10/debian/changelog python-launchpadlib-1.10.13/debian/changelog --- python-launchpadlib-1.10.10/debian/changelog 2020-02-04 14:59:12.000000000 +0000 +++ python-launchpadlib-1.10.13/debian/changelog 2020-04-19 09:35:39.000000000 +0000 @@ -1,3 +1,29 @@ +python-launchpadlib (1.10.13-1) unstable; urgency=medium + + * New upstream release. + - Fix test runs under sudo. + + -- Colin Watson Sun, 19 Apr 2020 10:35:39 +0100 + +python-launchpadlib (1.10.12-1) unstable; urgency=medium + + * New upstream release. + - Postpone keyring.errors import in the same way that we postpone + importing keyring itself. (This should fix autopkgtest failures on + Ubuntu.) + + -- Colin Watson Fri, 17 Apr 2020 10:34:55 +0100 + +python-launchpadlib (1.10.11-1) unstable; urgency=medium + + * New upstream release. + - Don't store credentials or open a browser window when running under + sudo (LP: #1825014, #1862948). + - Fall back to in-memory credentials store if no keyring backend is + available (LP: #1864204). + + -- Colin Watson Tue, 14 Apr 2020 12:42:47 +0100 + python-launchpadlib (1.10.10-1) unstable; urgency=medium [ Debian Janitor ] diff -Nru python-launchpadlib-1.10.10/NEWS.rst python-launchpadlib-1.10.13/NEWS.rst --- python-launchpadlib-1.10.10/NEWS.rst 2020-02-04 14:36:23.000000000 +0000 +++ python-launchpadlib-1.10.13/NEWS.rst 2020-04-19 09:31:30.000000000 +0000 @@ -2,6 +2,22 @@ NEWS for launchpadlib ===================== +1.10.13 (2020-04-19) +==================== +- Fix test runs under sudo. + +1.10.12 (2020-04-17) +==================== +- Postpone keyring.errors import in the same way that we postpone importing + keyring itself. + +1.10.11 (2020-04-14) +==================== +- Don't store credentials or open a browser window when running under sudo. + [bug=1825014,1862948] +- Fall back to in-memory credentials store if no keyring backend is + available. [bug=1864204] + 1.10.10 (2020-02-04) ==================== - Fix AccessToken.from_string crash on Python 3.8. [bug=1861873] diff -Nru python-launchpadlib-1.10.10/PKG-INFO python-launchpadlib-1.10.13/PKG-INFO --- python-launchpadlib-1.10.10/PKG-INFO 2020-02-04 14:37:30.000000000 +0000 +++ python-launchpadlib-1.10.13/PKG-INFO 2020-04-19 09:31:54.523252700 +0000 @@ -1,10 +1,12 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: launchpadlib -Version: 1.10.10 +Version: 1.10.13 Summary: Script Launchpad through its web services interfaces. Officially supported. Home-page: https://help.launchpad.net/API/launchpadlib -Author: LAZR Developers -Author-email: lazr-developers@lists.launchpad.net +Author: The Launchpad developers +Author-email: launchpadlib@lists.launchpad.net +Maintainer: LAZR Developers +Maintainer-email: lazr-developers@lists.launchpad.net License: LGPL v3 Download-URL: https://launchpad.net/launchpadlib/+download Description: .. @@ -32,6 +34,22 @@ NEWS for launchpadlib ===================== + 1.10.13 (2020-04-19) + ==================== + - Fix test runs under sudo. + + 1.10.12 (2020-04-17) + ==================== + - Postpone keyring.errors import in the same way that we postpone importing + keyring itself. + + 1.10.11 (2020-04-14) + ==================== + - Don't store credentials or open a browser window when running under sudo. + [bug=1825014,1862948] + - Fall back to in-memory credentials store if no keyring backend is + available. [bug=1864204] + 1.10.10 (2020-02-04) ==================== - Fix AccessToken.from_string crash on Python 3.8. [bug=1861873] @@ -365,3 +383,5 @@ Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 +Provides-Extra: docs +Provides-Extra: test diff -Nru python-launchpadlib-1.10.10/setup.cfg python-launchpadlib-1.10.13/setup.cfg --- python-launchpadlib-1.10.10/setup.cfg 2020-02-04 14:37:30.000000000 +0000 +++ python-launchpadlib-1.10.13/setup.cfg 2020-04-19 09:31:54.523252700 +0000 @@ -1,5 +1,4 @@ [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 diff -Nru python-launchpadlib-1.10.10/setup.py python-launchpadlib-1.10.13/setup.py --- python-launchpadlib-1.10.10/setup.py 2020-02-04 14:35:52.000000000 +0000 +++ python-launchpadlib-1.10.13/setup.py 2020-04-19 09:29:30.000000000 +0000 @@ -88,6 +88,9 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - extras_require={'docs': ['Sphinx']}, + extras_require={ + 'docs': ['Sphinx'], + 'test': ['mock; python_version < "3"'], + }, test_suite='launchpadlib.tests', ) diff -Nru python-launchpadlib-1.10.10/src/launchpadlib/credentials.py python-launchpadlib-1.10.13/src/launchpadlib/credentials.py --- python-launchpadlib-1.10.10/src/launchpadlib/credentials.py 2020-02-04 14:35:52.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib/credentials.py 2020-04-17 09:29:12.000000000 +0000 @@ -359,6 +359,12 @@ B64MARKER = b"" + def __init__(self, credential_save_failed=None, fallback=False): + super(KeyringCredentialStore, self).__init__(credential_save_failed) + self._fallback = None + if fallback: + self._fallback = MemoryCredentialStore(credential_save_failed) + @staticmethod def _ensure_keyring_imported(): """Ensure the keyring module is imported (postponing side effects). @@ -371,6 +377,12 @@ if 'keyring' not in globals(): global keyring import keyring + if 'NoKeyringError' not in globals(): + global NoKeyringError + try: + from keyring.errors import NoKeyringError + except ImportError: + NoKeyringError = RuntimeError def do_save(self, credentials, unique_key): """Store newly-authorized credentials in the keyring.""" @@ -380,14 +392,36 @@ # Gnome and KDE, when newlines are included in the password. Avoid # this problem by base 64 encoding the serialized value. serialized = self.B64MARKER + b64encode(serialized) - keyring.set_password( - 'launchpadlib', unique_key, serialized.decode('utf-8')) + try: + keyring.set_password( + 'launchpadlib', unique_key, serialized.decode('utf-8')) + except NoKeyringError as e: + # keyring < 21.2.0 raises RuntimeError rather than anything more + # specific. Make sure it's the exception we're interested in. + if (NoKeyringError == RuntimeError and + 'No recommended backend was available' not in str(e)): + raise + if self._fallback: + self._fallback.save(credentials, unique_key) + else: + raise def do_load(self, unique_key): """Retrieve credentials from the keyring.""" self._ensure_keyring_imported() - credential_string = keyring.get_password( - 'launchpadlib', unique_key) + try: + credential_string = keyring.get_password( + 'launchpadlib', unique_key) + except NoKeyringError as e: + # keyring < 21.2.0 raises RuntimeError rather than anything more + # specific. Make sure it's the exception we're interested in. + if (NoKeyringError == RuntimeError and + 'No recommended backend was available' not in str(e)): + raise + if self._fallback: + return self._fallback.load(unique_key) + else: + raise if credential_string is not None: if isinstance(credential_string, unicode_type): credential_string = credential_string.encode('utf8') @@ -434,6 +468,25 @@ return None +class MemoryCredentialStore(CredentialStore): + """CredentialStore that stores keys only in memory. + + This can be used to provide a CredentialStore instance without + actually saving any key to persistent storage. + """ + def __init__(self, credential_save_failed=None): + super(MemoryCredentialStore, self).__init__(credential_save_failed) + self._credentials = {} + + def do_save(self, credentials, unique_key): + """Store the credentials in our dict""" + self._credentials[unique_key] = credentials + + def do_load(self, unique_key): + """Retrieve the credentials from our dict""" + return self._credentials.get(unique_key) + + class RequestTokenAuthorizationEngine(object): """The superclass of all request token authorizers. @@ -579,12 +632,75 @@ raise NotImplementedError() -class AuthorizeRequestTokenWithBrowser(RequestTokenAuthorizationEngine): - """The simplest (and, right now, the only) request token authorizer. +class AuthorizeRequestTokenWithURL(RequestTokenAuthorizationEngine): + """Authorize using a URL. + + This authorizer simply shows the URL for the user to open for + authorization, and waits until the server responds. + """ + + WAITING_FOR_USER = ( + "Please open this authorization page:\n" + " (%s)\n" + "in your browser. Use your browser to authorize\n" + "this program to access Launchpad on your behalf.") + WAITING_FOR_LAUNCHPAD = ( + "Press Enter after authorizing in your browser.") + + def output(self, message): + """Display a message. + + By default, prints the message to standard output. The message + does not require any user interaction--it's solely + informative. + """ + print(message) + + def notify_end_user_authorization_url(self, authorization_url): + """Notify the end-user of the URL.""" + self.output(self.WAITING_FOR_USER % authorization_url) + + def check_end_user_authorization(self, credentials): + """Check if the end-user authorized""" + try: + credentials.exchange_request_token_for_access_token( + self.web_root) + except HTTPError as e: + if e.response.status == 403: + # The user decided not to authorize this + # application. + raise EndUserDeclinedAuthorization(e.content) + else: + if e.response.status != 401: + # There was an error accessing the server. + print("Unexpected response from Launchpad:") + print(e) + # The user has not made a decision yet. + raise EndUserNoAuthorization(e.content) + return credentials.access_token is not None + + def wait_for_end_user_authorization(self, credentials): + """Wait for the end-user to authorize""" + self.output(self.WAITING_FOR_LAUNCHPAD) + stdin.readline() + self.check_end_user_authorization(credentials) + + def make_end_user_authorize_token(self, credentials, request_token): + """Have the end-user authorize the token using a URL.""" + authorization_url = self.authorization_url(request_token) + self.notify_end_user_authorization_url(authorization_url) + self.wait_for_end_user_authorization(credentials) + + +class AuthorizeRequestTokenWithBrowser(AuthorizeRequestTokenWithURL): + """Authorize using a URL that pops-up automatically in a browser. This authorizer simply opens up the end-user's web browser to a Launchpad URL and lets the end-user authorize the request token themselves. + + This is the same as its superclass, except this class also + performs the browser automatic opening of the URL. """ WAITING_FOR_USER = ( @@ -594,10 +710,10 @@ "this program to access Launchpad on your behalf.") TIMEOUT_MESSAGE = "Press Enter to continue or wait (%d) seconds..." TIMEOUT = 5 - WAITING_FOR_LAUNCHPAD = ( - "Waiting to hear from Launchpad about your decision...") TERMINAL_BROWSERS = ('www-browser', 'links', 'links2', 'lynx', 'elinks', 'elinks-lite', 'netrik', 'w3m') + WAITING_FOR_LAUNCHPAD = ( + "Waiting to hear from Launchpad about your decision...") def __init__(self, service_root, application_name, consumer_name=None, credential_save_failed=None, allow_access_levels=None): @@ -620,20 +736,10 @@ service_root, application_name, None, credential_save_failed) - def output(self, message): - """Display a message. - - By default, prints the message to standard output. The message - does not require any user interaction--it's solely - informative. - """ - print(message) - - def make_end_user_authorize_token(self, credentials, request_token): - """Have the end-user authorize the token in their browser.""" - - authorization_url = self.authorization_url(request_token) - self.output(self.WAITING_FOR_USER % authorization_url) + def notify_end_user_authorization_url(self, authorization_url): + """Notify the end-user of the URL.""" + super(AuthorizeRequestTokenWithBrowser, + self).notify_end_user_authorization_url(authorization_url) try: browser_obj = webbrowser.get() @@ -651,28 +757,20 @@ if rlist: stdin.readline() - self.output(self.WAITING_FOR_LAUNCHPAD) if browser_obj is not None: webbrowser.open(authorization_url) + + def wait_for_end_user_authorization(self, credentials): + """Wait for the end-user to authorize""" + self.output(self.WAITING_FOR_LAUNCHPAD) start_time = time.time() while credentials.access_token is None: time.sleep(access_token_poll_time) try: - credentials.exchange_request_token_for_access_token( - self.web_root) - break - except HTTPError as e: - if e.response.status == 403: - # The user decided not to authorize this - # application. - raise EndUserDeclinedAuthorization(e.content) - elif e.response.status == 401: - # The user has not made a decision yet. - pass - else: - # There was an error accessing the server. - print("Unexpected response from Launchpad:") - print(e) + if self.check_end_user_authorization(credentials): + break + except EndUserNoAuthorization: + pass if time.time() >= start_time + access_token_poll_timeout: raise TokenAuthorizationTimedOut( "Timed out after %d seconds." % access_token_poll_timeout) @@ -686,11 +784,23 @@ pass -class EndUserDeclinedAuthorization(TokenAuthorizationException): +class EndUserAuthorizationFailed(TokenAuthorizationException): + """Superclass exception for all failures of end-user authorization""" + pass + + +class EndUserDeclinedAuthorization(EndUserAuthorizationFailed): + """End-user declined authorization""" + pass + + +class EndUserNoAuthorization(EndUserAuthorizationFailed): + """End-user did not perform any authorization""" pass -class TokenAuthorizationTimedOut(TokenAuthorizationException): +class TokenAuthorizationTimedOut(EndUserNoAuthorization): + """End-user did not perform any authorization in timeout period""" pass diff -Nru python-launchpadlib-1.10.10/src/launchpadlib/docs/NEWS.rst python-launchpadlib-1.10.13/src/launchpadlib/docs/NEWS.rst --- python-launchpadlib-1.10.10/src/launchpadlib/docs/NEWS.rst 2020-02-04 14:36:23.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib/docs/NEWS.rst 2020-04-19 09:31:30.000000000 +0000 @@ -2,6 +2,22 @@ NEWS for launchpadlib ===================== +1.10.13 (2020-04-19) +==================== +- Fix test runs under sudo. + +1.10.12 (2020-04-17) +==================== +- Postpone keyring.errors import in the same way that we postpone importing + keyring itself. + +1.10.11 (2020-04-14) +==================== +- Don't store credentials or open a browser window when running under sudo. + [bug=1825014,1862948] +- Fall back to in-memory credentials store if no keyring backend is + available. [bug=1864204] + 1.10.10 (2020-02-04) ==================== - Fix AccessToken.from_string crash on Python 3.8. [bug=1861873] diff -Nru python-launchpadlib-1.10.10/src/launchpadlib/launchpad.py python-launchpadlib-1.10.13/src/launchpadlib/launchpad.py --- python-launchpadlib-1.10.10/src/launchpadlib/launchpad.py 2015-11-17 18:20:08.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib/launchpad.py 2020-04-19 09:24:55.000000000 +0000 @@ -47,8 +47,10 @@ AccessToken, AnonymousAccessToken, AuthorizeRequestTokenWithBrowser, + AuthorizeRequestTokenWithURL, Consumer, Credentials, + MemoryCredentialStore, KeyringCredentialStore, UnencryptedFileCredentialStore, ) @@ -213,12 +215,29 @@ proxy_info) @classmethod + def _is_sudo(cls): + return (set(['SUDO_USER', 'SUDO_UID', 'SUDO_GID']) & + set(os.environ.keys())) + + @classmethod def authorization_engine_factory(cls, *args): + if cls._is_sudo(): + # Do not try to open browser window under sudo; + # we probably don't have access to the X session, + # and some browsers (e.g. chromium) won't run as root + # LP: #1825014 + return AuthorizeRequestTokenWithURL(*args) return AuthorizeRequestTokenWithBrowser(*args) @classmethod def credential_store_factory(cls, credential_save_failed): - return KeyringCredentialStore(credential_save_failed) + if cls._is_sudo(): + # Do not try to store credentials under sudo; + # it can be problematic with shared sudo access, + # and we may not have access to the normal keyring provider + # LP: #1862948 + return MemoryCredentialStore(credential_save_failed) + return KeyringCredentialStore(credential_save_failed, fallback=True) @classmethod def login(cls, consumer_name, token_string, access_secret, diff -Nru python-launchpadlib-1.10.10/src/launchpadlib/testing/helpers.py python-launchpadlib-1.10.13/src/launchpadlib/testing/helpers.py --- python-launchpadlib-1.10.10/src/launchpadlib/testing/helpers.py 2012-06-26 11:00:47.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib/testing/helpers.py 2020-04-17 09:29:12.000000000 +0000 @@ -133,10 +133,12 @@ # The real keyring package should never be imported during tests. assert_keyring_not_imported() launchpadlib.credentials.keyring = fake + launchpadlib.credentials.NoKeyringError = RuntimeError try: yield finally: del launchpadlib.credentials.keyring + del launchpadlib.credentials.NoKeyringError class FauxSocketModule: diff -Nru python-launchpadlib-1.10.10/src/launchpadlib/tests/test_launchpad.py python-launchpadlib-1.10.13/src/launchpadlib/tests/test_launchpad.py --- python-launchpadlib-1.10.10/src/launchpadlib/tests/test_launchpad.py 2019-11-22 17:14:30.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib/tests/test_launchpad.py 2020-04-19 09:29:30.000000000 +0000 @@ -25,6 +25,10 @@ import stat import tempfile import unittest +try: + from unittest.mock import patch +except ImportError: + from mock import patch import warnings from lazr.restfulclient.resource import ServiceRoot @@ -612,6 +616,7 @@ launchpadlib.launchpad.socket = socket shutil.rmtree(self.temp_dir) + @patch.object(NoNetworkLaunchpad, '_is_sudo', staticmethod(lambda: False)) def test_credentials_save_failed(self): # If saving the credentials did not succeed and a callback was # provided, it is called. @@ -631,6 +636,7 @@ credential_save_failed=callback) self.assertEqual(len(callback_called), 1) + @patch.object(NoNetworkLaunchpad, '_is_sudo', staticmethod(lambda: False)) def test_default_credentials_save_failed_is_to_raise_exception(self): # If saving the credentials did not succeed and no callback was # provided, the underlying exception is raised. @@ -643,6 +649,17 @@ 'not important', service_root=service_root, launchpadlib_dir=launchpadlib_dir) + @patch.object(NoNetworkLaunchpad, '_is_sudo', staticmethod(lambda: True)) + def test_credentials_save_fail_under_sudo_does_not_raise_exception(self): + # When running under sudo, Launchpad will not attempt to use + # the keyring, so credential save failure will never happen + launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib') + service_root = "http://api.example.com/" + with fake_keyring(BadSaveKeyring()): + NoNetworkLaunchpad.login_with( + 'not important', service_root=service_root, + launchpadlib_dir=launchpadlib_dir) + class TestMultipleSites(unittest.TestCase): # If the same application name (consumer name) is used to access more than @@ -660,6 +677,7 @@ launchpadlib.launchpad.socket = socket shutil.rmtree(self.temp_dir) + @patch.object(NoNetworkLaunchpad, '_is_sudo', staticmethod(lambda: False)) def test_components_of_application_key(self): launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib') keyring = InMemoryKeyring() @@ -684,6 +702,7 @@ # "forgotten"). self.assertEqual(application_key, consumer_name + '@' + service_root) + @patch.object(NoNetworkLaunchpad, '_is_sudo', staticmethod(lambda: False)) def test_same_app_different_servers(self): launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib') keyring = InMemoryKeyring() diff -Nru python-launchpadlib-1.10.10/src/launchpadlib/version.txt python-launchpadlib-1.10.13/src/launchpadlib/version.txt --- python-launchpadlib-1.10.10/src/launchpadlib/version.txt 2020-02-04 14:37:26.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib/version.txt 2020-04-19 09:31:34.000000000 +0000 @@ -1 +1 @@ -1.10.10 +1.10.13 diff -Nru python-launchpadlib-1.10.10/src/launchpadlib.egg-info/PKG-INFO python-launchpadlib-1.10.13/src/launchpadlib.egg-info/PKG-INFO --- python-launchpadlib-1.10.10/src/launchpadlib.egg-info/PKG-INFO 2020-02-04 14:37:28.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib.egg-info/PKG-INFO 2020-04-19 09:31:52.000000000 +0000 @@ -1,10 +1,12 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: launchpadlib -Version: 1.10.10 +Version: 1.10.13 Summary: Script Launchpad through its web services interfaces. Officially supported. Home-page: https://help.launchpad.net/API/launchpadlib -Author: LAZR Developers -Author-email: lazr-developers@lists.launchpad.net +Author: The Launchpad developers +Author-email: launchpadlib@lists.launchpad.net +Maintainer: LAZR Developers +Maintainer-email: lazr-developers@lists.launchpad.net License: LGPL v3 Download-URL: https://launchpad.net/launchpadlib/+download Description: .. @@ -32,6 +34,22 @@ NEWS for launchpadlib ===================== + 1.10.13 (2020-04-19) + ==================== + - Fix test runs under sudo. + + 1.10.12 (2020-04-17) + ==================== + - Postpone keyring.errors import in the same way that we postpone importing + keyring itself. + + 1.10.11 (2020-04-14) + ==================== + - Don't store credentials or open a browser window when running under sudo. + [bug=1825014,1862948] + - Fall back to in-memory credentials store if no keyring backend is + available. [bug=1864204] + 1.10.10 (2020-02-04) ==================== - Fix AccessToken.from_string crash on Python 3.8. [bug=1861873] @@ -365,3 +383,5 @@ Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 +Provides-Extra: docs +Provides-Extra: test diff -Nru python-launchpadlib-1.10.10/src/launchpadlib.egg-info/requires.txt python-launchpadlib-1.10.13/src/launchpadlib.egg-info/requires.txt --- python-launchpadlib-1.10.10/src/launchpadlib.egg-info/requires.txt 2020-02-04 14:37:28.000000000 +0000 +++ python-launchpadlib-1.10.13/src/launchpadlib.egg-info/requires.txt 2020-04-19 09:31:52.000000000 +0000 @@ -9,3 +9,8 @@ [docs] Sphinx + +[test] + +[test:python_version < "3"] +mock