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

Multiple fixes for repository http authentication #2990

Merged
merged 3 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions poetry/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ def create_poetry(

config.merge(local_config_file.read())

# Load local sources
repositories = {}
for source in base_poetry.pyproject.poetry_config.get("source", []):
name = source.get("name")
url = source.get("url")
if name and url:
repositories[name] = {"url": url}

config.merge({"repositories": repositories})

poetry = Poetry(
base_poetry.file.path,
base_poetry.local_config,
Expand Down Expand Up @@ -124,11 +134,9 @@ def create_config(cls, io=None): # type: (Optional[IO]) -> Config
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
Expand All @@ -137,19 +145,13 @@ def create_legacy_repository(
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),
)
34 changes: 22 additions & 12 deletions poetry/installation/authenticator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import time

from typing import TYPE_CHECKING
Expand All @@ -21,14 +22,27 @@
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:
Expand All @@ -40,9 +54,7 @@ def request(
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)
Expand Down Expand Up @@ -83,19 +95,16 @@ def request(
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)
Expand Down Expand Up @@ -135,7 +144,8 @@ def _get_credentials_for_netloc_from_config(
self, netloc
): # type: (str) -> Tuple[Optional[str], Optional[str]]
credentials = (None, None)
for repository_name in self._config.get("http-basic", {}):

for repository_name in self._config.get("repositories", []):
repository_config = self._config.get(
"repositories.{}".format(repository_name)
)
Expand Down
27 changes: 0 additions & 27 deletions poetry/repositories/auth.py

This file was deleted.

33 changes: 20 additions & 13 deletions poetry/repositories/legacy_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Union

import requests
import requests.auth

from cachecontrol import CacheControl
from cachecontrol.caches.file_cache import FileCache
Expand All @@ -24,8 +25,9 @@
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
Expand Down Expand Up @@ -159,15 +161,14 @@ def clean_link(self, url):

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
Expand All @@ -183,19 +184,25 @@ def __init__(
}
)

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

Expand All @@ -209,15 +216,15 @@ def client_cert(self): # type: () -> Optional[Path]

@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,
)
Expand Down
19 changes: 19 additions & 0 deletions tests/installation/test_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,22 @@ def callback(request, uri, response_headers):
assert excinfo.value.response.text == content

assert sleep.call_count == attempts


@pytest.fixture
def environment_repository_credentials(monkeypatch):
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_USERNAME", "bar")
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_PASSWORD", "baz")


def test_authenticator_uses_env_provided_credentials(
config, environ, mock_remote, http, environment_repository_credentials
):
config.merge({"repositories": {"foo": {"url": "https://foo.bar/simple/"}}})

authenticator = Authenticator(config, NullIO())
authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz")

request = http.last_request()

assert "Basic YmFyOmJheg==" == request.headers["Authorization"]
46 changes: 0 additions & 46 deletions tests/repositories/test_auth.py

This file was deleted.

14 changes: 3 additions & 11 deletions tests/repositories/test_legacy_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

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
Expand All @@ -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):
Expand Down Expand Up @@ -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%[email protected]" == 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():
Expand Down
Loading