use std::path::PathBuf;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::acl::{
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("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 now = now_epoch();
println!(
" {:<48} {:<14} {:<12} LABEL / 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 mut detail = e.label.clone().unwrap_or_default();
if !e.allowed_contexts.is_empty() {
if !detail.is_empty() {
detail.push_str(" ");
}
detail.push_str(&format!("contexts=[{}]", e.allowed_contexts.join(",")));
}
println!(
" {:<48} {:<14} {:<12} {}",
e.did,
e.role.to_string(),
expires,
detail
);
}
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("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(),
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("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(())
}