Skip to content

Commit ddb6f99

Browse files
alithegsbp
authored andcommitted
Automatically re-run signature checks when KEYS change
1 parent 5e1076d commit ddb6f99

5 files changed

Lines changed: 56 additions & 1 deletion

File tree

atr/storage/writers/keys.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import atr.storage as storage
4444
import atr.storage.outcome as outcome
4545
import atr.storage.types as types
46+
import atr.tasks as tasks
4647
import atr.user as user
4748
import atr.util as util
4849

@@ -226,6 +227,7 @@ async def delete_key(self, fingerprint: str) -> outcome.Outcome[sql.PublicSignin
226227
)
227228
for committee_key in sorted(affected_committee_keys):
228229
await self._sync_committee_keys_file(committee_key)
230+
await self._recheck_committee_drafts(*affected_committee_keys)
229231
return outcome.Result(key)
230232
except Exception as e:
231233
return outcome.Error(e)
@@ -310,6 +312,29 @@ async def _keys_file_text(self, committee: sql.Committee) -> str:
310312
def _committee_keys_path(self, committee: sql.Committee) -> safe.StatePath:
311313
return paths.committee_downloads_dir(committee) / "KEYS"
312314

315+
async def _recheck_committee_drafts(self, *committee_keys: str) -> None:
316+
# A KEYS change only invalidates signature checks, so we limit the re-queue to .asc files
317+
affected = {committee_key for committee_key in committee_keys if committee_key}
318+
if not affected:
319+
return
320+
drafts = await self.__data.release(
321+
phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
322+
_committee=True,
323+
).all()
324+
for draft in drafts:
325+
committee = draft.project.committee
326+
if (committee is None) or (committee.key not in affected):
327+
continue
328+
if not draft.latest_revision_number:
329+
continue
330+
await tasks.draft_checks(
331+
self.__asf_uid,
332+
draft.safe_project_key,
333+
draft.safe_version_key,
334+
draft.safe_latest_revision_number,
335+
suffix_filter=[".asc"],
336+
)
337+
313338
async def _sync_committee_keys_file(self, committee_key: str) -> str | None:
314339
committee = await self.__data.committee(key=committee_key, _public_signing_keys=True).demand(
315340
storage.AccessError(f"Committee not found: {committee_key}", status=404)
@@ -439,6 +464,8 @@ async def update_committee_associations(
439464
for committee_key in sorted(affected):
440465
await self._sync_committee_keys_file(committee_key)
441466

467+
await self._recheck_committee_drafts(*affected)
468+
442469
return affected
443470

444471
def __block_model(self, key_block: str, ldap_data: cache.EmailUidLookup) -> types.Key:
@@ -640,6 +667,7 @@ async def associate_fingerprint(self, fingerprint: str) -> outcome.Outcome[types
640667
fingerprint=fingerprint,
641668
committee_key=self.__committee_key,
642669
)
670+
await self._recheck_committee_drafts(self.__committee_key)
643671
try:
644672
autogenerated_outcome = await self.autogenerate_keys_file()
645673
except Exception as e:
@@ -854,6 +882,8 @@ def replace_with_linked(key: types.Key) -> types.Key:
854882
inserted_fingerprints=sorted(key_inserts),
855883
linked_fingerprints=sorted(link_inserts),
856884
)
885+
if link_inserts:
886+
await self._recheck_committee_drafts(self.__committee_key)
857887
return outcomes
858888

859889
async def __ensure(
@@ -974,6 +1004,7 @@ async def delete_committee_keys(self) -> tuple[int, int]:
9741004
)
9751005
raise
9761006

1007+
await self._recheck_committee_drafts(self.__committee_key)
9771008
return (num_unlinked, num_deleted)
9781009

9791010

atr/tasks/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ async def draft_checks(
145145
release_version: safe.VersionKey,
146146
revision_number: safe.RevisionNumber,
147147
caller_data: db.Session | None = None,
148+
suffix_filter: list[str] | None = None,
148149
) -> int:
149150
"""Core logic to analyse a draft revision and queue checks."""
150151
# Construct path to the specific revision
@@ -172,6 +173,8 @@ async def draft_checks(
172173
(v for v in release_versions if util.version_sort_key(str(v.version)) < release_version_sortable), None
173174
)
174175
for path in relative_paths:
176+
if suffix_filter and not str(path).endswith(tuple(suffix_filter)):
177+
continue
175178
await _draft_file_checks(
176179
asf_uid,
177180
caller_data,

atr/tasks/checks/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,20 @@ async def _resolve_committee_key(release: sql.Release, rel_path: str | None = No
472472
return release.committee.key
473473

474474

475+
async def _resolve_committee_signing_keys(release: sql.Release, rel_path: str | None = None) -> list[str]:
476+
if release.committee is None:
477+
raise ValueError("Release has no committee")
478+
via = sql.validate_instrumented_attribute
479+
committee_key = release.committee.key
480+
async with db.session() as data:
481+
statement = sqlmodel.select(via(sql.KeyLink.key_fingerprint)).where(
482+
via(sql.KeyLink.committee_key) == committee_key
483+
)
484+
result = await data.execute(statement)
485+
fingerprints = result.scalars().all()
486+
return sorted(fp for fp in fingerprints if fp)
487+
488+
475489
async def _resolve_github_tp_sha(release: sql.Release, rel_path: str | None = None) -> str:
476490
if not release.latest_revision_number:
477491
return ""
@@ -503,6 +517,7 @@ async def _resolve_unsuffixed_file_hash(release: sql.Release, rel_path: str | No
503517
_EXTRA_ARG_RESOLVERS: Final[dict[str, Callable[[sql.Release, str | None], Any]]] = {
504518
"all_files": _resolve_all_files,
505519
"committee_key": _resolve_committee_key,
520+
"committee_signing_keys": _resolve_committee_signing_keys,
506521
"github_tp_sha": _resolve_github_tp_sha,
507522
"is_podling": _resolve_is_podling,
508523
"unsuffixed_file_hash": _resolve_unsuffixed_file_hash,

atr/tasks/checks/signature.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
# Release policy fields which this check relies on - used for result caching
3333
INPUT_POLICY_KEYS: Final[list[str]] = []
34-
INPUT_EXTRA_ARGS: Final[list[str]] = ["committee_key", "unsuffixed_file_hash"]
34+
INPUT_EXTRA_ARGS: Final[list[str]] = ["committee_key", "committee_signing_keys", "unsuffixed_file_hash"]
3535
CHECK_VERSION: Final[str] = "3"
3636

3737

tests/unit/test_keys_writer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def __init__(self, value):
6363
async def get(self):
6464
return self._value
6565

66+
async def all(self):
67+
return [self._value]
68+
6669
async def demand(self, error: Exception):
6770
if self._value is None:
6871
raise error
@@ -85,6 +88,9 @@ def public_signing_key(self, **_kwargs):
8588
def committee(self, *, key: str, _public_signing_keys: bool = False):
8689
return Query(self._committees_after_commit[key])
8790

91+
def release(self, *_args, **_kwargs):
92+
return Query(SimpleNamespace(project=mock.AsyncMock()))
93+
8894

8995
@pytest.mark.asyncio
9096
async def test_database_add_model_audits_inserted_key():

0 commit comments

Comments
 (0)