Skip to content

Commit 2a8a126

Browse files
committed
Try trusted publishing at lower priority than keyring & .pypirc files
1 parent ea78045 commit 2a8a126

File tree

2 files changed

+51
-17
lines changed

2 files changed

+51
-17
lines changed

twine/auth.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import functools
22
import getpass
3+
import json
34
import logging
45
from typing import TYPE_CHECKING, Callable, Optional, Type, cast
56
from urllib.parse import urlparse
67

78
import requests
8-
from id import detect_credential # type: ignore
9+
from id import AmbientCredentialError # type: ignore
10+
from id import detect_credential
911

1012
# keyring has an indirect dependency on PyCA cryptography, which has no
1113
# pre-built wheels for ppc64le and s390x, see #1158.
@@ -61,24 +63,14 @@ def username(self) -> Optional[str]:
6163
@property
6264
@functools.lru_cache()
6365
def password(self) -> Optional[str]:
64-
if (
65-
self.is_pypi()
66-
and self.username == "__token__"
67-
and self.input.password is None
68-
):
69-
logger.info(
70-
"Trying to use trusted publishing (no token was explicitly provided)"
71-
)
72-
return self.make_trusted_publishing_token()
73-
7466
return utils.get_userpass_value(
7567
self.input.password,
7668
self.config,
7769
key="password",
78-
prompt_strategy=self.password_from_keyring_or_prompt,
70+
prompt_strategy=self.password_from_keyring_or_trusted_publishing_or_prompt,
7971
)
8072

81-
def make_trusted_publishing_token(self) -> str:
73+
def make_trusted_publishing_token(self) -> Optional[str]:
8274
# Trusted publishing (OpenID Connect): get one token from the CI
8375
# system, and exchange that for a PyPI token.
8476
repository_domain = cast(str, urlparse(self.system).netloc)
@@ -91,7 +83,20 @@ def make_trusted_publishing_token(self) -> str:
9183
resp.raise_for_status()
9284
audience = cast(str, resp.json()["audience"])
9385

94-
oidc_token = detect_credential(audience)
86+
try:
87+
oidc_token = detect_credential(audience)
88+
except AmbientCredentialError as e:
89+
# If we get here, we're on a supported CI platform for trusted
90+
# publishing, and we have not been given any token, so we can error.
91+
raise exceptions.TrustedPublishingFailure(
92+
"Unable to retrieve an OIDC token from the CI platform for "
93+
f"trusted publishing {e}"
94+
)
95+
96+
if oidc_token is None:
97+
logger.debug("This environment is not supported for trusted publishing")
98+
return None # Fall back to prompting for a token (if possible)
99+
95100
logger.debug("Got OIDC token for audience %s", audience)
96101

97102
token_exchange_url = f"https://{repository_domain}/_/oidc/mint-token"
@@ -101,9 +106,25 @@ def make_trusted_publishing_token(self) -> str:
101106
json={"token": oidc_token},
102107
timeout=5, # S113 wants a timeout
103108
)
104-
mint_token_resp.raise_for_status()
109+
try:
110+
mint_token_payload = mint_token_resp.json()
111+
except json.JSONDecodeError:
112+
raise exceptions.TrustedPublishingFailure(
113+
"The token-minting request returned invalid JSON"
114+
)
115+
116+
if not mint_token_resp.ok:
117+
reasons = "\n".join(
118+
f'* `{error["code"]}`: {error["description"]}'
119+
for error in mint_token_payload["errors"]
120+
)
121+
raise exceptions.TrustedPublishingFailure(
122+
"The token request failed; the index server gave the following "
123+
f"reasons:\n\n{reasons}"
124+
)
125+
105126
logger.debug("Minted upload token for trusted publishing")
106-
return cast(str, mint_token_resp.json()["token"])
127+
return cast(str, mint_token_payload["token"])
107128

108129
@property
109130
def system(self) -> Optional[str]:
@@ -147,12 +168,19 @@ def username_from_keyring_or_prompt(self) -> str:
147168

148169
return self.prompt("username", input)
149170

150-
def password_from_keyring_or_prompt(self) -> str:
171+
def password_from_keyring_or_trusted_publishing_or_prompt(self) -> str:
151172
password = self.get_password_from_keyring()
152173
if password:
153174
logger.info("password set from keyring")
154175
return password
155176

177+
if self.is_pypi() and self.username == "__token__":
178+
logger.debug(
179+
"Trying to use trusted publishing (no token was explicitly provided)"
180+
)
181+
if (token := self.make_trusted_publishing_token()) is not None:
182+
return token
183+
156184
# Prompt for API token when required.
157185
what = "API token" if self.is_pypi() else "password"
158186

twine/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ class NonInteractive(TwineException):
116116
pass
117117

118118

119+
class TrustedPublishingFailure(TwineException):
120+
"""Raised if we expected to use trusted publishing but couldn't."""
121+
122+
pass
123+
124+
119125
class InvalidPyPIUploadURL(TwineException):
120126
"""Repository configuration tries to use PyPI with an incorrect URL.
121127

0 commit comments

Comments
 (0)