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