Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kerberos support to authentication pip (when supported) #4854

Closed
wants to merge 11 commits into from
15 changes: 15 additions & 0 deletions docs/html/reference/pip_install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version number should be changed to the release it will actually be in.

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`:

Expand Down
1 change: 1 addition & 0 deletions news/4854.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add kerberos support to possible authenticators, when available. Vendor in requests_kerberos 0.11.0.
1 change: 1 addition & 0 deletions news/requests_kerberos.vendor
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Vendored requests_kerberos at requests_kerberos==0.11.0
4 changes: 1 addition & 3 deletions src/pip/_internal/cli/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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
Expand All @@ -100,9 +101,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):
Expand Down
63 changes: 62 additions & 1 deletion src/pip/_internal/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@

HAS_TLS = (ssl is not None) or IS_PYOPENSSL

try:
from pip._vendor.requests_kerberos import HTTPKerberosAuth
from pip._vendor.requests_kerberos import kerberos_ as ik
_KERBEROS_AVAILABLE = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some reason other that pip._vendor.requests_kerberos might result in ImportErrors, outside of them being missing?

If not, please import these unconditionally at the top of the file, since pip does vendoring to ensure that vendored packages are always available. Basically, in not-broken installations of pip, this variable will always be True, which makes it redundant.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If py/winkerberos is not installed, the behaviour is the same as before": requests_kerberos on its own will not make kerberos work, one has to have either py/winkerberos installed (which we cannot vendor because they are compiled/platform dependent?). requests_kerberos in that case will fail with an ImportError.


except ImportError:
_KERBEROS_AVAILABLE = False

__all__ = ['get_file_content',
'is_url', 'url_to_path', 'path_to_url',
'is_archive_file', 'unpack_vcs_link',
Expand Down Expand Up @@ -216,6 +224,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,
Expand Down Expand Up @@ -330,14 +379,26 @@ 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)

# Attach our User Agent to the request
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.
Expand Down
9 changes: 9 additions & 0 deletions src/pip/_internal/utils/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ 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

logging.getLogger('pip._vendor.requests_kerberos.kerberos_').setLevel(
log_level_kerberos
)

# The "root" logger should match the "console" level *unless* we also need
# to log to a user log file.
include_user_log = user_log_file is not None
Expand Down
2 changes: 2 additions & 0 deletions src/pip/_vendor/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ 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
Expand Down
25 changes: 25 additions & 0 deletions src/pip/_vendor/requests_kerberos/__init__.py
Original file line number Diff line number Diff line change
@@ -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'
14 changes: 14 additions & 0 deletions src/pip/_vendor/requests_kerberos/compat.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions src/pip/_vendor/requests_kerberos/exceptions.py
Original file line number Diff line number Diff line change
@@ -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"""
Loading