diff --git a/CHANGES.txt b/CHANGES.txt index e136b35a3f3..17de3de000b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,7 @@ **8.1.0 (unreleased)** +* Added support for ntlm authentication using ``--auth-ntlm`` + **8.0.2 (2016-01-21)** diff --git a/pip/basecommand.py b/pip/basecommand.py index a07043a8d67..6f32f1e9b73 100644 --- a/pip/basecommand.py +++ b/pip/basecommand.py @@ -10,7 +10,7 @@ from pip import cmdoptions from pip.index import PackageFinder from pip.locations import running_under_virtualenv -from pip.download import PipSession +from pip.download import PipSession, MultiDomainNtlmAuth from pip.exceptions import (BadCommand, InstallationError, UninstallationError, CommandError, PreviousBuildDirError) @@ -93,6 +93,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/pip/cmdoptions.py b/pip/cmdoptions.py index aced0c0f7ed..e4a04d67ff4 100644 --- a/pip/cmdoptions.py +++ b/pip/cmdoptions.py @@ -565,6 +565,14 @@ def _merge_hash(option, opt_str, value, parser): 'requirements file has a --hash option.') +auth_ntlm = partial( + Option, + '--auth-ntlm', + dest='auth_ntlm', + action='store_true', + default=False, + help='Authenticate on host using NTLM.') + ########## # groups # ########## @@ -592,6 +600,7 @@ def _merge_hash(option, opt_str, value, parser): cache_dir, no_cache, disable_pip_version_check, + auth_ntlm, ] } diff --git a/pip/download.py b/pip/download.py index e447b019351..12f97519c94 100644 --- a/pip/download.py +++ b/pip/download.py @@ -19,6 +19,11 @@ except ImportError: HAS_TLS = False +try: + from requests_ntlm import HttpNtlmAuth # noqa +except ImportError: + HttpNtlmAuth = None + from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib import request as urllib_request @@ -122,7 +127,7 @@ def user_agent(): ) -class MultiDomainBasicAuth(AuthBase): +class MultiDomainAuth(AuthBase): def __init__(self, prompting=True): self.prompting = prompting @@ -149,7 +154,7 @@ def __call__(self, req): self.passwords[netloc] = (username, password) # Send the basic auth with this request - req = HTTPBasicAuth(username or "", password or "")(req) + req = self.authlib(username or "", password or "")(req) # Attach a hook to handle 401 responses req.register_hook("response", self.handle_401) @@ -182,7 +187,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) # Send our new request new_resp = resp.connection.send(req, **kwargs) @@ -198,6 +203,31 @@ def parse_credentials(self, netloc): return userinfo, None return None, None + @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 HttpNtlmAuth 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 HttpNtlmAuth + class LocalFSAdapter(BaseAdapter): diff --git a/setup.py b/setup.py index 43bab40282f..1ab1ed84406 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ def find_version(*file_paths): zip_safe=False, extras_require={ 'testing': tests_require, + 'ntlm': ['requests_ntlm'], }, cmdclass={'test': PyTest}, ) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index cdb8d5b2032..74bfbb10dab 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -60,6 +60,23 @@ def test_with_setuptools_and_import_error(script, data): assert "ImportError: toto" in result.stderr +def test_without_ntlm(script, data): + result = script.run( + "python", "-c", + "import pip; pip.main([" + "'install', " + "'INITools==0.2', " + "'-f', '%s', " + "'--auth-ntlm'])" % data.packages, + expect_error=True, + ) + assert ( + "Dependencies for Ntlm authentication are missing. Install " + "dependencies via the 'pip install pip[ntlm]' command." + in result.stderr + ) + + def test_pip_second_command_line_interface_works(script, data): """ Check if ``pip`` commands behaves equally diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index ee4b11c7dbb..0b365681786 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -5,6 +5,7 @@ from tempfile import mkdtemp from pip._vendor.six.moves.urllib import request as urllib_request +from pip._vendor.requests.auth import HTTPBasicAuth from mock import Mock, patch import pytest @@ -13,7 +14,7 @@ from pip.exceptions import HashMismatch from pip.download import ( PipSession, SafeFileCache, path_to_url, unpack_http_url, url_to_path, - unpack_file_url, + unpack_file_url, MultiDomainBasicAuth, ) from pip.index import Link from pip.utils.hashes import Hashes @@ -313,6 +314,11 @@ def test_cache_defaults_off(self): assert not hasattr(session.adapters["http://"], "cache") assert not hasattr(session.adapters["https://"], "cache") + def test_authlib_returns_HTTPBasicAuth(self): + session = PipSession() + assert isinstance(session.auth, MultiDomainBasicAuth) + assert session.auth.authlib == HTTPBasicAuth + def test_cache_is_enabled(self, tmpdir): session = PipSession(cache=tmpdir.join("test-cache"))