1use reqwest::Client;
22use std::time::Duration;
23
24#[derive(Debug, thiserror::Error)]
26pub enum ClientError {
27 #[error("transport error: {0}")]
28 Transport(#[from] reqwest::Error),
29 #[error("daemon returned non-success status {status}: {body}")]
30 Status { status: u16, body: String },
31 #[error("decode error: {0}")]
32 Decode(String),
33}
34
35#[derive(serde::Serialize, Default, Debug, Clone)]
37pub struct DecideRequest {
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub actor: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub host_token: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub host_token_kind: Option<String>,
44 pub action: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub target: Option<String>,
47 #[serde(default)]
48 pub context: serde_json::Value,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub trace_id: Option<String>,
51}
52
53#[derive(serde::Deserialize, Debug, Clone)]
55pub struct DecideResponse {
56 pub decision: String,
57 #[serde(default)]
58 pub reason: String,
59 #[serde(default)]
60 pub approval_id: Option<String>,
61 #[serde(default)]
62 pub proof_id: String,
63 #[serde(default)]
64 pub actor_resolved: Option<String>,
65 #[serde(default)]
66 pub trust_level: Option<String>,
67 #[serde(default)]
68 pub authority_mode: Option<String>,
69 #[serde(default)]
70 pub danger_tags: Vec<String>,
71}
72
73#[derive(Clone, Debug)]
75pub struct TfDecideClient {
76 daemon_url: String,
77 admin_token: String,
78 http: Client,
79}
80
81impl TfDecideClient {
82 pub fn new(daemon_url: impl Into<String>, admin_token: impl Into<String>) -> Self {
84 let http = Client::builder()
85 .timeout(Duration::from_secs(5))
86 .build()
87 .expect("reqwest client");
88 let mut url = daemon_url.into();
89 while url.ends_with('/') {
90 url.pop();
91 }
92 Self {
93 daemon_url: url,
94 admin_token: admin_token.into(),
95 http,
96 }
97 }
98
99 pub fn with_client(
101 daemon_url: impl Into<String>,
102 admin_token: impl Into<String>,
103 http: Client,
104 ) -> Self {
105 let mut url = daemon_url.into();
106 while url.ends_with('/') {
107 url.pop();
108 }
109 Self {
110 daemon_url: url,
111 admin_token: admin_token.into(),
112 http,
113 }
114 }
115
116 pub fn daemon_url(&self) -> &str {
118 &self.daemon_url
119 }
120
121 pub async fn decide(&self, req: &DecideRequest) -> Result<DecideResponse, ClientError> {
123 let url = format!("{}/v1/decide", self.daemon_url);
124 let resp = self
125 .http
126 .post(&url)
127 .bearer_auth(&self.admin_token)
128 .json(req)
129 .send()
130 .await?;
131 let status = resp.status();
132 if !status.is_success() {
133 let body = resp.text().await.unwrap_or_default();
134 return Err(ClientError::Status {
135 status: status.as_u16(),
136 body,
137 });
138 }
139 let parsed: DecideResponse = resp
140 .json()
141 .await
142 .map_err(|e| ClientError::Decode(e.to_string()))?;
143 Ok(parsed)
144 }
145}
146
147pub fn is_allow(d: &DecideResponse) -> bool {
149 d.decision.eq_ignore_ascii_case("allow")
150}
151
152pub fn is_deny(d: &DecideResponse) -> bool {
154 d.decision.eq_ignore_ascii_case("deny")
155}
156
157pub fn is_approval(d: &DecideResponse) -> bool {
159 let s = d.decision.to_ascii_lowercase();
160 s == "approval" || s == "approval_required" || s == "approval-required"
161}