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

ntlm authentication #3731

Closed
wants to merge 11 commits into from
2 changes: 2 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
**8.1.0 (unreleased)**

* Added support for ntlm authentication using ``--auth-ntlm``


**8.0.2 (2016-01-21)**

Expand Down
10 changes: 9 additions & 1 deletion pip/basecommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions pip/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
##########
Expand Down Expand Up @@ -592,6 +600,7 @@ def _merge_hash(option, opt_str, value, parser):
cache_dir,
no_cache,
disable_pip_version_check,
auth_ntlm,
]
}

Expand Down
36 changes: 33 additions & 3 deletions pip/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -122,7 +127,7 @@ def user_agent():
)


class MultiDomainBasicAuth(AuthBase):
class MultiDomainAuth(AuthBase):

def __init__(self, prompting=True):
self.prompting = prompting
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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):

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def find_version(*file_paths):
zip_safe=False,
extras_require={
'testing': tests_require,
'ntlm': ['requests_ntlm'],
},
cmdclass={'test': PyTest},
)
17 changes: 17 additions & 0 deletions tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<PYVERSION>`` commands behaves equally
Expand Down
8 changes: 7 additions & 1 deletion tests/unit/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"))

Expand Down