Skip to main content

vta_cli_common/commands/
policy.rs

1//! `… policy …` — raw Rego policy management over the canonical `policy/*`
2//! Trust Tasks.
3//!
4//! This is the power-user surface: hand-authored Rego, for posture the
5//! declarative approvals model cannot express. The common case — "this task
6//! needs re-authentication / needs a human to approve" — belongs to
7//! `… approvals …`, which writes one reserved row through this same family.
8//!
9//! Transport-agnostic: every call goes through the SDK's `rpc_tt`, so this
10//! works on a VTA that advertises only DIDComm or TSP. The step-up policy
11//! surface it replaces was REST-only in the SDK, which meant an operator on a
12//! mediator-only VTA could not read the policy that was blocking them.
13
14use vta_sdk::prelude::*;
15use vta_sdk::protocols::policy_management::{
16    DeletePolicyBody, ListPoliciesBody, PolicyModuleView, UpsertPolicyBody,
17};
18
19/// `policy list` — enumerate stored policy modules.
20pub async fn cmd_list(
21    client: &VtaClient,
22    context: Option<String>,
23    enabled_only: bool,
24) -> Result<(), Box<dyn std::error::Error>> {
25    let result = client
26        .list_policies(ListPoliciesBody {
27            context_id: context,
28            enabled_only,
29            cursor: None,
30            page_size: None,
31        })
32        .await?;
33
34    if crate::render::is_json_output() {
35        println!("{}", serde_json::to_string_pretty(&result.policies)?);
36        return Ok(());
37    }
38
39    if result.policies.is_empty() {
40        println!("No policies stored.");
41        return Ok(());
42    }
43
44    println!(
45        "{:<24} {:<8} {:<8} {:<7} NAME",
46        "ID", "PRIORITY", "ENABLED", "VERSION"
47    );
48    for p in &result.policies {
49        println!(
50            "{:<24} {:<8} {:<8} {:<7} {}",
51            truncate(&p.id, 24),
52            p.priority,
53            if p.enabled { "yes" } else { "no" },
54            p.version,
55            p.name
56        );
57    }
58    if result.truncated {
59        println!(
60            "\n(more policies exist than were returned — this VTA does not page, \
61             so nothing further can be listed)"
62        );
63    }
64    Ok(())
65}
66
67/// `policy show <id>` — print one policy module, Rego and all.
68pub async fn cmd_show(client: &VtaClient, id: &str) -> Result<(), Box<dyn std::error::Error>> {
69    let p = client.get_policy(id).await?.policy;
70    if crate::render::is_json_output() {
71        println!("{}", serde_json::to_string_pretty(&p)?);
72        return Ok(());
73    }
74    print_header(&p);
75    println!("\n--- module ---\n{}", p.module);
76    if !p.ext.is_null() {
77        println!("--- ext ---\n{}", serde_json::to_string_pretty(&p.ext)?);
78    }
79    Ok(())
80}
81
82/// `policy upsert` — create or revise a hand-authored policy.
83#[allow(clippy::too_many_arguments)]
84pub async fn cmd_upsert(
85    client: &VtaClient,
86    id: Option<String>,
87    name: String,
88    module: String,
89    description: Option<String>,
90    contexts: Vec<String>,
91    priority: Option<i32>,
92    disabled: bool,
93    expected_version: Option<u64>,
94) -> Result<(), Box<dyn std::error::Error>> {
95    let result = client
96        .upsert_policy(UpsertPolicyBody {
97            id,
98            name,
99            description,
100            module,
101            applies_to: contexts,
102            priority,
103            enabled: !disabled,
104            expected_version,
105            // Only the reserved approvals row carries `ext`, and the VTA
106            // refuses it here — hand-authored Rego is not a declarative row.
107            ext: serde_json::Value::Null,
108        })
109        .await?;
110
111    println!(
112        "Policy {} ({}):",
113        if result.created { "created" } else { "updated" },
114        result.policy.id
115    );
116    print_header(&result.policy);
117    Ok(())
118}
119
120/// `policy delete <id>` — remove a policy module.
121pub async fn cmd_delete(
122    client: &VtaClient,
123    id: &str,
124    expected_version: Option<u64>,
125    reason: Option<String>,
126) -> Result<(), Box<dyn std::error::Error>> {
127    let result = client
128        .delete_policy(DeletePolicyBody {
129            id: id.to_string(),
130            expected_version,
131            reason,
132        })
133        .await?;
134    println!("Deleted policy {} at {}", result.id, result.deleted_at);
135    Ok(())
136}
137
138fn print_header(p: &PolicyModuleView) {
139    println!("  ID:        {}", p.id);
140    println!("  Name:      {}", p.name);
141    if let Some(d) = &p.description {
142        println!("  Purpose:   {d}");
143    }
144    println!("  Priority:  {}", p.priority);
145    println!("  Enabled:   {}", if p.enabled { "yes" } else { "no" });
146    println!("  Version:   {}", p.version);
147    println!(
148        "  Contexts:  {}",
149        if p.applies_to.is_empty() {
150            "all".to_string()
151        } else {
152            p.applies_to.join(", ")
153        }
154    );
155    println!("  Updated:   {}", p.updated_at);
156}
157
158fn truncate(s: &str, max: usize) -> String {
159    if s.chars().count() <= max {
160        return s.to_string();
161    }
162    let head: String = s.chars().take(max.saturating_sub(1)).collect();
163    format!("{head}…")
164}