Compare commits

...

15 Commits

Author SHA1 Message Date
Sviatoslav Sydorenko (Святослав Сидоренко) 411815e640
Address S113 @ `oidc-exchange.py` 2024-05-16 17:38:05 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) fa6d770e70
Fix the lask of Q000 @ `oidc-exchange.py` 2024-05-16 17:34:23 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) d1883f61f8
Flip the quotes @ `oidc-exchange.py` 2024-05-16 17:32:54 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 9da6dedb16
Address Q000 in `oidc-exchange.py` 2024-05-16 17:30:39 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 5569480d08
Address Q000 @ `print-hash.py` 2024-05-16 17:24:20 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) b02d39bbf5
Suppress S324 @ `print-hash.py` 2024-05-16 17:23:10 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) a047f618a1
Suppress false-positive S105 @ `oidc-exchange.py` 2024-05-16 17:21:36 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 7f0e83ee55
Normalize quotes in `oidc-exchange.py` to fix Q000 2024-05-16 17:19:18 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) e6ad7277fd
Merge branch 'unstable/v1' into pre-commit-ci-update-config 2024-05-16 11:17:05 -04:00
Peter Shen 67a07ebbed
Disable the progress bar when running `twine upload`
PR #231
Resolves #229

Co-authored-by: Sviatoslav Sydorenko <webknjaz@redhat.com>
2024-05-16 17:14:58 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) c48f2fe777
Merge branch 'unstable/v1' into pre-commit-ci-update-config 2024-05-16 11:09:16 -04:00
William Woodruff 771d60f44b
Eliminate future tense in the password nudge in `twine-upload`
Additionally, this turns the corresponding code branch into a hard error in case of the regular PyPI.

Signed-off-by: William Woodruff <william@trailofbits.com>

PR #234
Fixes #233
2024-05-16 17:07:28 +02:00
Sviatoslav Sydorenko 04f4e64de3
Set Python 3.11 for the `flake8-commas` linter
It doesn't yet support 3.12 and is an unconditional dependency of WPS.
2024-05-16 16:29:54 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 3fbcf7ccf4
Merge pull request #228 from pypa/dependabot/pip/requirements/idna-3.7
build(deps): bump idna from 3.6 to 3.7 in /requirements
2024-04-12 15:30:45 +02:00
dependabot[bot] 576aae3934
build(deps): bump idna from 3.6 to 3.7 in /requirements
Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-12 04:51:56 +00:00
5 changed files with 59 additions and 56 deletions

View File

@ -112,6 +112,7 @@ repos:
- flake8-2020 ~= 1.7.0
- flake8-pytest-style ~= 1.6.0
- wemake-python-styleguide ~= 0.19.0
language_version: python3.11 # flake8-commas doesn't work w/ Python 3.12
- repo: https://github.com/PyCQA/pylint.git
rev: v3.1.0

View File

@ -10,7 +10,7 @@ from urllib.parse import urlparse
import id # pylint: disable=redefined-builtin
import requests
_GITHUB_STEP_SUMMARY = Path(os.getenv("GITHUB_STEP_SUMMARY"))
_GITHUB_STEP_SUMMARY = Path(os.getenv('GITHUB_STEP_SUMMARY'))
# The top-level error message that gets rendered.
# This message wraps one of the other templates/messages defined below.
@ -45,7 +45,7 @@ permissions:
```
Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings.
"""
""" # noqa: S105; not a password
# Specialization of the token retrieval failure case, when we know that
# the failure cause is use within a third-party PR.
@ -59,7 +59,7 @@ 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).
"""
""" # noqa: S105; not a password
# Rendered if the package index refuses the given OIDC token.
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE = """
@ -71,7 +71,7 @@ This generally indicates a trusted publisher configuration error, but could
also indicate an internal error on GitHub or PyPI's part.
{rendered_claims}
"""
""" # noqa: S105; not a password
_RENDERED_CLAIMS = """
The claims rendered below are **for debugging purposes only**. You should **not**
@ -97,7 +97,7 @@ Token request failed: the index produced an unexpected
This strongly suggests a server configuration or downtime issue; wait
a few minutes and try again.
"""
""" # noqa: S105; not a password
# Rendered if the package index's token response isn't a valid API token payload.
_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE = """
@ -105,30 +105,30 @@ Token response error: the index gave us an invalid response.
This strongly suggests a server configuration or downtime issue; wait
a few minutes and try again.
"""
""" # noqa: S105; not a password
def die(msg: str) -> NoReturn:
with _GITHUB_STEP_SUMMARY.open("a", encoding="utf-8") as io:
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
print(_ERROR_SUMMARY_MESSAGE.format(message=msg), file=io)
# HACK: GitHub Actions' annotations don't work across multiple lines naively;
# translating `\n` into `%0A` (i.e., HTML percent-encoding) is known to work.
# See: https://github.com/actions/toolkit/issues/193
msg = msg.replace("\n", "%0A")
print(f"::error::Trusted publishing exchange failure: {msg}", file=sys.stderr)
msg = msg.replace('\n', '%0A')
print(f'::error::Trusted publishing exchange failure: {msg}', file=sys.stderr)
sys.exit(1)
def debug(msg: str):
print(f"::debug::{msg.title()}", file=sys.stderr)
print(f'::debug::{msg.title()}', file=sys.stderr)
def get_normalized_input(name: str) -> str | None:
name = f"INPUT_{name.upper()}"
name = f'INPUT_{name.upper()}'
if val := os.getenv(name):
return val
return os.getenv(name.replace("-", "_"))
return os.getenv(name.replace('-', '_'))
def assert_successful_audience_call(resp: requests.Response, domain: str):
@ -140,13 +140,13 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
# This index supports OIDC, but forbids the client from using
# it (either because it's disabled, ratelimited, etc.)
die(
f"audience retrieval failed: repository at {domain} has trusted publishing disabled",
f'audience retrieval failed: repository at {domain} has trusted publishing disabled',
)
case HTTPStatus.NOT_FOUND:
# This index does not support OIDC.
die(
"audience retrieval failed: repository at "
f"{domain} does not indicate trusted publishing support",
'audience retrieval failed: repository at '
f'{domain} does not indicate trusted publishing support',
)
case other:
status = HTTPStatus(other)
@ -154,67 +154,67 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
# something we expect. This can happen if the index is broken, in maintenance mode,
# misconfigured, etc.
die(
"audience retrieval failed: repository at "
f"{domain} responded with unexpected {other}: {status.phrase}",
'audience retrieval failed: repository at '
f'{domain} responded with unexpected {other}: {status.phrase}',
)
def render_claims(token: str) -> str:
_, payload, _ = token.split(".", 2)
_, payload, _ = token.split('.', 2)
# urlsafe_b64decode needs padding; JWT payloads don't contain any.
payload += "=" * (4 - (len(payload) % 4))
payload += '=' * (4 - (len(payload) % 4))
claims = json.loads(base64.urlsafe_b64decode(payload))
def _get(name: str) -> str: # noqa: WPS430
return claims.get(name, "MISSING")
return claims.get(name, 'MISSING')
return _RENDERED_CLAIMS.format(
sub=_get("sub"),
repository=_get("repository"),
repository_owner=_get("repository_owner"),
repository_owner_id=_get("repository_owner_id"),
job_workflow_ref=_get("job_workflow_ref"),
ref=_get("ref"),
sub=_get('sub'),
repository=_get('repository'),
repository_owner=_get('repository_owner'),
repository_owner_id=_get('repository_owner_id'),
job_workflow_ref=_get('job_workflow_ref'),
ref=_get('ref'),
)
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":
if os.getenv('GITHUB_EVENT_NAME') != 'pull_request':
return False
event_path = os.getenv("GITHUB_EVENT_PATH")
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")
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")
debug('unexpected: GITHUB_EVENT_PATH does not contain valid JSON')
return False
try:
return event["pull_request"]["head"]["repo"]["fork"]
return event['pull_request']['head']['repo']['fork']
except KeyError:
return False
repository_url = get_normalized_input("repository-url")
repository_url = get_normalized_input('repository-url')
repository_domain = urlparse(repository_url).netloc
token_exchange_url = f"https://{repository_domain}/_/oidc/mint-token"
token_exchange_url = f'https://{repository_domain}/_/oidc/mint-token'
# Indices are expected to support `https://{domain}/_/oidc/audience`,
# which tells OIDC exchange clients which audience to use.
audience_url = f"https://{repository_domain}/_/oidc/audience"
audience_resp = requests.get(audience_url)
audience_url = f'https://{repository_domain}/_/oidc/audience'
audience_resp = requests.get(audience_url, timeout=5) # S113 wants a timeout
assert_successful_audience_call(audience_resp, repository_domain)
oidc_audience = audience_resp.json()["audience"]
oidc_audience = audience_resp.json()['audience']
debug(f"selected trusted publishing exchange endpoint: {token_exchange_url}")
debug(f'selected trusted publishing exchange endpoint: {token_exchange_url}')
try:
oidc_token = id.detect_credential(audience=oidc_audience)
@ -229,7 +229,8 @@ except id.IdentityError as identity_error:
# Now we can do the actual token exchange.
mint_token_resp = requests.post(
token_exchange_url,
json={"token": oidc_token},
json={'token': oidc_token},
timeout=5, # S113 wants a timeout
)
try:
@ -246,9 +247,9 @@ except requests.JSONDecodeError:
# On failure, the JSON response includes the list of errors that
# occurred during minting.
if not mint_token_resp.ok:
reasons = "\n".join(
f"* `{error['code']}`: {error['description']}"
for error in mint_token_payload["errors"]
reasons = '\n'.join(
f'* `{error["code"]}`: {error["description"]}'
for error in mint_token_payload['errors']
)
rendered_claims = render_claims(oidc_token)
@ -260,12 +261,12 @@ if not mint_token_resp.ok:
),
)
pypi_token = mint_token_payload.get("token")
pypi_token = mint_token_payload.get('token')
if pypi_token is None:
die(_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE)
# Mask the newly minted PyPI token, so that we don't accidentally leak it in logs.
print(f"::add-mask::{pypi_token}", file=sys.stderr)
print(f'::add-mask::{pypi_token}', file=sys.stderr)
# This final print will be captured by the subshell in `twine-upload.sh`.
print(pypi_token)

View File

@ -4,15 +4,15 @@ import sys
packages_dir = pathlib.Path(sys.argv[1]).resolve().absolute()
print("Showing hash values of files to be uploaded:")
print('Showing hash values of files to be uploaded:')
for file_object in packages_dir.iterdir():
sha256 = hashlib.sha256()
md5 = hashlib.md5()
md5 = hashlib.md5() # noqa: S324; only use for reference
blake2_256 = hashlib.blake2b(digest_size=256 // 8)
print(file_object)
print("")
print('')
content = file_object.read_bytes()
@ -20,7 +20,7 @@ for file_object in packages_dir.iterdir():
md5.update(content)
blake2_256.update(content)
print(f"SHA256: {sha256.hexdigest()}")
print(f"MD5: {md5.hexdigest()}")
print(f"BLAKE2-256: {blake2_256.hexdigest()}")
print("")
print(f'SHA256: {sha256.hexdigest()}')
print(f'MD5: {md5.hexdigest()}')
print(f'BLAKE2-256: {blake2_256.hexdigest()}')
print('')

View File

@ -18,7 +18,7 @@ docutils==0.20.1
# via readme-renderer
id==1.3.0
# via -r runtime.in
idna==3.6
idna==3.7
# via requests
importlib-metadata==7.0.2
# via twine

View File

@ -40,9 +40,9 @@ INPUT_VERIFY_METADATA="$(get-normalized-input 'verify-metadata')"
INPUT_SKIP_EXISTING="$(get-normalized-input 'skip-existing')"
INPUT_PRINT_HASH="$(get-normalized-input 'print-hash')"
PASSWORD_DEPRECATION_NUDGE="::error title=Password-based uploads deprecated::\
Starting in 2024, PyPI will require all users to enable Two-Factor \
Authentication. This will consequently require all users to switch \
PASSWORD_DEPRECATION_NUDGE="::error title=Password-based uploads disabled::\
As of 2024, PyPI requires all users to enable Two-Factor \
Authentication. This consequently requires all users to switch \
to either Trusted Publishers (preferred) or API tokens for package \
uploads. Read more: \
https://blog.pypi.org/posts/2023-05-25-securing-pypi-with-2fa/"
@ -74,6 +74,7 @@ else
if [[ "${INPUT_REPOSITORY_URL}" =~ pypi\.org ]]; then
echo "${PASSWORD_DEPRECATION_NUDGE}"
echo "${TRUSTED_PUBLISHING_NUDGE}"
exit 1
fi
fi
@ -120,9 +121,9 @@ if [[ ${INPUT_VERIFY_METADATA,,} != "false" ]] ; then
twine check ${INPUT_PACKAGES_DIR%%/}/*
fi
TWINE_EXTRA_ARGS=
TWINE_EXTRA_ARGS=--disable-progress-bar
if [[ ${INPUT_SKIP_EXISTING,,} != "false" ]] ; then
TWINE_EXTRA_ARGS=--skip-existing
TWINE_EXTRA_ARGS="${TWINE_EXTRA_ARGS} --skip-existing"
fi
if [[ ${INPUT_VERBOSE,,} != "false" ]] ; then