Commit c8d6fbce by Arun Babu Neelicattu

repositories: use authenticator for legacy repos

This change unifies the use of authenticator for legacy repositories.
This ensures that we retrieve the correct credentials in all cases.

Relates-to: #2799 #2988
parent 408b0fb8
......@@ -134,11 +134,9 @@ class Factory(BaseFactory):
def create_legacy_repository(
self, source, auth_config
): # type: (Dict[str, str], Config) -> LegacyRepository
from .repositories.auth import Auth
from .repositories.legacy_repository import LegacyRepository
from .utils.helpers import get_cert
from .utils.helpers import get_client_cert
from .utils.password_manager import PasswordManager
if "url" in source:
# PyPI-like repository
......@@ -147,19 +145,13 @@ class Factory(BaseFactory):
else:
raise RuntimeError("Unsupported source specified")
password_manager = PasswordManager(auth_config)
name = source["name"]
url = source["url"]
credentials = password_manager.get_http_auth(name)
if credentials:
auth = Auth(url, credentials["username"], credentials["password"])
else:
auth = None
return LegacyRepository(
name,
url,
auth=auth,
config=auth_config,
cert=get_cert(auth_config, name),
client_cert=get_client_cert(auth_config, name),
)
import logging
import time
from typing import TYPE_CHECKING
......@@ -21,14 +22,27 @@ if TYPE_CHECKING:
from poetry.config.config import Config
logger = logging.getLogger()
class Authenticator(object):
def __init__(self, config, io): # type: (Config, IO) -> None
def __init__(self, config, io=None): # type: (Config, Optional[IO]) -> None
self._config = config
self._io = io
self._session = None
self._credentials = {}
self._password_manager = PasswordManager(self._config)
def _log(self, message, level="debug"): # type: (str, str) -> None
if self._io is not None:
self._io.write_line(
"<{level:s}>{message:s}</{level:s}>".format(
message=message, level=level
)
)
else:
getattr(logger, level, logger.debug)(message)
@property
def session(self): # type: () -> requests.Session
if self._session is None:
......@@ -40,9 +54,7 @@ class Authenticator(object):
self, method, url, **kwargs
): # type: (str, str, Any) -> requests.Response
request = requests.Request(method, url)
io = kwargs.get("io") or self._io
username, password = self._get_credentials_for_url(url)
username, password = self.get_credentials_for_url(url)
if username is not None and password is not None:
request = requests.auth.HTTPBasicAuth(username, password)(request)
......@@ -83,19 +95,16 @@ class Authenticator(object):
if not is_last_attempt:
attempt += 1
delay = 0.5 * attempt
if io is not None:
io.write_line(
"<debug>Retrying HTTP request in {} seconds.</debug>".format(
delay
)
)
self._log(
"Retrying HTTP request in {} seconds.".format(delay), level="debug"
)
time.sleep(delay)
continue
# this should never really be hit under any sane circumstance
raise PoetryException("Failed HTTP {} request", method.upper())
def _get_credentials_for_url(
def get_credentials_for_url(
self, url
): # type: (str) -> Tuple[Optional[str], Optional[str]]
parsed_url = urlparse.urlsplit(url)
......
from requests import Request
from requests.auth import AuthBase
from requests.auth import HTTPBasicAuth
from poetry.utils._compat import urlparse
class Auth(AuthBase):
def __init__(self, url, username, password): # type: (str, str, str) -> None
self._hostname = urlparse.urlparse(url).hostname
self._auth = HTTPBasicAuth(username, password)
@property
def hostname(self): # type: () -> str
return self._hostname
@property
def auth(self): # type: () -> HTTPBasicAuth
return self._auth
def __call__(self, r): # type: (Request) -> Request
if urlparse.urlparse(r.url).hostname != self._hostname:
return r
self._auth(r)
return r
......@@ -8,6 +8,7 @@ from typing import Optional
from typing import Union
import requests
import requests.auth
from cachecontrol import CacheControl
from cachecontrol.caches.file_cache import FileCache
......@@ -24,8 +25,9 @@ from poetry.utils._compat import Path
from poetry.utils.helpers import canonicalize_name
from poetry.utils.patterns import wheel_file_re
from ..config.config import Config
from ..inspection.info import PackageInfo
from .auth import Auth
from ..installation.authenticator import Authenticator
from .exceptions import PackageNotFound
from .exceptions import RepositoryError
from .pypi_repository import PyPiRepository
......@@ -159,15 +161,14 @@ class Page:
class LegacyRepository(PyPiRepository):
def __init__(
self, name, url, auth=None, disable_cache=False, cert=None, client_cert=None
): # type: (str, str, Optional[Auth], bool, Optional[Path], Optional[Path]) -> None
self, name, url, config=None, disable_cache=False, cert=None, client_cert=None
): # type: (str, str, Optional[Config], bool, Optional[Path], Optional[Path]) -> None
if name == "pypi":
raise ValueError("The name [pypi] is reserved for repositories")
self._packages = []
self._name = name
self._url = url.rstrip("/")
self._auth = auth
self._client_cert = client_cert
self._cert = cert
self._cache_dir = REPOSITORY_CACHE_DIR / name
......@@ -183,19 +184,25 @@ class LegacyRepository(PyPiRepository):
}
)
self._authenticator = Authenticator(
config=config or Config(use_environment=True)
)
self._session = CacheControl(
requests.session(), cache=FileCache(str(self._cache_dir / "_http"))
self._authenticator.session, cache=FileCache(str(self._cache_dir / "_http"))
)
url_parts = urlparse.urlparse(self._url)
if not url_parts.username and self._auth:
self._session.auth = self._auth
username, password = self._authenticator.get_credentials_for_url(self._url)
if username is not None and password is not None:
self._authenticator.session.auth = requests.auth.HTTPBasicAuth(
username, password
)
if self._cert:
self._session.verify = str(self._cert)
self._authenticator.session.verify = str(self._cert)
if self._client_cert:
self._session.cert = str(self._client_cert)
self._authenticator.session.cert = str(self._client_cert)
self._disable_cache = disable_cache
......@@ -209,15 +216,15 @@ class LegacyRepository(PyPiRepository):
@property
def authenticated_url(self): # type: () -> str
if not self._auth:
if not self._session.auth:
return self.url
parsed = urlparse.urlparse(self.url)
return "{scheme}://{username}:{password}@{netloc}{path}".format(
scheme=parsed.scheme,
username=quote(self._auth.auth.username, safe=""),
password=quote(self._auth.auth.password, safe=""),
username=quote(self._session.auth.username, safe=""),
password=quote(self._session.auth.password, safe=""),
netloc=parsed.netloc,
path=parsed.path,
)
......
import base64
from requests import Request
from poetry.repositories.auth import Auth
from poetry.utils._compat import decode
from poetry.utils._compat import encode
def test_auth_with_request_on_the_same_host():
auth = Auth("https://python-poetry.org", "foo", "bar")
request = Request("GET", "https://python-poetry.org/docs/")
assert "Authorization" not in request.headers
request = auth(request)
assert "Authorization" in request.headers
assert request.headers["Authorization"] == "Basic {}".format(
decode(base64.b64encode(encode(":".join(("foo", "bar")))))
)
def test_auth_with_request_with_same_authentication():
auth = Auth("https://python-poetry.org", "foo", "bar")
request = Request("GET", "https://foo:bar@python-poetry.org/docs/")
assert "Authorization" not in request.headers
request = auth(request)
assert "Authorization" in request.headers
assert request.headers["Authorization"] == "Basic {}".format(
decode(base64.b64encode(encode(":".join(("foo", "bar")))))
)
def test_auth_with_request_on_different_hosts():
auth = Auth("https://python-poetry.org", "foo", "bar")
request = Request("GET", "https://pendulum.eustace.io/docs/")
assert "Authorization" not in request.headers
request = auth(request)
assert "Authorization" not in request.headers
......@@ -4,7 +4,6 @@ import pytest
from poetry.core.packages import Dependency
from poetry.factory import Factory
from poetry.repositories.auth import Auth
from poetry.repositories.exceptions import PackageNotFound
from poetry.repositories.exceptions import RepositoryError
from poetry.repositories.legacy_repository import LegacyRepository
......@@ -23,9 +22,9 @@ class MockRepository(LegacyRepository):
FIXTURES = Path(__file__).parent / "fixtures" / "legacy"
def __init__(self, auth=None):
def __init__(self):
super(MockRepository, self).__init__(
"legacy", url="http://legacy.foo.bar", auth=auth, disable_cache=True
"legacy", url="http://legacy.foo.bar", disable_cache=True
)
def _get(self, endpoint):
......@@ -302,18 +301,11 @@ def test_get_package_retrieves_packages_with_no_hashes():
assert [] == package.files
def test_username_password_special_chars():
auth = Auth("http://legacy.foo.bar", "user:", "/%2Fp@ssword")
repo = MockRepository(auth=auth)
assert "http://user%3A:%2F%252Fp%40ssword@legacy.foo.bar" == repo.authenticated_url
class MockHttpRepository(LegacyRepository):
def __init__(self, endpoint_responses, http):
base_url = "http://legacy.foo.bar"
super(MockHttpRepository, self).__init__(
"legacy", url=base_url, auth=None, disable_cache=True
"legacy", url=base_url, disable_cache=True
)
for endpoint, response in endpoint_responses.items():
......
......@@ -4,7 +4,6 @@ import pytest
from poetry.factory import Factory
from poetry.packages import Locker as BaseLocker
from poetry.repositories.auth import Auth
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.utils._compat import Path
from poetry.utils.exporter import Exporter
......@@ -654,11 +653,7 @@ tests/fixtures/distributions/demo-0.1.0.tar.gz; python_version < "3.7"
def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, poetry):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
auth=Auth("https://example.com/simple", "foo", "bar"),
)
LegacyRepository("custom", "https://example.com/simple",)
)
poetry.locker.mock_lock_data(
{
......@@ -713,11 +708,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so
tmp_dir, poetry
):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
auth=Auth("https://example.com/simple", "foo", "bar"),
)
LegacyRepository("custom", "https://example.com/simple",)
)
poetry.pool.add_repository(LegacyRepository("custom", "https://foobaz.com/simple",))
poetry.locker.mock_lock_data(
......@@ -792,12 +783,14 @@ foo==1.2.3 \\
def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials(
tmp_dir, poetry, config
):
poetry.config.merge(
{
"repositories": {"custom": {"url": "https://example.com/simple"}},
"http-basic": {"custom": {"username": "foo", "password": "bar"}},
}
)
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
auth=Auth("https://example.com/simple", "foo", "bar"),
)
LegacyRepository("custom", "https://example.com/simple", config=poetry.config)
)
poetry.locker.mock_lock_data(
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment