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

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


The following commit(s) were added to refs/heads/sbp by this push:
     new cbec8f50 Add audit logging to key writer methods
cbec8f50 is described below

commit cbec8f50ddde1b0a940b7bd36fe09d107bdc9a17
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Apr 3 19:28:28 2026 +0100

    Add audit logging to key writer methods
---
 atr/storage/writers/keys.py    | 63 ++++++++++++++++++++++++++++++++++++++++--
 tests/unit/test_keys_writer.py | 57 ++++++++++++++++++++++++++++++++++++--
 2 files changed, 114 insertions(+), 6 deletions(-)

diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index 2b7d1313..b4e69594 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -15,7 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# TODO: Add auditing
 # TODO: Always raise and catch AccessError
 
 # Removing this will cause circular imports
@@ -86,6 +85,12 @@ class FoundationCommitter(GeneralPublic):
             affected_committee_keys = {committee.key for committee in 
key.committees}
             await self.__data.delete(key)
             await self.__data.commit()
+            self.__write_as.append_to_audit_log(
+                action="key_delete",
+                asf_uid=self.__asf_uid,
+                fingerprint=key.fingerprint,
+                committee_keys=[k for k in sorted(affected_committee_keys)],
+            )
             for committee_key in sorted(affected_committee_keys):
                 await self._sync_committee_keys_file(committee_key)
             return outcome.Result(key)
@@ -227,6 +232,7 @@ class FoundationCommitter(GeneralPublic):
             test_user_keys = await 
self.__data.public_signing_key(apache_uid=test_uid, _committees=True).all()
 
             deleted_count = 0
+            deleted_fingerprints: list[str] = []
             for key in test_user_keys:
                 # We must do this here otherwise SQLAlchemy does not know 
about the deletions
                 key.committees.clear()
@@ -240,8 +246,17 @@ class FoundationCommitter(GeneralPublic):
 
                 await self.__data.delete(key)
                 deleted_count += 1
+                deleted_fingerprints.append(key.fingerprint)
 
             await self.__data.commit()
+            if deleted_count > 0:
+                self.__write_as.append_to_audit_log(
+                    action="key_delete_all_for_test_user",
+                    asf_uid=self.__asf_uid,
+                    target_asf_uid=test_uid,
+                    keys_deleted=deleted_count,
+                    fingerprints=[f for f in sorted(deleted_fingerprints)],
+                )
             return outcome.Result(deleted_count)
         except Exception as e:
             return outcome.Error(e)
@@ -292,6 +307,13 @@ class FoundationCommitter(GeneralPublic):
             )
 
         await self.__data.commit()
+        self.__write_as.append_to_audit_log(
+            action="key_update_committee_associations",
+            asf_uid=self.__asf_uid,
+            fingerprint=fingerprint,
+            committees_added=[k for k in sorted(to_add)],
+            committees_removed=[k for k in sorted(to_remove)],
+        )
 
         for committee_key in sorted(affected):
             await self._sync_committee_keys_file(committee_key)
@@ -353,6 +375,12 @@ class FoundationCommitter(GeneralPublic):
             return outcome.Result(types.Key(status=types.KeyStatus.PARSED, 
key_model=key.key_model))
 
         log.info(f"Inserted key {key.key_model.fingerprint}")
+        self.__write_as.append_to_audit_log(
+            action="key_insert",
+            asf_uid=self.__asf_uid,
+            fingerprint=key.key_model.fingerprint,
+            key_apache_uid=key.key_model.apache_uid,
+        )
 
         # TODO: PARSED now acts as "ALREADY_ADDED"
         return outcome.Result(types.Key(status=types.KeyStatus.INSERTED, 
key_model=key.key_model))
@@ -483,13 +511,21 @@ class CommitteeParticipant(FoundationCommitter):
                 .on_conflict_do_nothing(index_elements=["committee_key", 
"key_fingerprint"])
                 .returning(via(sql.KeyLink.key_fingerprint))
             )
-            if link_insert_result.one_or_none() is None:
+            link_inserted = link_insert_result.one_or_none() is not None
+            if not link_inserted:
                 # e = storage.AccessError(f"Key not found: {fingerprint}")
                 # return storage.OutcomeException(e)
                 pass
             await self.__data.commit()
         except Exception as e:
             return outcome.Error(e)
+        if link_inserted:
+            self.__write_as.append_to_audit_log(
+                action="key_associate_committee",
+                asf_uid=self.__asf_uid,
+                fingerprint=fingerprint,
+                committee_key=self.__committee_key,
+            )
         try:
             autogenerated_outcome = await self.autogenerate_keys_file()
         except Exception as e:
@@ -559,6 +595,7 @@ class CommitteeParticipant(FoundationCommitter):
             )
 
         outcomes = await self.ensure_associated(keys_file_text)
+        release_keys_removed = False
         # Remove the KEYS file if 100% imported
         if (outcomes.result_count > 0) and (outcomes.error_count == 0):
             description = "Removed KEYS file after successful import through 
web interface"
@@ -575,6 +612,17 @@ class CommitteeParticipant(FoundationCommitter):
                 description=description,
                 modify=modify,
             )
+            release_keys_removed = True
+        self.__write_as.append_to_audit_log(
+            action="key_import",
+            asf_uid=self.__asf_uid,
+            committee_key=self.__committee_key,
+            project_key=str(project_key),
+            version_key=str(version_key),
+            imported_keys=outcomes.result_count,
+            failed_keys=outcomes.error_count,
+            release_keys_removed=release_keys_removed,
+        )
         return outcomes
 
     def __block_models(self, key_block: str, ldap_data: dict[str, str]) -> 
list[types.Key | Exception]:
@@ -625,7 +673,7 @@ class CommitteeParticipant(FoundationCommitter):
             outcomes.update_roes(Exception, raise_post_parse_error)
         return outcomes
 
-    async def __database_add_models_core(
+    async def __database_add_models_core(  # noqa: C901
         self,
         outcomes: outcome.List[types.Key],
         associate: bool = True,
@@ -665,6 +713,7 @@ class CommitteeParticipant(FoundationCommitter):
 
         existing_fingerprints = {k.fingerprint for k in 
committee.public_signing_keys}
         new_fingerprints = persisted_fingerprints - existing_fingerprints
+        link_inserts = set()
         if new_fingerprints and associate:
             link_values = [{"committee_key": self.__committee_key, 
"key_fingerprint": fp} for fp in new_fingerprints]
             link_insert_result = await self.__data.execute(
@@ -692,6 +741,14 @@ class CommitteeParticipant(FoundationCommitter):
             log.info("Inserted 0 key links (none to insert)")
 
         await self.__data.commit()
+        if key_inserts or link_inserts:
+            self.__write_as.append_to_audit_log(
+                action="key_insert_or_associate_bulk",
+                asf_uid=self.__asf_uid,
+                committee_key=self.__committee_key,
+                inserted_fingerprints=sorted(key_inserts),
+                linked_fingerprints=sorted(link_inserts),
+            )
         return outcomes
 
     async def __ensure(self, keys_file_text: str, associate: bool = True) -> 
outcome.List[types.Key]:
diff --git a/tests/unit/test_keys_writer.py b/tests/unit/test_keys_writer.py
index f60433b6..d183e20e 100644
--- a/tests/unit/test_keys_writer.py
+++ b/tests/unit/test_keys_writer.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import datetime
 import unittest.mock as mock
 from types import SimpleNamespace
 
@@ -57,6 +58,40 @@ class MockData:
         return Query(self._committees_after_commit[key])
 
 
[email protected]
+async def test_database_add_model_audits_inserted_key():
+    data = MockData(None, committees_after_commit={})
+    writer, _write, write_as = _make_foundation_committer_with_audit(data)
+    insert_result = mock.MagicMock()
+    insert_result.one_or_none.return_value = object()
+    data.execute.return_value = insert_result
+    key_model = keys_writer.sql.PublicSigningKey(
+        fingerprint="fp1",
+        algorithm=1,
+        length=4096,
+        created=datetime.datetime.now(datetime.UTC),
+        latest_self_signature=None,
+        expires=None,
+        primary_declared_uid="Alice <[email protected]>",
+        secondary_declared_uids=[],
+        apache_uid="alice",
+        ascii_armored_key="-----BEGIN PGP PUBLIC KEY 
BLOCK-----\nbody\n-----END PGP PUBLIC KEY BLOCK-----\n",
+    )
+    key = SimpleNamespace(key_model=key_model)
+
+    result = await writer._FoundationCommitter__database_add_model(key)
+
+    assert isinstance(result, outcome.Result)
+    data.begin_immediate.assert_awaited_once()
+    data.commit.assert_awaited_once()
+    write_as.append_to_audit_log.assert_called_once()
+    audit_kwargs = write_as.append_to_audit_log.call_args.kwargs
+    assert audit_kwargs["action"] == "key_insert"
+    assert audit_kwargs["asf_uid"] == "alice"
+    assert audit_kwargs["fingerprint"] == "fp1"
+    assert audit_kwargs["key_apache_uid"] == "alice"
+
+
 @pytest.mark.asyncio
 async def test_delete_committee_keys_audits_committed_delete_when_sync_fails():
     key_orphaned = SimpleNamespace(fingerprint="fp1", committees=[])
@@ -140,7 +175,7 @@ async def 
test_delete_key_removal_deletes_empty_keys_file(tmp_path):
         owned_key,
         committees_after_commit={"alpha": _committee("alpha", [])},
     )
-    writer, _write = _make_foundation_committer(data)
+    writer, _write, write_as = _make_foundation_committer_with_audit(data)
 
     keys_path = tmp_path / "alpha" / "KEYS"
     keys_path.parent.mkdir(parents=True)
@@ -156,6 +191,11 @@ async def 
test_delete_key_removal_deletes_empty_keys_file(tmp_path):
     assert not keys_path.exists()
     data.delete.assert_awaited_once_with(owned_key)
     data.commit.assert_awaited_once()
+    write_as.append_to_audit_log.assert_called_once()
+    audit_kwargs = write_as.append_to_audit_log.call_args.kwargs
+    assert audit_kwargs["action"] == "key_delete"
+    assert audit_kwargs["fingerprint"] == "fp1"
+    assert audit_kwargs["committee_keys"] == ["alpha"]
 
 
 @pytest.mark.asyncio
@@ -165,7 +205,7 @@ async def 
test_update_committee_associations_removal_deletes_empty_keys_file(tmp
         owned_key,
         committees_after_commit={"alpha": _committee("alpha", [])},
     )
-    writer, write = _make_foundation_committer(data)
+    writer, write, write_as = _make_foundation_committer_with_audit(data)
 
     keys_path = tmp_path / "alpha" / "KEYS"
     keys_path.parent.mkdir(parents=True)
@@ -182,6 +222,12 @@ async def 
test_update_committee_associations_removal_deletes_empty_keys_file(tmp
     assert write.as_committee_participant.call_count == 0
     data.begin_immediate.assert_awaited_once()
     data.commit.assert_awaited_once()
+    write_as.append_to_audit_log.assert_called_once()
+    audit_kwargs = write_as.append_to_audit_log.call_args.kwargs
+    assert audit_kwargs["action"] == "key_update_committee_associations"
+    assert audit_kwargs["fingerprint"] == "fp1"
+    assert audit_kwargs["committees_added"] == []
+    assert audit_kwargs["committees_removed"] == ["alpha"]
 
 
 @pytest.mark.asyncio
@@ -230,11 +276,16 @@ def _make_foundation_admin(data: MockData, committee_key: 
str):
 
 
 def _make_foundation_committer(data: MockData):
+    writer, write, _write_as = _make_foundation_committer_with_audit(data)
+    return writer, write
+
+
+def _make_foundation_committer_with_audit(data: MockData):
     write = mock.MagicMock()
     write.authorisation.asf_uid = "alice"
     write.as_committee_participant = mock.MagicMock()
     write_as = mock.MagicMock()
-    return keys_writer.FoundationCommitter(write, write_as, data), write
+    return keys_writer.FoundationCommitter(write, write_as, data), write, 
write_as
 
 
 def _public_key(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to