feat(cli/org): org get + list with per-member grant filtering
This commit is contained in:
@@ -848,6 +848,131 @@ pub fn run_add(dir: &Path, collection: &str, kind: OrgAddKind, tags: Vec<String>
|
||||
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
|
||||
/// `Manifest::upsert`. Keyed by item id.
|
||||
fn upsert_org_entry(
|
||||
|
||||
@@ -512,8 +512,18 @@ pub(crate) enum OrgCommands {
|
||||
#[command(subcommand)]
|
||||
kind: OrgAddKind,
|
||||
},
|
||||
// Item subcommands (Get/List/Edit/Rm/Restore/Purge) are added by
|
||||
// Tasks B11–B13, which extend this enum.
|
||||
/// Print an org item (secrets masked unless --show).
|
||||
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 B12–B13, which extend this enum.
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
@@ -654,8 +664,16 @@ fn main() -> Result<()> {
|
||||
};
|
||||
commands::org::run_add(&d, &collection, add_kind, tags)?;
|
||||
}
|
||||
// Item dispatch arms (Get/List/Edit/Rm/Restore/Purge) added by
|
||||
// Tasks B11–B13.
|
||||
OrgCommands::Get { query, show } => {
|
||||
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 B12–B13.
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user