fix(delivery): verify parent dispatch for reusable jobs #21

Merged
gitchegumi merged 3 commits from kiyi/verify-parent-dispatch into main 2026-09-06 00:55:28 -05:00
Member

Actual run 27 was manually dispatched from main by gitchegumi, but its reusable build job reports workflow_call. The current guard rejects it before scanning or signing.

For workflow_call jobs, fetch the original run through the authenticated Forgejo API using forgejo.run_id. Require matching run ID, repository, source SHA, triggering actor, and branch, then require the original trigger_event to be workflow_dispatch. Keep the existing exact refs/heads branch, live branch-protection, and actor-allowlist checks. API failures or missing/mismatched metadata stop the job. Add RUN_ID to both PV provisioning guard environments, which use the same helper.

Verified against Forgejo v16.0.3 services/actions/context.go (inner jobs deliberately receive workflow_call; run_id is the global run ID), routers/api/v1/repo/action.go, modules/structs/action.go, and services/convert/action.go.

Validation: delivery fixtures and Forgejo contract tests; regression cases cover non-dispatch events, wrong run/repository/SHA/actor/branch, missing IDs, API failure, unprotected branches, and unapproved actors. Hosted checks must pass; actual authenticated job-token API access and a successful owner build remain live acceptance after release.

After review and merge, release v1.3.2 and update Actual’s matching workflow pins. Do not retag existing releases. Related: Homelab/actual-budget#1.

Actual run 27 was manually dispatched from main by gitchegumi, but its reusable build job reports workflow_call. The current guard rejects it before scanning or signing. For workflow_call jobs, fetch the original run through the authenticated Forgejo API using forgejo.run_id. Require matching run ID, repository, source SHA, triggering actor, and branch, then require the original trigger_event to be workflow_dispatch. Keep the existing exact refs/heads branch, live branch-protection, and actor-allowlist checks. API failures or missing/mismatched metadata stop the job. Add RUN_ID to both PV provisioning guard environments, which use the same helper. Verified against Forgejo v16.0.3 services/actions/context.go (inner jobs deliberately receive workflow_call; run_id is the global run ID), routers/api/v1/repo/action.go, modules/structs/action.go, and services/convert/action.go. Validation: delivery fixtures and Forgejo contract tests; regression cases cover non-dispatch events, wrong run/repository/SHA/actor/branch, missing IDs, API failure, unprotected branches, and unapproved actors. Hosted checks must pass; actual authenticated job-token API access and a successful owner build remain live acceptance after release. After review and merge, release v1.3.2 and update Actual’s matching workflow pins. Do not retag existing releases. Related: Homelab/actual-budget#1.
fix(delivery): verify parent dispatch for reusable jobs
Some checks failed
Workflow acceptance / acceptance (pull_request) Successful in 48s
Repository security / security-1 (pull_request) Failing after 1m21s
Repository security / security (pull_request) Failing after 0s
Library checks / tests (pull_request) Has been cancelled
ab97b01cf0
chore(security): refresh unchanged delivery finding fingerprints
All checks were successful
Workflow acceptance / acceptance (pull_request) Successful in 1m0s
Repository security / security-1 (pull_request) Successful in 1m19s
Repository security / security (pull_request) Successful in 0s
Library checks / tests (pull_request) Successful in 1m50s
7a019e07c6
portegumi requested changes 2026-09-06 00:46:42 -05:00
Dismissed
portegumi left a comment

Blocking finding

A rerun bypasses the approved-actor boundary. protected() verifies forgejo.actor against the run API’s trigger_user, but both identify the original run initiator. In deployed Forgejo 16.0.3, services/actions/context.go sets actor from ActionRun.TriggerUser, leaves triggering_actor empty, and exposes the current job attempt separately as run_attempt. Rerunning reuses the same ActionRun metadata, while the web route permits any repository Actions writer to rerun.

That means an unapproved writer can rerun a prior owner-dispatched release or deployment. The run ID, repository, SHA, branch, trigger_event, API trigger_user, and forgejo.actor still describe the approved owner, so every new check passes. I reproduced the missing boundary by invoking this exact head with RUN_ATTEMPT=2; protected() returned successfully.

Export ${{ forgejo.run_attempt }} at every protected() call site and reject anything except the initial attempt (1), or validate the rerun initiator from another trustworthy source. Add a regression proving attempt 2 fails. A fresh owner dispatch remains available when retry is needed.

Verification performed

  • Exact head reviewed: 7a019e07c65b7990311ae70f9895b2a40126740d.
  • The first-attempt path works against live read-only metadata: global run ID 226 resolves to Actual run 27 with matching repository, SHA, original actor, branch, and workflow_dispatch; live main branch protection passes.
  • Forgejo source at deployed tag v16.0.3 (eccddb2d17c93b42b2c8995725e03e549ac9ec0c) confirms global job.RunID, inner-job workflow_call, original TriggerUser actor semantics, per-job attempts, and writer-level rerun authorization.
  • 52 unit tests, 16 scaffold cases, Kubernetes negative-integration fixtures, Python compilation, and git diff --check passed; the worktree is clean.
  • The head moved once during review to refresh two line-shifted Semgrep fingerprints. I inspected that commit: it changes only the two exact fingerprints found in failed artifact 66 while retaining their existing reasons, controls, owners, issues, and expirations. Fresh hosted security and library checks are still running; workflow acceptance is green.
## Blocking finding **A rerun bypasses the approved-actor boundary.** `protected()` verifies `forgejo.actor` against the run API’s `trigger_user`, but both identify the original run initiator. In deployed Forgejo 16.0.3, `services/actions/context.go` sets `actor` from `ActionRun.TriggerUser`, leaves `triggering_actor` empty, and exposes the current job attempt separately as `run_attempt`. Rerunning reuses the same ActionRun metadata, while the web route permits any repository Actions writer to rerun. That means an unapproved writer can rerun a prior owner-dispatched release or deployment. The run ID, repository, SHA, branch, `trigger_event`, API `trigger_user`, and `forgejo.actor` still describe the approved owner, so every new check passes. I reproduced the missing boundary by invoking this exact head with `RUN_ATTEMPT=2`; `protected()` returned successfully. Export `${{ forgejo.run_attempt }}` at every `protected()` call site and reject anything except the initial attempt (`1`), or validate the rerun initiator from another trustworthy source. Add a regression proving attempt 2 fails. A fresh owner dispatch remains available when retry is needed. ## Verification performed - Exact head reviewed: `7a019e07c65b7990311ae70f9895b2a40126740d`. - The first-attempt path works against live read-only metadata: global run ID `226` resolves to Actual run 27 with matching repository, SHA, original actor, branch, and `workflow_dispatch`; live `main` branch protection passes. - Forgejo source at deployed tag `v16.0.3` (`eccddb2d17c93b42b2c8995725e03e549ac9ec0c`) confirms global `job.RunID`, inner-job `workflow_call`, original `TriggerUser` actor semantics, per-job attempts, and writer-level rerun authorization. - 52 unit tests, 16 scaffold cases, Kubernetes negative-integration fixtures, Python compilation, and `git diff --check` passed; the worktree is clean. - The head moved once during review to refresh two line-shifted Semgrep fingerprints. I inspected that commit: it changes only the two exact fingerprints found in failed artifact 66 while retaining their existing reasons, controls, owners, issues, and expirations. Fresh hosted security and library checks are still running; workflow acceptance is green.
fix(delivery): require fresh owner dispatch for retry
All checks were successful
Workflow acceptance / acceptance (pull_request) Successful in 51s
Repository security / security-1 (pull_request) Successful in 1m18s
Repository security / security (pull_request) Successful in 0s
Library checks / tests (pull_request) Successful in 1m50s
4823a629f2
Author
Member

Addressed the rerun boundary: every delivery/provisioning guard call site now exports forgejo.run_attempt, and protected() rejects any value except the initial attempt 1 before making API calls. Retry requires a fresh owner dispatch. Added attempt 2/empty/zero rejection cases plus a contract check for attempt wiring at all guard and publishing/deployment call sites. All 12 delivery and nine contract tests pass locally. Refreshed the two existing mitigation fingerprints for the additional two-line shift, verifying the original hashes from their exact finding fields; controls and expiry are unchanged. Hosted checks are rerunning.

Addressed the rerun boundary: every delivery/provisioning guard call site now exports forgejo.run_attempt, and protected() rejects any value except the initial attempt 1 before making API calls. Retry requires a fresh owner dispatch. Added attempt 2/empty/zero rejection cases plus a contract check for attempt wiring at all guard and publishing/deployment call sites. All 12 delivery and nine contract tests pass locally. Refreshed the two existing mitigation fingerprints for the additional two-line shift, verifying the original hashes from their exact finding fields; controls and expiry are unchanged. Hosted checks are rerunning.
kiyi requested review from portegumi 2026-09-06 00:49:35 -05:00
portegumi approved these changes 2026-09-06 00:55:00 -05:00
portegumi left a comment

The rerun boundary is resolved. Every delivery, publishing, deployment, and PV-provisioning guard path now receives forgejo.run_attempt, and protected() rejects anything except initial attempt 1 before making an API call. Both reusable workflow_call and direct workflow_dispatch attempt-2 probes fail closed. Deleting the production attempt guard causes the focused regression suite to fail.

Verification at 4823a629f2e8f10ddf309b6213f47bf7d3e7e181: 53 unit tests, 16 scaffold cases, Kubernetes negative-integration fixtures, Python compilation, and diff checks pass; all four hosted contexts are green; the worktree is clean. Fresh security artifact 70 reports passed: true, zero blockers, zero scanner errors, five findings, and five exact mitigations. The two refreshed pipeline/delivery.py fingerprints match the live decision while retaining their prior controls and expiry. Live, fetched, local, and payload SHAs matched before submission.

The rerun boundary is resolved. Every delivery, publishing, deployment, and PV-provisioning guard path now receives `forgejo.run_attempt`, and `protected()` rejects anything except initial attempt `1` before making an API call. Both reusable `workflow_call` and direct `workflow_dispatch` attempt-2 probes fail closed. Deleting the production attempt guard causes the focused regression suite to fail. Verification at `4823a629f2e8f10ddf309b6213f47bf7d3e7e181`: 53 unit tests, 16 scaffold cases, Kubernetes negative-integration fixtures, Python compilation, and diff checks pass; all four hosted contexts are green; the worktree is clean. Fresh security artifact 70 reports `passed: true`, zero blockers, zero scanner errors, five findings, and five exact mitigations. The two refreshed `pipeline/delivery.py` fingerprints match the live decision while retaining their prior controls and expiry. Live, fetched, local, and payload SHAs matched before submission.
gitchegumi deleted branch kiyi/verify-parent-dispatch 2026-09-06 00:55:28 -05:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Homelab/workflows!21
No description provided.