diff --git a/oidc-exchange.py b/oidc-exchange.py index 781a181..fb53e4f 100644 --- a/oidc-exchange.py +++ b/oidc-exchange.py @@ -47,6 +47,20 @@ permissions: Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings. """ +# Specialization of the token retrieval failure case, when we know that +# the failure cause is use within a third-party PR. +_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE = """ +OpenID Connect token retrieval failed: {identity_error} + +The workflow context indicates that this action was called from a +pull request on a fork. GitHub doesn't give these workflows OIDC permissions, +even if `id-token: write` is explicitly configured. + +To fix this, change your publishing workflow to use an event that +forks of your repository cannot trigger (such as tag or release +creation, or a manually triggered workflow dispatch). +""" + # Rendered if the package index refuses the given OIDC token. _SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE = """ Token request failed: the server refused the request for the following reasons: @@ -165,6 +179,29 @@ def render_claims(token: str) -> str: ) +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": + return False + + event_path = os.getenv("GITHUB_EVENT_PATH") + if not event_path: + # No GITHUB_EVENT_PATH indicates a weird GitHub or runner bug. + debug("unexpected: no GITHUB_EVENT_PATH to check") + return False + + try: + event = json.loads(Path(event_path).read_bytes()) + except json.JSONDecodeError: + debug("unexpected: GITHUB_EVENT_PATH does not contain valid JSON") + return False + + try: + return event["pull_request"]["head"]["repo"]["fork"] + except KeyError: + return False + + repository_url = get_normalized_input("repository-url") repository_domain = urlparse(repository_url).netloc token_exchange_url = f"https://{repository_domain}/_/oidc/mint-token" @@ -182,7 +219,12 @@ debug(f"selected trusted publishing exchange endpoint: {token_exchange_url}") try: oidc_token = id.detect_credential(audience=oidc_audience) except id.IdentityError as identity_error: - die(_TOKEN_RETRIEVAL_FAILED_MESSAGE.format(identity_error=identity_error)) + cause_msg_tmpl = ( + _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) # Now we can do the actual token exchange. mint_token_resp = requests.post(