use std::path::PathBuf;
use chrono::{TimeZone, Utc};
use dialoguer::Confirm;
use crate::acl::{
AclEntry, Role, delete_acl_entry, get_acl_entry, list_acl_entries, store_acl_entry,
};
use crate::config::AppConfig;
use crate::store::Store;
pub async fn run_acl_list(
config_path: Option<PathBuf>,
context: Option<String>,
role: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let role_filter = role.map(|r| Role::parse(&r)).transpose()?;
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 let Some(ref role) = role_filter {
entries.retain(|e| &e.role == role);
}
if let Some(ref ctx) = context {
entries.retain(|e| e.allowed_contexts.is_empty() || e.allowed_contexts.contains(ctx));
}
if entries.is_empty() {
eprintln!("No ACL entries found.");
return Ok(());
}
eprintln!("{} ACL entries:\n", entries.len());
for entry in &entries {
eprintln!(" DID: {}", entry.did);
eprintln!(" Role: {}", format_role(entry));
if let Some(label) = &entry.label {
eprintln!(" Label: {label}");
}
eprintln!(" Contexts: {}", format_contexts(&entry.allowed_contexts));
eprintln!(" Created: {}", format_timestamp(entry.created_at));
eprintln!();
}
Ok(())
}
pub async fn run_acl_get(
config_path: Option<PathBuf>,
did: String,
) -> Result<(), Box<dyn std::error::Error>> {
let config = AppConfig::load(config_path)?;
let store = Store::open(&config.store)?;
let acl_ks = store.keyspace("acl")?;
let entry = get_acl_entry(&acl_ks, &did)
.await?
.ok_or_else(|| format!("no ACL entry found for {did}"))?;
print_entry_details(&entry);
Ok(())
}
pub async fn run_acl_update(
config_path: Option<PathBuf>,
did: String,
role: Option<String>,
label: Option<String>,
contexts: Option<Vec<String>>,
) -> Result<(), Box<dyn std::error::Error>> {
if role.is_none() && label.is_none() && contexts.is_none() {
return Err("nothing to update — specify --role, --label, or --contexts".into());
}
let new_role = role.map(|r| Role::parse(&r)).transpose()?;
let config = AppConfig::load(config_path)?;
let store = Store::open(&config.store)?;
let acl_ks = store.keyspace("acl")?;
let mut entry = get_acl_entry(&acl_ks, &did)
.await?
.ok_or_else(|| format!("no ACL entry found for {did}"))?;
if let Some(role) = new_role {
entry.role = role;
}
if let Some(label) = label {
entry.label = if label.is_empty() { None } else { Some(label) };
}
if let Some(contexts) = contexts {
entry.allowed_contexts = contexts;
}
store_acl_entry(&acl_ks, &entry).await?;
store.persist().await?;
eprintln!("ACL entry updated:\n");
print_entry_details(&entry);
Ok(())
}
pub async fn run_acl_delete(
config_path: Option<PathBuf>,
did: String,
skip_confirm: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let config = AppConfig::load(config_path)?;
let store = Store::open(&config.store)?;
let acl_ks = store.keyspace("acl")?;
let entry = get_acl_entry(&acl_ks, &did)
.await?
.ok_or_else(|| format!("no ACL entry found for {did}"))?;
eprintln!("About to delete:\n");
print_entry_details(&entry);
if !skip_confirm
&& !Confirm::new()
.with_prompt("Delete this ACL entry?")
.default(false)
.interact()?
{
eprintln!("Aborted.");
return Ok(());
}
delete_acl_entry(&acl_ks, &did).await?;
store.persist().await?;
eprintln!("ACL entry deleted: {did}");
Ok(())
}
fn print_entry_details(entry: &AclEntry) {
eprintln!(" DID: {}", entry.did);
eprintln!(" Role: {}", format_role(entry));
if let Some(label) = &entry.label {
eprintln!(" Label: {label}");
}
eprintln!(" Contexts: {}", format_contexts(&entry.allowed_contexts));
eprintln!(" Created: {}", format_timestamp(entry.created_at));
eprintln!(" Created by: {}", entry.created_by);
eprintln!();
}
fn format_contexts(contexts: &[String]) -> String {
if contexts.is_empty() {
"(unrestricted)".into()
} else {
contexts.join(", ")
}
}
fn format_role(entry: &AclEntry) -> String {
if entry.role == Role::Admin && entry.allowed_contexts.is_empty() {
"admin (super admin)".into()
} else {
entry.role.to_string()
}
}
fn format_timestamp(epoch: u64) -> String {
match Utc.timestamp_opt(epoch as i64, 0) {
chrono::LocalResult::Single(dt) => dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(),
_ => format!("{epoch}"),
}
}