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(())
|
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(
|
||||||
|
|||||||
@@ -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 B11–B13, 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 B12–B13, 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 B11–B13.
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user