use crate::store::keyspaces;
use vta_sdk::display_name::{NameBook, NameSource, shorten_did};
use std::path::PathBuf;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::acl::{
ActScope, VtcAclEntry, VtcRole, delete_acl_entry, get_acl_entry, list_acl_entries,
store_acl_entry,
};
use crate::config::AppConfig;
use crate::store::Store;
type CliResult = Result<(), Box<dyn std::error::Error>>;
fn now_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub async fn run_acl_list(config_path: Option<PathBuf>) -> CliResult {
let config = AppConfig::load(config_path)?;
let store = Store::open(&config.store)?;
let acl_ks = store.keyspace(keyspaces::ACL)?;
let mut entries = list_acl_entries(&acl_ks).await?;
if entries.is_empty() {
println!("No ACL entries.");
return Ok(());
}
entries.sort_by(|a, b| a.did.cmp(&b.did));
let mut book = NameBook::new();
for e in &entries {
book.insert_opt(&e.did, e.label.as_deref(), NameSource::AclLabel);
}
let show_names = book.names_any(entries.iter().map(|e| e.did.as_str()));
let now = now_epoch();
if show_names {
println!(
" {:<24} {:<44} {:<14} {:<12} CONTEXTS",
"NAME", "DID", "ROLE", "EXPIRES"
);
} else {
println!(" {:<44} {:<14} {:<12} CONTEXTS", "DID", "ROLE", "EXPIRES");
}
for e in &entries {
let expires = match e.expires_at {
None => "never".to_string(),
Some(t) if t <= now => "EXPIRED".to_string(),
Some(t) => format!("{}s", t - now),
};
let contexts = match e.act_scope() {
ActScope::Contexts(cs) => format!("[{}]", cs.join(",")),
other => other.to_string(),
};
let did = shorten_did(&e.did);
if show_names {
let name = book.name_of(&e.did).unwrap_or_else(|| "\u{2014}".into());
println!(
" {:<24} {:<44} {:<14} {:<12} {}",
name,
did,
e.role.to_string(),
expires,
contexts
);
} else {
println!(
" {:<44} {:<14} {:<12} {}",
did,
e.role.to_string(),
expires,
contexts
);
}
}
println!(
"\n{} entr{}.",
entries.len(),
if entries.len() == 1 { "y" } else { "ies" }
);
Ok(())
}
pub struct AclAddArgs {
pub config_path: Option<PathBuf>,
pub did: String,
pub role: String,
pub label: Option<String>,
pub contexts: Vec<String>,
pub expires: Option<u64>,
}
pub async fn run_acl_add(args: AclAddArgs) -> CliResult {
let role = VtcRole::from_str(&args.role)?;
let config = AppConfig::load(args.config_path)?;
let store = Store::open(&config.store)?;
let acl_ks = store.keyspace(keyspaces::ACL)?;
let now = now_epoch();
let existing = get_acl_entry(&acl_ks, &args.did).await?;
let entry = VtcAclEntry {
did: args.did.clone(),
role,
label: args.label,
allowed_contexts: args.contexts,
created_at: existing.as_ref().map(|e| e.created_at).unwrap_or(now),
created_by: "cli:acl-add".into(),
updated_at: None,
updated_by: None,
expires_at: args.expires.map(|ttl| now.saturating_add(ttl)),
};
store_acl_entry(&acl_ks, &entry).await?;
store.persist().await?;
println!(
"{} ACL entry for {} (role {}).",
if existing.is_some() {
"Updated"
} else {
"Added"
},
args.did,
entry.role
);
Ok(())
}
pub async fn run_acl_remove(config_path: Option<PathBuf>, did: String) -> CliResult {
let config = AppConfig::load(config_path)?;
let store = Store::open(&config.store)?;
let acl_ks = store.keyspace(keyspaces::ACL)?;
if get_acl_entry(&acl_ks, &did).await?.is_none() {
println!("No ACL entry for {did} — nothing to remove.");
return Ok(());
}
delete_acl_entry(&acl_ks, &did).await?;
store.persist().await?;
println!("Removed ACL entry for {did}.");
Ok(())
}