Skip to main content

liminal_server/auth_lys/
live.rs

1//! The live Lys question over HTTP (ACCESS-002 R1, ACCESS-003 R1).
2//!
3//! `lys-pass`'s batch route is asynchronous; the connection's decision path
4//! is not. Each ask runs its future to completion on a scoped thread against
5//! the server's tokio runtime, so a connection process never blocks the
6//! runtime's own workers and never nests a runtime. Deliberate asks are rare
7//! by definition (opening a link, deploying a service, an administrator's
8//! act), so a thread per ask is the honest cost; nothing is cached, and no
9//! failure falls back to the hot seam. A held answer is recorded as a draft
10//! with the holder's own pass before it is reported, so a held open is a
11//! draft Lys retains, never a promise liminal invented.
12
13use 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};
18
19/// `lys-pass`'s client behind the [`LiveLysAsk`] seam.
20pub struct HttpLysAsk {
21    client: Client,
22    handle: tokio::runtime::Handle,
23}
24
25impl std::fmt::Debug for HttpLysAsk {
26    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        formatter.debug_struct("HttpLysAsk").finish_non_exhaustive()
28    }
29}
30
31impl HttpLysAsk {
32    /// Asks `issuer` through a fresh HTTP client driven by `handle`'s runtime.
33    ///
34    /// # Errors
35    /// The issuer is not an HTTP origin.
36    pub fn new(issuer: &str, handle: tokio::runtime::Handle) -> Result<Self, Error> {
37        let issuer = issuer
38            .parse()
39            .map_err(|_| Error::Invalid("issuer is not a URL"))?;
40        Ok(Self {
41            client: Client::new(reqwest::Client::default(), issuer)?,
42            handle,
43        })
44    }
45
46    /// Runs one ask to completion on its own thread against the runtime.
47    fn block<T, F>(&self, future: F) -> Result<T, Error>
48    where
49        T: Send,
50        F: std::future::Future<Output = Result<T, Error>> + Send,
51    {
52        let handle = &self.handle;
53        std::thread::scope(|scope| scope.spawn(move || handle.block_on(future)).join())
54            .unwrap_or_else(|_| Err(Error::CannotAsk("the live ask did not complete")))
55    }
56
57    /// Records the held act as a draft with the holder's own pass, returning
58    /// the draft Lys retained.
59    fn draft(
60        &self,
61        pass: &str,
62        holder: &str,
63        target: &Target,
64        grant: &str,
65        judgment: &CheckAnswer,
66    ) -> Result<String, Error> {
67        let words = format!(
68            "{holder} asks {} on {}:{}",
69            target.action, target.kind, target.id
70        );
71        let draft = DraftRequest {
72            operation: format!(
73                "liminal:{holder}:{}:{}:{}",
74                target.kind, target.id, target.action
75            ),
76            grant: grant.to_owned(),
77            target: Target::new(&target.kind, &target.id, &target.action)?,
78            request_digest: request_digest(&words),
79            words,
80        };
81        self.block(self.client.create_draft(pass, &draft, judgment))
82            .map(|created| created.draft)
83    }
84}
85
86impl LiveLysAsk for HttpLysAsk {
87    fn ask(
88        &self,
89        pass: &str,
90        holder: &str,
91        target: &Target,
92        _now: u64,
93    ) -> Result<LiveAnswer, Error> {
94        let answer =
95            self.block(
96                self.client
97                    .check_batch(pass, holder, std::slice::from_ref(target), None),
98            )?;
99        let Some(result) = answer.results.into_iter().next() else {
100            return Err(Error::CannotAsk("the live answer carried no result"));
101        };
102        match (
103            result.allowed,
104            result.mode.as_ref(),
105            result.grant.as_deref(),
106            result.refusal.as_deref(),
107        ) {
108            (true, Some(Mode::Outright), Some(grant), None) => Ok(LiveAnswer::Outright {
109                grant: grant.to_owned(),
110            }),
111            (false, Some(Mode::ByDraft), Some(grant), None) => Ok(LiveAnswer::Held {
112                mode: GrantMode::ByDraft,
113                grant: grant.to_owned(),
114                draft: self.draft(pass, holder, target, grant, &result)?,
115            }),
116            (false, Some(Mode::ByTwo), Some(grant), None) => Ok(LiveAnswer::Held {
117                mode: GrantMode::ByTwo,
118                grant: grant.to_owned(),
119                draft: self.draft(pass, holder, target, grant, &result)?,
120            }),
121            (false, None, None, Some(refusal)) => Ok(LiveAnswer::Refused {
122                refusal: refusal.to_owned(),
123            }),
124            _ => Err(Error::CannotAsk("inconsistent live permission answer")),
125        }
126    }
127}