diff --git a/twine/auth.py b/twine/auth.py index 90bcb182..09d24bca 100644 --- a/twine/auth.py +++ b/twine/auth.py @@ -16,6 +16,11 @@ except ModuleNotFoundError: # pragma: no cover keyring = None +try: + from id import detect_credential # type: ignore +except ModuleNotFoundError: # pragma: no cover + detect_credential = None + from twine import exceptions from twine import utils @@ -35,11 +40,9 @@ def __init__( self, config: utils.RepositoryConfig, input: CredentialInput, - trusted_publishing: bool = False, ) -> None: self.config = config self.input = input - self.trusted_publishing = trusted_publishing @classmethod def choose(cls, interactive: bool) -> Type["Resolver"]: @@ -62,24 +65,16 @@ def username(self) -> Optional[str]: @property @functools.lru_cache() def password(self) -> Optional[str]: - if self.trusted_publishing: - # Trusted publishing (OpenID Connect): get one token from the CI - # system, and exchange that for a PyPI token. - from id import detect_credential # type: ignore - - repository_domain = cast(str, urlparse(self.system).netloc) - audience = self._oidc_audience(repository_domain) - oidc_token = detect_credential(audience) - - token_exchange_url = f"https://{repository_domain}/_/oidc/mint-token" - - mint_token_resp = requests.post( - token_exchange_url, - json={"token": oidc_token}, - timeout=5, # S113 wants a timeout + if ( + self.is_pypi() + and self.username == "__token__" + and self.input.password is None + and detect_credential is not None + ): + logger.info( + "Trying to use trusted publishing (no token was explicitly provided)" ) - mint_token_resp.raise_for_status() - return cast(str, mint_token_resp.json()["token"]) + return self.make_trusted_publishing_token() return utils.get_userpass_value( self.input.password, @@ -88,14 +83,32 @@ def password(self) -> Optional[str]: prompt_strategy=self.password_from_keyring_or_prompt, ) - @staticmethod - def _oidc_audience(repository_domain: str) -> str: + def make_trusted_publishing_token(self) -> str: + # Trusted publishing (OpenID Connect): get one token from the CI + # system, and exchange that for a PyPI token. + repository_domain = cast(str, urlparse(self.system).netloc) + session = requests.Session() # TODO: user agent & retries + # Indices are expected to support `https://{domain}/_/oidc/audience`, # which tells OIDC exchange clients which audience to use. audience_url = f"https://{repository_domain}/_/oidc/audience" - resp = requests.get(audience_url, timeout=5) + resp = session.get(audience_url, timeout=5) resp.raise_for_status() - return cast(str, resp.json()["audience"]) + audience = cast(str, resp.json()["audience"]) + + oidc_token = detect_credential(audience) + logger.debug("Got OIDC token for audience %s", audience) + + token_exchange_url = f"https://{repository_domain}/_/oidc/mint-token" + + mint_token_resp = session.post( + token_exchange_url, + json={"token": oidc_token}, + timeout=5, # S113 wants a timeout + ) + mint_token_resp.raise_for_status() + logger.debug("Minted upload token for trusted publishing") + return cast(str, mint_token_resp.json()["token"]) @property def system(self) -> Optional[str]: diff --git a/twine/settings.py b/twine/settings.py index 9033ed3b..5aa4d964 100644 --- a/twine/settings.py +++ b/twine/settings.py @@ -51,7 +51,6 @@ def __init__( identity: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None, - trusted_publish: bool = False, non_interactive: bool = False, comment: Optional[str] = None, config_file: str = utils.DEFAULT_CONFIG_FILE, @@ -129,7 +128,6 @@ def __init__( self.auth = auth.Resolver.choose(not non_interactive)( self.repository_config, auth.CredentialInput(username, password), - trusted_publishing=trusted_publish, ) @property @@ -224,15 +222,6 @@ def register_argparse_arguments(parser: argparse.ArgumentParser) -> None: "(package index) with. (Can also be set via " "%(env)s environment variable.)", ) - parser.add_argument( - "--trusted-publish", - default=False, - required=False, - action="store_true", - help="Upload from CI using trusted publishing. Use this without " - "specifying username & password. Requires an optional extra " - "dependency (install twine[trusted-publishing]).", - ) parser.add_argument( "--non-interactive", action=utils.EnvironmentFlag,