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"
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")
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"
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()
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")
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, )
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, )
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