hive reads the forge for nix as its own account, not core
hive-c0re copies the hive's store read token to /run/hyperhive/forge-hive-token for nix's git credential helper, and no longer mints, reads or falls back to the core admin token. drops the core user sweep, avatar upload, webhook cleanup and hive-priv's RunForgeAdmin.
This commit is contained in:
parent
32a4821cb4
commit
b79896c237
1 changed files with 236 additions and 1118 deletions
|
|
@ -1,16 +1,16 @@
|
|||
//! The credential this hive reads the forge with: its own read-only account,
|
||||
//! whose token `swarm-controller` mints into the store at
|
||||
//! `swarm/hives/<hive>/forge/token`, and the core token file where the store
|
||||
//! holds none.
|
||||
//! `swarm/hives/<hive>/forge/token`.
|
||||
//!
|
||||
//! The file is the fallback so a hive deployed before the controller mints its
|
||||
//! token keeps the reads it had: the forge host holds the file, no other hive
|
||||
//! does.
|
||||
//! nix's `git` cannot reach the store, so [`ReaderToken::publish_for_nix`]
|
||||
//! copies the token to a runtime file that `git-credential-hive-forge`
|
||||
//! (`nix/host-modules/lib/hive-forge-git.nix`) reads.
|
||||
|
||||
use super::{basic_auth_header, core_token};
|
||||
use std::path::Path;
|
||||
|
||||
/// The basic-auth user [`ReaderToken::auth_header`] names for the core token.
|
||||
const CORE_LOGIN: &str = "core";
|
||||
use anyhow::Result;
|
||||
|
||||
use super::basic_auth_header;
|
||||
|
||||
/// A forge token and the account it belongs to.
|
||||
///
|
||||
|
|
@ -39,37 +39,59 @@ impl ReaderToken {
|
|||
pub(crate) fn api(&self) -> anyhow::Result<forgejo_api::Forgejo> {
|
||||
super::api(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
/// The token to read the forge with, or `None` when this hive has neither.
|
||||
///
|
||||
/// 🩸 Logs the store path and which source won, never a value.
|
||||
pub async fn reader_token() -> Option<ReaderToken> {
|
||||
pick(stored_reader_token().await, core_token())
|
||||
}
|
||||
|
||||
/// The store's token wins over the core file's.
|
||||
fn pick(stored: Option<ReaderToken>, core: Option<String>) -> Option<ReaderToken> {
|
||||
let picked = stored.or_else(|| {
|
||||
core.map(|value| ReaderToken {
|
||||
login: CORE_LOGIN.to_owned(),
|
||||
value,
|
||||
})
|
||||
});
|
||||
if let Some(t) = &picked {
|
||||
tracing::debug!(login = %t.login, "forge: reading as this account");
|
||||
} else {
|
||||
tracing::debug!("forge: no read token in the store or the core token file");
|
||||
/// Copy this token to the file nix's `git` authenticates from
|
||||
/// ([`crate::paths::FORGE_HIVE_TOKEN`]). Whether the file now holds it; a
|
||||
/// failed write is logged and leaves the file as it was.
|
||||
pub(crate) fn publish_for_nix(&self) -> bool {
|
||||
let path = Path::new(crate::paths::FORGE_HIVE_TOKEN);
|
||||
match self.write_credential(path) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
tracing::warn!(path = %path.display(), error = ?e, "forge: could not write the nix forge credential");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
picked
|
||||
|
||||
/// Login on the first line, token on the second. Written only when the
|
||||
/// content differs, so a build's resolve is not a disk write.
|
||||
fn write_credential(&self, path: &Path) -> Result<()> {
|
||||
let content = format!("{}\n{}\n", self.login, self.value);
|
||||
if std::fs::read(path).is_ok_and(|have| have == content.as_bytes()) {
|
||||
return Ok(());
|
||||
}
|
||||
crate::secret_file::write_secret_0600(path, content.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// The token to read the forge with, or `None` when the store holds none for
|
||||
/// this hive.
|
||||
///
|
||||
/// 🩸 Logs the store path, never a value.
|
||||
pub async fn reader_token() -> Option<ReaderToken> {
|
||||
let token = stored_reader_token().await;
|
||||
if let Some(t) = &token {
|
||||
tracing::debug!(login = %t.login, "forge: reading as this account");
|
||||
}
|
||||
token
|
||||
}
|
||||
|
||||
/// Publish this hive's read token for nix; see [`ReaderToken::publish_for_nix`].
|
||||
/// Whether the file holds a token.
|
||||
pub async fn publish_nix_credential() -> bool {
|
||||
let Some(token) = reader_token().await else {
|
||||
tracing::debug!("forge: no hive read token in the store; nix has no forge credential");
|
||||
return false;
|
||||
};
|
||||
token.publish_for_nix()
|
||||
}
|
||||
|
||||
/// This hive's token from the store, under the hive's own identity.
|
||||
///
|
||||
/// `None`, never an error, for every way it can come up empty — no hive name,
|
||||
/// no `BAO_*` identity, an unreachable store, nothing at the path — the same
|
||||
/// as `matrix::stored_sender_token`, and for the same reason: each sends the
|
||||
/// caller to the fallback.
|
||||
/// as `matrix::stored_sender_token`.
|
||||
async fn stored_reader_token() -> Option<ReaderToken> {
|
||||
let hive = std::env::var("HYPERHIVE_HIVE_NAME")
|
||||
.ok()
|
||||
|
|
@ -110,6 +132,7 @@ async fn stored_reader_token() -> Option<ReaderToken> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
|
||||
fn stored() -> ReaderToken {
|
||||
ReaderToken {
|
||||
|
|
@ -118,23 +141,71 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_store_wins_over_the_core_file() {
|
||||
let t = pick(Some(stored()), Some("from-the-file".to_owned())).expect("a token");
|
||||
assert_eq!(t.value, "from-the-store");
|
||||
assert_eq!(t.login, "hive-alpha");
|
||||
fn mode(path: &Path) -> u32 {
|
||||
std::fs::metadata(path).expect("stat").permissions().mode() & 0o777
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_core_file_is_the_fallback() {
|
||||
let t = pick(None, Some("from-the-file".to_owned())).expect("a token");
|
||||
assert_eq!(t.value, "from-the-file");
|
||||
assert_eq!(t.login, CORE_LOGIN);
|
||||
fn the_nix_credential_is_the_hives_login_then_its_token_at_0600() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("forge-hive-token");
|
||||
stored().write_credential(&path).expect("written");
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&path).expect("read"),
|
||||
"hive-alpha\nfrom-the-store\n"
|
||||
);
|
||||
assert_eq!(mode(&path), 0o600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neither_is_none() {
|
||||
assert!(pick(None, None).is_none());
|
||||
fn a_rotated_token_replaces_the_nix_credential() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("forge-hive-token");
|
||||
stored().write_credential(&path).expect("first");
|
||||
let rotated = ReaderToken {
|
||||
login: "hive-alpha".to_owned(),
|
||||
value: "rotated".to_owned(),
|
||||
};
|
||||
rotated.write_credential(&path).expect("second");
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&path).expect("read"),
|
||||
"hive-alpha\nrotated\n"
|
||||
);
|
||||
assert_eq!(mode(&path), 0o600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unchanged_credential_is_not_rewritten() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("forge-hive-token");
|
||||
stored().write_credential(&path).expect("first");
|
||||
// A rewrite goes through a sibling temp file and a rename, which
|
||||
// gives the file a new inode.
|
||||
let before = std::fs::metadata(&path).expect("stat");
|
||||
stored().write_credential(&path).expect("second");
|
||||
let after = std::fs::metadata(&path).expect("stat");
|
||||
assert_eq!(
|
||||
std::os::unix::fs::MetadataExt::ino(&before),
|
||||
std::os::unix::fs::MetadataExt::ino(&after)
|
||||
);
|
||||
}
|
||||
|
||||
/// `hive-forge-git.nix`'s credential helper names this path as a literal.
|
||||
#[test]
|
||||
fn the_nix_credential_lives_on_the_runtime_tmpfs_where_the_helper_reads_it() {
|
||||
assert_eq!(
|
||||
crate::paths::FORGE_HIVE_TOKEN,
|
||||
"/run/hyperhive/forge-hive-token"
|
||||
);
|
||||
assert!(crate::paths::FORGE_HIVE_TOKEN.starts_with(crate::paths::RUNTIME_ROOT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_that_cannot_be_written_is_an_error_not_a_panic() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let file = dir.path().join("a-file");
|
||||
std::fs::write(&file, "").expect("file");
|
||||
assert!(stored().write_credential(&file.join("sub")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue