1use super::GrantMode;
14use super::lys_pass_adapter::{LiveAnswer, LiveLysAsk};
15use lys_pass::deliberate::CheckAnswer;
16use lys_pass::drafts::{DraftRequest, request_digest};
17use lys_pass::{Client, Error, Mode, Target};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::{Mutex, Weak};
21use std::time::{Duration, Instant};
22
23static ASK_METRICS: Mutex<Vec<Weak<AskMetrics>>> = Mutex::new(Vec::new());
24
25#[derive(Debug, Default)]
26struct AskMetrics {
27 count: AtomicU64,
28 maximum_ns: AtomicU64,
29}
30
31#[derive(Clone, Copy, Debug, Default, serde::Serialize)]
33pub(crate) struct AskStatistics {
34 pub count: u64,
35 pub max_elapsed_ms: f64,
36}
37
38impl AskMetrics {
39 fn record(&self, elapsed: Duration) {
40 let nanos = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
41 self.maximum_ns.fetch_max(nanos, Ordering::Relaxed);
42 self.count.fetch_add(1, Ordering::Release);
43 }
44
45 fn snapshot(&self) -> AskStatistics {
46 AskStatistics {
47 count: self.count.load(Ordering::Acquire),
48 max_elapsed_ms: Duration::from_nanos(self.maximum_ns.load(Ordering::Relaxed))
49 .as_secs_f64()
50 * 1000.0,
51 }
52 }
53}
54
55pub(crate) fn ask_statistics() -> Result<AskStatistics, Error> {
57 let mut clients = ASK_METRICS
58 .lock()
59 .map_err(|_| Error::CannotAsk("Lys ask metrics registry poisoned"))?;
60 let mut total = AskStatistics::default();
61 clients.retain(|client| {
62 let Some(client) = client.upgrade() else {
63 return false;
64 };
65 let snapshot = client.snapshot();
66 total.count = total.count.saturating_add(snapshot.count);
67 total.max_elapsed_ms = total.max_elapsed_ms.max(snapshot.max_elapsed_ms);
68 true
69 });
70 drop(clients);
71 Ok(total)
72}
73
74#[path = "drafts.rs"]
75pub mod drafts;
76
77pub struct PreparedAct<'a> {
79 pub holder: &'a str,
81 pub target: &'a Target,
83 pub operation: &'a str,
85 pub words: &'a str,
87 pub at_ms: u64,
89}
90
91pub trait PreparedDraftKeeper: Send + Sync {
96 fn prepare(&self, holder: &str, request: &DraftRequest, at_ms: u64) -> Result<(), Error>;
101 fn held(&self, holder: &str, id: &str, request: &DraftRequest, at_ms: u64)
106 -> Result<(), Error>;
107}
108
109pub struct PreparedAnswer {
111 pub decision: LiveAnswer,
113 pub words: Option<String>,
115}
116
117impl std::fmt::Debug for PreparedAnswer {
118 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 formatter
120 .debug_struct("PreparedAnswer")
121 .finish_non_exhaustive()
122 }
123}
124pub struct HttpLysAsk {
126 client: Client,
127 metrics: Arc<AskMetrics>,
128 handle: tokio::runtime::Handle,
129 asker: Option<String>,
132 keeper: Option<Arc<dyn PreparedDraftKeeper>>,
133}
134
135pub fn lys_origin(url: &str) -> Result<reqwest::Url, Error> {
141 let mut url: reqwest::Url = url
142 .parse()
143 .map_err(|_| Error::Invalid("issuer is not a URL"))?;
144 if matches!(url.path(), "/api" | "/api/") {
145 url.set_path("/");
146 }
147 Ok(url)
148}
149
150impl std::fmt::Debug for HttpLysAsk {
151 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 formatter.debug_struct("HttpLysAsk").finish_non_exhaustive()
153 }
154}
155
156impl HttpLysAsk {
157 pub fn new(issuer: &str, handle: tokio::runtime::Handle) -> Result<Self, Error> {
162 let issuer = lys_origin(issuer)?;
163 let metrics = Arc::new(AskMetrics::default());
164 {
165 let mut clients = ASK_METRICS
166 .lock()
167 .map_err(|_| Error::CannotAsk("Lys ask metrics registry poisoned"))?;
168 clients.retain(|client| client.strong_count() > 0);
169 clients.push(Arc::downgrade(&metrics));
170 }
171 Ok(Self {
172 client: Client::new(
173 reqwest::Client::builder()
174 .no_proxy()
175 .redirect(reqwest::redirect::Policy::none())
176 .tcp_keepalive(Duration::from_secs(30))
177 .pool_idle_timeout(None)
178 .build()
179 .map_err(|error| Error::Transport(Box::new(error)))?,
180 issuer,
181 )?,
182 handle,
183 metrics,
184 asker: None,
185 keeper: None,
186 })
187 }
188
189 #[must_use]
192 pub fn asking_as(mut self, credential: String) -> Self {
193 self.asker = Some(credential);
194 self
195 }
196
197 #[must_use]
199 pub fn with_drafts(mut self, keeper: Arc<dyn PreparedDraftKeeper>) -> Self {
200 self.keeper = Some(keeper);
201 self
202 }
203
204 #[cfg(test)]
205 fn statistics(&self) -> AskStatistics {
206 self.metrics.snapshot()
207 }
208
209 fn block<T, F>(&self, route: &'static str, future: F) -> Result<T, Error>
211 where
212 T: Send,
213 F: std::future::Future<Output = Result<T, Error>> + Send,
214 {
215 let started = Instant::now();
216 let answer = drafts::block(&self.handle, future);
217 let elapsed = started.elapsed();
218 self.metrics.record(elapsed);
219 tracing::debug!(target: "liminal::lys_ask", route, ok = answer.is_ok(),
220 elapsed_ms = elapsed.as_secs_f64() * 1000.0, "Lys ask completed");
221 answer
222 }
223
224 fn draft(
225 &self,
226 pass: &str,
227 act: &PreparedAct<'_>,
228 grant: &str,
229 judgment: &CheckAnswer,
230 ) -> Result<String, Error> {
231 let draft = DraftRequest {
232 operation: act.operation.to_owned(),
233 grant: grant.to_owned(),
234 target: Target::new(&act.target.kind, &act.target.id, &act.target.action)?,
235 request_digest: request_digest(act.words),
236 words: act.words.to_owned(),
237 };
238 if let Some(keeper) = &self.keeper {
239 keeper.prepare(act.holder, &draft, act.at_ms)?;
240 }
241 let created = self.block(
242 "/api/product-drafts",
243 self.client.create_draft(pass, &draft, judgment),
244 )?;
245 if let Some(keeper) = &self.keeper {
246 keeper.held(act.holder, &created.draft, &draft, act.at_ms)?;
247 }
248 Ok(created.draft)
249 }
250 pub fn ask_prepared(
255 &self,
256 pass: &str,
257 act: &PreparedAct<'_>,
258 limits: drafts::DraftLimits,
259 ) -> Result<PreparedAnswer, Error> {
260 if self.keeper.is_none() {
261 return Err(Error::CannotAsk("draft preparation writer is missing"));
262 }
263 if pass.is_empty()
265 || act.words.is_empty()
266 || act.words.len() > limits.words_bytes.get()
267 || [
268 act.holder,
269 act.operation,
270 &act.target.kind,
271 &act.target.id,
272 &act.target.action,
273 ]
274 .iter()
275 .any(|value| {
276 value.is_empty()
277 || value.len() > limits.metadata_bytes.get()
278 || value.chars().any(char::is_control)
279 })
280 {
281 return Err(Error::Invalid("prepared draft bound exceeded"));
282 }
283 let decision = self.prepared_decision(pass, act)?;
284 let words = matches!(decision, LiveAnswer::Held { .. }).then(|| act.words.to_owned());
285 Ok(PreparedAnswer { decision, words })
286 }
287
288 fn prepared_decision(&self, pass: &str, act: &PreparedAct<'_>) -> Result<LiveAnswer, Error> {
289 let answer = self.block(
290 "/api/grants/check/batch",
291 self.client.check_batch(
292 self.asker.as_deref().unwrap_or(pass),
293 act.holder,
294 std::slice::from_ref(act.target),
295 None,
296 ),
297 )?;
298 if answer.degraded.is_some() {
299 return Err(Error::CannotAsk("live permission projection degraded"));
300 }
301 let Some(result) = answer.results.into_iter().next() else {
302 return Err(Error::CannotAsk("the live answer carried no result"));
303 };
304 if result.degraded.is_some() {
305 return Err(Error::CannotAsk("live permission projection degraded"));
306 }
307 match (
308 result.allowed,
309 result.mode.as_ref(),
310 result.grant.as_deref(),
311 result.refusal.as_deref(),
312 ) {
313 (true, Some(Mode::Outright), Some(grant), None) => Ok(LiveAnswer::Outright {
314 grant: grant.to_owned(),
315 }),
316 (false, Some(Mode::ByDraft), Some(grant), None) => Ok(LiveAnswer::Held {
317 mode: GrantMode::ByDraft,
318 grant: grant.to_owned(),
319 draft: self.draft(pass, act, grant, &result)?,
320 }),
321 (false, Some(Mode::ByTwo), Some(grant), None) => Ok(LiveAnswer::Held {
322 mode: GrantMode::ByTwo,
323 grant: grant.to_owned(),
324 draft: self.draft(pass, act, grant, &result)?,
325 }),
326 (false, None, None, Some(refusal)) => Ok(LiveAnswer::Refused {
327 refusal: refusal.to_owned(),
328 }),
329 _ => Err(Error::CannotAsk("inconsistent live permission answer")),
330 }
331 }
332}
333
334impl LiveLysAsk for HttpLysAsk {
335 fn ask(
336 &self,
337 pass: &str,
338 holder: &str,
339 target: &Target,
340 now: u64,
341 ) -> Result<LiveAnswer, Error> {
342 let words = format!(
343 "{holder} asks {} on {}:{}",
344 target.action, target.kind, target.id
345 );
346 let operation = format!(
347 "liminal:{holder}:{}:{}:{}",
348 target.kind, target.id, target.action
349 );
350 let at_ms = now
351 .checked_mul(1000)
352 .ok_or(Error::Invalid("draft instant overflow"))?;
353 self.prepared_decision(
354 pass,
355 &PreparedAct {
356 holder,
357 target,
358 operation: &operation,
359 words: &words,
360 at_ms,
361 },
362 )
363 }
364}
365
366#[path = "live/drafts_join.rs"]
367mod drafts_join;
368pub use drafts_join::PreparedOwner;
369
370#[cfg(test)]
371#[path = "live/timing_tests.rs"]
372mod timing_tests;