//! Integration tests for `relicario-server verify-org-commit` privilege gating. //! //! H-C1: only an Owner may introduce or elevate an owner/admin. An Admin who //! writes members.json must not be able to mint owners/admins. use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use assert_cmd::Command as AssertCommand; use predicates::prelude::*; use relicario_core::device::generate_keypair; use tempfile::TempDir; fn write_keypair(dir: &Path, name: &str) -> (PathBuf, String) { let (priv_pem, pub_line) = generate_keypair().expect("generate keypair"); let priv_path = dir.join(format!("{name}.key")); fs::write(&priv_path, priv_pem.as_str()).unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(&priv_path, fs::Permissions::from_mode(0o600)).unwrap(); } (priv_path, pub_line) } fn git(repo: &Path, args: &[&str]) { let status = Command::new("git").current_dir(repo).args(args).status().unwrap(); assert!(status.success(), "git {args:?} failed"); } /// members.json content with two members; `member_id`s are fixed 16-hex. fn members_json(owner_pub: &str, admin_pub: &str, admin_role: &str) -> String { format!( r#"{{ "schema_version": 1, "members": [ {{ "member_id": "1111111111111111", "display_name": "Owner", "role": "owner", "ed25519_pubkey": "{}", "collections": [], "added_at": 0, "added_by": "1111111111111111" }}, {{ "member_id": "2222222222222222", "display_name": "Admin", "role": "{admin_role}", "ed25519_pubkey": "{}", "collections": [], "added_at": 0, "added_by": "1111111111111111" }} ] }}"#, owner_pub.trim(), admin_pub.trim() ) } /// Stage members.json, sign the commit with `signing_key`, return its SHA. fn signed_members_commit( repo: &Path, signing_key: &Path, allowed: &Path, msg: &str, content: &str, ) -> String { fs::write(repo.join("members.json"), content).unwrap(); git(repo, &["add", "members.json"]); let status = Command::new("git") .current_dir(repo) .args([ "-c", "gpg.format=ssh", "-c", &format!("user.signingkey={}", signing_key.display()), "-c", &format!("gpg.ssh.allowedSignersFile={}", allowed.display()), "commit", "-S", "-q", "-m", msg, ]) .status() .unwrap(); assert!(status.success()); let out = Command::new("git").current_dir(repo).args(["rev-parse", "HEAD"]).output().unwrap(); String::from_utf8(out.stdout).unwrap().trim().to_string() } /// Set up an org repo whose root commit (signed by the owner) registers an /// owner + an admin. Returns (repo tmp, owner priv, admin priv, allowed file). fn bootstrap() -> (TempDir, PathBuf, PathBuf, PathBuf) { let tmp = TempDir::new().unwrap(); let repo = tmp.path(); git(repo, &["init", "-q", "-b", "main"]); git(repo, &["config", "user.email", "t@t"]); git(repo, &["config", "user.name", "t"]); let (owner_priv, owner_pub) = write_keypair(repo, "owner"); let (admin_priv, admin_pub) = write_keypair(repo, "admin"); let allowed = repo.join("allowed_signers"); fs::write( &allowed, format!("relicario {}\nrelicario {}\n", owner_pub.trim(), admin_pub.trim()), ) .unwrap(); // Genesis: owner registers both members (admin starts as `admin`). let genesis = members_json(&owner_pub, &admin_pub, "admin"); signed_members_commit(repo, &owner_priv, &allowed, "org-init", &genesis); // also write org.json + collections.json so later commits are well-formed fs::write(repo.join("org.json"), r#"{"schema_version":1,"org_id":"abc0abc0abc0abc0","display_name":"Acme","created_at":0}"#).unwrap(); fs::write(repo.join("collections.json"), r#"{"schema_version":1,"collections":[]}"#).unwrap(); git(repo, &["add", "org.json", "collections.json"]); // sign this housekeeping commit with the owner too let _ = signed_members_commit(repo, &owner_priv, &allowed, "scaffold", &members_json(&owner_pub, &admin_pub, "admin")); (tmp, owner_priv, admin_priv, allowed) } #[test] fn admin_self_promote_to_owner_is_rejected() { let (tmp, owner_priv, admin_priv, allowed) = bootstrap(); let repo = tmp.path(); let owner_pub = fs::read_to_string(repo.join("allowed_signers")).unwrap(); // Reconstruct pubkeys from the allowed_signers file (two "relicario " lines). let lines: Vec = owner_pub.lines() .map(|l| l.trim_start_matches("relicario ").to_string()).collect(); let (op, ap) = (lines[0].clone(), lines[1].clone()); let _ = owner_priv; // Admin signs a members.json that elevates THEMSELVES to owner. let escalated = members_json(&op, &ap, "owner"); let sha = signed_members_commit(repo, &admin_priv, &allowed, "self-promote", &escalated); AssertCommand::cargo_bin("relicario-server") .unwrap() .current_dir(repo) .args(["verify-org-commit", &sha]) .assert() .failure() .stderr(predicate::str::contains("only an owner")); } #[test] fn owner_promoting_an_admin_is_accepted() { let (tmp, owner_priv, _admin_priv, allowed) = bootstrap(); let repo = tmp.path(); let allowed_body = fs::read_to_string(repo.join("allowed_signers")).unwrap(); let lines: Vec = allowed_body.lines() .map(|l| l.trim_start_matches("relicario ").to_string()).collect(); let (op, ap) = (lines[0].clone(), lines[1].clone()); // Owner signs a members.json that elevates the admin to owner — allowed. let promoted = members_json(&op, &ap, "owner"); let sha = signed_members_commit(repo, &owner_priv, &allowed, "promote-admin", &promoted); AssertCommand::cargo_bin("relicario-server") .unwrap() .current_dir(repo) .args(["verify-org-commit", &sha]) .assert() .success(); }