feat(cli/org): wire Commands::Org admin subcommands + parse_org_role + transfer-ownership/delete-org
This commit is contained in:
@@ -441,6 +441,73 @@ pub fn run_rotate_key(dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_transfer_ownership(dir: &Path, member_id_prefix: &str, keep_owner: bool) -> Result<()> {
|
||||
let vault = crate::org_session::open_org_vault(Some(dir))?;
|
||||
let caller = vault.current_member()?;
|
||||
if !caller.role.can_manage_owners() {
|
||||
anyhow::bail!("only an owner can transfer ownership");
|
||||
}
|
||||
let mut members = vault.load_members()?;
|
||||
let target_id = resolve_member_id(&members, member_id_prefix)?;
|
||||
if target_id == caller.member_id {
|
||||
anyhow::bail!("you are already the owner");
|
||||
}
|
||||
// Promote the target to Owner.
|
||||
{
|
||||
let target = members.find_by_id_mut(&target_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("member not found"))?;
|
||||
target.role = OrgRole::Owner;
|
||||
}
|
||||
// Real transfer: also demote the CALLER to Admin, unless --keep-owner was
|
||||
// passed (explicit co-ownership). The spec says "owner → another member",
|
||||
// so demotion is the default.
|
||||
if !keep_owner {
|
||||
if let Some(me) = members.find_by_id_mut(&caller.member_id) {
|
||||
me.role = OrgRole::Admin;
|
||||
}
|
||||
}
|
||||
vault.save_members(&members)?;
|
||||
|
||||
let mode = if keep_owner { "co-ownership (caller kept owner)" } else { "caller demoted to admin" };
|
||||
let commit_msg = format!(
|
||||
"org: transfer ownership to {} ({mode})\n\nRelicario-Actor: {} {}\nRelicario-Action: ownership-transfer\nRelicario-Member: {}",
|
||||
target_id.as_str(), caller.display_name, caller.member_id.as_str(), target_id.as_str()
|
||||
);
|
||||
crate::org_session::org_git_run(&vault.root, &["add", "members.json"], "git add")?;
|
||||
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "git commit")?;
|
||||
if keep_owner {
|
||||
println!("Ownership shared with {} (you remain an owner).", target_id.as_str());
|
||||
} else {
|
||||
println!("Ownership transferred to {} (you are now an admin).", target_id.as_str());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_delete_org(dir: &Path, confirm: bool) -> Result<()> {
|
||||
let vault = crate::org_session::open_org_vault(Some(dir))?;
|
||||
let caller = vault.current_member()?;
|
||||
if !caller.role.can_manage_owners() {
|
||||
anyhow::bail!("only an owner can delete the org");
|
||||
}
|
||||
if !confirm {
|
||||
anyhow::bail!("refusing to delete org without --confirm");
|
||||
}
|
||||
let commit_msg = format!(
|
||||
"org: delete org\n\nRelicario-Actor: {} {}\nRelicario-Action: org-delete",
|
||||
caller.display_name, caller.member_id.as_str()
|
||||
);
|
||||
// Remove org files (the git history is retained as the audit record).
|
||||
for f in ["org.json", "members.json", "collections.json", "manifest.enc"] {
|
||||
let _ = fs::remove_file(vault.root.join(f));
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(vault.root.join("items"));
|
||||
let _ = std::fs::remove_dir_all(vault.root.join("keys"));
|
||||
crate::org_session::org_git_run(&vault.root, &["add", "-A"], "git add")?;
|
||||
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "git commit")?;
|
||||
println!("Org deleted (git history retained as audit record).");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_status(dir: &Path) -> Result<()> {
|
||||
let root = crate::org_session::org_dir(Some(dir))?;
|
||||
|
||||
|
||||
@@ -438,7 +438,77 @@ pub(crate) enum OrgCommands {
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
},
|
||||
// Admin + item subcommands are added by later tasks (B10-B14).
|
||||
/// Add a member to the org.
|
||||
AddMember {
|
||||
/// OpenSSH ed25519 public key of the new member.
|
||||
#[arg(long)]
|
||||
key: String,
|
||||
/// Display name.
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
/// Role: owner, admin, or member.
|
||||
#[arg(long, default_value = "member")]
|
||||
role: String,
|
||||
},
|
||||
/// Remove a member from the org.
|
||||
RemoveMember {
|
||||
/// Member ID prefix.
|
||||
member_id: String,
|
||||
},
|
||||
/// Change a member's role.
|
||||
SetRole {
|
||||
member_id: String,
|
||||
role: String,
|
||||
},
|
||||
/// Create a collection.
|
||||
CreateCollection {
|
||||
slug: String,
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
},
|
||||
/// Grant a member access to a collection.
|
||||
Grant {
|
||||
member_id: String,
|
||||
collection: String,
|
||||
},
|
||||
/// Revoke a member's access to a collection.
|
||||
Revoke {
|
||||
member_id: String,
|
||||
collection: String,
|
||||
},
|
||||
/// Rotate the org master key (run after removing a member).
|
||||
RotateKey,
|
||||
/// Transfer ownership to another member (owner only). By default the caller
|
||||
/// is demoted to admin; pass --keep-owner for explicit co-ownership.
|
||||
TransferOwnership {
|
||||
member_id: String,
|
||||
/// Keep the caller as an owner too (co-ownership) instead of demoting.
|
||||
#[arg(long)]
|
||||
keep_owner: bool,
|
||||
},
|
||||
/// Delete the org (owner only; requires --confirm).
|
||||
DeleteOrg {
|
||||
#[arg(long)]
|
||||
confirm: bool,
|
||||
},
|
||||
/// Show org members and collections.
|
||||
Status,
|
||||
/// Query the org audit log.
|
||||
Audit {
|
||||
#[arg(long)]
|
||||
since: Option<String>,
|
||||
#[arg(long)]
|
||||
member: Option<String>,
|
||||
#[arg(long)]
|
||||
collection: Option<String>,
|
||||
#[arg(long)]
|
||||
action: Option<String>,
|
||||
/// Output format: `table` (default) or `json`.
|
||||
#[arg(long, default_value = "table")]
|
||||
format: String,
|
||||
},
|
||||
// Item subcommands (Add/Get/List/Edit/Rm/Restore/Purge) are added by
|
||||
// Tasks B10–B13, which extend this enum.
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -481,13 +551,71 @@ fn main() -> Result<()> {
|
||||
OrgCommands::Init { name } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_init(&d, &name)?;
|
||||
Ok(())
|
||||
}
|
||||
OrgCommands::AddMember { key, name, role } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
let role = parse_org_role(&role)?;
|
||||
commands::org::run_add_member(&d, &key, &name, role)?;
|
||||
}
|
||||
OrgCommands::RemoveMember { member_id } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_remove_member(&d, &member_id)?;
|
||||
}
|
||||
OrgCommands::SetRole { member_id, role } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
let role = parse_org_role(&role)?;
|
||||
commands::org::run_set_role(&d, &member_id, role)?;
|
||||
}
|
||||
OrgCommands::CreateCollection { slug, name } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_create_collection(&d, &slug, &name)?;
|
||||
}
|
||||
OrgCommands::Grant { member_id, collection } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_grant(&d, &member_id, &collection)?;
|
||||
}
|
||||
OrgCommands::Revoke { member_id, collection } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_revoke(&d, &member_id, &collection)?;
|
||||
}
|
||||
OrgCommands::RotateKey => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_rotate_key(&d)?;
|
||||
}
|
||||
OrgCommands::TransferOwnership { member_id, keep_owner } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_transfer_ownership(&d, &member_id, keep_owner)?;
|
||||
}
|
||||
OrgCommands::DeleteOrg { confirm } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_delete_org(&d, confirm)?;
|
||||
}
|
||||
OrgCommands::Status => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_status(&d)?;
|
||||
}
|
||||
OrgCommands::Audit { since, member, collection, action, format } => {
|
||||
let d = crate::org_session::org_dir(dir_path)?;
|
||||
commands::org::run_audit(&d, since.as_deref(), member.as_deref(),
|
||||
collection.as_deref(), action.as_deref(), &format)?;
|
||||
}
|
||||
// Item dispatch arms (Add/Get/List/Edit/Rm/Restore/Purge) added by
|
||||
// Tasks B10–B13.
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_org_role(s: &str) -> anyhow::Result<relicario_core::OrgRole> {
|
||||
match s {
|
||||
"owner" => Ok(relicario_core::OrgRole::Owner),
|
||||
"admin" => Ok(relicario_core::OrgRole::Admin),
|
||||
"member" => Ok(relicario_core::OrgRole::Member),
|
||||
other => anyhow::bail!("unknown role `{other}` — use owner, admin, or member"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for test passphrase override (debug builds only; stripped from release).
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn test_passphrase_override() -> Option<String> {
|
||||
|
||||
Reference in New Issue
Block a user