Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-c0re/src/forge/mod.rs

141 lines
5.8 KiB
Rust

//! The hive's forge access: reads of the forge go through [`reader_token`] on
//! every hive. The
//! swarm-wide orgs, teams and repos, and each agent's read grant on
//! `internal/docs`, are swarm-controller's. Full design:
//! `docs/integrations/forge.md`.
mod reader;
pub use reader::reader_token;
use std::sync::OnceLock;
use anyhow::{Context, Result};
use url::Url;
const FORGE_CONTAINER: &str = "hive-forge";
/// Base HTTP URL for the local Forgejo instance, from `HIVE_FORGE_URL`
/// (set unconditionally by the hyperhive NixOS module on the `hive-c0re`
/// unit, to `http://<forge.domain>`).
///
/// # Panics
///
/// When `HIVE_FORGE_URL` is unset (`validate_env` rejects that at startup, so
/// the daemon never reaches this). That is deliberate: this daemon only
/// runs under the NixOS module, which always sets it, so an unset var
/// means the deployment is broken. There is no loopback fallback,
/// because a guess is wrong in exactly the cases that matter — the
/// forge may live on a different host from the daemon, and a fallback
/// turns "misconfigured" into "silently talking to the wrong machine"
/// or, worse, "connection refused" surfacing far from its cause.
pub(crate) fn forge_http_base() -> &'static str {
static BASE: OnceLock<String> = OnceLock::new();
BASE.get_or_init(|| {
std::env::var("HIVE_FORGE_URL").expect(
"HIVE_FORGE_URL is unset — the hyperhive NixOS module sets it \
unconditionally on the hive-c0re unit, so this process was \
started outside that unit",
)
})
}
/// Check the env the forge integration cannot run without: `HIVE_FORGE_URL`
/// set and a valid URL. Called once from `main` before any worker is
/// spawned, so a missing or malformed value fails the unit start naming the
/// variable; the lazy accessors then never hit their `expect`s.
pub(crate) fn validate_env() -> Result<()> {
let url = std::env::var("HIVE_FORGE_URL").context(
"HIVE_FORGE_URL is unset (the hyperhive NixOS module sets it on the hive-c0re unit)",
)?;
Url::parse(&url).with_context(|| format!("HIVE_FORGE_URL is not a valid URL: {url:?}"))?;
Ok(())
}
/// Git remote for `repo` (e.g. `"hyperhive/hyperhive"`) — **credential-free**.
///
/// The token does not go here. A URL is a process argument, and `argv` is
/// world-readable through `/proc/<pid>/cmdline` for as long as the git child
/// lives, so a credentialed remote publishes the forge token to every
/// local user on the host. Credentials travel in the environment instead, via
/// [`basic_auth_header`] and [`crate::lifecycle::git_command_authed`] —
/// `/proc/<pid>/environ` is owner-only.
pub(crate) fn forge_git_url(repo: &str) -> String {
git_url_with_base(forge_http_base(), repo)
}
/// The pure half of [`forge_git_url`], split out so it can be tested without a
/// process-wide env var (which would race every other test in this binary).
fn git_url_with_base(base: &str, repo: &str) -> String {
format!("{base}/{repo}.git")
}
/// The `http.extraHeader` value carrying `token`. Forgejo takes the account
/// from the token, so `login` only labels the request.
///
/// Basic auth over a header rather than userinfo in the URL, so the secret
/// reaches git through the environment (see [`forge_git_url`]). Pair with
/// [`crate::lifecycle::git_command_authed`], which is the only thing that
/// should ever hold the result.
fn basic_auth_header(login: &str, token: &str) -> String {
use base64::Engine as _;
let basic = base64::engine::general_purpose::STANDARD.encode(format!("{login}:{token}"));
format!("Authorization: Basic {basic}")
}
/// Probe whether `hive-forge` exists as a nixos-container. Cheap —
/// `nixos-container list` is just a directory scan in /etc. Routed
/// through hive-priv: `nixos-container` needs root, and hive-c0re runs
/// unprivileged (privsep).
pub async fn is_present() -> bool {
let Ok(stdout) = crate::priv_client::list_containers().await else {
return false;
};
stdout.lines().any(|l| l.trim() == FORGE_CONTAINER)
}
#[cfg(test)]
mod tests {
use super::git_url_with_base;
/// The remote carries **no credential**. `argv` is world-readable through
/// `/proc/<pid>/cmdline`, so a token spliced in here would be published to
/// every local user for the life of the git child.
///
/// Deliberately not via `forge_git_url`, which reads `HIVE_FORGE_URL` —
/// setting that here would race every other test in this binary.
#[test]
fn forge_git_url_carries_no_credential() {
let url = git_url_with_base("http://forge.example.test", "a/iris");
assert_eq!(url, "http://forge.example.test/a/iris.git");
assert!(!url.contains('@'), "no userinfo: {url}");
}
#[test]
fn forge_git_url_preserves_https() {
// The scheme is carried through rather than assumed: a swarm
// whose forge is behind TLS must not be downgraded to http.
let url = git_url_with_base("https://forge.example.test", "a/iris");
assert!(url.starts_with("https://"), "https must survive: {url}");
}
/// The credential goes in an `Authorization` header instead — decodable
/// back to `<login>:<token>`, so the swap is genuinely equivalent auth and
/// not a silent downgrade to anonymous.
#[test]
fn basic_auth_header_is_basic_login_token() {
use base64::Engine as _;
let header = super::basic_auth_header("hive-alpha", "s3cret");
let b64 = header
.strip_prefix("Authorization: Basic ")
.expect("basic auth header");
let decoded = base64::engine::general_purpose::STANDARD
.decode(b64)
.expect("valid base64");
assert_eq!(String::from_utf8_lossy(&decoded), "hive-alpha:s3cret");
assert!(
!header.contains("s3cret"),
"token not in the clear: {header}"
);
}
}