diff --git a/news/1182.feature b/news/1182.feature new file mode 100644 index 00000000000..9626866b6fa --- /dev/null +++ b/news/1182.feature @@ -0,0 +1 @@ +Added support for ntlm authentication using ``--auth-ntlm`` option. diff --git a/setup.py b/setup.py index 7dab1fd45b2..d60e5a08ca3 100644 --- a/setup.py +++ b/setup.py @@ -83,5 +83,8 @@ def get_version(rel_path): }, zip_safe=False, + extras_require={ + 'ntlm': ['requests_negotiate_sspi'], + }, python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', ) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ff9acfd4644..94835ae0989 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -930,6 +930,15 @@ def check_list_path_option(options): ) # type: Callable[..., Option] +auth_ntlm = partial( + Option, + '--auth-ntlm', + dest='auth_ntlm', + action='store_true', + default=False, + help='Authenticate on host using NTLM.') + + ########## # groups # ########## @@ -959,6 +968,7 @@ def check_list_path_option(options): no_color, no_python_version_warning, unstable_feature, + auth_ntlm, ] } # type: Dict[str, Any] diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 104b033281f..372bb865e36 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -12,9 +12,14 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn -from pip._internal.exceptions import CommandError, PreviousBuildDirError +from pip._internal.exceptions import ( + CommandError, + InstallationError, + PreviousBuildDirError, +) from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.network.auth import MultiDomainNtlmAuth from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer @@ -121,6 +126,14 @@ def _build_session(self, options, retries=None, timeout=None): "https": options.proxy, } + if options.auth_ntlm: + try: + session.auth = MultiDomainNtlmAuth() + except InstallationError: + # Needed to allow pip to check for updates + options.auth_ntlm = False + raise + # Determine if we can prompt the user for authentication or not session.auth.prompting = not options.no_input diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 94da3d46aaa..804a815c9b1 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -13,6 +13,7 @@ from pip._vendor.requests.utils import get_netrc_auth from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._internal.exceptions import InstallationError from pip._internal.utils.misc import ( ask, ask_input, @@ -22,6 +23,11 @@ ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING +try: + from requests_negotiate_sspi import HttpNegotiateAuth # noqa +except ImportError: + HttpNegotiateAuth = None + if MYPY_CHECK_RUNNING: from optparse import Values from typing import Dict, Optional, Tuple @@ -72,7 +78,7 @@ def get_keyring_auth(url, username): ) -class MultiDomainBasicAuth(AuthBase): +class MultiDomainAuth(AuthBase): def __init__(self, prompting=True, index_urls=None): # type: (bool, Optional[Values]) -> None @@ -206,7 +212,7 @@ def __call__(self, req): if username is not None and password is not None: # Send the basic auth with this request - req = HTTPBasicAuth(username, password)(req) + req = self.authlib(username, password)(req) # Attach a hook to handle 401 responses req.register_hook("response", self.handle_401) @@ -260,7 +266,7 @@ def handle_401(self, resp, **kwargs): resp.raw.release_conn() # Add our new username and password to the request - req = HTTPBasicAuth(username or "", password or "")(resp.request) + req = self.authlib(username or "", password or "")(resp.request) req.register_hook("response", self.warn_on_401) # On successful request, save the credentials that were used to @@ -296,3 +302,28 @@ def save_credentials(self, resp, **kwargs): keyring.set_password(*creds) except Exception: logger.exception('Failed to save credentials') + + @property + def authlib(self): + # Place holder for Authentication Class + raise NotImplementedError + + +class MultiDomainBasicAuth(MultiDomainAuth): + @property + def authlib(self): + return HTTPBasicAuth + + +class MultiDomainNtlmAuth(MultiDomainAuth): + def __init__(self, *args, **kwargs): + if HttpNegotiateAuth is None: + raise InstallationError( + "Dependencies for Ntlm authentication are missing. Install " + "dependencies via the 'pip install pip[ntlm]' command." + ) + super(MultiDomainNtlmAuth, self).__init__(*args, **kwargs) + + @property + def authlib(self): + return HttpNegotiateAuth diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2cb44269502..388dbdbdb21 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -186,6 +186,20 @@ def test_pep518_forkbombs(script, data, common_wheels, command, package): ) in result.stderr, str(result) +def test_without_ntlm(script, data): + result = script.pip( + 'install', '--auth-ntlm', + '-f', data.packages, + 'INITools==0.2', + expect_error=True, + ) + assert ( + "Dependencies for Ntlm authentication are missing. Install " + "dependencies via the 'pip install pip[ntlm]' command." + in result.stderr + ) + + @pytest.mark.network def test_pip_second_command_line_interface_works( script, pip_src, data, common_wheels, deprecated_python):