10
10
import id # pylint: disable=redefined-builtin
11
11
import requests
12
12
13
- _GITHUB_STEP_SUMMARY = Path (os .getenv (" GITHUB_STEP_SUMMARY" ))
13
+ _GITHUB_STEP_SUMMARY = Path (os .getenv (' GITHUB_STEP_SUMMARY' ))
14
14
15
15
# The top-level error message that gets rendered.
16
16
# This message wraps one of the other templates/messages defined below.
45
45
```
46
46
47
47
Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings.
48
- """
48
+ """ # noqa: S105; not a password
49
49
50
50
# Specialization of the token retrieval failure case, when we know that
51
51
# the failure cause is use within a third-party PR.
59
59
To fix this, change your publishing workflow to use an event that
60
60
forks of your repository cannot trigger (such as tag or release
61
61
creation, or a manually triggered workflow dispatch).
62
- """
62
+ """ # noqa: S105; not a password
63
63
64
64
# Rendered if the package index refuses the given OIDC token.
65
65
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE = """
71
71
also indicate an internal error on GitHub or PyPI's part.
72
72
73
73
{rendered_claims}
74
- """
74
+ """ # noqa: S105; not a password
75
75
76
76
_RENDERED_CLAIMS = """
77
77
The claims rendered below are **for debugging purposes only**. You should **not**
97
97
98
98
This strongly suggests a server configuration or downtime issue; wait
99
99
a few minutes and try again.
100
- """
100
+ """ # noqa: S105; not a password
101
101
102
102
# Rendered if the package index's token response isn't a valid API token payload.
103
103
_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE = """
104
104
Token response error: the index gave us an invalid response.
105
105
106
106
This strongly suggests a server configuration or downtime issue; wait
107
107
a few minutes and try again.
108
- """
108
+ """ # noqa: S105; not a password
109
109
110
110
111
111
def die (msg : str ) -> NoReturn :
112
- with _GITHUB_STEP_SUMMARY .open ("a" , encoding = " utf-8" ) as io :
112
+ with _GITHUB_STEP_SUMMARY .open ('a' , encoding = ' utf-8' ) as io :
113
113
print (_ERROR_SUMMARY_MESSAGE .format (message = msg ), file = io )
114
114
115
115
# HACK: GitHub Actions' annotations don't work across multiple lines naively;
116
116
# translating `\n` into `%0A` (i.e., HTML percent-encoding) is known to work.
117
117
# See: https://github.com/actions/toolkit/issues/193
118
- msg = msg .replace (" \n " , " %0A" )
119
- print (f" ::error::Trusted publishing exchange failure: { msg } " , file = sys .stderr )
118
+ msg = msg .replace (' \n ' , ' %0A' )
119
+ print (f' ::error::Trusted publishing exchange failure: { msg } ' , file = sys .stderr )
120
120
sys .exit (1 )
121
121
122
122
123
123
def debug (msg : str ):
124
- print (f" ::debug::{ msg .title ()} " , file = sys .stderr )
124
+ print (f' ::debug::{ msg .title ()} ' , file = sys .stderr )
125
125
126
126
127
127
def get_normalized_input (name : str ) -> str | None :
128
- name = f" INPUT_{ name .upper ()} "
128
+ name = f' INPUT_{ name .upper ()} '
129
129
if val := os .getenv (name ):
130
130
return val
131
- return os .getenv (name .replace ("-" , "_" ))
131
+ return os .getenv (name .replace ('-' , '_' ))
132
132
133
133
134
134
def assert_successful_audience_call (resp : requests .Response , domain : str ):
@@ -140,81 +140,81 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
140
140
# This index supports OIDC, but forbids the client from using
141
141
# it (either because it's disabled, ratelimited, etc.)
142
142
die (
143
- f" audience retrieval failed: repository at { domain } has trusted publishing disabled" ,
143
+ f' audience retrieval failed: repository at { domain } has trusted publishing disabled' ,
144
144
)
145
145
case HTTPStatus .NOT_FOUND :
146
146
# This index does not support OIDC.
147
147
die (
148
- " audience retrieval failed: repository at "
149
- f" { domain } does not indicate trusted publishing support" ,
148
+ ' audience retrieval failed: repository at '
149
+ f' { domain } does not indicate trusted publishing support' ,
150
150
)
151
151
case other :
152
152
status = HTTPStatus (other )
153
153
# Unknown: the index may or may not support OIDC, but didn't respond with
154
154
# something we expect. This can happen if the index is broken, in maintenance mode,
155
155
# misconfigured, etc.
156
156
die (
157
- " audience retrieval failed: repository at "
158
- f" { domain } responded with unexpected { other } : { status .phrase } " ,
157
+ ' audience retrieval failed: repository at '
158
+ f' { domain } responded with unexpected { other } : { status .phrase } ' ,
159
159
)
160
160
161
161
162
162
def render_claims (token : str ) -> str :
163
- _ , payload , _ = token .split ("." , 2 )
163
+ _ , payload , _ = token .split ('.' , 2 )
164
164
165
165
# urlsafe_b64decode needs padding; JWT payloads don't contain any.
166
- payload += "=" * (4 - (len (payload ) % 4 ))
166
+ payload += '=' * (4 - (len (payload ) % 4 ))
167
167
claims = json .loads (base64 .urlsafe_b64decode (payload ))
168
168
169
169
def _get (name : str ) -> str : # noqa: WPS430
170
- return claims .get (name , " MISSING" )
170
+ return claims .get (name , ' MISSING' )
171
171
172
172
return _RENDERED_CLAIMS .format (
173
- sub = _get (" sub" ),
174
- repository = _get (" repository" ),
175
- repository_owner = _get (" repository_owner" ),
176
- repository_owner_id = _get (" repository_owner_id" ),
177
- job_workflow_ref = _get (" job_workflow_ref" ),
178
- ref = _get (" ref" ),
173
+ sub = _get (' sub' ),
174
+ repository = _get (' repository' ),
175
+ repository_owner = _get (' repository_owner' ),
176
+ repository_owner_id = _get (' repository_owner_id' ),
177
+ job_workflow_ref = _get (' job_workflow_ref' ),
178
+ ref = _get (' ref' ),
179
179
)
180
180
181
181
182
182
def event_is_third_party_pr () -> bool :
183
183
# Non-`pull_request` events cannot be from third-party PRs.
184
- if os .getenv (" GITHUB_EVENT_NAME" ) != " pull_request" :
184
+ if os .getenv (' GITHUB_EVENT_NAME' ) != ' pull_request' :
185
185
return False
186
186
187
- event_path = os .getenv (" GITHUB_EVENT_PATH" )
187
+ event_path = os .getenv (' GITHUB_EVENT_PATH' )
188
188
if not event_path :
189
189
# No GITHUB_EVENT_PATH indicates a weird GitHub or runner bug.
190
- debug (" unexpected: no GITHUB_EVENT_PATH to check" )
190
+ debug (' unexpected: no GITHUB_EVENT_PATH to check' )
191
191
return False
192
192
193
193
try :
194
194
event = json .loads (Path (event_path ).read_bytes ())
195
195
except json .JSONDecodeError :
196
- debug (" unexpected: GITHUB_EVENT_PATH does not contain valid JSON" )
196
+ debug (' unexpected: GITHUB_EVENT_PATH does not contain valid JSON' )
197
197
return False
198
198
199
199
try :
200
- return event [" pull_request" ][ " head" ][ " repo" ][ " fork" ]
200
+ return event [' pull_request' ][ ' head' ][ ' repo' ][ ' fork' ]
201
201
except KeyError :
202
202
return False
203
203
204
204
205
- repository_url = get_normalized_input (" repository-url" )
205
+ repository_url = get_normalized_input (' repository-url' )
206
206
repository_domain = urlparse (repository_url ).netloc
207
- token_exchange_url = f" https://{ repository_domain } /_/oidc/mint-token"
207
+ token_exchange_url = f' https://{ repository_domain } /_/oidc/mint-token'
208
208
209
209
# Indices are expected to support `https://{domain}/_/oidc/audience`,
210
210
# which tells OIDC exchange clients which audience to use.
211
- audience_url = f" https://{ repository_domain } /_/oidc/audience"
212
- audience_resp = requests .get (audience_url )
211
+ audience_url = f' https://{ repository_domain } /_/oidc/audience'
212
+ audience_resp = requests .get (audience_url , timeout = 5 ) # S113 wants a timeout
213
213
assert_successful_audience_call (audience_resp , repository_domain )
214
214
215
- oidc_audience = audience_resp .json ()[" audience" ]
215
+ oidc_audience = audience_resp .json ()[' audience' ]
216
216
217
- debug (f" selected trusted publishing exchange endpoint: { token_exchange_url } " )
217
+ debug (f' selected trusted publishing exchange endpoint: { token_exchange_url } ' )
218
218
219
219
try :
220
220
oidc_token = id .detect_credential (audience = oidc_audience )
@@ -229,7 +229,8 @@ def event_is_third_party_pr() -> bool:
229
229
# Now we can do the actual token exchange.
230
230
mint_token_resp = requests .post (
231
231
token_exchange_url ,
232
- json = {"token" : oidc_token },
232
+ json = {'token' : oidc_token },
233
+ timeout = 5 , # S113 wants a timeout
233
234
)
234
235
235
236
try :
@@ -246,9 +247,9 @@ def event_is_third_party_pr() -> bool:
246
247
# On failure, the JSON response includes the list of errors that
247
248
# occurred during minting.
248
249
if not mint_token_resp .ok :
249
- reasons = " \n " .join (
250
- f" * `{ error [' code' ]} `: { error [' description' ] } "
251
- for error in mint_token_payload [" errors" ]
250
+ reasons = ' \n ' .join (
251
+ f' * `{ error [" code" ]} `: { error [" description" ] } '
252
+ for error in mint_token_payload [' errors' ]
252
253
)
253
254
254
255
rendered_claims = render_claims (oidc_token )
@@ -260,12 +261,12 @@ def event_is_third_party_pr() -> bool:
260
261
),
261
262
)
262
263
263
- pypi_token = mint_token_payload .get (" token" )
264
+ pypi_token = mint_token_payload .get (' token' )
264
265
if pypi_token is None :
265
266
die (_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE )
266
267
267
268
# Mask the newly minted PyPI token, so that we don't accidentally leak it in logs.
268
- print (f" ::add-mask::{ pypi_token } " , file = sys .stderr )
269
+ print (f' ::add-mask::{ pypi_token } ' , file = sys .stderr )
269
270
270
271
# This final print will be captured by the subshell in `twine-upload.sh`.
271
272
print (pypi_token )
0 commit comments