1use ratatui::{
2 layout::Constraint,
3 style::{Color, Modifier, Style},
4 widgets::{Block, Cell, Row, Table},
5};
6use vta_sdk::prelude::*;
7
8use crate::render::{is_full_display, print_full_entry, print_full_list_title, print_widget};
9
10pub fn format_contexts(contexts: &[String]) -> String {
11 if contexts.is_empty() {
12 "(unrestricted)".to_string()
13 } else {
14 contexts.join(", ")
15 }
16}
17
18pub fn format_role(role: &str, contexts: &[String]) -> String {
19 if role == "admin" && contexts.is_empty() {
20 "super admin".to_string()
21 } else {
22 role.to_string()
23 }
24}
25
26pub fn validate_role(role: &str) -> Result<(), Box<dyn std::error::Error>> {
27 match role {
28 "admin" | "initiator" | "application" | "reader" => Ok(()),
29 _ => Err(format!(
30 "invalid role '{role}', expected: admin, initiator, application, or reader"
31 )
32 .into()),
33 }
34}
35
36pub async fn cmd_acl_list(
37 client: &VtaClient,
38 context: Option<&str>,
39) -> Result<(), Box<dyn std::error::Error>> {
40 let resp = client.list_acl(context).await?;
41
42 if crate::render::is_json_output() {
47 crate::render::print_json(&resp.entries)?;
48 return Ok(());
49 }
50
51 if resp.entries.is_empty() {
52 println!("No ACL entries found.");
53 return Ok(());
54 }
55
56 if is_full_display() {
57 print_full_list_title("ACL Entries", resp.entries.len());
58 for entry in &resp.entries {
59 let label = entry.label.as_deref().unwrap_or("—");
60 let contexts = format_contexts(&entry.allowed_contexts);
61 let role = format_role(&entry.role, &entry.allowed_contexts);
62 print_full_entry(&[
63 ("DID", &entry.did),
64 ("Role", &role),
65 ("Label", label),
66 ("Contexts", &contexts),
67 ("Created By", &entry.created_by),
68 ]);
69 }
70 return Ok(());
71 }
72
73 let header_style = Style::default()
74 .fg(Color::White)
75 .add_modifier(Modifier::BOLD);
76 let header = Row::new(vec!["DID", "Role", "Label", "Contexts", "Created By"])
77 .style(header_style)
78 .bottom_margin(1);
79
80 let rows: Vec<Row> = resp
81 .entries
82 .iter()
83 .map(|entry| {
84 let label = entry.label.clone().unwrap_or_else(|| "\u{2014}".into());
85 let contexts = format_contexts(&entry.allowed_contexts);
86
87 Row::new(vec![
88 Cell::from(entry.did.clone()).style(Style::default().fg(Color::DarkGray)),
89 Cell::from(format_role(&entry.role, &entry.allowed_contexts)),
90 Cell::from(label),
91 Cell::from(contexts),
92 Cell::from(entry.created_by.clone()).style(Style::default().fg(Color::DarkGray)),
93 ])
94 })
95 .collect();
96
97 let title = format!(" ACL Entries ({}) ", resp.entries.len());
98
99 let table = Table::new(
103 rows,
104 [
105 Constraint::Min(60), Constraint::Length(12), Constraint::Min(16), Constraint::Length(24), Constraint::Min(52), ],
111 )
112 .header(header)
113 .column_spacing(2)
114 .block(
115 Block::bordered()
116 .title(title)
117 .border_style(Style::default().fg(Color::DarkGray)),
118 );
119
120 let height = resp.entries.len() as u16 + 4;
121 print_widget(table, height);
122
123 Ok(())
124}
125
126pub async fn cmd_acl_get(client: &VtaClient, did: &str) -> Result<(), Box<dyn std::error::Error>> {
127 let entry = client.get_acl(did).await?;
128 println!("DID: {}", entry.did);
129 println!(
130 "Role: {}",
131 format_role(&entry.role, &entry.allowed_contexts)
132 );
133 println!(
134 "Label: {}",
135 entry.label.as_deref().unwrap_or("(not set)")
136 );
137 println!(
138 "Contexts: {}",
139 format_contexts(&entry.allowed_contexts)
140 );
141 println!("Created At: {}", entry.created_at);
142 println!("Created By: {}", entry.created_by);
143 Ok(())
144}
145
146pub async fn cmd_acl_create(
147 client: &VtaClient,
148 did: String,
149 role: String,
150 label: Option<String>,
151 contexts: Vec<String>,
152 expires_at: Option<u64>,
153 step_up_approver: Option<String>,
154 step_up_require: Option<String>,
155) -> Result<(), Box<dyn std::error::Error>> {
156 validate_role(&role)?;
157 let mut req = CreateAclRequest::new(did, role).contexts(contexts);
158 if let Some(l) = label {
159 req = req.label(l);
160 }
161 if let Some(secs) = expires_at {
162 req = req.expires_at(secs);
163 }
164 if let Some(ref approver) = step_up_approver {
165 req = req.step_up_approver(approver.clone());
166 }
167 if let Some(ref require) = step_up_require {
168 req = req.step_up_require(require.clone());
169 }
170 let entry = client.create_acl(req).await?;
171 println!("ACL entry created:");
172 println!(" DID: {}", entry.did);
173 println!(
174 " Role: {}",
175 format_role(&entry.role, &entry.allowed_contexts)
176 );
177 if let Some(label) = &entry.label {
178 println!(" Label: {label}");
179 }
180 println!(" Contexts: {}", format_contexts(&entry.allowed_contexts));
181 if let Some(approver) = &step_up_approver {
182 println!(" Step-up approver: {approver}");
183 }
184 if let Some(require) = &step_up_require {
185 println!(" Step-up require: {require}");
186 }
187 match entry.expires_at {
188 Some(secs) => println!(
189 " Expires at: {} ({})",
190 crate::duration::format_local_time(secs),
191 crate::duration::format_remaining(secs),
192 ),
193 None => println!(" Expires at: (permanent)"),
194 }
195 Ok(())
196}
197
198pub async fn cmd_acl_update(
199 client: &VtaClient,
200 did: &str,
201 role: Option<String>,
202 label: Option<String>,
203 contexts: Option<Vec<String>>,
204 step_up_approver: Option<String>,
205 step_up_require: Option<String>,
206) -> Result<(), Box<dyn std::error::Error>> {
207 if let Some(ref r) = role {
208 validate_role(r)?;
209 }
210 let req = UpdateAclRequest {
211 role,
212 label,
213 allowed_contexts: contexts,
214 step_up_approver: step_up_approver.clone(),
215 step_up_require: step_up_require.clone(),
216 };
217 let entry = client.update_acl(did, req).await?;
218 println!("ACL entry updated:");
219 println!(" DID: {}", entry.did);
220 println!(
221 " Role: {}",
222 format_role(&entry.role, &entry.allowed_contexts)
223 );
224 if let Some(label) = &entry.label {
225 println!(" Label: {label}");
226 }
227 println!(" Contexts: {}", format_contexts(&entry.allowed_contexts));
228 if let Some(approver) = &step_up_approver {
229 if approver.is_empty() {
230 println!(" Step-up approver: (cleared)");
231 } else {
232 println!(" Step-up approver: {approver}");
233 }
234 }
235 if let Some(require) = &step_up_require {
236 if require.is_empty() {
237 println!(" Step-up require: (cleared)");
238 } else {
239 println!(" Step-up require: {require}");
240 }
241 }
242 Ok(())
243}
244
245pub async fn cmd_acl_delete(
246 client: &VtaClient,
247 did: &str,
248) -> Result<(), Box<dyn std::error::Error>> {
249 client.delete_acl(did).await?;
250 println!("ACL entry deleted: {did}");
251 Ok(())
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
261 fn test_format_contexts_empty_shows_unrestricted() {
262 assert_eq!(format_contexts(&[]), "(unrestricted)");
263 }
264
265 #[test]
266 fn test_format_contexts_single() {
267 let ctx = vec!["vta".to_string()];
268 assert_eq!(format_contexts(&ctx), "vta");
269 }
270
271 #[test]
272 fn test_format_contexts_multiple() {
273 let ctx = vec!["vta".to_string(), "payments".to_string()];
274 assert_eq!(format_contexts(&ctx), "vta, payments");
275 }
276
277 #[test]
280 fn test_format_role_admin_no_contexts_is_super_admin() {
281 assert_eq!(format_role("admin", &[]), "super admin");
282 }
283
284 #[test]
285 fn test_format_role_admin_with_contexts_stays_admin() {
286 let ctx = vec!["vta".to_string()];
287 assert_eq!(format_role("admin", &ctx), "admin");
288 }
289
290 #[test]
291 fn test_format_role_initiator_unchanged() {
292 assert_eq!(format_role("initiator", &[]), "initiator");
293 }
294
295 #[test]
296 fn test_format_role_application_unchanged() {
297 let ctx = vec!["app".to_string()];
298 assert_eq!(format_role("application", &ctx), "application");
299 }
300
301 #[test]
304 fn test_validate_role_admin_ok() {
305 assert!(validate_role("admin").is_ok());
306 }
307
308 #[test]
309 fn test_validate_role_initiator_ok() {
310 assert!(validate_role("initiator").is_ok());
311 }
312
313 #[test]
314 fn test_validate_role_application_ok() {
315 assert!(validate_role("application").is_ok());
316 }
317
318 #[test]
319 fn test_validate_role_reader_ok() {
320 assert!(validate_role("reader").is_ok());
321 }
322
323 #[test]
324 fn test_validate_role_unknown_fails() {
325 let err = validate_role("superuser").unwrap_err();
326 assert!(err.to_string().contains("invalid role 'superuser'"));
327 }
328
329 #[test]
330 fn test_validate_role_empty_fails() {
331 assert!(validate_role("").is_err());
332 }
333}