Skip to content

Commit 77db1b7

Browse files
committed
Merge branch PR #306, GHSA-vxmw-7h4f-hqxh fix and PR #378 into unstable/v1
4 parents d417ba7 + ee3c702 + 1293b8c + 280b3a1 commit 77db1b7

File tree

7 files changed

+119
-31
lines changed

7 files changed

+119
-31
lines changed

.github/workflows/build-and-push-docker-image.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ on: # yamllint disable-line rule:truthy
1313
required: true
1414
type: string
1515

16+
permissions: {}
17+
1618
jobs:
1719
smoke-test:
1820
uses: ./.github/workflows/reusable-smoke-test.yml
@@ -34,13 +36,17 @@ jobs:
3436
jobs: ${{ toJSON(needs) }}
3537

3638
build-and-push:
39+
permissions:
40+
packages: write
3741
if: github.event_name != 'pull_request'
3842
runs-on: ubuntu-latest
3943
needs:
4044
- check
4145
timeout-minutes: 10
4246
steps:
4347
- uses: actions/checkout@v4
48+
with:
49+
persist-credentials: false
4450
- name: Build Docker image
4551
run: |
4652
DOCKER_TAG="${DOCKER_TAG/'/'/'-'}"

.github/workflows/reusable-smoke-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
uses: actions/checkout@v4
5555
with:
5656
path: test
57+
persist-credentials: false
5758
- name: Fail-fast in unsupported environments
5859
continue-on-error: true
5960
id: fail-fast
@@ -89,6 +90,7 @@ jobs:
8990
uses: actions/checkout@v4
9091
with:
9192
path: test
93+
persist-credentials: false
9294
- name: Install the packaging-related tools
9395
run: python3 -m pip install build twine
9496
env:

.github/workflows/zizmor.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
3+
name: GitHub Actions Security Analysis with zizmor 🌈
4+
5+
on: # yamllint disable-line rule:truthy
6+
push:
7+
pull_request:
8+
9+
jobs:
10+
zizmor:
11+
name: 🌈 zizmor
12+
13+
permissions:
14+
security-events: write
15+
16+
# yamllint disable-line rule:line-length
17+
uses: zizmorcore/workflow/.github/workflows/reusable-zizmor.yml@3bb5e95068d0f44b6d2f3f7e91379bed1d2f96a8
18+
19+
...

.github/zizmor.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
rules:
3+
unpinned-uses:
4+
config:
5+
policies:
6+
actions/*: ref-pin
7+
github/*: ref-pin
8+
re-actors/*: ref-pin

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ repos:
109109
WPS440,
110110
WPS441,
111111
WPS453,
112-
- --max-module-members=8 # WPS202
112+
- --per-file-ignores=attestations.py:WPS202 oidc-exchange.py:WPS202
113113
additional_dependencies:
114114
- flake8-2020 ~= 1.8.1
115115
- flake8-pytest-style ~= 2.1.0

action.yml

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ runs:
100100
exit 1
101101
shell: bash -eEuo pipefail {0}
102102
- name: Reset path if needed
103-
run: |
103+
run: | # zizmor: ignore[github-env] PATH is not user-controlled
104104
# Reset path if needed
105105
# https://github.com/pypa/gh-action-pypi-publish/issues/112
106106
if [[ $PATH != *"/usr/bin"* ]]; then
@@ -111,25 +111,6 @@ runs:
111111
echo "\$PATH reset. \$PATH=$PATH"
112112
fi
113113
shell: bash
114-
- name: Set repo and ref from which to run Docker container action
115-
id: set-repo-and-ref
116-
run: |
117-
# Set repo and ref from which to run Docker container action
118-
# to handle cases in which `github.action_` context is not set
119-
# https://github.com/actions/runner/issues/2473
120-
REF=${{ env.ACTION_REF || env.PR_REF || github.ref_name }}
121-
REPO=${{ env.ACTION_REPO || env.PR_REPO || github.repository }}
122-
REPO_ID=${{ env.PR_REPO_ID || github.repository_id }}
123-
echo "ref=$REF" >>"$GITHUB_OUTPUT"
124-
echo "repo=$REPO" >>"$GITHUB_OUTPUT"
125-
echo "repo-id=$REPO_ID" >>"$GITHUB_OUTPUT"
126-
shell: bash
127-
env:
128-
ACTION_REF: ${{ github.action_ref }}
129-
ACTION_REPO: ${{ github.action_repository }}
130-
PR_REF: ${{ github.event.pull_request.head.ref }}
131-
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
132-
PR_REPO_ID: ${{ github.event.pull_request.base.repo.id }}
133114
- name: Discover pre-installed Python
134115
id: pre-installed-python
135116
run: |
@@ -139,7 +120,8 @@ runs:
139120
- name: Install Python 3
140121
if: steps.pre-installed-python.outputs.python-path == ''
141122
id: new-python
142-
uses: actions/setup-python@v5
123+
# yamllint disable-line rule:line-length
124+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
143125
with:
144126
python-version: 3.x
145127
- name: Create Docker container action
@@ -151,9 +133,26 @@ runs:
151133
|| steps.pre-installed-python.outputs.python-path
152134
}} '${{ github.action_path }}/create-docker-action.py'
153135
env:
154-
REF: ${{ steps.set-repo-and-ref.outputs.ref }}
155-
REPO: ${{ steps.set-repo-and-ref.outputs.repo }}
156-
REPO_ID: ${{ steps.set-repo-and-ref.outputs.repo-id }}
136+
# Set repo and ref from which to run Docker container action
137+
# to handle cases in which `github.action_` context is not set
138+
# https://github.com/actions/runner/issues/2473
139+
REF: >-
140+
${{
141+
github.action_ref
142+
|| github.event.pull_request.head.ref
143+
|| github.ref_name
144+
}}
145+
REPO: >-
146+
${{
147+
github.action_repository
148+
|| github.event.pull_request.head.repo.full_name
149+
|| github.repository
150+
}}
151+
REPO_ID: >-
152+
${{
153+
github.event.pull_request.base.repo.id
154+
|| github.repository_id
155+
}}
157156
shell: bash
158157
- name: Run Docker container
159158
# The generated trampoline action must exist in the allowlisted

oidc-exchange.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import json
33
import os
44
import sys
5+
import typing as t
56
from http import HTTPStatus
67
from pathlib import Path
7-
from typing import NoReturn
88
from urllib.parse import urlparse
99

1010
import id # pylint: disable=redefined-builtin
@@ -91,6 +91,30 @@
9191
See https://docs.pypi.org/trusted-publishers/troubleshooting/ for more help.
9292
"""
9393

94+
_REUSABLE_WORKFLOW_WARNING = """
95+
The claims in this token suggest that the calling workflow is a reusable workflow.
96+
97+
In particular, this action was initiated by:
98+
99+
{job_workflow_ref}
100+
101+
Whereas its parent workflow is:
102+
103+
{workflow_ref}
104+
105+
Reusable workflows are **not currently supported** by PyPI's Trusted Publishing
106+
functionality, and are subject to breakage. Users are **strongly encouraged**
107+
to avoid using reusable workflows for Trusted Publishing until support
108+
becomes official. Please, do not report bugs if this breaks.
109+
110+
For more information, see:
111+
112+
* https://docs.pypi.org/trusted-publishers/troubleshooting/#reusable-workflows-on-github
113+
* https://github.com/pypa/gh-action-pypi-publish/issues/166 — subscribe to
114+
this issue to watch the progress and learn when reusable workflows become
115+
supported officially
116+
"""
117+
94118
# Rendered if the package index's token response isn't valid JSON.
95119
_SERVER_TOKEN_RESPONSE_MALFORMED_JSON = """
96120
Token request failed: the index produced an unexpected
@@ -111,7 +135,7 @@
111135
""" # noqa: S105; not a password
112136

113137

114-
def die(msg: str) -> NoReturn:
138+
def die(msg: str) -> t.NoReturn:
115139
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
116140
print(_ERROR_SUMMARY_MESSAGE.format(message=msg), file=io)
117141

@@ -123,6 +147,14 @@ def die(msg: str) -> NoReturn:
123147
sys.exit(1)
124148

125149

150+
def warn(msg: str) -> None:
151+
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
152+
print(msg, file=io)
153+
154+
msg = msg.replace('\n', '%0A')
155+
print(f'::warning::Potential workflow misconfiguration: {msg}', file=sys.stderr)
156+
157+
126158
def debug(msg: str):
127159
print(f'::debug::{msg.title()}', file=sys.stderr)
128160

@@ -162,13 +194,15 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
162194
)
163195

164196

165-
def render_claims(token: str) -> str:
197+
def extract_claims(token: str) -> dict[str, object]:
166198
_, payload, _ = token.split('.', 2)
167199

168200
# urlsafe_b64decode needs padding; JWT payloads don't contain any.
169201
payload += '=' * (4 - (len(payload) % 4))
170-
claims = json.loads(base64.urlsafe_b64decode(payload))
202+
return json.loads(base64.urlsafe_b64decode(payload))
171203

204+
205+
def render_claims(claims: dict[str, object]) -> str:
172206
def _get(name: str) -> str: # noqa: WPS430
173207
return claims.get(name, 'MISSING')
174208

@@ -184,6 +218,19 @@ def _get(name: str) -> str: # noqa: WPS430
184218
)
185219

186220

221+
def warn_on_reusable_workflow(claims: dict[str, object]) -> None:
222+
# A reusable workflow is identified by having different values
223+
# for its workflow_ref (the initiating workflow) and job_workflow_ref
224+
# (the reusable workflow).
225+
workflow_ref = claims.get('workflow_ref')
226+
job_workflow_ref = claims.get('job_workflow_ref')
227+
228+
if workflow_ref == job_workflow_ref:
229+
return
230+
231+
warn(_REUSABLE_WORKFLOW_WARNING.format_map(locals()))
232+
233+
187234
def event_is_third_party_pr() -> bool:
188235
# Non-`pull_request` events cannot be from third-party PRs.
189236
if os.getenv('GITHUB_EVENT_NAME') != 'pull_request':
@@ -225,12 +272,19 @@ def event_is_third_party_pr() -> bool:
225272
oidc_token = id.detect_credential(audience=oidc_audience)
226273
except id.IdentityError as identity_error:
227274
cause_msg_tmpl = (
228-
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE if event_is_third_party_pr()
275+
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE
276+
if event_is_third_party_pr()
229277
else _TOKEN_RETRIEVAL_FAILED_MESSAGE
230278
)
231279
for_cause_msg = cause_msg_tmpl.format(identity_error=identity_error)
232280
die(for_cause_msg)
233281

282+
283+
# Perform a non-fatal check to see if we're running on a reusable
284+
# workflow, and emit a warning if so.
285+
oidc_claims = extract_claims(oidc_token)
286+
warn_on_reusable_workflow(oidc_claims)
287+
234288
# Now we can do the actual token exchange.
235289
mint_token_resp = requests.post(
236290
token_exchange_url,
@@ -257,7 +311,7 @@ def event_is_third_party_pr() -> bool:
257311
for error in mint_token_payload['errors']
258312
)
259313

260-
rendered_claims = render_claims(oidc_token)
314+
rendered_claims = render_claims(oidc_claims)
261315

262316
die(
263317
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE.format(

0 commit comments

Comments
 (0)