Esempio n. 1
0
def test_get_pypi_token(config: Config, with_simple_keyring: None,
                        dummy_keyring: DummyBackend):
    dummy_keyring.set_password("poetry-repository-foo", "__token__", "baz")
    manager = PasswordManager(config)

    assert manager.keyring.is_available()
    assert manager.get_pypi_token("foo") == "baz"
Esempio n. 2
0
def test_get_pypi_token_with_unavailable_backend(config,
                                                 mock_unavailable_backend):
    config.auth_config_source.add_property("pypi-token.foo", "baz")
    manager = PasswordManager(config)

    assert not manager.keyring.is_available()
    assert "baz" == manager.get_pypi_token("foo")
Esempio n. 3
0
def test_get_pypi_token_with_unavailable_backend(config: "Config",
                                                 with_fail_keyring: None):
    config.auth_config_source.add_property("pypi-token.foo", "baz")
    manager = PasswordManager(config)

    assert not manager.keyring.is_available()
    assert manager.get_pypi_token("foo") == "baz"
Esempio n. 4
0
class Authenticator:
    def __init__(
        self,
        config: Config | None = None,
        io: IO | None = None,
        cache_id: str | None = None,
        disable_cache: bool = False,
    ) -> None:
        self._config = config or Config.create()
        self._io = io
        self._sessions_for_netloc: dict[str, requests.Session] = {}
        self._credentials: dict[str, HTTPAuthCredential] = {}
        self._certs: dict[str, RepositoryCertificateConfig] = {}
        self._configured_repositories: dict[
            str, AuthenticatorRepositoryConfig] | None = None
        self._password_manager = PasswordManager(self._config)
        self._cache_control = (FileCache(
            str(self._config.repository_cache_directory /
                (cache_id or "_default_cache") /
                "_http")) if not disable_cache else None)
        self.get_repository_config_for_url = functools.lru_cache(maxsize=None)(
            self._get_repository_config_for_url)

    @property
    def cache(self) -> FileCache | None:
        return self._cache_control

    @property
    def is_cached(self) -> bool:
        return self._cache_control is not None

    def create_session(self) -> requests.Session:
        session = requests.Session()

        if not self.is_cached:
            return session

        session = CacheControl(sess=session, cache=self._cache_control)
        return session

    def get_session(self, url: str | None = None) -> requests.Session:
        if not url:
            return self.create_session()

        parsed_url = urllib.parse.urlsplit(url)
        netloc = parsed_url.netloc

        if netloc not in self._sessions_for_netloc:
            logger.debug("Creating new session for %s", netloc)
            self._sessions_for_netloc[netloc] = self.create_session()

        return self._sessions_for_netloc[netloc]

    def close(self) -> None:
        for session in self._sessions_for_netloc.values():
            if session is not None:
                with contextlib.suppress(AttributeError):
                    session.close()

    def __del__(self) -> None:
        self.close()

    def delete_cache(self, url: str) -> None:
        if self.is_cached:
            self._cache_control.delete(key=url)

    def authenticated_url(self, url: str) -> str:
        parsed = urllib.parse.urlparse(url)
        credential = self.get_credentials_for_url(url)

        if credential.username is not None and credential.password is not None:
            username = urllib.parse.quote(credential.username, safe="")
            password = urllib.parse.quote(credential.password, safe="")

            return (
                f"{parsed.scheme}://{username}:{password}@{parsed.netloc}{parsed.path}"
            )

        return url

    def request(self,
                method: str,
                url: str,
                raise_for_status: bool = True,
                **kwargs: Any) -> requests.Response:
        request = requests.Request(method, url)
        credential = self.get_credentials_for_url(url)

        if credential.username is not None or credential.password is not None:
            request = requests.auth.HTTPBasicAuth(credential.username or "",
                                                  credential.password
                                                  or "")(request)

        session = self.get_session(url=url)
        prepared_request = session.prepare_request(request)

        proxies = kwargs.get("proxies", {})
        stream = kwargs.get("stream")

        certs = self.get_certs_for_url(url)
        verify = kwargs.get("verify") or certs.cert or certs.verify
        cert = kwargs.get("cert") or certs.client_cert

        if cert is not None:
            cert = str(cert)

        verify = str(verify) if isinstance(verify, Path) else verify

        settings = session.merge_environment_settings(  # type: ignore[no-untyped-call]
            prepared_request.url, proxies, stream, verify, cert)

        # Send the request.
        send_kwargs = {
            "timeout": kwargs.get("timeout", REQUESTS_TIMEOUT),
            "allow_redirects": kwargs.get("allow_redirects", True),
        }
        send_kwargs.update(settings)

        attempt = 0

        while True:
            is_last_attempt = attempt >= 5
            try:
                resp = session.send(prepared_request, **send_kwargs)
            except (requests.exceptions.ConnectionError, OSError) as e:
                if is_last_attempt:
                    raise e
            else:
                if resp.status_code not in [502, 503, 504] or is_last_attempt:
                    if raise_for_status:
                        resp.raise_for_status()
                    return resp

            if not is_last_attempt:
                attempt += 1
                delay = 0.5 * attempt
                logger.debug(f"Retrying HTTP request in {delay} seconds.")
                time.sleep(delay)
                continue

        # this should never really be hit under any sane circumstance
        raise PoetryException("Failed HTTP {} request", method.upper())

    def get(self, url: str, **kwargs: Any) -> requests.Response:
        return self.request("get", url, **kwargs)

    def post(self, url: str, **kwargs: Any) -> requests.Response:
        return self.request("post", url, **kwargs)

    def _get_credentials_for_repository(
            self,
            repository: AuthenticatorRepositoryConfig,
            username: str | None = None) -> HTTPAuthCredential:
        # cache repository credentials by repository url to avoid multiple keyring
        # backend queries when packages are being downloaded from the same source
        key = f"{repository.url}#username={username or ''}"

        if key not in self._credentials:
            self._credentials[key] = repository.get_http_credentials(
                password_manager=self._password_manager, username=username)

        return self._credentials[key]

    def _get_credentials_for_url(self,
                                 url: str,
                                 exact_match: bool = False
                                 ) -> HTTPAuthCredential:
        repository = self.get_repository_config_for_url(url, exact_match)

        credential = (self._get_credentials_for_repository(
            repository=repository)
                      if repository is not None else HTTPAuthCredential())

        if credential.password is None:
            parsed_url = urllib.parse.urlsplit(url)
            netloc = parsed_url.netloc
            credential = self._password_manager.keyring.get_credential(
                url, netloc, username=credential.username)

            return HTTPAuthCredential(username=credential.username,
                                      password=credential.password)

        return credential

    def get_credentials_for_git_url(self, url: str) -> HTTPAuthCredential:
        parsed_url = urllib.parse.urlsplit(url)

        if parsed_url.scheme not in {"http", "https"}:
            return HTTPAuthCredential()

        key = f"git+{url}"

        if key not in self._credentials:
            self._credentials[key] = self._get_credentials_for_url(url, True)

        return self._credentials[key]

    def get_credentials_for_url(self, url: str) -> HTTPAuthCredential:
        parsed_url = urllib.parse.urlsplit(url)
        netloc = parsed_url.netloc

        if url not in self._credentials:
            if "@" not in netloc:
                # no credentials were provided in the url, try finding the
                # best repository configuration
                self._credentials[url] = self._get_credentials_for_url(url)
            else:
                # Split from the right because that's how urllib.parse.urlsplit()
                # behaves if more than one @ is present (which can be checked using
                # the password attribute of urlsplit()'s return value).
                auth, netloc = netloc.rsplit("@", 1)
                # Split from the left because that's how urllib.parse.urlsplit()
                # behaves if more than one : is present (which again can be checked
                # using the password attribute of the return value)
                user, password = auth.split(":", 1) if ":" in auth else (auth,
                                                                         "")
                self._credentials[url] = HTTPAuthCredential(
                    urllib.parse.unquote(user),
                    urllib.parse.unquote(password),
                )

        return self._credentials[url]

    def get_pypi_token(self, name: str) -> str | None:
        return self._password_manager.get_pypi_token(name)

    def get_http_auth(
            self,
            name: str,
            username: str | None = None) -> HTTPAuthCredential | None:
        if name == "pypi":
            repository = AuthenticatorRepositoryConfig(
                name, "https://upload.pypi.org/legacy/")
        else:
            if name not in self.configured_repositories:
                return None
            repository = self.configured_repositories[name]

        return self._get_credentials_for_repository(repository=repository,
                                                    username=username)

    def get_certs_for_repository(self,
                                 name: str) -> RepositoryCertificateConfig:
        if name.lower() == "pypi" or name not in self.configured_repositories:
            return RepositoryCertificateConfig()
        return self.configured_repositories[name].certs(self._config)

    @property
    def configured_repositories(
            self) -> dict[str, AuthenticatorRepositoryConfig]:
        if self._configured_repositories is None:
            self._configured_repositories = {}
            for repository_name in self._config.get("repositories", []):
                url = self._config.get(f"repositories.{repository_name}.url")
                self._configured_repositories[
                    repository_name] = AuthenticatorRepositoryConfig(
                        repository_name, url)

        return self._configured_repositories

    def reset_credentials_cache(self) -> None:
        self.get_repository_config_for_url.cache_clear()
        self._credentials = {}

    def add_repository(self, name: str, url: str) -> None:
        self.configured_repositories[name] = AuthenticatorRepositoryConfig(
            name, url)
        self.reset_credentials_cache()

    def get_certs_for_url(self, url: str) -> RepositoryCertificateConfig:
        if url not in self._certs:
            self._certs[url] = self._get_certs_for_url(url)
        return self._certs[url]

    def _get_repository_config_for_url(
            self,
            url: str,
            exact_match: bool = False) -> AuthenticatorRepositoryConfig | None:
        parsed_url = urllib.parse.urlsplit(url)
        candidates_netloc_only = []
        candidates_path_match = []

        for repository in self.configured_repositories.values():
            if exact_match:
                if parsed_url.path == repository.path:
                    return repository
                continue

            if repository.netloc == parsed_url.netloc:
                if parsed_url.path.startswith(repository.path) or commonprefix(
                    (parsed_url.path, repository.path)):
                    candidates_path_match.append(repository)
                    continue
                candidates_netloc_only.append(repository)

        if candidates_path_match:
            candidates = candidates_path_match
        elif candidates_netloc_only:
            candidates = candidates_netloc_only
        else:
            return None

        if len(candidates) > 1:
            logger.debug(
                "Multiple source configurations found for %s - %s",
                parsed_url.netloc,
                ", ".join(c.name for c in candidates),
            )
            # prefer the more specific path
            candidates.sort(
                key=lambda c: len(commonprefix([parsed_url.path, c.path])),
                reverse=True)

        return candidates[0]

    def _get_certs_for_url(self, url: str) -> RepositoryCertificateConfig:
        selected = self.get_repository_config_for_url(url)
        if selected:
            return selected.certs(config=self._config)
        return RepositoryCertificateConfig()
Esempio n. 5
0
def test_get_pypi_token(config, mock_available_backend, backend):
    backend.set_password("poetry-repository-foo", "__token__", "baz")
    manager = PasswordManager(config)

    assert manager.keyring.is_available()
    assert "baz" == manager.get_pypi_token("foo")
Esempio n. 6
0
class Publisher:
    """
    Registers and publishes packages to remote repositories.
    """
    def __init__(self, poetry, io):
        self._poetry = poetry
        self._package = poetry.package
        self._io = io
        self._uploader = Uploader(poetry, io)
        self._password_manager = PasswordManager(poetry.config)

    @property
    def files(self):
        return self._uploader.files

    def publish(self,
                repository_name,
                username,
                password,
                cert=None,
                client_cert=None):
        if repository_name:
            self._io.write_line("Publishing <c1>{}</c1> (<b>{}</b>) "
                                "to <info>{}</info>".format(
                                    self._package.pretty_name,
                                    self._package.pretty_version,
                                    repository_name,
                                ))
        else:
            self._io.write_line("Publishing <c1>{}</c1> (<b>{}</b>) "
                                "to <info>PyPI</info>".format(
                                    self._package.pretty_name,
                                    self._package.pretty_version))

        if not repository_name:
            url = "https://upload.pypi.org/legacy/"
            repository_name = "pypi"
        else:
            # Retrieving config information
            repository = self._poetry.config.get(
                "repositories.{}".format(repository_name))
            if repository is None:
                raise RuntimeError(
                    "Repository {} is not defined".format(repository_name))

            url = repository["url"]

        if not (username and password):
            # Check if we have a token first
            token = self._password_manager.get_pypi_token(repository_name)
            if token:
                logger.debug(
                    "Found an API token for {}.".format(repository_name))
                username = "******"
                password = token
            else:
                auth = self._password_manager.get_http_auth(repository_name)
                if auth:
                    logger.debug(
                        "Found authentication information for {}.".format(
                            repository_name))
                    username = auth["username"]
                    password = auth["password"]

        resolved_client_cert = client_cert or get_client_cert(
            self._poetry.config, repository_name)
        # Requesting missing credentials but only if there is not a client cert defined.
        if not resolved_client_cert:
            if username is None:
                username = self._io.ask("Username:"******"Password:")

        self._uploader.auth(username, password)

        return self._uploader.upload(
            url,
            cert=cert or get_cert(self._poetry.config, repository_name),
            client_cert=resolved_client_cert,
        )
Esempio n. 7
0
class Publisher:
    """
    Registers and publishes packages to remote repositories.
    """
    def __init__(self, poetry: "Poetry", io: Union["BufferedIO",
                                                   "ConsoleIO"]) -> None:
        self._poetry = poetry
        self._package = poetry.package
        self._io = io
        self._uploader = Uploader(poetry, io)
        self._password_manager = PasswordManager(poetry.config)

    @property
    def files(self) -> List[Path]:
        return self._uploader.files

    def publish(
        self,
        repository_name: Optional[str],
        username: Optional[str],
        password: Optional[str],
        cert: Optional[Path] = None,
        client_cert: Optional[Path] = None,
        dry_run: Optional[bool] = False,
    ) -> None:
        if not repository_name:
            url = "https://upload.pypi.org/legacy/"
            repository_name = "pypi"
        else:
            # Retrieving config information
            url = self._poetry.config.get(
                "repositories.{}.url".format(repository_name))
            if url is None:
                raise RuntimeError(
                    "Repository {} is not defined".format(repository_name))

        if not (username and password):
            # Check if we have a token first
            token = self._password_manager.get_pypi_token(repository_name)
            if token:
                logger.debug(
                    "Found an API token for {}.".format(repository_name))
                username = "******"
                password = token
            else:
                auth = self._password_manager.get_http_auth(repository_name)
                if auth:
                    logger.debug(
                        "Found authentication information for {}.".format(
                            repository_name))
                    username = auth["username"]
                    password = auth["password"]

        resolved_client_cert = client_cert or get_client_cert(
            self._poetry.config, repository_name)
        # Requesting missing credentials but only if there is not a client cert defined.
        if not resolved_client_cert:
            if username is None:
                username = self._io.ask("Username:"******"Password:"******"Publishing <c1>{}</c1> (<c2>{}</c2>) "
            "to <info>{}</info>".format(
                self._package.pretty_name,
                self._package.pretty_version,
                "PyPI" if repository_name == "pypi" else repository_name,
            ))

        self._uploader.upload(
            url,
            cert=cert or get_cert(self._poetry.config, repository_name),
            client_cert=resolved_client_cert,
            dry_run=dry_run,
        )
Esempio n. 8
0
class Authenticator:
    def __init__(self, config: "Config", io: Optional["IO"] = None) -> None:
        self._config = config
        self._io = io
        self._session = None
        self._credentials = {}
        self._password_manager = PasswordManager(self._config)

    def _log(self, message: str, level: str = "debug") -> 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) -> requests.Session:
        if self._session is None:
            self._session = requests.Session()

        return self._session

    def __del__(self) -> None:
        if self._session is not None:
            self._session.close()

    def request(self, method: str, url: str,
                **kwargs: Any) -> requests.Response:
        request = requests.Request(method, 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)

        session = self.session
        prepared_request = session.prepare_request(request)

        proxies = kwargs.get("proxies", {})
        stream = kwargs.get("stream")
        verify = kwargs.get("verify")
        cert = kwargs.get("cert")

        settings = session.merge_environment_settings(prepared_request.url,
                                                      proxies, stream, verify,
                                                      cert)

        # Send the request.
        send_kwargs = {
            "timeout": kwargs.get("timeout"),
            "allow_redirects": kwargs.get("allow_redirects", True),
        }
        send_kwargs.update(settings)

        attempt = 0

        while True:
            is_last_attempt = attempt >= 5
            try:
                resp = session.send(prepared_request, **send_kwargs)
            except (requests.exceptions.ConnectionError, OSError) as e:
                if is_last_attempt:
                    raise e
            else:
                if resp.status_code not in [502, 503, 504] or is_last_attempt:
                    resp.raise_for_status()
                    return resp

            if not is_last_attempt:
                attempt += 1
                delay = 0.5 * attempt
                self._log(f"Retrying HTTP request in {delay} seconds.",
                          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(
            self, url: str) -> Tuple[Optional[str], Optional[str]]:
        parsed_url = urllib.parse.urlsplit(url)

        netloc = parsed_url.netloc

        credentials = self._credentials.get(netloc, (None, None))

        if credentials == (None, None):
            if "@" not in netloc:
                credentials = self._get_credentials_for_netloc(netloc)
            else:
                # Split from the right because that's how urllib.parse.urlsplit()
                # behaves if more than one @ is present (which can be checked using
                # the password attribute of urlsplit()'s return value).
                auth, netloc = netloc.rsplit("@", 1)
                if ":" in auth:
                    # Split from the left because that's how urllib.parse.urlsplit()
                    # behaves if more than one : is present (which again can be checked
                    # using the password attribute of the return value)
                    credentials = auth.split(":", 1)
                else:
                    credentials = auth, None

                credentials = tuple(
                    None if x is None else urllib.parse.unquote(x)
                    for x in credentials)

        if credentials[0] is not None or credentials[1] is not None:
            credentials = (credentials[0] or "", credentials[1] or "")

            self._credentials[netloc] = credentials

        return credentials[0], credentials[1]

    def get_pypi_token(self, name: str) -> str:
        return self._password_manager.get_pypi_token(name)

    def get_http_auth(self, name: str) -> Optional[Dict[str, str]]:
        return self._get_http_auth(name, None)

    def _get_http_auth(self, name: str,
                       netloc: Optional[str]) -> Optional[Dict[str, str]]:
        if name == "pypi":
            url = "https://upload.pypi.org/legacy/"
        else:
            url = self._config.get(f"repositories.{name}.url")
            if not url:
                return None

        parsed_url = urllib.parse.urlsplit(url)

        if netloc is None or netloc == parsed_url.netloc:
            auth = self._password_manager.get_http_auth(name)

            if auth is None or auth["password"] is None:
                username = auth["username"] if auth else None
                auth = self._get_credentials_for_netloc_from_keyring(
                    url, parsed_url.netloc, username)

            return auth

    def _get_credentials_for_netloc(
            self, netloc: str) -> Tuple[Optional[str], Optional[str]]:
        credentials = (None, None)

        for repository_name in self._config.get("repositories", []):
            auth = self._get_http_auth(repository_name, netloc)

            if auth is None:
                continue

            return auth["username"], auth["password"]

        return credentials

    def _get_credentials_for_netloc_from_keyring(
            self, url: str, netloc: str,
            username: Optional[str]) -> Optional[Dict[str, str]]:
        import keyring

        cred = keyring.get_credential(url, username)
        if cred is not None:
            return {
                "username": cred.username,
                "password": cred.password,
            }

        cred = keyring.get_credential(netloc, username)
        if cred is not None:
            return {
                "username": cred.username,
                "password": cred.password,
            }

        if username:
            return {
                "username": username,
                "password": None,
            }

        return None