| 1 | //! Keeping the pre-receive hook installed (spec §4, §9). | |
| 2 | //! | |
| 3 | //! > Hooks are the compiled `dogfood-hook` binary, installed into each repo's | |
| 4 | //! > `hooks/` directory at creation. | |
| 5 | //! | |
| 6 | //! Installing at creation is necessary but not sufficient. A repository created | |
| 7 | //! before the hook existed, restored from a backup taken before it, or created | |
| 8 | //! by a code path that forgot the call, has no hook — and a *missing* hook fails | |
| 9 | //! open. Nothing errors; pushes simply stop being validated. Protected bookmarks | |
| 10 | //! silently become unprotected and the ref-name allowlist silently stops | |
| 11 | //! applying. | |
| 12 | //! | |
| 13 | //! That is exactly the shape of bug this sweep exists to make impossible: on | |
| 14 | //! every boot, every repository gets the hook reinstalled. The write is | |
| 15 | //! idempotent and costs one small file per repository, which is nothing next to | |
| 16 | //! a validation control that is off without anybody noticing. | |
| 17 | ||
| 18 | use std::sync::Arc; | |
| 19 | ||
| 20 | use df_db::PgPool; | |
| 21 | use df_store::{RepoId, RepoStore}; | |
| 22 | use uuid::Uuid; | |
| 23 | ||
| 24 | /// Install the hook into one repository. | |
| 25 | /// | |
| 26 | /// Called at creation, and by the boot sweep. Errors are the caller's to | |
| 27 | /// report: at creation a failure should be loud, during the sweep it should not | |
| 28 | /// stop the process from starting. | |
| 29 | pub async fn install(store: &dyn RepoStore, repo_id: Uuid, hook_binary: &str) -> anyhow::Result<()> { | |
| 30 | store | |
| 31 | .configure_receive_validation(RepoId(repo_id), hook_binary) | |
| 32 | .await | |
| 33 | .map_err(|e| anyhow::anyhow!("installing the pre-receive hook: {e}")) | |
| 34 | } | |
| 35 | ||
| 36 | /// Reinstall the hook on every repository, at startup. | |
| 37 | /// | |
| 38 | /// Runs in the background so a slow filesystem cannot delay serving, and never | |
| 39 | /// fails the process — an instance that cannot install hooks should still come | |
| 40 | /// up, loudly complaining, rather than refuse to start. | |
| 41 | pub fn sweep_in_background(db: PgPool, store: Arc<dyn RepoStore>, hook_binary: String) { | |
| 42 | tokio::spawn(async move { | |
| 43 | let repos: Vec<(Uuid,)> = match sqlx::query_as("SELECT id FROM repos").fetch_all(&db).await { | |
| 44 | Ok(r) => r, | |
| 45 | Err(e) => { | |
| 46 | tracing::error!("could not list repositories to install hooks: {e}"); | |
| 47 | return; | |
| 48 | } | |
| 49 | }; | |
| 50 | ||
| 51 | let total = repos.len(); | |
| 52 | let mut failed = 0usize; | |
| 53 | ||
| 54 | for (id,) in repos { | |
| 55 | // A repository row whose storage is missing is a real state after a | |
| 56 | // partial restore (spec §10). It is reported by the reindex path, | |
| 57 | // not here, so this only counts hook failures. | |
| 58 | if !store.exists(RepoId(id)).await { | |
| 59 | continue; | |
| 60 | } | |
| 61 | if let Err(e) = install(store.as_ref(), id, &hook_binary).await { | |
| 62 | failed += 1; | |
| 63 | tracing::error!(repo = %id, "{e:#}"); | |
| 64 | } | |
| 65 | } | |
| 66 | ||
| 67 | if failed > 0 { | |
| 68 | tracing::error!( | |
| 69 | total, | |
| 70 | failed, | |
| 71 | "some repositories are accepting pushes WITHOUT validation" | |
| 72 | ); | |
| 73 | } else { | |
| 74 | tracing::info!(total, "pre-receive hooks verified on every repository"); | |
| 75 | } | |
| 76 | }); | |
| 77 | } |
77 lines · Rust