feat(cli/org): org get + list with per-member grant filtering

This commit is contained in:
adlee-was-taken
2026-06-20 14:08:22 -04:00
parent 87b1d166c2
commit 2acd57a4a5
2 changed files with 147 additions and 4 deletions

View File

@@ -848,6 +848,131 @@ pub fn run_add(dir: &Path, collection: &str, kind: OrgAddKind, tags: Vec<String>
Ok(()) Ok(())
} }
pub fn run_list(dir: &Path, trashed: bool) -> Result<()> {
let vault = crate::org_session::open_org_vault(Some(dir))?;
let caller = vault.current_member()?;
let manifest = vault.load_manifest()?;
// filter_for_member restricts to the caller's granted collections.
let visible = manifest.filter_for_member(&caller);
let mut entries: Vec<_> = visible.entries.iter()
.filter(|e| if trashed { e.trashed_at.is_some() } else { e.trashed_at.is_none() })
.collect();
entries.sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase()));
if entries.is_empty() {
eprintln!("(no items match)");
return Ok(());
}
println!("{:<16} {:<14} {:<12} TITLE", "ID", "TYPE", "COLLECTION");
for e in entries {
println!(
"{:<16} {:<14} {:<12} {}",
e.id.as_str(),
format!("{:?}", e.r#type),
e.collection,
e.title
);
}
Ok(())
}
pub fn run_get(dir: &Path, query: &str, show: bool) -> Result<()> {
use relicario_core::ItemCore;
use zeroize::Zeroizing;
let vault = crate::org_session::open_org_vault(Some(dir))?;
let caller = vault.current_member()?;
let manifest = vault.load_manifest()?;
let visible = manifest.filter_for_member(&caller);
let entry = resolve_org_query(&visible, query)?;
// Double-check the grant for the resolved collection (defense in depth).
crate::org_session::UnlockedOrgVault::ensure_grant(&caller, &entry.collection)?;
let item = vault.load_item(&entry.collection, &entry.id)?;
println!("ID: {}", item.id.as_str());
println!("Title: {}", item.title);
println!("Type: {:?}", item.r#type);
println!("Collection: {}", entry.collection);
if !item.tags.is_empty() { println!("Tags: {}", item.tags.join(", ")); }
println!("Modified: {}", crate::helpers::iso8601(item.modified));
if let Some(t) = item.trashed_at { println!("Trashed: {}", crate::helpers::iso8601(t)); }
println!();
let primary_secret: Option<Zeroizing<String>> = match &item.core {
ItemCore::Login(l) => {
if let Some(u) = &l.username { println!("Username: {u}"); }
if let Some(u) = &l.url { println!("URL: {u}"); }
l.password.clone()
}
ItemCore::SecureNote(n) => {
if show { println!("Body:\n{}", n.body.as_str()); }
else { println!("Body: ********"); }
None
}
ItemCore::Identity(i) => {
if let Some(v) = &i.full_name { println!("Name: {v}"); }
if let Some(v) = &i.email { println!("Email: {v}"); }
if let Some(v) = &i.phone { println!("Phone: {v}"); }
None
}
ItemCore::Card(c) => {
if let Some(h) = &c.holder { println!("Holder: {h}"); }
c.number.clone()
}
ItemCore::Key(k) => {
if let Some(l) = &k.label { println!("Label: {l}"); }
Some(k.key_material.clone())
}
ItemCore::Document(d) => {
println!("Filename: {}", d.filename);
println!("MIME: {}", d.mime_type);
None
}
ItemCore::Totp(t) => {
if let Some(i) = &t.issuer { println!("Issuer: {i}"); }
if let Some(l) = &t.label { println!("Label: {l}"); }
None
}
};
if let Some(secret) = primary_secret {
if show {
println!("Secret: {}", secret.as_str());
} else {
println!("Secret: ******** (use --show to reveal)");
}
}
Ok(())
}
/// Resolve a query (exact id, else case-insensitive title substring) against an
/// already-grant-filtered manifest.
fn resolve_org_query<'a>(
manifest: &'a relicario_core::OrgManifest,
query: &str,
) -> Result<&'a relicario_core::OrgManifestEntry> {
if let Some(entry) = manifest.entries.iter().find(|e| e.id.as_str() == query) {
return Ok(entry);
}
let needle = query.to_lowercase();
let hits: Vec<&relicario_core::OrgManifestEntry> = manifest.entries.iter()
.filter(|e| e.title.to_lowercase().contains(&needle))
.collect();
match hits.len() {
0 => anyhow::bail!("no item matches `{query}`"),
1 => Ok(hits[0]),
_ => {
let titles: Vec<&str> = hits.iter().map(|e| e.title.as_str()).collect();
anyhow::bail!("ambiguous — {} matches: {}", hits.len(), titles.join(", "))
}
}
}
/// Insert-or-replace an `OrgManifestEntry` mirroring the personal-vault /// Insert-or-replace an `OrgManifestEntry` mirroring the personal-vault
/// `Manifest::upsert`. Keyed by item id. /// `Manifest::upsert`. Keyed by item id.
fn upsert_org_entry( fn upsert_org_entry(

View File

@@ -512,8 +512,18 @@ pub(crate) enum OrgCommands {
#[command(subcommand)] #[command(subcommand)]
kind: OrgAddKind, kind: OrgAddKind,
}, },
// Item subcommands (Get/List/Edit/Rm/Restore/Purge) are added by /// Print an org item (secrets masked unless --show).
// Tasks B11B13, which extend this enum. Get {
/// Item id or case-insensitive title substring.
query: String,
#[arg(long)] show: bool,
},
/// List org items visible to you (filtered by your collection grants).
List {
#[arg(long)] trashed: bool,
},
// Item subcommands (Edit/Rm/Restore/Purge) are added by
// Tasks B12B13, which extend this enum.
} }
#[derive(clap::Subcommand)] #[derive(clap::Subcommand)]
@@ -654,8 +664,16 @@ fn main() -> Result<()> {
}; };
commands::org::run_add(&d, &collection, add_kind, tags)?; commands::org::run_add(&d, &collection, add_kind, tags)?;
} }
// Item dispatch arms (Get/List/Edit/Rm/Restore/Purge) added by OrgCommands::Get { query, show } => {
// Tasks B11B13. let d = crate::org_session::org_dir(dir_path)?;
commands::org::run_get(&d, &query, show)?;
}
OrgCommands::List { trashed } => {
let d = crate::org_session::org_dir(dir_path)?;
commands::org::run_list(&d, trashed)?;
}
// Item dispatch arms (Edit/Rm/Restore/Purge) added by
// Tasks B12B13.
} }
Ok(()) Ok(())
} }