diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst
index f56dc9a0e9f..785b1bb2f8f 100644
--- a/docs/html/reference/pip_install.rst
+++ b/docs/html/reference/pip_install.rst
@@ -513,6 +513,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`:
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.
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
diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py
index b681ccb0681..567b3874569 100644
--- a/src/pip/_internal/download.py
+++ b/src/pip/_internal/download.py
@@ -101,6 +101,14 @@
)
+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',
@@ -494,6 +502,47 @@ def save_credentials(self, resp, **kwargs):
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
+
+
class LocalFSAdapter(BaseAdapter):
def send(self, request, stream=None, timeout=None, verify=None, cert=None,
@@ -579,8 +628,10 @@ def __init__(self, *args, **kwargs):
"""
retries = kwargs.pop("retries", 0)
cache = kwargs.pop("cache", None)
+
trusted_hosts = kwargs.pop("trusted_hosts", []) # type: List[str]
index_urls = kwargs.pop("index_urls", None)
+ prompting = kwargs.pop("prompting", True)
super(PipSession, self).__init__(*args, **kwargs)
@@ -592,7 +643,21 @@ def __init__(self, *args, **kwargs):
self.headers["User-Agent"] = user_agent()
# Attach our Authentication handler to the session
- self.auth = MultiDomainBasicAuth(index_urls=index_urls)
+ 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,
+ index_urls=index_urls
+ )]
+
+ self.auth = MultiAuth(*auths)
# Create our urllib3.Retry instance which will allow us to customize
# how we handle retries.
diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py
index 3fbec712709..e577077eb63 100644
--- a/src/pip/_internal/utils/logging.py
+++ b/src/pip/_internal/utils/logging.py
@@ -304,6 +304,13 @@ def setup_logging(verbosity, no_color, user_log_file):
# 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",
@@ -387,8 +394,11 @@ def setup_logging(verbosity, no_color, user_log_file):
"loggers": {
"pip._vendor": {
"level": vendored_log_level
+ },
+ "pip._vendor.requests_kerberos.kerberos_": {
+ "level": kerberos_log_level
}
- },
+ }
})
return level_number
diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst
index 539634204cf..7401d97378b 100644
--- a/src/pip/_vendor/README.rst
+++ b/src/pip/_vendor/README.rst
@@ -104,6 +104,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``
Automatic Vendoring
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 dbf7d664a58..2e8e25df63b 100644
--- a/src/pip/_vendor/vendor.txt
+++ b/src/pip/_vendor/vendor.txt
@@ -21,3 +21,4 @@ retrying==1.3.3
setuptools==41.0.1
six==1.12.0
webencodings==0.5.1
+requests_kerberos==0.11.0