This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new f33786c  Migrate KEYS export to the storage interface, and remove old 
code
f33786c is described below

commit f33786cbef309830aa4e79b5aaeb6241d971b31e
Author: Sean B. Palmer <s...@miscoranda.com>
AuthorDate: Tue Jul 22 18:40:37 2025 +0100

    Migrate KEYS export to the storage interface, and remove old code
---
 atr/routes/keys.py          | 148 ++---------------------------------
 atr/storage/writers/keys.py | 183 ++++++++++++++++++++++----------------------
 2 files changed, 100 insertions(+), 231 deletions(-)

diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index b1d9aff..e361989 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -24,8 +24,6 @@ import datetime
 import hashlib
 import logging
 import logging.handlers
-import pathlib
-import textwrap
 from collections.abc import Awaitable, Callable, Sequence
 
 import aiofiles.os
@@ -307,13 +305,11 @@ async def details(session: routes.CommitterSession, 
fingerprint: str) -> str | r
 @routes.committer("/keys/export/<committee_name>")
 async def export(session: routes.CommitterSession, committee_name: str) -> 
quart.Response:
     """Export a KEYS file for a specific committee."""
-    if committee_name not in (session.committees + session.projects):
-        quart.abort(403, description=f"You are not authorised to update the 
KEYS file for {committee_name}")
-
-    async with db.session() as data:
-        full_keys_file_content = await _keys_formatter(committee_name, data)
+    async with storage.write(session.uid) as write:
+        wafm = write.as_foundation_member().result_or_raise()
+        keys_file_text = await wafm.keys.keys_file_text(committee_name)
 
-    return quart.Response(full_keys_file_content, mimetype="text/plain")
+    return quart.Response(keys_file_text, mimetype="text/plain")
 
 
 @routes.committer("/keys/import/<project_name>/<version_name>", 
methods=["POST"])
@@ -551,7 +547,9 @@ async def upload(session: routes.CommitterSession) -> str:
         if not selected_committee:
             return await render(error="You must select at least one committee")
 
-        outcomes = await _upload_keys(session.uid, keys_text, 
selected_committee)
+        async with storage.write(session.uid) as write:
+            wacm = 
write.as_committee_member(selected_committee).result_or_raise()
+            outcomes = await wacm.keys.ensure_associated(keys_text)
         results = outcomes
         success_count = outcomes.result_count
         error_count = outcomes.exception_count
@@ -569,54 +567,6 @@ async def upload(session: routes.CommitterSession) -> str:
     return await render()
 
 
-async def _format_keys_file(
-    committee_name_for_header: str,
-    key_count_for_header: int,
-    key_blocks_str: str,
-) -> str:
-    timestamp_str = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d 
%H:%M:%S")
-    purpose_text = (
-        f"This file contains the {key_count_for_header} OpenPGP public keys 
used by "
-        f"committers of the Apache {committee_name_for_header} projects to 
sign official "
-        f"release artifacts. Verifying the signature on a downloaded artifact 
using one "
-        f"of the keys in this file provides confidence that the artifact is 
authentic "
-        f"and was published by the committee."
-    )
-    wrapped_purpose = "\n".join(
-        textwrap.wrap(
-            purpose_text,
-            width=62,
-            initial_indent="# ",
-            subsequent_indent="# ",
-            break_long_words=False,
-            replace_whitespace=False,
-        )
-    )
-
-    header_content = (
-        f"""\
-# Apache Software Foundation (ASF)
-# Signing keys for the {committee_name_for_header} committee
-# Generated on {timestamp_str} UTC
-#
-{wrapped_purpose}
-#
-# 1. Import these keys into your GPG keyring:
-#    gpg --import KEYS
-#
-# 2. Verify the signature file against the release artifact:
-#    gpg --verify "${{ARTIFACT}}.asc" "${{ARTIFACT}}"
-#
-# For details on Apache release signing and verification, see:
-# https://infra.apache.org/release-signing.html
-"""
-        + "\n\n"
-    )
-
-    full_keys_file_content = header_content + key_blocks_str
-    return full_keys_file_content
-
-
 async def _get_keys_text(keys_url: str, render: Callable[[str], 
Awaitable[str]]) -> str:
     try:
         async with aiohttp.ClientSession() as session:
@@ -656,87 +606,3 @@ async def _key_and_is_owner(
         quart.abort(403, description="You are not authorised to view this key")
 
     return key, is_owner
-
-
-async def _keys_formatter(committee_name: str, data: db.Session) -> str:
-    committee = await data.committee(name=committee_name, 
_public_signing_keys=True, _projects=True).demand(
-        base.ASFQuartException(f"Committee {committee_name} not found", 
errorcode=404)
-    )
-
-    if not committee.public_signing_keys:
-        raise base.ASFQuartException(
-            f"No keys found for committee {committee_name} to generate KEYS 
file.", errorcode=404
-        )
-
-    if (not committee.projects) and (committee.name != "incubator"):
-        raise base.ASFQuartException(f"No projects found associated with 
committee {committee_name}.", errorcode=404)
-
-    sorted_keys = sorted(committee.public_signing_keys, key=lambda k: 
k.fingerprint)
-
-    keys_content_list = []
-    for key in sorted_keys:
-        apache_uid = key.apache_uid.lower() if key.apache_uid else None
-        # TODO: What if there is no email?
-        email = util.email_from_uid(key.primary_declared_uid or "") or ""
-        comments = []
-        comments.append(f"Comment: {key.fingerprint.upper()}")
-        if (apache_uid is None) or (email == f"{apache_uid}@apache.org"):
-            comments.append(f"Comment: {email}")
-        else:
-            comments.append(f"Comment: {email} ({apache_uid})")
-        comment_lines = "\n".join(comments)
-        armored_key = key.ascii_armored_key
-        # Use the Sequoia format
-        # -----BEGIN PGP PUBLIC KEY BLOCK-----
-        # Comment: C46D 6658 489D DE09 CE93  8AF8 7B6A 6401 BF99 B4A3
-        # Comment: Redacted Name (CODE SIGNING KEY) <redac...@apache.org>
-        #
-        # [...]
-        if isinstance(armored_key, bytes):
-            # TODO: This should not happen, but it does
-            armored_key = armored_key.decode("utf-8", errors="replace")
-        armored_key = armored_key.replace("BLOCK-----", "BLOCK-----\n" + 
comment_lines, 1)
-        keys_content_list.append(armored_key)
-
-    key_blocks_str = "\n\n\n".join(keys_content_list) + "\n"
-
-    committee_name_for_header = committee.display_name or committee.name
-    key_count_for_header = len(committee.public_signing_keys)
-
-    return await _format_keys_file(
-        committee_name_for_header=committee_name_for_header,
-        key_count_for_header=key_count_for_header,
-        key_blocks_str=key_blocks_str,
-    )
-
-
-async def _upload_keys(
-    asf_uid: str,
-    filetext: str,
-    selected_committee: str,
-) -> types.Outcomes[types.Key]:
-    async with storage.write(asf_uid) as write:
-        wacm = write.as_committee_member(selected_committee).result_or_raise()
-        outcomes: types.Outcomes[types.Key] = await 
wacm.keys.ensure_associated(filetext)
-    return outcomes
-
-
-async def _write_keys_file(
-    committee_keys_dir: pathlib.Path,
-    full_keys_file_content: str,
-    committee_keys_path: pathlib.Path,
-    committee_name: str,
-) -> str | None:
-    try:
-        await asyncio.to_thread(committee_keys_dir.mkdir, parents=True, 
exist_ok=True)
-        await asyncio.to_thread(util.chmod_directories, committee_keys_dir, 
permissions=0o755)
-        await asyncio.to_thread(committee_keys_path.write_text, 
full_keys_file_content, encoding="utf-8")
-    except OSError as e:
-        error_msg = f"Failed to write KEYS file for committee 
{committee_name}: {e}"
-        logging.exception(error_msg)
-        return error_msg
-    except Exception as e:
-        error_msg = f"An unexpected error occurred writing KEYS for committee 
{committee_name}: {e}"
-        logging.exception(error_msg)
-        return error_msg
-    return None
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index 945c281..57eeb0f 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -110,6 +110,50 @@ class FoundationMember:
     async def ensure_stored_one(self, key_file_text: str) -> 
types.Outcome[types.Key]:
         return await self.__ensure_one(key_file_text, associate=False)
 
+    @performance_async
+    async def keys_file_text(self, committee_name: str) -> str:
+        committee = await self.__data.committee(name=committee_name, 
_public_signing_keys=True).demand(
+            storage.AccessError(f"Committee not found: {committee_name}")
+        )
+        if not committee.public_signing_keys:
+            raise storage.AccessError(f"No keys found for committee 
{committee_name} to generate KEYS file.")
+
+        sorted_keys = sorted(committee.public_signing_keys, key=lambda k: 
k.fingerprint)
+
+        keys_content_list = []
+        for key in sorted_keys:
+            apache_uid = key.apache_uid.lower() if key.apache_uid else None
+            # TODO: What if there is no email?
+            email = util.email_from_uid(key.primary_declared_uid or "") or ""
+            comments = []
+            comments.append(f"Comment: {key.fingerprint.upper()}")
+            if (apache_uid is None) or (email == f"{apache_uid}@apache.org"):
+                comments.append(f"Comment: {email}")
+            else:
+                comments.append(f"Comment: {email} ({apache_uid})")
+            comment_lines = "\n".join(comments)
+            armored_key = key.ascii_armored_key
+            # Use the Sequoia format
+            # -----BEGIN PGP PUBLIC KEY BLOCK-----
+            # Comment: C46D 6658 489D DE09 CE93  8AF8 7B6A 6401 BF99 B4A3
+            # Comment: Redacted Name (CODE SIGNING KEY) <redac...@apache.org>
+            #
+            # [...]
+            if isinstance(armored_key, bytes):
+                # TODO: This should not happen, but it does
+                armored_key = armored_key.decode("utf-8", errors="replace")
+            armored_key = armored_key.replace("BLOCK-----", "BLOCK-----\n" + 
comment_lines, 1)
+            keys_content_list.append(armored_key)
+
+        key_blocks_str = "\n\n\n".join(keys_content_list) + "\n"
+        key_count_for_header = len(committee.public_signing_keys)
+
+        return await self.__keys_file_format(
+            committee_name=committee_name,
+            key_count_for_header=key_count_for_header,
+            key_blocks_str=key_blocks_str,
+        )
+
     @performance
     def __block_model(self, key_block: str, ldap_data: dict[str, str]) -> 
types.Key:
         # This cache is only held for the session
@@ -184,6 +228,54 @@ class FoundationMember:
         outcome = await self.__database_add_model(key)
         return outcome
 
+    @performance_async
+    async def __keys_file_format(
+        self,
+        committee_name: str,
+        key_count_for_header: int,
+        key_blocks_str: str,
+    ) -> str:
+        timestamp_str = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d 
%H:%M:%S")
+        purpose_text = f"""\
+This file contains the {key_count_for_header} OpenPGP public keys used by \
+committers of the Apache {committee_name} projects to sign official \
+release artifacts. Verifying the signature on a downloaded artifact using one \
+of the keys in this file provides confidence that the artifact is authentic \
+and was published by the committee.\
+"""
+        wrapped_purpose = "\n".join(
+            textwrap.wrap(
+                purpose_text,
+                width=62,
+                initial_indent="# ",
+                subsequent_indent="# ",
+                break_long_words=False,
+                replace_whitespace=False,
+            )
+        )
+
+        header_content = f"""\
+# Apache Software Foundation (ASF)
+# Signing keys for the {committee_name} committee
+# Generated at {timestamp_str} UTC
+#
+{wrapped_purpose}
+#
+# 1. Import these keys into your GPG keyring:
+#    gpg --import KEYS
+#
+# 2. Verify the signature file against the release artifact:
+#    gpg --verify "${{ARTIFACT}}.asc" "${{ARTIFACT}}"
+#
+# For details on Apache release signing and verification, see:
+# https://infra.apache.org/release-signing.html
+
+
+"""
+
+        full_keys_file_content = header_content + key_blocks_str
+        return full_keys_file_content
+
     @performance
     def __keyring_fingerprint_model(
         self, keyring: pgpy.PGPKeyring, fingerprint: str, ldap_data: dict[str, 
str]
@@ -316,7 +408,7 @@ class CommitteeMember(CommitteeParticipant):
             committee = await self.committee()
             is_podling = committee.is_podling
 
-            full_keys_file_content = await self.__keys_formatter()
+            full_keys_file_content = await self.keys_file_text()
             if is_podling:
                 committee_keys_dir = base_downloads_dir / "incubator" / 
self.__committee_name
             else:
@@ -489,95 +581,6 @@ class CommitteeMember(CommitteeParticipant):
                 logging.info(f"{key}: {value}")
         return outcomes
 
-    async def __keys_file_format(
-        self,
-        key_count_for_header: int,
-        key_blocks_str: str,
-    ) -> str:
-        timestamp_str = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d 
%H:%M:%S")
-        purpose_text = f"""\
-This file contains the {key_count_for_header} OpenPGP public keys used by \
-committers of the Apache {self.__committee_name} projects to sign official \
-release artifacts. Verifying the signature on a downloaded artifact using one \
-of the keys in this file provides confidence that the artifact is authentic \
-and was published by the committee.\
-"""
-        wrapped_purpose = "\n".join(
-            textwrap.wrap(
-                purpose_text,
-                width=62,
-                initial_indent="# ",
-                subsequent_indent="# ",
-                break_long_words=False,
-                replace_whitespace=False,
-            )
-        )
-
-        header_content = f"""\
-# Apache Software Foundation (ASF)
-# Signing keys for the {self.__committee_name} committee
-# Generated on {timestamp_str} UTC
-#
-{wrapped_purpose}
-#
-# 1. Import these keys into your GPG keyring:
-#    gpg --import KEYS
-#
-# 2. Verify the signature file against the release artifact:
-#    gpg --verify "${{ARTIFACT}}.asc" "${{ARTIFACT}}"
-#
-# For details on Apache release signing and verification, see:
-# https://infra.apache.org/release-signing.html
-
-
-"""
-
-        full_keys_file_content = header_content + key_blocks_str
-        return full_keys_file_content
-
-    async def __keys_formatter(self) -> str:
-        committee = await self.committee()
-        if not committee.public_signing_keys:
-            raise storage.AccessError(f"No keys found for committee 
{self.__committee_name} to generate KEYS file.")
-
-        # if (not committee.projects) and (committee.name != "incubator"):
-        #     raise storage.AccessError(f"No projects found associated with 
committee {self.__committee_name}.")
-
-        sorted_keys = sorted(committee.public_signing_keys, key=lambda k: 
k.fingerprint)
-
-        keys_content_list = []
-        for key in sorted_keys:
-            apache_uid = key.apache_uid.lower() if key.apache_uid else None
-            # TODO: What if there is no email?
-            email = util.email_from_uid(key.primary_declared_uid or "") or ""
-            comments = []
-            comments.append(f"Comment: {key.fingerprint.upper()}")
-            if (apache_uid is None) or (email == f"{apache_uid}@apache.org"):
-                comments.append(f"Comment: {email}")
-            else:
-                comments.append(f"Comment: {email} ({apache_uid})")
-            comment_lines = "\n".join(comments)
-            armored_key = key.ascii_armored_key
-            # Use the Sequoia format
-            # -----BEGIN PGP PUBLIC KEY BLOCK-----
-            # Comment: C46D 6658 489D DE09 CE93  8AF8 7B6A 6401 BF99 B4A3
-            # Comment: Redacted Name (CODE SIGNING KEY) <redac...@apache.org>
-            #
-            # [...]
-            if isinstance(armored_key, bytes):
-                # TODO: This should not happen, but it does
-                armored_key = armored_key.decode("utf-8", errors="replace")
-            armored_key = armored_key.replace("BLOCK-----", "BLOCK-----\n" + 
comment_lines, 1)
-            keys_content_list.append(armored_key)
-
-        key_blocks_str = "\n\n\n".join(keys_content_list) + "\n"
-        key_count_for_header = len(committee.public_signing_keys)
-
-        return await self.__keys_file_format(
-            key_count_for_header=key_count_for_header,
-            key_blocks_str=key_blocks_str,
-        )
-
 
 # class FoundationAdmin(FoundationMember):
 #     def __init__(


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@tooling.apache.org
For additional commands, e-mail: commits-h...@tooling.apache.org

Reply via email to