From 08dc580cd08bdeae121d7cde65fbc4c6c1a4aa36 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Sat, 11 Nov 2017 02:49:57 +0000 Subject: [PATCH 01/26] Add requests_kerberos as vendored package. --- src/pip/_vendor/README.rst | 2 + src/pip/_vendor/requests_kerberos/__init__.py | 25 ++ src/pip/_vendor/requests_kerberos/compat.py | 14 + .../_vendor/requests_kerberos/exceptions.py | 15 + .../_vendor/requests_kerberos/kerberos_.py | 323 ++++++++++++++++++ src/pip/_vendor/vendor.txt | 1 + 6 files changed, 380 insertions(+) create mode 100644 src/pip/_vendor/requests_kerberos/__init__.py create mode 100644 src/pip/_vendor/requests_kerberos/compat.py create mode 100644 src/pip/_vendor/requests_kerberos/exceptions.py create mode 100644 src/pip/_vendor/requests_kerberos/kerberos_.py diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 2f304b1a39a..1034b186801 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -97,6 +97,8 @@ Modifications * ``pkg_resources`` has been modified to import its dependencies from ``pip._vendor`` * ``CacheControl`` has been modified to import its dependencies from ``pip._vendor`` * ``packaging`` has been modified to import its dependencies from ``pip._vendor`` +* ``requests_kerberos`` has been modified to import its dependencies from ``pip +._vendor`` * ``requests`` has been modified *not* to optionally load any C dependencies * Modified distro to delay importing ``argparse`` to avoid errors on 2.6 diff --git a/src/pip/_vendor/requests_kerberos/__init__.py b/src/pip/_vendor/requests_kerberos/__init__.py new file mode 100644 index 00000000000..d8dea418e7b --- /dev/null +++ b/src/pip/_vendor/requests_kerberos/__init__.py @@ -0,0 +1,25 @@ +""" +requests Kerberos/GSSAPI authentication library +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Requests is an HTTP library, written in Python, for human beings. This library +adds optional Kerberos/GSSAPI authentication support and supports mutual +authentication. Basic GET usage: + + >>> import pip._vendor.requests + >>> from pip._vendor.requests_kerberos import HTTPKerberosAuth + >>> r = pip._vendor.requests.get("http://example.org", auth=HTTPKerberosAuth()) + +The entire `requests.api` should be supported. +""" +import logging + +from .kerberos_ import HTTPKerberosAuth, REQUIRED, OPTIONAL, DISABLED +from .exceptions import MutualAuthenticationError +from .compat import NullHandler + +logging.getLogger(__name__).addHandler(NullHandler()) + +__all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', + 'OPTIONAL', 'DISABLED') +__version__ = '0.11.0' diff --git a/src/pip/_vendor/requests_kerberos/compat.py b/src/pip/_vendor/requests_kerberos/compat.py new file mode 100644 index 00000000000..01b75009805 --- /dev/null +++ b/src/pip/_vendor/requests_kerberos/compat.py @@ -0,0 +1,14 @@ +""" +Compatibility library for older versions of python +""" +import sys + +# python 2.7 introduced a NullHandler which we want to use, but to support +# older versions, we implement our own if needed. +if sys.version_info[:2] > (2, 6): + from logging import NullHandler +else: + from logging import Handler + class NullHandler(Handler): + def emit(self, record): + pass diff --git a/src/pip/_vendor/requests_kerberos/exceptions.py b/src/pip/_vendor/requests_kerberos/exceptions.py new file mode 100644 index 00000000000..db1ca771495 --- /dev/null +++ b/src/pip/_vendor/requests_kerberos/exceptions.py @@ -0,0 +1,15 @@ +""" +requests_kerberos.exceptions +~~~~~~~~~~~~~~~~~~~ + +This module contains the set of exceptions. + +""" +from pip._vendor.requests.exceptions import RequestException + + +class MutualAuthenticationError(RequestException): + """Mutual Authentication Error""" + +class KerberosExchangeError(RequestException): + """Kerberos Exchange Failed Error""" diff --git a/src/pip/_vendor/requests_kerberos/kerberos_.py b/src/pip/_vendor/requests_kerberos/kerberos_.py new file mode 100644 index 00000000000..353187af234 --- /dev/null +++ b/src/pip/_vendor/requests_kerberos/kerberos_.py @@ -0,0 +1,323 @@ +try: + import kerberos +except ImportError: + import winkerberos as kerberos +import re +import logging + +from pip._vendor.requests.auth import AuthBase +from pip._vendor.requests.models import Response +from pip._vendor.requests.compat import urlparse, StringIO +from pip._vendor.requests.structures import CaseInsensitiveDict +from pip._vendor.requests.cookies import cookiejar_from_dict + +from .exceptions import MutualAuthenticationError, KerberosExchangeError + +log = logging.getLogger(__name__) + +# Different types of mutual authentication: +# with mutual_authentication set to REQUIRED, all responses will be +# authenticated with the exception of errors. Errors will have their contents +# and headers stripped. If a non-error response cannot be authenticated, a +# MutualAuthenticationError exception will be raised. +# with mutual_authentication set to OPTIONAL, mutual authentication will be +# attempted if supported, and if supported and failed, a +# MutualAuthenticationError exception will be raised. Responses which do not +# support mutual authentication will be returned directly to the user. +# with mutual_authentication set to DISABLED, mutual authentication will not be +# attempted, even if supported. +REQUIRED = 1 +OPTIONAL = 2 +DISABLED = 3 + +class SanitizedResponse(Response): + """The :class:`Response ` object, which contains a server's + response to an HTTP request. + + This differs from `requests.models.Response` in that it's headers and + content have been sanitized. This is only used for HTTP Error messages + which do not support mutual authentication when mutual authentication is + required.""" + + def __init__(self, response): + super(SanitizedResponse, self).__init__() + self.status_code = response.status_code + self.encoding = response.encoding + self.raw = response.raw + self.reason = response.reason + self.url = response.url + self.request = response.request + self.connection = response.connection + self._content_consumed = True + + self._content = "" + self.cookies = cookiejar_from_dict({}) + self.headers = CaseInsensitiveDict() + self.headers['content-length'] = '0' + for header in ('date', 'server'): + if header in response.headers: + self.headers[header] = response.headers[header] + + +def _negotiate_value(response): + """Extracts the gssapi authentication token from the appropriate header""" + if hasattr(_negotiate_value, 'regex'): + regex = _negotiate_value.regex + else: + # There's no need to re-compile this EVERY time it is called. Compile + # it once and you won't have the performance hit of the compilation. + regex = re.compile('(?:.*,)*\s*Negotiate\s*([^,]*),?', re.I) + _negotiate_value.regex = regex + + authreq = response.headers.get('www-authenticate', None) + + if authreq: + match_obj = regex.search(authreq) + if match_obj: + return match_obj.group(1) + + return None + + +class HTTPKerberosAuth(AuthBase): + """Attaches HTTP GSSAPI/Kerberos Authentication to the given Request + object.""" + def __init__( + self, mutual_authentication=REQUIRED, + service="HTTP", delegate=False, force_preemptive=False, + principal=None, hostname_override=None, sanitize_mutual_error_response=True): + self.context = {} + self.mutual_authentication = mutual_authentication + self.delegate = delegate + self.pos = None + self.service = service + self.force_preemptive = force_preemptive + self.principal = principal + self.hostname_override = hostname_override + self.sanitize_mutual_error_response = sanitize_mutual_error_response + + def generate_request_header(self, response, host, is_preemptive=False): + """ + Generates the GSSAPI authentication token with kerberos. + + If any GSSAPI step fails, raise KerberosExchangeError + with failure detail. + + """ + + # Flags used by kerberos module. + gssflags = kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG + if self.delegate: + gssflags |= kerberos.GSS_C_DELEG_FLAG + + try: + kerb_stage = "authGSSClientInit()" + # contexts still need to be stored by host, but hostname_override + # allows use of an arbitrary hostname for the kerberos exchange + # (eg, in cases of aliased hosts, internal vs external, CNAMEs + # w/ name-based HTTP hosting) + kerb_host = self.hostname_override if self.hostname_override is not None else host + kerb_spn = "{0}@{1}".format(self.service, kerb_host) + + result, self.context[host] = kerberos.authGSSClientInit(kerb_spn, + gssflags=gssflags, principal=self.principal) + + if result < 1: + raise EnvironmentError(result, kerb_stage) + + # if we have a previous response from the server, use it to continue + # the auth process, otherwise use an empty value + negotiate_resp_value = '' if is_preemptive else _negotiate_value(response) + + kerb_stage = "authGSSClientStep()" + result = kerberos.authGSSClientStep(self.context[host], + negotiate_resp_value) + + if result < 0: + raise EnvironmentError(result, kerb_stage) + + kerb_stage = "authGSSClientResponse()" + gss_response = kerberos.authGSSClientResponse(self.context[host]) + + return "Negotiate {0}".format(gss_response) + + except kerberos.GSSError as error: + log.exception( + "generate_request_header(): {0} failed:".format(kerb_stage)) + log.exception(error) + raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error.args))) + + except EnvironmentError as error: + # ensure we raised this for translation to KerberosExchangeError + # by comparing errno to result, re-raise if not + if error.errno != result: + raise + message = "{0} failed, result: {1}".format(kerb_stage, result) + log.error("generate_request_header(): {0}".format(message)) + raise KerberosExchangeError(message) + + def authenticate_user(self, response, **kwargs): + """Handles user authentication with gssapi/kerberos""" + + host = urlparse(response.url).hostname + + try: + auth_header = self.generate_request_header(response, host) + except KerberosExchangeError: + # GSS Failure, return existing response + return response + + log.debug("authenticate_user(): Authorization header: {0}".format( + auth_header)) + response.request.headers['Authorization'] = auth_header + + # Consume the content so we can reuse the connection for the next + # request. + response.content + response.raw.release_conn() + + _r = response.connection.send(response.request, **kwargs) + _r.history.append(response) + + log.debug("authenticate_user(): returning {0}".format(_r)) + return _r + + def handle_401(self, response, **kwargs): + """Handles 401's, attempts to use gssapi/kerberos authentication""" + + log.debug("handle_401(): Handling: 401") + if _negotiate_value(response) is not None: + _r = self.authenticate_user(response, **kwargs) + log.debug("handle_401(): returning {0}".format(_r)) + return _r + else: + log.debug("handle_401(): Kerberos is not supported") + log.debug("handle_401(): returning {0}".format(response)) + return response + + def handle_other(self, response): + """Handles all responses with the exception of 401s. + + This is necessary so that we can authenticate responses if requested""" + + log.debug("handle_other(): Handling: %d" % response.status_code) + + if self.mutual_authentication in (REQUIRED, OPTIONAL): + + is_http_error = response.status_code >= 400 + + if _negotiate_value(response) is not None: + log.debug("handle_other(): Authenticating the server") + if not self.authenticate_server(response): + # Mutual authentication failure when mutual auth is wanted, + # raise an exception so the user doesn't use an untrusted + # response. + log.error("handle_other(): Mutual authentication failed") + raise MutualAuthenticationError("Unable to authenticate " + "{0}".format(response)) + + # Authentication successful + log.debug("handle_other(): returning {0}".format(response)) + return response + + elif is_http_error or self.mutual_authentication == OPTIONAL: + if not response.ok: + log.error("handle_other(): Mutual authentication unavailable " + "on {0} response".format(response.status_code)) + + if(self.mutual_authentication == REQUIRED and + self.sanitize_mutual_error_response): + return SanitizedResponse(response) + else: + return response + else: + # Unable to attempt mutual authentication when mutual auth is + # required, raise an exception so the user doesnt use an + # untrusted response. + log.error("handle_other(): Mutual authentication failed") + raise MutualAuthenticationError("Unable to authenticate " + "{0}".format(response)) + else: + log.debug("handle_other(): returning {0}".format(response)) + return response + + def authenticate_server(self, response): + """ + Uses GSSAPI to authenticate the server. + + Returns True on success, False on failure. + """ + + log.debug("authenticate_server(): Authenticate header: {0}".format( + _negotiate_value(response))) + + host = urlparse(response.url).hostname + + try: + result = kerberos.authGSSClientStep(self.context[host], + _negotiate_value(response)) + except kerberos.GSSError: + log.exception("authenticate_server(): authGSSClientStep() failed:") + return False + + if result < 1: + log.error("authenticate_server(): authGSSClientStep() failed: " + "{0}".format(result)) + return False + + log.debug("authenticate_server(): returning {0}".format(response)) + return True + + def handle_response(self, response, **kwargs): + """Takes the given response and tries kerberos-auth, as needed.""" + num_401s = kwargs.pop('num_401s', 0) + + if self.pos is not None: + # Rewind the file position indicator of the body to where + # it was to resend the request. + response.request.body.seek(self.pos) + + if response.status_code == 401 and num_401s < 2: + # 401 Unauthorized. Handle it, and if it still comes back as 401, + # that means authentication failed. + _r = self.handle_401(response, **kwargs) + log.debug("handle_response(): returning %s", _r) + log.debug("handle_response() has seen %d 401 responses", num_401s) + num_401s += 1 + return self.handle_response(_r, num_401s=num_401s, **kwargs) + elif response.status_code == 401 and num_401s >= 2: + # Still receiving 401 responses after attempting to handle them. + # Authentication has failed. Return the 401 response. + log.debug("handle_response(): returning 401 %s", response) + return response + else: + _r = self.handle_other(response) + log.debug("handle_response(): returning %s", _r) + return _r + + def deregister(self, response): + """Deregisters the response handler""" + response.request.deregister_hook('response', self.handle_response) + + def __call__(self, request): + if self.force_preemptive: + # add Authorization header before we receive a 401 + # by the 401 handler + host = urlparse(request.url).hostname + + auth_header = self.generate_request_header(None, host, is_preemptive=True) + + log.debug("HTTPKerberosAuth: Preemptive Authorization header: {0}".format(auth_header)) + + request.headers['Authorization'] = auth_header + + request.register_hook('response', self.handle_response) + try: + self.pos = request.body.tell() + except AttributeError: + # In the case of HTTPKerberosAuth being reused and the body + # of the previous request was a file-like object, pos has + # the file position of the previous body. Ensure it's set to + # None. + self.pos = None + return request diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 82594b141be..dc4ebd957a9 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -20,3 +20,4 @@ requests==2.18.4 certifi==2017.7.27.1 setuptools==36.4.0 webencodings==0.5.1 +requests_kerberos==0.11.0 From c936e14135dd9f4a6dd834466b843815f3bf160b Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Sat, 11 Nov 2017 03:25:28 +0000 Subject: [PATCH 02/26] Add kerberos support for authentication in sending requests. --- src/pip/_internal/basecommand.py | 13 +++++-- src/pip/_internal/download.py | 63 +++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/basecommand.py b/src/pip/_internal/basecommand.py index 21aa8d950f9..ec3b257518d 100644 --- a/src/pip/_internal/basecommand.py +++ b/src/pip/_internal/basecommand.py @@ -79,6 +79,7 @@ def _build_session(self, options, retries=None, timeout=None): ), retries=retries if retries is not None else options.retries, insecure_hosts=options.trusted_hosts, + prompting=not options.no_input ) # Handle custom ca-bundles from the user @@ -102,9 +103,6 @@ def _build_session(self, options, retries=None, timeout=None): "https": options.proxy, } - # Determine if we can prompt the user for authentication or not - session.auth.prompting = not options.no_input - return session def parse_args(self, args): @@ -195,6 +193,15 @@ def main(self, args): ), }) + if level in ["INFO", "ERROR"]: + log_level_kerberos = logging.CRITICAL + 1 + else: + log_level_kerberos = logging.DEBUG + + logging.getLogger('pip._vendor.requests_kerberos.kerberos_').setLevel( + log_level_kerberos + ) + if sys.version_info[:2] == (3, 3): warnings.warn( "Python 3.3 supported has been deprecated and support for it " diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 180ea77bfaf..e5a60d63551 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -52,6 +52,14 @@ except ImportError: HAS_TLS = False +try: + from pip._vendor.requests_kerberos import HTTPKerberosAuth + from pip._vendor.requests_kerberos import kerberos_ as ik + _KERBEROS_AVAILABLE = True + +except ImportError: + _KERBEROS_AVAILABLE = False + __all__ = ['get_file_content', 'is_url', 'url_to_path', 'path_to_url', 'is_archive_file', 'unpack_vcs_link', @@ -214,6 +222,47 @@ def parse_credentials(self, netloc): return None, None +class MultiAuth(AuthBase): + def __init__(self, initial_auth=None, *auths): + if initial_auth is None: + self.initial_auth = MultiDomainBasicAuth(prompting=False) + else: + self.initial_auth = initial_auth + + self.auths = auths + + def __call__(self, req): + req = self.initial_auth(req) + self._register_hook(req, 0) # register hook after auth itself + return req + + def _register_hook(self, req, i): + if i >= len(self.auths): + return + + def hook(resp, **kwargs): + self.handle_response(resp, i, **kwargs) + + req.register_hook("response", hook) + + def handle_response(self, resp, i, **kwargs): + if resp.status_code != 401: # authorization required + return resp + + # clear response + resp.content + resp.raw.release_conn() + + req = self.auths[i](resp.request) # deletegate to ith auth + logger.info('registering hook {}'.format(i + 1)) + self._register_hook(req, i + 1) # register hook after auth itself + + new_resp = resp.connection.send(req, **kwargs) + new_resp.history.append(resp) + + return new_resp + + class LocalFSAdapter(BaseAdapter): def send(self, request, stream=None, timeout=None, verify=None, cert=None, @@ -328,6 +377,7 @@ def __init__(self, *args, **kwargs): retries = kwargs.pop("retries", 0) cache = kwargs.pop("cache", None) insecure_hosts = kwargs.pop("insecure_hosts", []) + prompting = kwargs.pop("prompting", True) super(PipSession, self).__init__(*args, **kwargs) @@ -335,7 +385,18 @@ def __init__(self, *args, **kwargs): self.headers["User-Agent"] = user_agent() # Attach our Authentication handler to the session - self.auth = MultiDomainBasicAuth() + no_prompt = MultiDomainBasicAuth(prompting=False) + prompt = MultiDomainBasicAuth(prompting=True) + prompt.passwords = no_prompt.passwords # share same dict of passwords + + if _KERBEROS_AVAILABLE and prompting: + auths = [no_prompt, HTTPKerberosAuth(ik.REQUIRED), prompt] + elif _KERBEROS_AVAILABLE and not prompting: + auths = [no_prompt, HTTPKerberosAuth(ik.REQUIRED)] + else: + auths = [MultiDomainBasicAuth(prompting=prompting)] + + self.auth = MultiAuth(*auths) # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. From 87086bde9acf49277a3606250c8cf71906ad706f Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Sat, 11 Nov 2017 03:40:45 +0000 Subject: [PATCH 03/26] Add news entry for kerberos authentication --- news/4854.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/4854.feature diff --git a/news/4854.feature b/news/4854.feature new file mode 100644 index 00000000000..ddd5c6ecaa5 --- /dev/null +++ b/news/4854.feature @@ -0,0 +1 @@ +Add kerberos support to possible authenticators, when available. Vendor in requests_kerberos 0.11.0. From e6ce81424ca7dbc88def729a152bc392f408fb94 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Sat, 11 Nov 2017 03:42:30 +0000 Subject: [PATCH 04/26] Add news item vendoring requests_kerberos. --- news/requests_kerberos.vendor | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/requests_kerberos.vendor diff --git a/news/requests_kerberos.vendor b/news/requests_kerberos.vendor new file mode 100644 index 00000000000..cf17078720b --- /dev/null +++ b/news/requests_kerberos.vendor @@ -0,0 +1 @@ +Vendored requests_kerberos at requests_kerberos==0.11.0 From f9391d3c17b0dcc01cf883b66ea8022ae4c07a19 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Tue, 14 Nov 2017 09:05:18 +0000 Subject: [PATCH 05/26] Add documentation on Kerberos authentication feature. --- docs/reference/pip_install.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/reference/pip_install.rst b/docs/reference/pip_install.rst index a1fba2c2638..9b0eea8cb9a 100644 --- a/docs/reference/pip_install.rst +++ b/docs/reference/pip_install.rst @@ -459,6 +459,21 @@ SSL Certificate Verification Starting with v1.3, pip provides SSL certificate verification over https, to prevent man-in-the-middle attacks against PyPI downloads. +.. _`Kerberos Authentication`: + +Kerberos Authentication +++++++++++++++++++++++++++++ + +Starting with v10.0, pip supports using a Kerberos ticket to authenticate +with servers. This feature requires that ``pykerberos`` or ``winkerberos`` +is installed in the same environment as pip. + +If you wish to ignore Kerberos authenticated (index) servers for bootstrapping +the installation of ``pykerberos`` or ``winkerberos`` or are not authenticated +for all servers by default pip will ask for input. To change this behaviour +to ignore those servers use the ``--no-input`` command line option. Your system +administrator can also set this in the config files or an environment variable, +see :ref:`Configuration`. .. _`Caching`: From 5965c29ffdd7996e6ca148bf9fe1d9fdf2615c45 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Wed, 23 Jan 2019 12:42:08 +0000 Subject: [PATCH 06/26] Fix a few oopsies in last commit --- src/pip/_internal/utils/logging.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 1bd85913b05..b0bed785099 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -232,11 +232,6 @@ def setup_logging(verbosity, no_color, user_log_file): else: level = "INFO" - if level in ["INFO", "ERROR"]: - log_level_kerberos = logging.CRITICAL + 1 - else: - log_level_kerberos = logging.DEBUG - level_number = getattr(logging, level) # The "root" logger should match the "console" level *unless* we also need @@ -255,7 +250,10 @@ def setup_logging(verbosity, no_color, user_log_file): # Similar for vendored Kerberos, which is a bit trigger happy. logging.addLevelName(logging.CRITICAL + 1, "SUPERCRITICAL") - kerberos_log_level = "SUPERCRITICAL" if level in ["INFO", "ERROR"] else "DEBUG" + kerberos_log_level = ( + "SUPERCRITICAL" if level in ["INFO", "ERROR"] else + "DEBUG" + ) # Shorthands for clarity log_streams = { @@ -320,9 +318,7 @@ def setup_logging(verbosity, no_color, user_log_file): "loggers": { "pip._vendor": { "level": vendored_log_level - } - }, - "loggers": { + }, "pip._vendor.requests_kerberos.kerberos_": { "level": kerberos_log_level } From dc3936bf443e00a9c7cfeac24a99d8cd7730cebf Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Wed, 23 Jan 2019 13:12:28 +0000 Subject: [PATCH 07/26] remove linebreak introduced by hard word-wrapping --- src/pip/_vendor/README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 41b17910b69..a55bb6d44d4 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -103,8 +103,7 @@ Modifications * ``CacheControl`` has been modified to import its dependencies from ``pip._vendor`` * ``requests`` has been modified to import its other dependencies from ``pip._vendor`` and to *not* load ``simplejson`` (all platforms) and ``pyopenssl`` (Windows). -* ``requests_kerberos`` has been modified to import its dependencies from ``pip -._vendor`` +* ``requests_kerberos`` has been modified to import its dependencies from ``pip._vendor`` Automatic Vendoring From 84f62ad30feb237f89513df9342a0337da1e10f8 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Mon, 26 Aug 2019 14:06:21 +0100 Subject: [PATCH 08/26] Fix linter --- src/pip/_internal/download.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 3513b67ea1d..567b3874569 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -652,7 +652,10 @@ def __init__(self, *args, **kwargs): elif _KERBEROS_AVAILABLE and not prompting: auths = [no_prompt, HTTPKerberosAuth(ik.REQUIRED)] else: - auths = [MultiDomainBasicAuth(prompting=prompting, index_urls=index_urls)] + auths = [MultiDomainBasicAuth( + prompting=prompting, + index_urls=index_urls + )] self.auth = MultiAuth(*auths) From 12e284066129b9fbea27983e1b6f6e3e6e7e6523 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 00:26:45 +0100 Subject: [PATCH 09/26] Update vendoring --- src/pip/_vendor/README.rst | 2 +- src/pip/_vendor/requests_kerberos/__init__.py | 10 +- src/pip/_vendor/requests_kerberos/compat.py | 14 - .../_vendor/requests_kerberos/kerberos_.py | 239 +++++++++++++----- src/pip/_vendor/vendor.txt | 2 +- 5 files changed, 186 insertions(+), 81 deletions(-) delete mode 100644 src/pip/_vendor/requests_kerberos/compat.py diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 091337af2c3..186b710375d 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -113,7 +113,7 @@ Modifications ``pip._vendor`` and to *not* load ``simplejson`` (all platforms) and ``pyopenssl`` (Windows). * ``platformdirs`` has been modified to import its submodules from ``pip._vendor.platformdirs``. -* ``requests_kerberos`` has been modified to import its dependencies from ``pip._vendor`` +* ``requests_kerberos`` has been modified to import some of its dependencies from ``pip._vendor`` Automatic Vendoring =================== diff --git a/src/pip/_vendor/requests_kerberos/__init__.py b/src/pip/_vendor/requests_kerberos/__init__.py index d8dea418e7b..3a70c4a7d4b 100644 --- a/src/pip/_vendor/requests_kerberos/__init__.py +++ b/src/pip/_vendor/requests_kerberos/__init__.py @@ -1,25 +1,21 @@ """ requests Kerberos/GSSAPI authentication library ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Requests is an HTTP library, written in Python, for human beings. This library adds optional Kerberos/GSSAPI authentication support and supports mutual authentication. Basic GET usage: - >>> import pip._vendor.requests >>> from pip._vendor.requests_kerberos import HTTPKerberosAuth - >>> r = pip._vendor.requests.get("http://example.org", auth=HTTPKerberosAuth()) - + >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) The entire `requests.api` should be supported. """ import logging from .kerberos_ import HTTPKerberosAuth, REQUIRED, OPTIONAL, DISABLED from .exceptions import MutualAuthenticationError -from .compat import NullHandler -logging.getLogger(__name__).addHandler(NullHandler()) +logging.getLogger(__name__).addHandler(logging.NullHandler()) __all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', 'OPTIONAL', 'DISABLED') -__version__ = '0.11.0' +__version__ = '0.14.0' diff --git a/src/pip/_vendor/requests_kerberos/compat.py b/src/pip/_vendor/requests_kerberos/compat.py deleted file mode 100644 index 01b75009805..00000000000 --- a/src/pip/_vendor/requests_kerberos/compat.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Compatibility library for older versions of python -""" -import sys - -# python 2.7 introduced a NullHandler which we want to use, but to support -# older versions, we implement our own if needed. -if sys.version_info[:2] > (2, 6): - from logging import NullHandler -else: - from logging import Handler - class NullHandler(Handler): - def emit(self, record): - pass diff --git a/src/pip/_vendor/requests_kerberos/kerberos_.py b/src/pip/_vendor/requests_kerberos/kerberos_.py index 353187af234..97331cd1cba 100644 --- a/src/pip/_vendor/requests_kerberos/kerberos_.py +++ b/src/pip/_vendor/requests_kerberos/kerberos_.py @@ -1,15 +1,24 @@ -try: - import kerberos -except ImportError: - import winkerberos as kerberos -import re +import base64 import logging +import re +import warnings + +import spnego +import spnego.channel_bindings +import spnego.exceptions + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.exceptions import UnsupportedAlgorithm from pip._vendor.requests.auth import AuthBase from pip._vendor.requests.models import Response -from pip._vendor.requests.compat import urlparse, StringIO from pip._vendor.requests.structures import CaseInsensitiveDict from pip._vendor.requests.cookies import cookiejar_from_dict +from pip._vendor.requests.packages.urllib3 import HTTPResponse + +from urllib.parse import urlparse from .exceptions import MutualAuthenticationError, KerberosExchangeError @@ -30,6 +39,14 @@ OPTIONAL = 2 DISABLED = 3 + +class NoCertificateRetrievedWarning(Warning): + pass + +class UnknownSignatureAlgorithmOID(Warning): + pass + + class SanitizedResponse(Response): """The :class:`Response ` object, which contains a server's response to an HTTP request. @@ -66,27 +83,94 @@ def _negotiate_value(response): else: # There's no need to re-compile this EVERY time it is called. Compile # it once and you won't have the performance hit of the compilation. - regex = re.compile('(?:.*,)*\s*Negotiate\s*([^,]*),?', re.I) + regex = re.compile(r'Negotiate\s*([^,]*)', re.I) _negotiate_value.regex = regex - authreq = response.headers.get('www-authenticate', None) + if response.status_code == 407: + authreq = response.headers.get('proxy-authenticate', None) + else: + authreq = response.headers.get('www-authenticate', None) if authreq: match_obj = regex.search(authreq) if match_obj: - return match_obj.group(1) + return base64.b64decode(match_obj.group(1)) return None +def _get_certificate_hash(certificate_der): + # https://tools.ietf.org/html/rfc5929#section-4.1 + cert = x509.load_der_x509_certificate(certificate_der, default_backend()) + + try: + hash_algorithm = cert.signature_hash_algorithm + except UnsupportedAlgorithm as ex: + warnings.warn("Failed to get signature algorithm from certificate, " + "unable to pass channel bindings: %s" % str(ex), UnknownSignatureAlgorithmOID) + return None + + # if the cert signature algorithm is either md5 or sha1 then use sha256 + # otherwise use the signature algorithm + if hash_algorithm.name in ['md5', 'sha1']: + digest = hashes.Hash(hashes.SHA256(), default_backend()) + else: + digest = hashes.Hash(hash_algorithm, default_backend()) + + digest.update(certificate_der) + certificate_hash = digest.finalize() + + return certificate_hash + + +def _get_channel_bindings_application_data(response): + """ + https://tools.ietf.org/html/rfc5929 4. The 'tls-server-end-point' Channel Binding Type + + Gets the application_data value for the 'tls-server-end-point' CBT Type. + This is ultimately the SHA256 hash of the certificate of the HTTPS endpoint + appended onto tls-server-end-point. This value is then passed along to the + kerberos library to bind to the auth response. If the socket is not an SSL + socket or the raw HTTP object is not a urllib3 HTTPResponse then None will + be returned and the Kerberos auth will use GSS_C_NO_CHANNEL_BINDINGS + + :param response: The original 401 response from the server + :return: byte string used on the application_data.value field on the CBT struct + """ + + application_data = None + raw_response = response.raw + + if isinstance(raw_response, HTTPResponse): + try: + socket = raw_response._fp.fp.raw._sock + except AttributeError: + warnings.warn("Failed to get raw socket for CBT; has urllib3 impl changed", + NoCertificateRetrievedWarning) + else: + try: + server_certificate = socket.getpeercert(True) + except AttributeError: + pass + else: + certificate_hash = _get_certificate_hash(server_certificate) + application_data = b'tls-server-end-point:' + certificate_hash + else: + warnings.warn( + "Requests is running with a non urllib3 backend, cannot retrieve server certificate for CBT", + NoCertificateRetrievedWarning) + + return application_data + class HTTPKerberosAuth(AuthBase): """Attaches HTTP GSSAPI/Kerberos Authentication to the given Request object.""" def __init__( self, mutual_authentication=REQUIRED, service="HTTP", delegate=False, force_preemptive=False, - principal=None, hostname_override=None, sanitize_mutual_error_response=True): - self.context = {} + principal=None, hostname_override=None, + sanitize_mutual_error_response=True, send_cbt=True): + self._context = {} self.mutual_authentication = mutual_authentication self.delegate = delegate self.pos = None @@ -95,6 +179,12 @@ def __init__( self.principal = principal self.hostname_override = hostname_override self.sanitize_mutual_error_response = sanitize_mutual_error_response + self.auth_done = False + + # Set the CBT values populated after the first response + self.send_cbt = send_cbt + self.cbt_binding_tried = False + self.cbt_struct = None def generate_request_header(self, response, host, is_preemptive=False): """ @@ -106,60 +196,51 @@ def generate_request_header(self, response, host, is_preemptive=False): """ # Flags used by kerberos module. - gssflags = kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG + gssflags = spnego.ContextReq.sequence_detect if self.delegate: - gssflags |= kerberos.GSS_C_DELEG_FLAG + gssflags |= spnego.ContextReq.delegate + if self.mutual_authentication != DISABLED: + gssflags |= spnego.ContextReq.mutual_auth try: - kerb_stage = "authGSSClientInit()" + kerb_stage = "ctx init" # contexts still need to be stored by host, but hostname_override # allows use of an arbitrary hostname for the kerberos exchange # (eg, in cases of aliased hosts, internal vs external, CNAMEs # w/ name-based HTTP hosting) kerb_host = self.hostname_override if self.hostname_override is not None else host - kerb_spn = "{0}@{1}".format(self.service, kerb_host) - - result, self.context[host] = kerberos.authGSSClientInit(kerb_spn, - gssflags=gssflags, principal=self.principal) - if result < 1: - raise EnvironmentError(result, kerb_stage) + self._context[host] = ctx = spnego.client( + username=self.principal, + hostname=kerb_host, + service=self.service, + channel_bindings=self.cbt_struct, + context_req=gssflags, + protocol="kerberos", + ) # if we have a previous response from the server, use it to continue # the auth process, otherwise use an empty value - negotiate_resp_value = '' if is_preemptive else _negotiate_value(response) + negotiate_resp_value = None if is_preemptive else _negotiate_value(response) - kerb_stage = "authGSSClientStep()" - result = kerberos.authGSSClientStep(self.context[host], - negotiate_resp_value) + kerb_stage = "ctx step" + gss_response = ctx.step(in_token=negotiate_resp_value) - if result < 0: - raise EnvironmentError(result, kerb_stage) + return "Negotiate {0}".format(base64.b64encode(gss_response).decode()) - kerb_stage = "authGSSClientResponse()" - gss_response = kerberos.authGSSClientResponse(self.context[host]) - - return "Negotiate {0}".format(gss_response) - - except kerberos.GSSError as error: + except spnego.exceptions.SpnegoError as error: log.exception( "generate_request_header(): {0} failed:".format(kerb_stage)) log.exception(error) - raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error.args))) - - except EnvironmentError as error: - # ensure we raised this for translation to KerberosExchangeError - # by comparing errno to result, re-raise if not - if error.errno != result: - raise - message = "{0} failed, result: {1}".format(kerb_stage, result) - log.error("generate_request_header(): {0}".format(message)) - raise KerberosExchangeError(message) + raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error))) from error def authenticate_user(self, response, **kwargs): """Handles user authentication with gssapi/kerberos""" host = urlparse(response.url).hostname + if response.status_code == 407: + if 'proxies' in kwargs and urlparse(response.url).scheme in kwargs['proxies']: + host = urlparse(kwargs['proxies'][urlparse(response.url).scheme]).hostname try: auth_header = self.generate_request_header(response, host) @@ -167,9 +248,14 @@ def authenticate_user(self, response, **kwargs): # GSS Failure, return existing response return response - log.debug("authenticate_user(): Authorization header: {0}".format( - auth_header)) - response.request.headers['Authorization'] = auth_header + if response.status_code == 407: + log.debug("authenticate_user(): Proxy-Authorization header: {0}".format( + auth_header)) + response.request.headers['Proxy-Authorization'] = auth_header + else: + log.debug("authenticate_user(): Authorization header: {0}".format( + auth_header)) + response.request.headers['Authorization'] = auth_header # Consume the content so we can reuse the connection for the next # request. @@ -195,6 +281,19 @@ def handle_401(self, response, **kwargs): log.debug("handle_401(): returning {0}".format(response)) return response + def handle_407(self, response, **kwargs): + """Handles 407's, attempts to use gssapi/kerberos authentication""" + + log.debug("handle_407(): Handling: 407") + if _negotiate_value(response) is not None: + _r = self.authenticate_user(response, **kwargs) + log.debug("handle_407(): returning {0}".format(_r)) + return _r + else: + log.debug("handle_407(): Kerberos is not supported") + log.debug("handle_407(): returning {0}".format(response)) + return response + def handle_other(self, response): """Handles all responses with the exception of 401s. @@ -202,7 +301,7 @@ def handle_other(self, response): log.debug("handle_other(): Handling: %d" % response.status_code) - if self.mutual_authentication in (REQUIRED, OPTIONAL): + if self.mutual_authentication in (REQUIRED, OPTIONAL) and not self.auth_done: is_http_error = response.status_code >= 400 @@ -218,6 +317,7 @@ def handle_other(self, response): # Authentication successful log.debug("handle_other(): returning {0}".format(response)) + self.auth_done = True return response elif is_http_error or self.mutual_authentication == OPTIONAL: @@ -232,7 +332,7 @@ def handle_other(self, response): return response else: # Unable to attempt mutual authentication when mutual auth is - # required, raise an exception so the user doesnt use an + # required, raise an exception so the user doesn't use an # untrusted response. log.error("handle_other(): Mutual authentication failed") raise MutualAuthenticationError("Unable to authenticate " @@ -248,21 +348,18 @@ def authenticate_server(self, response): Returns True on success, False on failure. """ + response_token = _negotiate_value(response) log.debug("authenticate_server(): Authenticate header: {0}".format( - _negotiate_value(response))) + base64.b64encode(response_token).decode() + if response_token + else "")) host = urlparse(response.url).hostname try: - result = kerberos.authGSSClientStep(self.context[host], - _negotiate_value(response)) - except kerberos.GSSError: - log.exception("authenticate_server(): authGSSClientStep() failed:") - return False - - if result < 1: - log.error("authenticate_server(): authGSSClientStep() failed: " - "{0}".format(result)) + self._context[host].step(in_token=response_token) + except spnego.exceptions.SpnegoError: + log.exception("authenticate_server(): ctx step() failed:") return False log.debug("authenticate_server(): returning {0}".format(response)) @@ -271,6 +368,19 @@ def authenticate_server(self, response): def handle_response(self, response, **kwargs): """Takes the given response and tries kerberos-auth, as needed.""" num_401s = kwargs.pop('num_401s', 0) + num_407s = kwargs.pop('num_407s', 0) + + # Check if we have already tried to get the CBT data value + if not self.cbt_binding_tried and self.send_cbt: + # If we haven't tried, try getting it now + cbt_application_data = _get_channel_bindings_application_data(response) + if cbt_application_data: + self.cbt_struct = spnego.channel_bindings.GssChannelBindings( + application_data=cbt_application_data, + ) + + # Regardless of the result, set tried to True so we don't waste time next time + self.cbt_binding_tried = True if self.pos is not None: # Rewind the file position indicator of the body to where @@ -290,6 +400,19 @@ def handle_response(self, response, **kwargs): # Authentication has failed. Return the 401 response. log.debug("handle_response(): returning 401 %s", response) return response + elif response.status_code == 407 and num_407s < 2: + # 407 Unauthorized. Handle it, and if it still comes back as 407, + # that means authentication failed. + _r = self.handle_407(response, **kwargs) + log.debug("handle_response(): returning %s", _r) + log.debug("handle_response() has seen %d 407 responses", num_407s) + num_407s += 1 + return self.handle_response(_r, num_407s=num_407s, **kwargs) + elif response.status_code == 407 and num_407s >= 2: + # Still receiving 407 responses after attempting to handle them. + # Authentication has failed. Return the 407 response. + log.debug("handle_response(): returning 407 %s", response) + return response else: _r = self.handle_other(response) log.debug("handle_response(): returning %s", _r) @@ -300,7 +423,7 @@ def deregister(self, response): response.request.deregister_hook('response', self.handle_response) def __call__(self, request): - if self.force_preemptive: + if self.force_preemptive and not self.auth_done: # add Authorization header before we receive a 401 # by the 401 handler host = urlparse(request.url).hostname diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 61788313592..b000f0ed860 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -13,6 +13,7 @@ requests==2.27.1 chardet==4.0.0 idna==3.3 urllib3==1.26.9 +requests_kerberos==0.14.0 rich==12.2.0 pygments==2.11.2 typing_extensions==4.2.0 @@ -22,4 +23,3 @@ six==1.16.0 tenacity==8.0.1 tomli==2.0.1 webencodings==0.5.1 -requests_kerberos==0.11.0 From b5bd385900e1fcc71e285a728cdffe285db2b475 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 00:33:21 +0100 Subject: [PATCH 10/26] Improve docs --- docs/html/cli/pip_install.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index cfff4f7e270..31f738faba8 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -225,6 +225,26 @@ overridden by using ``--cert`` option or by using ``PIP_CERT``, ``REQUESTS_CA_BUNDLE``, or ``CURL_CA_BUNDLE`` environment variables. +.. _`Kerberos Authentication`: + +Kerberos Authentication +++++++++++++++++++++++++++++ + +Starting with vXX.X, pip supports using a Kerberos ticket to authenticate +with servers. To use Kerberos one must: + +- Install ``spegno`` and ``cryptography`` into the same environment as ``pip``. +- Run ``pip`` with the flag ``--enable-kerberos``, this can be globally set using ``pip config set global.--enable-kerberos true``. + +Bugs reported with pip in relation to Kerberos will likely not +be addressed directly by pip's maintainers. Pull Requests to fix Kerberos +only bugs will be considered, and merged (subject to normal review processes). +Note that there may be delays due to the lack of developer resources for +reviewing such pull requests. + + + + .. _`Caching`: Caching From e069cb576c14590c088f9e8d7c250470bb1a5ced Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 00:45:15 +0100 Subject: [PATCH 11/26] Better docs --- docs/html/cli/pip_install.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index 31f738faba8..4b06dfabce8 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -234,7 +234,11 @@ Starting with vXX.X, pip supports using a Kerberos ticket to authenticate with servers. To use Kerberos one must: - Install ``spegno`` and ``cryptography`` into the same environment as ``pip``. -- Run ``pip`` with the flag ``--enable-kerberos``, this can be globally set using ``pip config set global.--enable-kerberos true``. +- Run ``pip`` with the flag ``--enable-kerberos``. + + +Your system administrator can also set this in the config files or an environment variable, +see :ref:`Configuration`. Bugs reported with pip in relation to Kerberos will likely not be addressed directly by pip's maintainers. Pull Requests to fix Kerberos From fedf20673518aacfbe54f8354a573e5d833b794b Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:02:24 +0100 Subject: [PATCH 12/26] Improve docs --- docs/html/cli/pip_install.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index 4b06dfabce8..b56f723611a 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -234,11 +234,11 @@ Starting with vXX.X, pip supports using a Kerberos ticket to authenticate with servers. To use Kerberos one must: - Install ``spegno`` and ``cryptography`` into the same environment as ``pip``. -- Run ``pip`` with the flag ``--enable-kerberos``. +- Run ``pip`` with the flag ``--enable-kerberos``. Your system administrator + can also set this in the config files or an environment variable, see + :ref:`Configuration`. - -Your system administrator can also set this in the config files or an environment variable, -see :ref:`Configuration`. +It is likely that you will also want to use `--no-input` at the same time. Bugs reported with pip in relation to Kerberos will likely not be addressed directly by pip's maintainers. Pull Requests to fix Kerberos From f53e289b2872566fe88385b88ca6c805ba6b298d Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:02:32 +0100 Subject: [PATCH 13/26] Make things work? --- src/pip/_internal/cli/cmdoptions.py | 11 ++++++++ src/pip/_internal/cli/req_command.py | 5 ++-- src/pip/_internal/network/auth.py | 41 ++++++++++++++++++++++++++++ src/pip/_internal/network/session.py | 19 ++++++++++++- 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index c84ecabd380..a67399d5258 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -262,6 +262,16 @@ class PipOption(Option): help="Disable prompting for input.", ) +enable_kerberos: Callable[..., Option] = partial( + Option, + # Enable kerberos + "--enable-kerberos", + dest="enable_kerberos", + action="store_true", + default=False, + help="Enable Kerberos authentication.", +) + proxy: Callable[..., Option] = partial( Option, "--proxy", @@ -1027,6 +1037,7 @@ def check_list_path_option(options: Values) -> None: quiet, log, no_input, + enable_kerberos, proxy, retries, timeout, diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index aab177002d4..d0adcb7142a 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -93,6 +93,8 @@ def _build_session( retries=retries if retries is not None else options.retries, trusted_hosts=options.trusted_hosts, index_urls=self._get_index_urls(options), + prompting=not options.no_input, + enable_kerberos=options.enable_kerberos, ) # Handle custom ca-bundles from the user @@ -114,9 +116,6 @@ def _build_session( "https": options.proxy, } - # Determine if we can prompt the user for authentication or not - session.auth.prompting = not options.no_input - return session diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index e40ebfb2785..d2934039ac9 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -322,3 +322,44 @@ def save_credentials(self, resp: Response, **kwargs: Any) -> None: keyring.set_password(*creds) except Exception: logger.exception("Failed to save credentials") + + +class MultiAuth(AuthBase): + def __init__(self, initial_auth=None, *auths): + if initial_auth is None: + self.initial_auth = MultiDomainBasicAuth(prompting=False) + else: + self.initial_auth = initial_auth + + self.auths = auths + + def __call__(self, req): + req = self.initial_auth(req) + self._register_hook(req, 0) # register hook after auth itself + return req + + def _register_hook(self, req, i): + if i >= len(self.auths): + return + + def hook(resp, **kwargs): + self.handle_response(resp, i, **kwargs) + + req.register_hook("response", hook) + + def handle_response(self, resp, i, **kwargs): + if resp.status_code != 401: # authorization required + return resp + + # clear response + resp.content + resp.raw.release_conn() + + req = self.auths[i](resp.request) # deletegate to ith auth + logger.info('registering hook {}'.format(i + 1)) + self._register_hook(req, i + 1) # register hook after auth itself + + new_resp = resp.connection.send(req, **kwargs) + new_resp.history.append(resp) + + return new_resp diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index e2c8582e506..42ecf5d8177 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -266,6 +266,8 @@ def __init__( cache: Optional[str] = None, trusted_hosts: Sequence[str] = (), index_urls: Optional[List[str]] = None, + prompting: bool = False, + enable_kerberos: bool = False, **kwargs: Any, ) -> None: """ @@ -281,8 +283,23 @@ def __init__( # Attach our User Agent to the request self.headers["User-Agent"] = user_agent() + no_prompt = MultiDomainBasicAuth(prompting=False, index_urls=index_urls) + prompt = MultiDomainBasicAuth(prompting=True, index_urls=index_urls) + prompt.passwords = no_prompt.passwords # share same dict of passwords + # Attach our Authentication handler to the session - self.auth = MultiDomainBasicAuth(index_urls=index_urls) + self.auth = prompt if prompting else no_prompt + + if enable_kerberos: + try: + from pip._vendor.requests_kerberos import HTTPKerberosAuth, REQUIRED + except ImportError: + logger.critical("Are you sure you `psegno` and `cryptography` are available in the same environment as pip?") + raise + if prompting: + self.auth = [no_prompt, HTTPKerberosAuth(REQUIRED), prompt] + elif prompting: + self.auth = [no_prompt, HTTPKerberosAuth(REQUIRED)] # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. From 33938ddbab6405816fbf073963d1341049efcb86 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:07:34 +0100 Subject: [PATCH 14/26] Fix up things --- docs/html/cli/pip_install.rst | 2 +- news/{4854.feature => 4854.feature.rst} | 2 +- news/requests_kerberos.vendor | 1 - news/requests_kerberos.vendor.rst | 1 + src/pip/_internal/network/auth.py | 4 ++-- src/pip/_internal/network/session.py | 7 +++++-- src/pip/_internal/utils/logging.py | 12 +++++++----- 7 files changed, 17 insertions(+), 12 deletions(-) rename news/{4854.feature => 4854.feature.rst} (63%) delete mode 100644 news/requests_kerberos.vendor create mode 100644 news/requests_kerberos.vendor.rst diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index b56f723611a..3287c16d830 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -238,7 +238,7 @@ with servers. To use Kerberos one must: can also set this in the config files or an environment variable, see :ref:`Configuration`. -It is likely that you will also want to use `--no-input` at the same time. +It is likely that you will also want to use ``--no-input`` at the same time. Bugs reported with pip in relation to Kerberos will likely not be addressed directly by pip's maintainers. Pull Requests to fix Kerberos diff --git a/news/4854.feature b/news/4854.feature.rst similarity index 63% rename from news/4854.feature rename to news/4854.feature.rst index ddd5c6ecaa5..14692de3da6 100644 --- a/news/4854.feature +++ b/news/4854.feature.rst @@ -1 +1 @@ -Add kerberos support to possible authenticators, when available. Vendor in requests_kerberos 0.11.0. +Add kerberos support to possible authenticators, when available. Vendor in requests_kerberos 0.14.0. diff --git a/news/requests_kerberos.vendor b/news/requests_kerberos.vendor deleted file mode 100644 index cf17078720b..00000000000 --- a/news/requests_kerberos.vendor +++ /dev/null @@ -1 +0,0 @@ -Vendored requests_kerberos at requests_kerberos==0.11.0 diff --git a/news/requests_kerberos.vendor.rst b/news/requests_kerberos.vendor.rst new file mode 100644 index 00000000000..e485f3b7b67 --- /dev/null +++ b/news/requests_kerberos.vendor.rst @@ -0,0 +1 @@ +Vendored requests_kerberos at requests_kerberos==0.14.0 diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index d2934039ac9..27752fade69 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -355,8 +355,8 @@ def handle_response(self, resp, i, **kwargs): resp.content resp.raw.release_conn() - req = self.auths[i](resp.request) # deletegate to ith auth - logger.info('registering hook {}'.format(i + 1)) + req = self.auths[i](resp.request) # delegate to ith auth + logger.info("registering hook %d", i + 1) self._register_hook(req, i + 1) # register hook after auth itself new_resp = resp.connection.send(req, **kwargs) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 42ecf5d8177..fb8dec90ddd 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -292,9 +292,12 @@ def __init__( if enable_kerberos: try: - from pip._vendor.requests_kerberos import HTTPKerberosAuth, REQUIRED + from pip._vendor.requests_kerberos import REQUIRED, HTTPKerberosAuth except ImportError: - logger.critical("Are you sure you `psegno` and `cryptography` are available in the same environment as pip?") + logger.critical( + "Are you sure you `psegno` and `cryptography` are " + "available in the same environment as pip?" + ) raise if prompting: self.auth = [no_prompt, HTTPKerberosAuth(REQUIRED), prompt] diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 326661f75d9..1c17e373b68 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -259,10 +259,7 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) # Similar for vendored Kerberos, which is a bit trigger happy. logging.addLevelName(logging.CRITICAL + 1, "SUPERCRITICAL") - kerberos_log_level = ( - "SUPERCRITICAL" if level in ["INFO", "ERROR"] else - "DEBUG" - ) + kerberos_log_level = "SUPERCRITICAL" if level in ["INFO", "ERROR"] else "DEBUG" # Shorthands for clarity log_streams = { @@ -346,7 +343,12 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) "level": root_level, "handlers": handlers, }, - "loggers": {"pip._vendor": {"level": vendored_log_level}}, + "loggers": { + "pip._vendor": {"level": vendored_log_level}, + "pip._vendor.requests_kerberos.kerberos_": { + "level": kerberos_log_level + }, + }, } ) From 910b8a7e899b3bb90e6cdf4f10e769c9a675262c Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:17:02 +0100 Subject: [PATCH 15/26] Make mypy happy --- src/pip/_internal/network/auth.py | 10 +++++----- src/pip/_internal/network/session.py | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 27752fade69..4af20a44560 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -325,7 +325,7 @@ def save_credentials(self, resp: Response, **kwargs: Any) -> None: class MultiAuth(AuthBase): - def __init__(self, initial_auth=None, *auths): + def __init__(self, initial_auth: AuthBase = None, *auths: AuthBase): if initial_auth is None: self.initial_auth = MultiDomainBasicAuth(prompting=False) else: @@ -333,21 +333,21 @@ def __init__(self, initial_auth=None, *auths): self.auths = auths - def __call__(self, req): + def __call__(self, req: Request) -> Request: req = self.initial_auth(req) self._register_hook(req, 0) # register hook after auth itself return req - def _register_hook(self, req, i): + def _register_hook(self, req: Request, i: int) -> None: if i >= len(self.auths): return - def hook(resp, **kwargs): + def hook(resp: Response, **kwargs: Any) -> None: self.handle_response(resp, i, **kwargs) req.register_hook("response", hook) - def handle_response(self, resp, i, **kwargs): + def handle_response(self, resp: Response, i: int, **kwargs: Any) -> Response: if resp.status_code != 401: # authorization required return resp diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index fb8dec90ddd..199cce0c9d4 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -28,7 +28,7 @@ from pip import __version__ from pip._internal.metadata import get_default_environment from pip._internal.models.link import Link -from pip._internal.network.auth import MultiDomainBasicAuth +from pip._internal.network.auth import MultiAuth, MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. @@ -300,9 +300,11 @@ def __init__( ) raise if prompting: - self.auth = [no_prompt, HTTPKerberosAuth(REQUIRED), prompt] + auths = [no_prompt, HTTPKerberosAuth(REQUIRED), prompt] elif prompting: - self.auth = [no_prompt, HTTPKerberosAuth(REQUIRED)] + auths = [no_prompt, HTTPKerberosAuth(REQUIRED)] + + self.auth = MultiAuth(auths) # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. From e2e1ba40a9bc975152d398a82c224273db73efc4 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:19:07 +0100 Subject: [PATCH 16/26] Satisfy vendoring --- src/pip/_vendor/requests_kerberos.pyi | 1 + src/pip/_vendor/requests_kerberos/LICENSE | 15 +++++++++++++++ src/pip/_vendor/requests_kerberos/__init__.py | 7 +++++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/pip/_vendor/requests_kerberos.pyi create mode 100644 src/pip/_vendor/requests_kerberos/LICENSE diff --git a/src/pip/_vendor/requests_kerberos.pyi b/src/pip/_vendor/requests_kerberos.pyi new file mode 100644 index 00000000000..52b7388800c --- /dev/null +++ b/src/pip/_vendor/requests_kerberos.pyi @@ -0,0 +1 @@ +from requests_kerberos import * \ No newline at end of file diff --git a/src/pip/_vendor/requests_kerberos/LICENSE b/src/pip/_vendor/requests_kerberos/LICENSE new file mode 100644 index 00000000000..581f115e787 --- /dev/null +++ b/src/pip/_vendor/requests_kerberos/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012 Kenneth Reitz + +Permission to use, copy, modify and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/pip/_vendor/requests_kerberos/__init__.py b/src/pip/_vendor/requests_kerberos/__init__.py index 3a70c4a7d4b..54212ee9320 100644 --- a/src/pip/_vendor/requests_kerberos/__init__.py +++ b/src/pip/_vendor/requests_kerberos/__init__.py @@ -1,12 +1,15 @@ """ requests Kerberos/GSSAPI authentication library ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Requests is an HTTP library, written in Python, for human beings. This library adds optional Kerberos/GSSAPI authentication support and supports mutual authentication. Basic GET usage: - >>> import pip._vendor.requests - >>> from pip._vendor.requests_kerberos import HTTPKerberosAuth + + >>> import requests + >>> from requests_kerberos import HTTPKerberosAuth >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) + The entire `requests.api` should be supported. """ import logging From e8f8dbe3c48f6cc5287a3d7965a40eadd9146c7c Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:25:56 +0100 Subject: [PATCH 17/26] Remove vendoring --- docs/html/cli/pip_install.rst | 2 +- news/4854.feature.rst | 2 +- news/requests_kerberos.vendor.rst | 1 - src/pip/_internal/network/session.py | 6 +- src/pip/_vendor/requests_kerberos/LICENSE | 15 - src/pip/_vendor/requests_kerberos/__init__.py | 24 - .../_vendor/requests_kerberos/exceptions.py | 15 - .../_vendor/requests_kerberos/kerberos_.py | 446 ------------------ 8 files changed, 5 insertions(+), 506 deletions(-) delete mode 100644 news/requests_kerberos.vendor.rst delete mode 100644 src/pip/_vendor/requests_kerberos/LICENSE delete mode 100644 src/pip/_vendor/requests_kerberos/__init__.py delete mode 100644 src/pip/_vendor/requests_kerberos/exceptions.py delete mode 100644 src/pip/_vendor/requests_kerberos/kerberos_.py diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index 3287c16d830..c86283fcf1c 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -233,7 +233,7 @@ Kerberos Authentication Starting with vXX.X, pip supports using a Kerberos ticket to authenticate with servers. To use Kerberos one must: -- Install ``spegno`` and ``cryptography`` into the same environment as ``pip``. +- Install ``requests_kerberos`` into the same environment as ``pip`` (tested with 0.14.0). - Run ``pip`` with the flag ``--enable-kerberos``. Your system administrator can also set this in the config files or an environment variable, see :ref:`Configuration`. diff --git a/news/4854.feature.rst b/news/4854.feature.rst index 14692de3da6..bbc7e80c1b8 100644 --- a/news/4854.feature.rst +++ b/news/4854.feature.rst @@ -1 +1 @@ -Add kerberos support to possible authenticators, when available. Vendor in requests_kerberos 0.14.0. +Add kerberos support for authentication with the ``--enable-kerberos`` flag. diff --git a/news/requests_kerberos.vendor.rst b/news/requests_kerberos.vendor.rst deleted file mode 100644 index e485f3b7b67..00000000000 --- a/news/requests_kerberos.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Vendored requests_kerberos at requests_kerberos==0.14.0 diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 199cce0c9d4..b1413bb0ec3 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -292,11 +292,11 @@ def __init__( if enable_kerberos: try: - from pip._vendor.requests_kerberos import REQUIRED, HTTPKerberosAuth + from requests_kerberos import REQUIRED, HTTPKerberosAuth except ImportError: logger.critical( - "Are you sure you `psegno` and `cryptography` are " - "available in the same environment as pip?" + "Are you sure you `requests_kerberos` and its dependencies " + "are available in the same environment as pip?" ) raise if prompting: diff --git a/src/pip/_vendor/requests_kerberos/LICENSE b/src/pip/_vendor/requests_kerberos/LICENSE deleted file mode 100644 index 581f115e787..00000000000 --- a/src/pip/_vendor/requests_kerberos/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -ISC License - -Copyright (c) 2012 Kenneth Reitz - -Permission to use, copy, modify and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/pip/_vendor/requests_kerberos/__init__.py b/src/pip/_vendor/requests_kerberos/__init__.py deleted file mode 100644 index 54212ee9320..00000000000 --- a/src/pip/_vendor/requests_kerberos/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -requests Kerberos/GSSAPI authentication library -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Requests is an HTTP library, written in Python, for human beings. This library -adds optional Kerberos/GSSAPI authentication support and supports mutual -authentication. Basic GET usage: - - >>> import requests - >>> from requests_kerberos import HTTPKerberosAuth - >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) - -The entire `requests.api` should be supported. -""" -import logging - -from .kerberos_ import HTTPKerberosAuth, REQUIRED, OPTIONAL, DISABLED -from .exceptions import MutualAuthenticationError - -logging.getLogger(__name__).addHandler(logging.NullHandler()) - -__all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', - 'OPTIONAL', 'DISABLED') -__version__ = '0.14.0' diff --git a/src/pip/_vendor/requests_kerberos/exceptions.py b/src/pip/_vendor/requests_kerberos/exceptions.py deleted file mode 100644 index db1ca771495..00000000000 --- a/src/pip/_vendor/requests_kerberos/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -requests_kerberos.exceptions -~~~~~~~~~~~~~~~~~~~ - -This module contains the set of exceptions. - -""" -from pip._vendor.requests.exceptions import RequestException - - -class MutualAuthenticationError(RequestException): - """Mutual Authentication Error""" - -class KerberosExchangeError(RequestException): - """Kerberos Exchange Failed Error""" diff --git a/src/pip/_vendor/requests_kerberos/kerberos_.py b/src/pip/_vendor/requests_kerberos/kerberos_.py deleted file mode 100644 index 97331cd1cba..00000000000 --- a/src/pip/_vendor/requests_kerberos/kerberos_.py +++ /dev/null @@ -1,446 +0,0 @@ -import base64 -import logging -import re -import warnings - -import spnego -import spnego.channel_bindings -import spnego.exceptions - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.exceptions import UnsupportedAlgorithm - -from pip._vendor.requests.auth import AuthBase -from pip._vendor.requests.models import Response -from pip._vendor.requests.structures import CaseInsensitiveDict -from pip._vendor.requests.cookies import cookiejar_from_dict -from pip._vendor.requests.packages.urllib3 import HTTPResponse - -from urllib.parse import urlparse - -from .exceptions import MutualAuthenticationError, KerberosExchangeError - -log = logging.getLogger(__name__) - -# Different types of mutual authentication: -# with mutual_authentication set to REQUIRED, all responses will be -# authenticated with the exception of errors. Errors will have their contents -# and headers stripped. If a non-error response cannot be authenticated, a -# MutualAuthenticationError exception will be raised. -# with mutual_authentication set to OPTIONAL, mutual authentication will be -# attempted if supported, and if supported and failed, a -# MutualAuthenticationError exception will be raised. Responses which do not -# support mutual authentication will be returned directly to the user. -# with mutual_authentication set to DISABLED, mutual authentication will not be -# attempted, even if supported. -REQUIRED = 1 -OPTIONAL = 2 -DISABLED = 3 - - -class NoCertificateRetrievedWarning(Warning): - pass - -class UnknownSignatureAlgorithmOID(Warning): - pass - - -class SanitizedResponse(Response): - """The :class:`Response ` object, which contains a server's - response to an HTTP request. - - This differs from `requests.models.Response` in that it's headers and - content have been sanitized. This is only used for HTTP Error messages - which do not support mutual authentication when mutual authentication is - required.""" - - def __init__(self, response): - super(SanitizedResponse, self).__init__() - self.status_code = response.status_code - self.encoding = response.encoding - self.raw = response.raw - self.reason = response.reason - self.url = response.url - self.request = response.request - self.connection = response.connection - self._content_consumed = True - - self._content = "" - self.cookies = cookiejar_from_dict({}) - self.headers = CaseInsensitiveDict() - self.headers['content-length'] = '0' - for header in ('date', 'server'): - if header in response.headers: - self.headers[header] = response.headers[header] - - -def _negotiate_value(response): - """Extracts the gssapi authentication token from the appropriate header""" - if hasattr(_negotiate_value, 'regex'): - regex = _negotiate_value.regex - else: - # There's no need to re-compile this EVERY time it is called. Compile - # it once and you won't have the performance hit of the compilation. - regex = re.compile(r'Negotiate\s*([^,]*)', re.I) - _negotiate_value.regex = regex - - if response.status_code == 407: - authreq = response.headers.get('proxy-authenticate', None) - else: - authreq = response.headers.get('www-authenticate', None) - - if authreq: - match_obj = regex.search(authreq) - if match_obj: - return base64.b64decode(match_obj.group(1)) - - return None - - -def _get_certificate_hash(certificate_der): - # https://tools.ietf.org/html/rfc5929#section-4.1 - cert = x509.load_der_x509_certificate(certificate_der, default_backend()) - - try: - hash_algorithm = cert.signature_hash_algorithm - except UnsupportedAlgorithm as ex: - warnings.warn("Failed to get signature algorithm from certificate, " - "unable to pass channel bindings: %s" % str(ex), UnknownSignatureAlgorithmOID) - return None - - # if the cert signature algorithm is either md5 or sha1 then use sha256 - # otherwise use the signature algorithm - if hash_algorithm.name in ['md5', 'sha1']: - digest = hashes.Hash(hashes.SHA256(), default_backend()) - else: - digest = hashes.Hash(hash_algorithm, default_backend()) - - digest.update(certificate_der) - certificate_hash = digest.finalize() - - return certificate_hash - - -def _get_channel_bindings_application_data(response): - """ - https://tools.ietf.org/html/rfc5929 4. The 'tls-server-end-point' Channel Binding Type - - Gets the application_data value for the 'tls-server-end-point' CBT Type. - This is ultimately the SHA256 hash of the certificate of the HTTPS endpoint - appended onto tls-server-end-point. This value is then passed along to the - kerberos library to bind to the auth response. If the socket is not an SSL - socket or the raw HTTP object is not a urllib3 HTTPResponse then None will - be returned and the Kerberos auth will use GSS_C_NO_CHANNEL_BINDINGS - - :param response: The original 401 response from the server - :return: byte string used on the application_data.value field on the CBT struct - """ - - application_data = None - raw_response = response.raw - - if isinstance(raw_response, HTTPResponse): - try: - socket = raw_response._fp.fp.raw._sock - except AttributeError: - warnings.warn("Failed to get raw socket for CBT; has urllib3 impl changed", - NoCertificateRetrievedWarning) - else: - try: - server_certificate = socket.getpeercert(True) - except AttributeError: - pass - else: - certificate_hash = _get_certificate_hash(server_certificate) - application_data = b'tls-server-end-point:' + certificate_hash - else: - warnings.warn( - "Requests is running with a non urllib3 backend, cannot retrieve server certificate for CBT", - NoCertificateRetrievedWarning) - - return application_data - -class HTTPKerberosAuth(AuthBase): - """Attaches HTTP GSSAPI/Kerberos Authentication to the given Request - object.""" - def __init__( - self, mutual_authentication=REQUIRED, - service="HTTP", delegate=False, force_preemptive=False, - principal=None, hostname_override=None, - sanitize_mutual_error_response=True, send_cbt=True): - self._context = {} - self.mutual_authentication = mutual_authentication - self.delegate = delegate - self.pos = None - self.service = service - self.force_preemptive = force_preemptive - self.principal = principal - self.hostname_override = hostname_override - self.sanitize_mutual_error_response = sanitize_mutual_error_response - self.auth_done = False - - # Set the CBT values populated after the first response - self.send_cbt = send_cbt - self.cbt_binding_tried = False - self.cbt_struct = None - - def generate_request_header(self, response, host, is_preemptive=False): - """ - Generates the GSSAPI authentication token with kerberos. - - If any GSSAPI step fails, raise KerberosExchangeError - with failure detail. - - """ - - # Flags used by kerberos module. - gssflags = spnego.ContextReq.sequence_detect - if self.delegate: - gssflags |= spnego.ContextReq.delegate - if self.mutual_authentication != DISABLED: - gssflags |= spnego.ContextReq.mutual_auth - - try: - kerb_stage = "ctx init" - # contexts still need to be stored by host, but hostname_override - # allows use of an arbitrary hostname for the kerberos exchange - # (eg, in cases of aliased hosts, internal vs external, CNAMEs - # w/ name-based HTTP hosting) - kerb_host = self.hostname_override if self.hostname_override is not None else host - - self._context[host] = ctx = spnego.client( - username=self.principal, - hostname=kerb_host, - service=self.service, - channel_bindings=self.cbt_struct, - context_req=gssflags, - protocol="kerberos", - ) - - # if we have a previous response from the server, use it to continue - # the auth process, otherwise use an empty value - negotiate_resp_value = None if is_preemptive else _negotiate_value(response) - - kerb_stage = "ctx step" - gss_response = ctx.step(in_token=negotiate_resp_value) - - return "Negotiate {0}".format(base64.b64encode(gss_response).decode()) - - except spnego.exceptions.SpnegoError as error: - log.exception( - "generate_request_header(): {0} failed:".format(kerb_stage)) - log.exception(error) - raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error))) from error - - def authenticate_user(self, response, **kwargs): - """Handles user authentication with gssapi/kerberos""" - - host = urlparse(response.url).hostname - if response.status_code == 407: - if 'proxies' in kwargs and urlparse(response.url).scheme in kwargs['proxies']: - host = urlparse(kwargs['proxies'][urlparse(response.url).scheme]).hostname - - try: - auth_header = self.generate_request_header(response, host) - except KerberosExchangeError: - # GSS Failure, return existing response - return response - - if response.status_code == 407: - log.debug("authenticate_user(): Proxy-Authorization header: {0}".format( - auth_header)) - response.request.headers['Proxy-Authorization'] = auth_header - else: - log.debug("authenticate_user(): Authorization header: {0}".format( - auth_header)) - response.request.headers['Authorization'] = auth_header - - # Consume the content so we can reuse the connection for the next - # request. - response.content - response.raw.release_conn() - - _r = response.connection.send(response.request, **kwargs) - _r.history.append(response) - - log.debug("authenticate_user(): returning {0}".format(_r)) - return _r - - def handle_401(self, response, **kwargs): - """Handles 401's, attempts to use gssapi/kerberos authentication""" - - log.debug("handle_401(): Handling: 401") - if _negotiate_value(response) is not None: - _r = self.authenticate_user(response, **kwargs) - log.debug("handle_401(): returning {0}".format(_r)) - return _r - else: - log.debug("handle_401(): Kerberos is not supported") - log.debug("handle_401(): returning {0}".format(response)) - return response - - def handle_407(self, response, **kwargs): - """Handles 407's, attempts to use gssapi/kerberos authentication""" - - log.debug("handle_407(): Handling: 407") - if _negotiate_value(response) is not None: - _r = self.authenticate_user(response, **kwargs) - log.debug("handle_407(): returning {0}".format(_r)) - return _r - else: - log.debug("handle_407(): Kerberos is not supported") - log.debug("handle_407(): returning {0}".format(response)) - return response - - def handle_other(self, response): - """Handles all responses with the exception of 401s. - - This is necessary so that we can authenticate responses if requested""" - - log.debug("handle_other(): Handling: %d" % response.status_code) - - if self.mutual_authentication in (REQUIRED, OPTIONAL) and not self.auth_done: - - is_http_error = response.status_code >= 400 - - if _negotiate_value(response) is not None: - log.debug("handle_other(): Authenticating the server") - if not self.authenticate_server(response): - # Mutual authentication failure when mutual auth is wanted, - # raise an exception so the user doesn't use an untrusted - # response. - log.error("handle_other(): Mutual authentication failed") - raise MutualAuthenticationError("Unable to authenticate " - "{0}".format(response)) - - # Authentication successful - log.debug("handle_other(): returning {0}".format(response)) - self.auth_done = True - return response - - elif is_http_error or self.mutual_authentication == OPTIONAL: - if not response.ok: - log.error("handle_other(): Mutual authentication unavailable " - "on {0} response".format(response.status_code)) - - if(self.mutual_authentication == REQUIRED and - self.sanitize_mutual_error_response): - return SanitizedResponse(response) - else: - return response - else: - # Unable to attempt mutual authentication when mutual auth is - # required, raise an exception so the user doesn't use an - # untrusted response. - log.error("handle_other(): Mutual authentication failed") - raise MutualAuthenticationError("Unable to authenticate " - "{0}".format(response)) - else: - log.debug("handle_other(): returning {0}".format(response)) - return response - - def authenticate_server(self, response): - """ - Uses GSSAPI to authenticate the server. - - Returns True on success, False on failure. - """ - - response_token = _negotiate_value(response) - log.debug("authenticate_server(): Authenticate header: {0}".format( - base64.b64encode(response_token).decode() - if response_token - else "")) - - host = urlparse(response.url).hostname - - try: - self._context[host].step(in_token=response_token) - except spnego.exceptions.SpnegoError: - log.exception("authenticate_server(): ctx step() failed:") - return False - - log.debug("authenticate_server(): returning {0}".format(response)) - return True - - def handle_response(self, response, **kwargs): - """Takes the given response and tries kerberos-auth, as needed.""" - num_401s = kwargs.pop('num_401s', 0) - num_407s = kwargs.pop('num_407s', 0) - - # Check if we have already tried to get the CBT data value - if not self.cbt_binding_tried and self.send_cbt: - # If we haven't tried, try getting it now - cbt_application_data = _get_channel_bindings_application_data(response) - if cbt_application_data: - self.cbt_struct = spnego.channel_bindings.GssChannelBindings( - application_data=cbt_application_data, - ) - - # Regardless of the result, set tried to True so we don't waste time next time - self.cbt_binding_tried = True - - if self.pos is not None: - # Rewind the file position indicator of the body to where - # it was to resend the request. - response.request.body.seek(self.pos) - - if response.status_code == 401 and num_401s < 2: - # 401 Unauthorized. Handle it, and if it still comes back as 401, - # that means authentication failed. - _r = self.handle_401(response, **kwargs) - log.debug("handle_response(): returning %s", _r) - log.debug("handle_response() has seen %d 401 responses", num_401s) - num_401s += 1 - return self.handle_response(_r, num_401s=num_401s, **kwargs) - elif response.status_code == 401 and num_401s >= 2: - # Still receiving 401 responses after attempting to handle them. - # Authentication has failed. Return the 401 response. - log.debug("handle_response(): returning 401 %s", response) - return response - elif response.status_code == 407 and num_407s < 2: - # 407 Unauthorized. Handle it, and if it still comes back as 407, - # that means authentication failed. - _r = self.handle_407(response, **kwargs) - log.debug("handle_response(): returning %s", _r) - log.debug("handle_response() has seen %d 407 responses", num_407s) - num_407s += 1 - return self.handle_response(_r, num_407s=num_407s, **kwargs) - elif response.status_code == 407 and num_407s >= 2: - # Still receiving 407 responses after attempting to handle them. - # Authentication has failed. Return the 407 response. - log.debug("handle_response(): returning 407 %s", response) - return response - else: - _r = self.handle_other(response) - log.debug("handle_response(): returning %s", _r) - return _r - - def deregister(self, response): - """Deregisters the response handler""" - response.request.deregister_hook('response', self.handle_response) - - def __call__(self, request): - if self.force_preemptive and not self.auth_done: - # add Authorization header before we receive a 401 - # by the 401 handler - host = urlparse(request.url).hostname - - auth_header = self.generate_request_header(None, host, is_preemptive=True) - - log.debug("HTTPKerberosAuth: Preemptive Authorization header: {0}".format(auth_header)) - - request.headers['Authorization'] = auth_header - - request.register_hook('response', self.handle_response) - try: - self.pos = request.body.tell() - except AttributeError: - # In the case of HTTPKerberosAuth being reused and the body - # of the previous request was a file-like object, pos has - # the file position of the previous body. Ensure it's set to - # None. - self.pos = None - return request From d966b5a59d408112c63c43456e21d0b19b3364fe Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:31:37 +0100 Subject: [PATCH 18/26] Remove vendoring artefacts --- docs/html/cli/pip_install.rst | 3 --- src/pip/_vendor/README.rst | 1 - src/pip/_vendor/requests_kerberos.pyi | 1 - 3 files changed, 5 deletions(-) delete mode 100644 src/pip/_vendor/requests_kerberos.pyi diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index c86283fcf1c..34244febd68 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -246,9 +246,6 @@ only bugs will be considered, and merged (subject to normal review processes). Note that there may be delays due to the lack of developer resources for reviewing such pull requests. - - - .. _`Caching`: Caching diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 186b710375d..26904ca251a 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -113,7 +113,6 @@ Modifications ``pip._vendor`` and to *not* load ``simplejson`` (all platforms) and ``pyopenssl`` (Windows). * ``platformdirs`` has been modified to import its submodules from ``pip._vendor.platformdirs``. -* ``requests_kerberos`` has been modified to import some of its dependencies from ``pip._vendor`` Automatic Vendoring =================== diff --git a/src/pip/_vendor/requests_kerberos.pyi b/src/pip/_vendor/requests_kerberos.pyi deleted file mode 100644 index 52b7388800c..00000000000 --- a/src/pip/_vendor/requests_kerberos.pyi +++ /dev/null @@ -1 +0,0 @@ -from requests_kerberos import * \ No newline at end of file From 02cdfbf06426a71739dcf7c223dbe824bff5fc54 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:31:55 +0100 Subject: [PATCH 19/26] Remove vendoring artefacts --- src/pip/_vendor/vendor.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index b000f0ed860..345b1f2c623 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -13,7 +13,6 @@ requests==2.27.1 chardet==4.0.0 idna==3.3 urllib3==1.26.9 -requests_kerberos==0.14.0 rich==12.2.0 pygments==2.11.2 typing_extensions==4.2.0 From a1049e4fe3dfd956ba0c62971da0f5af52a27f9a Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:35:40 +0100 Subject: [PATCH 20/26] Level has changed --- docs/html/cli/pip_install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index 34244febd68..11be637b63b 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -228,7 +228,7 @@ overridden by using ``--cert`` option or by using ``PIP_CERT``, .. _`Kerberos Authentication`: Kerberos Authentication -++++++++++++++++++++++++++++ +----------------------- Starting with vXX.X, pip supports using a Kerberos ticket to authenticate with servers. To use Kerberos one must: From 3c24f5b57da75c74463f3a1a13b2fd73caeb10d1 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 01:44:47 +0100 Subject: [PATCH 21/26] Use new PR number --- news/{4854.feature.rst => 11090.feature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{4854.feature.rst => 11090.feature.rst} (100%) diff --git a/news/4854.feature.rst b/news/11090.feature.rst similarity index 100% rename from news/4854.feature.rst rename to news/11090.feature.rst From c8142af9e15fdf97135420e8bc4175e847a8f59e Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 02:47:46 +0100 Subject: [PATCH 22/26] Set start --- src/pip/_internal/network/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index b1413bb0ec3..9aa439165d7 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -304,7 +304,7 @@ def __init__( elif prompting: auths = [no_prompt, HTTPKerberosAuth(REQUIRED)] - self.auth = MultiAuth(auths) + self.auth = MultiAuth(*auths) # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. From 8199d810521f74002ac6a38d4981b3eb561ec27a Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 02:57:38 +0100 Subject: [PATCH 23/26] Fix code --- src/pip/_internal/network/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 9aa439165d7..3edcab9dced 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -301,7 +301,7 @@ def __init__( raise if prompting: auths = [no_prompt, HTTPKerberosAuth(REQUIRED), prompt] - elif prompting: + else: auths = [no_prompt, HTTPKerberosAuth(REQUIRED)] self.auth = MultiAuth(*auths) From 5acc560745594ea78055869744ac6c998b3db371 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 03:51:41 +0100 Subject: [PATCH 24/26] Simplify --- docs/html/cli/pip_install.rst | 3 +- src/pip/_internal/network/auth.py | 41 ---------------------------- src/pip/_internal/network/session.py | 9 ++---- 3 files changed, 4 insertions(+), 49 deletions(-) diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index 11be637b63b..3280ea787f4 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -237,8 +237,9 @@ with servers. To use Kerberos one must: - Run ``pip`` with the flag ``--enable-kerberos``. Your system administrator can also set this in the config files or an environment variable, see :ref:`Configuration`. +- Have a valid Kerberos ticket. -It is likely that you will also want to use ``--no-input`` at the same time. +Note that setting this flag *enforces* the connection uses Kerberos. Bugs reported with pip in relation to Kerberos will likely not be addressed directly by pip's maintainers. Pull Requests to fix Kerberos diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 4af20a44560..e40ebfb2785 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -322,44 +322,3 @@ def save_credentials(self, resp: Response, **kwargs: Any) -> None: keyring.set_password(*creds) except Exception: logger.exception("Failed to save credentials") - - -class MultiAuth(AuthBase): - def __init__(self, initial_auth: AuthBase = None, *auths: AuthBase): - if initial_auth is None: - self.initial_auth = MultiDomainBasicAuth(prompting=False) - else: - self.initial_auth = initial_auth - - self.auths = auths - - def __call__(self, req: Request) -> Request: - req = self.initial_auth(req) - self._register_hook(req, 0) # register hook after auth itself - return req - - def _register_hook(self, req: Request, i: int) -> None: - if i >= len(self.auths): - return - - def hook(resp: Response, **kwargs: Any) -> None: - self.handle_response(resp, i, **kwargs) - - req.register_hook("response", hook) - - def handle_response(self, resp: Response, i: int, **kwargs: Any) -> Response: - if resp.status_code != 401: # authorization required - return resp - - # clear response - resp.content - resp.raw.release_conn() - - req = self.auths[i](resp.request) # delegate to ith auth - logger.info("registering hook %d", i + 1) - self._register_hook(req, i + 1) # register hook after auth itself - - new_resp = resp.connection.send(req, **kwargs) - new_resp.history.append(resp) - - return new_resp diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 3edcab9dced..26718ce7864 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -28,7 +28,7 @@ from pip import __version__ from pip._internal.metadata import get_default_environment from pip._internal.models.link import Link -from pip._internal.network.auth import MultiAuth, MultiDomainBasicAuth +from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. @@ -299,12 +299,7 @@ def __init__( "are available in the same environment as pip?" ) raise - if prompting: - auths = [no_prompt, HTTPKerberosAuth(REQUIRED), prompt] - else: - auths = [no_prompt, HTTPKerberosAuth(REQUIRED)] - - self.auth = MultiAuth(*auths) + self.auth = HTTPKerberosAuth(REQUIRED) # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. From 1d96d55e52a3d579c358b503292b693bb1036c90 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Fri, 6 May 2022 03:54:30 +0100 Subject: [PATCH 25/26] Simplify more --- src/pip/_internal/cli/req_command.py | 4 +++- src/pip/_internal/network/session.py | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index d0adcb7142a..95a06cb9dfa 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -93,7 +93,6 @@ def _build_session( retries=retries if retries is not None else options.retries, trusted_hosts=options.trusted_hosts, index_urls=self._get_index_urls(options), - prompting=not options.no_input, enable_kerberos=options.enable_kerberos, ) @@ -116,6 +115,9 @@ def _build_session( "https": options.proxy, } + # Determine if we can prompt the user for authentication or not + session.auth.prompting = not options.no_input + return session diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 26718ce7864..eb26f803c7b 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -266,7 +266,6 @@ def __init__( cache: Optional[str] = None, trusted_hosts: Sequence[str] = (), index_urls: Optional[List[str]] = None, - prompting: bool = False, enable_kerberos: bool = False, **kwargs: Any, ) -> None: @@ -288,8 +287,6 @@ def __init__( prompt.passwords = no_prompt.passwords # share same dict of passwords # Attach our Authentication handler to the session - self.auth = prompt if prompting else no_prompt - if enable_kerberos: try: from requests_kerberos import REQUIRED, HTTPKerberosAuth @@ -300,6 +297,8 @@ def __init__( ) raise self.auth = HTTPKerberosAuth(REQUIRED) + else: + self.auth = MultiDomainBasicAuth(index_urls=index_urls) # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. From 647a4f4875647b76449b90571cb63098f8c0b0b8 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Mon, 6 Jun 2022 13:53:29 +0100 Subject: [PATCH 26/26] Don't cargo cult unnecessary changes in logging.py --- src/pip/_internal/utils/logging.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 1c17e373b68..d6e991b89ec 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -257,10 +257,6 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) # enabled for vendored libraries. vendored_log_level = "WARNING" if level in ["INFO", "ERROR"] else "DEBUG" - # Similar for vendored Kerberos, which is a bit trigger happy. - logging.addLevelName(logging.CRITICAL + 1, "SUPERCRITICAL") - kerberos_log_level = "SUPERCRITICAL" if level in ["INFO", "ERROR"] else "DEBUG" - # Shorthands for clarity log_streams = { "stdout": "ext://sys.stdout", @@ -343,12 +339,7 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) "level": root_level, "handlers": handlers, }, - "loggers": { - "pip._vendor": {"level": vendored_log_level}, - "pip._vendor.requests_kerberos.kerberos_": { - "level": kerberos_log_level - }, - }, + "loggers": {"pip._vendor": {"level": vendored_log_level}}, } )