This commit is contained in:
@@ -13,6 +13,8 @@ on: # yamllint disable-line rule:truthy
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
uses: ./.github/workflows/reusable-smoke-test.yml
|
||||
@@ -34,6 +36,8 @@ jobs:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
|
||||
build-and-push:
|
||||
permissions:
|
||||
packages: write
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
@@ -41,6 +45,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
DOCKER_TAG="${DOCKER_TAG/'/'/'-'}"
|
||||
|
||||
@@ -54,6 +54,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: test
|
||||
persist-credentials: false
|
||||
- name: Fail-fast in unsupported environments
|
||||
continue-on-error: true
|
||||
id: fail-fast
|
||||
@@ -89,6 +90,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: test
|
||||
persist-credentials: false
|
||||
- name: Install the packaging-related tools
|
||||
run: python3 -m pip install build twine
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: 🌈 zizmor
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
# yamllint disable-line rule:line-length
|
||||
uses: zizmorcore/workflow/.github/workflows/reusable-zizmor.yml@3bb5e95068d0f44b6d2f3f7e91379bed1d2f96a8
|
||||
|
||||
...
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
policies:
|
||||
actions/*: ref-pin
|
||||
github/*: ref-pin
|
||||
re-actors/*: ref-pin
|
||||
@@ -109,7 +109,7 @@ repos:
|
||||
WPS440,
|
||||
WPS441,
|
||||
WPS453,
|
||||
- --max-module-members=8 # WPS202
|
||||
- --per-file-ignores=attestations.py:WPS202 oidc-exchange.py:WPS202
|
||||
additional_dependencies:
|
||||
- flake8-2020 ~= 1.8.1
|
||||
- flake8-pytest-style ~= 2.1.0
|
||||
|
||||
+23
-24
@@ -100,7 +100,7 @@ runs:
|
||||
exit 1
|
||||
shell: bash -eEuo pipefail {0}
|
||||
- name: Reset path if needed
|
||||
run: |
|
||||
run: | # zizmor: ignore[github-env] PATH is not user-controlled
|
||||
# Reset path if needed
|
||||
# https://github.com/pypa/gh-action-pypi-publish/issues/112
|
||||
if [[ $PATH != *"/usr/bin"* ]]; then
|
||||
@@ -111,25 +111,6 @@ runs:
|
||||
echo "\$PATH reset. \$PATH=$PATH"
|
||||
fi
|
||||
shell: bash
|
||||
- name: Set repo and ref from which to run Docker container action
|
||||
id: set-repo-and-ref
|
||||
run: |
|
||||
# Set repo and ref from which to run Docker container action
|
||||
# to handle cases in which `github.action_` context is not set
|
||||
# https://github.com/actions/runner/issues/2473
|
||||
REF=${{ env.ACTION_REF || env.PR_REF || github.ref_name }}
|
||||
REPO=${{ env.ACTION_REPO || env.PR_REPO || github.repository }}
|
||||
REPO_ID=${{ env.PR_REPO_ID || github.repository_id }}
|
||||
echo "ref=$REF" >>"$GITHUB_OUTPUT"
|
||||
echo "repo=$REPO" >>"$GITHUB_OUTPUT"
|
||||
echo "repo-id=$REPO_ID" >>"$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
env:
|
||||
ACTION_REF: ${{ github.action_ref }}
|
||||
ACTION_REPO: ${{ github.action_repository }}
|
||||
PR_REF: ${{ github.event.pull_request.head.ref }}
|
||||
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
PR_REPO_ID: ${{ github.event.pull_request.base.repo.id }}
|
||||
- name: Discover pre-installed Python
|
||||
id: pre-installed-python
|
||||
run: |
|
||||
@@ -139,7 +120,8 @@ runs:
|
||||
- name: Install Python 3
|
||||
if: steps.pre-installed-python.outputs.python-path == ''
|
||||
id: new-python
|
||||
uses: actions/setup-python@v5
|
||||
# yamllint disable-line rule:line-length
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: 3.x
|
||||
- name: Create Docker container action
|
||||
@@ -151,9 +133,26 @@ runs:
|
||||
|| steps.pre-installed-python.outputs.python-path
|
||||
}} '${{ github.action_path }}/create-docker-action.py'
|
||||
env:
|
||||
REF: ${{ steps.set-repo-and-ref.outputs.ref }}
|
||||
REPO: ${{ steps.set-repo-and-ref.outputs.repo }}
|
||||
REPO_ID: ${{ steps.set-repo-and-ref.outputs.repo-id }}
|
||||
# Set repo and ref from which to run Docker container action
|
||||
# to handle cases in which `github.action_` context is not set
|
||||
# https://github.com/actions/runner/issues/2473
|
||||
REF: >-
|
||||
${{
|
||||
github.action_ref
|
||||
|| github.event.pull_request.head.ref
|
||||
|| github.ref_name
|
||||
}}
|
||||
REPO: >-
|
||||
${{
|
||||
github.action_repository
|
||||
|| github.event.pull_request.head.repo.full_name
|
||||
|| github.repository
|
||||
}}
|
||||
REPO_ID: >-
|
||||
${{
|
||||
github.event.pull_request.base.repo.id
|
||||
|| github.repository_id
|
||||
}}
|
||||
shell: bash
|
||||
- name: Run Docker container
|
||||
# The generated trampoline action must exist in the allowlisted
|
||||
|
||||
+60
-6
@@ -2,9 +2,9 @@ import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import typing as t
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import id # pylint: disable=redefined-builtin
|
||||
@@ -91,6 +91,30 @@ If a claim is not present in the claim set, then it is rendered as `MISSING`.
|
||||
See https://docs.pypi.org/trusted-publishers/troubleshooting/ for more help.
|
||||
"""
|
||||
|
||||
_REUSABLE_WORKFLOW_WARNING = """
|
||||
The claims in this token suggest that the calling workflow is a reusable workflow.
|
||||
|
||||
In particular, this action was initiated by:
|
||||
|
||||
{job_workflow_ref}
|
||||
|
||||
Whereas its parent workflow is:
|
||||
|
||||
{workflow_ref}
|
||||
|
||||
Reusable workflows are **not currently supported** by PyPI's Trusted Publishing
|
||||
functionality, and are subject to breakage. Users are **strongly encouraged**
|
||||
to avoid using reusable workflows for Trusted Publishing until support
|
||||
becomes official. Please, do not report bugs if this breaks.
|
||||
|
||||
For more information, see:
|
||||
|
||||
* https://docs.pypi.org/trusted-publishers/troubleshooting/#reusable-workflows-on-github
|
||||
* https://github.com/pypa/gh-action-pypi-publish/issues/166 — subscribe to
|
||||
this issue to watch the progress and learn when reusable workflows become
|
||||
supported officially
|
||||
"""
|
||||
|
||||
# Rendered if the package index's token response isn't valid JSON.
|
||||
_SERVER_TOKEN_RESPONSE_MALFORMED_JSON = """
|
||||
Token request failed: the index produced an unexpected
|
||||
@@ -111,7 +135,7 @@ a few minutes and try again.
|
||||
""" # noqa: S105; not a password
|
||||
|
||||
|
||||
def die(msg: str) -> NoReturn:
|
||||
def die(msg: str) -> t.NoReturn:
|
||||
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
|
||||
print(_ERROR_SUMMARY_MESSAGE.format(message=msg), file=io)
|
||||
|
||||
@@ -123,6 +147,14 @@ def die(msg: str) -> NoReturn:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
|
||||
print(msg, file=io)
|
||||
|
||||
msg = msg.replace('\n', '%0A')
|
||||
print(f'::warning::Potential workflow misconfiguration: {msg}', file=sys.stderr)
|
||||
|
||||
|
||||
def debug(msg: str):
|
||||
print(f'::debug::{msg.title()}', file=sys.stderr)
|
||||
|
||||
@@ -162,13 +194,15 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
|
||||
)
|
||||
|
||||
|
||||
def render_claims(token: str) -> str:
|
||||
def extract_claims(token: str) -> dict[str, object]:
|
||||
_, payload, _ = token.split('.', 2)
|
||||
|
||||
# urlsafe_b64decode needs padding; JWT payloads don't contain any.
|
||||
payload += '=' * (4 - (len(payload) % 4))
|
||||
claims = json.loads(base64.urlsafe_b64decode(payload))
|
||||
return json.loads(base64.urlsafe_b64decode(payload))
|
||||
|
||||
|
||||
def render_claims(claims: dict[str, object]) -> str:
|
||||
def _get(name: str) -> str: # noqa: WPS430
|
||||
return claims.get(name, 'MISSING')
|
||||
|
||||
@@ -184,6 +218,19 @@ def render_claims(token: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def warn_on_reusable_workflow(claims: dict[str, object]) -> None:
|
||||
# A reusable workflow is identified by having different values
|
||||
# for its workflow_ref (the initiating workflow) and job_workflow_ref
|
||||
# (the reusable workflow).
|
||||
workflow_ref = claims.get('workflow_ref')
|
||||
job_workflow_ref = claims.get('job_workflow_ref')
|
||||
|
||||
if workflow_ref == job_workflow_ref:
|
||||
return
|
||||
|
||||
warn(_REUSABLE_WORKFLOW_WARNING.format_map(locals()))
|
||||
|
||||
|
||||
def event_is_third_party_pr() -> bool:
|
||||
# Non-`pull_request` events cannot be from third-party PRs.
|
||||
if os.getenv('GITHUB_EVENT_NAME') != 'pull_request':
|
||||
@@ -225,12 +272,19 @@ try:
|
||||
oidc_token = id.detect_credential(audience=oidc_audience)
|
||||
except id.IdentityError as identity_error:
|
||||
cause_msg_tmpl = (
|
||||
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE if event_is_third_party_pr()
|
||||
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE
|
||||
if event_is_third_party_pr()
|
||||
else _TOKEN_RETRIEVAL_FAILED_MESSAGE
|
||||
)
|
||||
for_cause_msg = cause_msg_tmpl.format(identity_error=identity_error)
|
||||
die(for_cause_msg)
|
||||
|
||||
|
||||
# Perform a non-fatal check to see if we're running on a reusable
|
||||
# workflow, and emit a warning if so.
|
||||
oidc_claims = extract_claims(oidc_token)
|
||||
warn_on_reusable_workflow(oidc_claims)
|
||||
|
||||
# Now we can do the actual token exchange.
|
||||
mint_token_resp = requests.post(
|
||||
token_exchange_url,
|
||||
@@ -257,7 +311,7 @@ if not mint_token_resp.ok:
|
||||
for error in mint_token_payload['errors']
|
||||
)
|
||||
|
||||
rendered_claims = render_claims(oidc_token)
|
||||
rendered_claims = render_claims(oidc_claims)
|
||||
|
||||
die(
|
||||
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE.format(
|
||||
|
||||
Reference in New Issue
Block a user