OXYGENHUB

Coverage / src/cli/identity.rs 54.9%

79 of 144 lines run, 12 of 40 functions · run #7 on main · run missed

1
//! `spectra init` and `spectra identity`: the owner and the instance
2
//! (spec §8.3, §9.5). The owner proves possession of their identity by
3
//! signing with it; Spectra never holds the owner's secret key.
4
5
use super::{ui, Ctx};
6
use anyhow::{bail, Context, Result};
7
use oxy_core::identity::Identity;
8
use spectra_core::keys::KeyPair;
9
use spectra_core::ledger::Ledger;
10
use spectra_core::principals::{identity_payload, signed_event, Principals};
11
use std::path::{Path, PathBuf};
12
13
/// The owner's `OXY_HOME`: `--owner-home`, else `[owner] home` in
14
/// spectra.toml, else oxy's own default.
1515×
pub fn owner_home(ctx: &Ctx, flag: Option<PathBuf>) -> PathBuf {
1615×
    flag.or_else(|| ctx.config_owner_home()).unwrap_or_else(oxy_core::repo::default_home)
1715×
}
18
197×
pub async fn init(ctx: &Ctx, owner_home_flag: Option<PathBuf>, name: Option<String>) -> Result<i32> {
207×
    let data = &ctx.data;
217×
    let db = data.join("spectra.db");
227×
    if db.exists() {
231×
        bail!("{} already holds a Spectra (spectra identity show)", data.display());
246×
    }
256×
    let home = owner_home(ctx, owner_home_flag);
266×
    let owner = KeyPair::load_home(&home).with_context(|| {
271×
        format!("the owner signs with their own identity: none usable in {} (oxy identity create, or --owner-home)", home.display())
281×
    })?;
295×
    std::fs::create_dir_all(data)?;
30
    #[cfg(unix)]
31
    {
32
        use std::os::unix::fs::PermissionsExt;
335×
        std::fs::set_permissions(data, std::fs::Permissions::from_mode(0o700))?;
34
    }
355×
    let name = name.unwrap_or_else(hostname);
365×
    let instance = KeyPair::instance(data, &name)?;
375×
    let ledger = Ledger::open(&db, Some(instance.clone())).await?;
385×
    let mut first = vec![
395×
        signed_event(&owner, "human", "identity.owner", identity_payload(&owner.identity)),
405×
        signed_event(&instance, "system", "identity.instance", identity_payload(&instance.identity)),
41
    ];
425×
    first.extend(spectra_core::conversation::default_channel_events());
435×
    ledger.append(first).await?;
445×
    ledger.checkpoint().await?;
455×
    let home_s = home.display().to_string();
465×
    std::fs::write(
475×
        data.join("spectra.toml"),
485×
        format!("# Spectra's configuration (spec §22.1).\n\n[owner]\nkey = \"{}\"\nhome = {}\n", owner.key.to_hex(), toml_str(&home_s)),
490×
    )?;
505×
    println!("{} {}  {}", ui::done("Initialised"), ui::subject("Spectra"), ui::path(data.display()));
515×
    println!("{}", ui::kv("owner", format!("{}{}{}", ui::author(&owner.identity.name), ui::sep(), ui::hash(owner.key.short(12)))));
525×
    println!("{}", ui::kv("instance", format!("{}{}{}", name, ui::sep(), ui::hash(instance.key.short(12)))));
535×
    println!("{}", ui::kv("ledger", format!("{}{}", ui::path(db.display()), ui::dim(" (verified with spectra admin verify)"))));
545×
    println!();
555×
    println!("  {} {}", ui::note(), ui::dim("so that checkpoints are verifiably from your machine, delegate to the instance key:"));
565×
    println!(
57
        "    {}",
585×
        ui::id(format!("oxy identity delegate add {} --scope changes", data.join("keys").join("instance.identity").display()))
59
    );
605×
    println!(
61
        "  {} {} {}",
625×
        ui::note(),
635×
        ui::dim("then add agents with"),
645×
        ui::id("spectra agent add NAME --kind KIND --project PROJECT --home DIR")
65
    );
665×
    Ok(0)
677×
}
68
690×
pub async fn show(ctx: &Ctx, json: bool) -> Result<i32> {
700×
    let ledger = Ledger::open_read_only(&ctx.db()?).await?;
710×
    let p = Principals::load(&ledger).await?;
720×
    let owner = p.owner().context("no owner is recorded")?;
730×
    let instance = p.instance();
740×
    let delegated =
750×
        instance.is_some_and(|i| owner.delegates_to(&i.hash(), spectra_core::principals::DELEGATION_SCOPE, spectra_core::ids::now_ms()));
760×
    if json {
770×
        println!(
78
            "{}",
790×
            serde_json::json!({
800×
                "owner": { "name": owner.name, "key": owner.hash().to_hex() },
810×
                "instance": instance.map(|i| serde_json::json!({ "name": i.name, "key": i.hash().to_hex(), "delegated": delegated })),
82
            })
83
        );
840×
        return Ok(0);
850×
    }
860×
    println!("{}", ui::section("Owner"));
870×
    println!("{}", ui::kv("name", ui::author(format!("{} <{}>", owner.name, owner.email))));
880×
    println!("{}", ui::kv("key", ui::hash(owner.hash().to_hex())));
890×
    println!("{}", ui::kv("delegates", owner.delegates.len()));
900×
    if let Some(i) = instance {
910×
        println!("{}", ui::section("Instance"));
920×
        println!("{}", ui::kv("name", &i.name));
930×
        println!("{}", ui::kv("key", ui::hash(i.hash().to_hex())));
940×
        if delegated {
950×
            println!("{}", ui::kv("status", ui::ok("delegated by the owner: checkpoints are verifiably yours")));
960×
        } else {
970×
            println!("{}", ui::kv("status", ui::warn("not delegated by the owner yet")));
980×
            println!(
990×
                "  {} {}",
1000×
                ui::note(),
1010×
                ui::id(format!("oxy identity delegate add {} --scope changes", ctx.data.join("keys").join("instance.identity").display()))
1020×
            );
1030×
        }
1040×
    }
1050×
    Ok(0)
1060×
}
107
108
/// Record the owner's current identity when it has moved on (a delegation
109
/// added or removed): `spectra identity refresh`.
1100×
pub async fn refresh(ctx: &Ctx, owner_home_flag: Option<PathBuf>) -> Result<i32> {
1110×
    let ledger = ctx.writer().await?;
1120×
    let outcome = refresh_owner(&ledger, &owner_home(ctx, owner_home_flag)).await?;
1130×
    ledger.checkpoint().await?;
1140×
    match outcome {
1150×
        Some(id) => println!(
116
            "{} {}{}{}",
1170×
            ui::done("Recorded"),
1180×
            ui::subject("the owner's current identity"),
1190×
            ui::sep(),
1200×
            ui::hash(id.hash().short(12))
121
        ),
1220×
        None => println!("{} {}", ui::done("Current"), ui::subject("the owner's identity has not moved")),
123
    }
1240×
    Ok(0)
1250×
}
126
127
/// If the owner's home names a successor of the recorded owner, record it,
128
/// signed with it, with the identities between. Returns the new identity.
1298×
pub async fn refresh_owner(ledger: &Ledger, home: &Path) -> Result<Option<Identity>> {
1308×
    let p = Principals::load(ledger).await?;
1318×
    let recorded = p.owner().context("no owner is recorded (spectra init)")?.clone();
1328×
    let pair = KeyPair::load_home(home)?;
1338×
    if pair.key == recorded.hash() {
1347×
        return Ok(None);
1351×
    }
1361×
    let mut chain = Vec::new();
1371×
    let mut at = pair.identity.clone();
1381×
    while let Some(prev) = at.previous {
1391×
        if prev == recorded.hash() {
1401×
            break;
1410×
        }
1420×
        let file = home.join("keys").join(format!("{}.identity", prev.to_hex()));
1430×
        at = Identity::decode(
1440×
            &std::fs::read(&file).with_context(|| format!("{} is not a successor of the recorded owner", pair.identity.name))?,
145
        )
1460×
        .map_err(|e| anyhow::anyhow!("{}: {e}", file.display()))?;
1470×
        chain.push(hex::encode(at.encode()));
1480×
        if chain.len() > 1000 {
1490×
            bail!("the owner's identity chain is too long to follow");
1500×
        }
151
    }
1521×
    if at.previous != Some(recorded.hash()) && pair.identity.previous != Some(recorded.hash()) {
1530×
        bail!("{} in {} is not a successor of the recorded owner {}", pair.identity.name, home.display(), recorded.hash().short(12));
1541×
    }
1551×
    let mut payload = identity_payload(&pair.identity);
1561×
    payload["chain"] = chain.into();
1571×
    ledger.append_one(signed_event(&pair, "human", "identity.rotated", payload)).await?;
1581×
    Ok(Some(pair.identity))
1598×
}
160
1614×
fn hostname() -> String {
1624×
    std::fs::read_to_string("/etc/hostname")
1634×
        .ok()
1644×
        .map(|h| h.trim().to_string())
1654×
        .filter(|h| !h.is_empty())
1664×
        .unwrap_or_else(|| "spectra".into())
1674×
}
168
1695×
fn toml_str(s: &str) -> String {
1705×
    serde_json::to_string(s).unwrap_or_default()
1715×
}