fix(#2911): keep the forge token out of argv
`forge_git_url` spliced `core:<token>@` between scheme and authority, and that URL is a process argument. `/proc/<pid>/cmdline` is mode 0444 — world-readable — so the core admin token, which provisions every agent's forge account, was published to any local user for the lifetime of each git child. Seven call sites built such a URL. The credential now travels in the environment instead: `git_command_authed` sets `http.extraHeader` via `GIT_CONFIG_*`, which git reads exactly like a config file, and `/proc/<pid>/environ` is 0400 — owner-only. Same credential, materially smaller audience. The remote is a plain `http://forge/<org>/<repo>.git`, and `forge_git_url` no longer takes a token, so the old shape cannot be rebuilt by accident. `knowledge`'s clone was the one place a credentialed URL was stored as a named remote — git persists the clone URL into `.git/config`, so the token sat on disk and every later `pull` authenticated from there. That is the case `forge::repos::push_config` documents as forbidden ("the tokenised URL ... deliberately never stored as a named remote"). `pull` now rewrites `origin` to the plain URL first, which also scrubs the persisted token from existing deployments, and authenticates from the environment when a token is available. The repo is public, so the pull still works without one. Three call sites also stopped spawning `Command::new("git")` directly, so they honour the `HYPERHIVE_GIT` path the NixOS module bakes in and the `kill_on_drop` every other git spawn gets. The two URL-shape tests now assert the *absence* of a credential, and a new one decodes the header back to `core:<token>` — without that, a malformed header would leave every forge operation silently anonymous with the other assertions still green.
This commit is contained in:
parent
3617578341
commit
44572d1e1a
7 changed files with 134 additions and 56 deletions
|
|
@ -16,13 +16,12 @@ use forgejo_api::structs::{
|
|||
};
|
||||
use forgejo_api::{ApiErrorKind, ForgejoError};
|
||||
use reqwest::StatusCode;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
use super::{
|
||||
AGENTS_ORG, CONFIG_ORG, KNOWLEDGE_REPO, OPERATORS_TEAM, SHARED_DOCS_REPO, SHARED_ORG, api,
|
||||
core_token, forge_git_url, forge_http_base, is_present,
|
||||
core_auth_header, core_token, forge_git_url, forge_http_base, is_present,
|
||||
};
|
||||
|
||||
/// Creation options for an empty repo defaulting to `main`.
|
||||
|
|
@ -239,8 +238,8 @@ pub async fn push_meta(dir: &Path) -> Result<()> {
|
|||
// raise a dashboard warning and leave the remote intact rather than
|
||||
// silently erasing its history — consistent with `push_config`'s
|
||||
// non-fast-forward handling.
|
||||
let url = forge_git_url(&token, "core/meta");
|
||||
let out = Command::new("git")
|
||||
let url = forge_git_url("core/meta");
|
||||
let out = crate::lifecycle::git_command_authed(&core_auth_header(&token))
|
||||
.current_dir(dir)
|
||||
.args(["push", &url, "HEAD:main"])
|
||||
.output()
|
||||
|
|
@ -482,9 +481,10 @@ pub async fn push_config(name: &str) -> Result<()> {
|
|||
if !dir.join(".git").exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let url = forge_git_url(&token, &format!("{CONFIG_ORG}/{name}"));
|
||||
let url = forge_git_url(&format!("{CONFIG_ORG}/{name}"));
|
||||
let auth = core_auth_header(&token);
|
||||
// Tags first, in their own push, so they land regardless of main's fate.
|
||||
let out = run_config_push(&dir, &url, "refs/tags/*:refs/tags/*").await?;
|
||||
let out = run_config_push(&dir, &url, &auth, "refs/tags/*:refs/tags/*").await?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"git push tags {CONFIG_ORG}/{name} failed ({}): {}",
|
||||
|
|
@ -495,7 +495,7 @@ pub async fn push_config(name: &str) -> Result<()> {
|
|||
// Then main on its own — a protected-branch / non-ff reject of an
|
||||
// established main is expected (pr_merge owns main); only the initial
|
||||
// empty-repo seed actually advances it here.
|
||||
let out = run_config_push(&dir, &url, "refs/heads/main:refs/heads/main").await?;
|
||||
let out = run_config_push(&dir, &url, &auth, "refs/heads/main:refs/heads/main").await?;
|
||||
if !out.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
if stderr.contains("non-fast-forward")
|
||||
|
|
@ -525,9 +525,10 @@ pub async fn push_config(name: &str) -> Result<()> {
|
|||
async fn run_config_push(
|
||||
dir: &std::path::Path,
|
||||
url: &str,
|
||||
auth: &str,
|
||||
refspec: &str,
|
||||
) -> Result<std::process::Output> {
|
||||
crate::lifecycle::git_command()
|
||||
crate::lifecycle::git_command_authed(auth)
|
||||
.current_dir(dir)
|
||||
.args(["push", url, refspec])
|
||||
.output()
|
||||
|
|
|
|||
Loading…
Reference in a new issue