Skip to main content

lys_pass/
deliberate.rs

1//! Deliberate permission questions require a complete live Lys answer.
2
3use serde::{Deserialize, Serialize};
4
5use crate::Mode;
6#[cfg(feature = "http")]
7use crate::{Client, Error, Target};
8
9#[cfg(feature = "http")]
10#[derive(Serialize)]
11struct Check<'a> {
12    subject: &'a str,
13    #[serde(flatten)]
14    target: &'a Target,
15}
16
17#[cfg(feature = "http")]
18#[derive(Serialize)]
19struct Batch<'a> {
20    checks: Vec<Check<'a>>,
21    at_least: Option<u64>,
22}
23
24/// A degraded live projection, preserved rather than silently discarded.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct Degraded {
28    /// The step that failed.
29    pub step: String,
30    /// Its named refusal.
31    pub refusal: String,
32    /// The revision actually selected.
33    pub revision: u64,
34}
35
36/// The exact result of one live question in request order.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct CheckAnswer {
40    /// Whether the action is allowed outright.
41    pub allowed: bool,
42    /// The effective granting receipt, absent on an ordinary refusal.
43    pub grant: Option<String>,
44    /// The effective grant mode, absent on an ordinary refusal.
45    pub mode: Option<Mode>,
46    /// The grant chain, omitted when no chain was returned.
47    #[serde(default)]
48    pub path: Vec<String>,
49    /// The directly granted or reached resource.
50    pub via: Option<String>,
51    /// A named ordinary refusal.
52    pub refusal: Option<String>,
53    /// Its words.
54    pub reason: Option<String>,
55    /// A degraded reading selected by the issuer.
56    pub degraded: Option<Degraded>,
57}
58
59impl CheckAnswer {
60    #[cfg(feature = "http")]
61    fn validate(&self) -> Result<(), Error> {
62        let grant = self.grant.as_deref().is_some_and(|grant| !grant.is_empty());
63        match (self.allowed, self.mode) {
64            (true, Some(Mode::Outright)) | (false, Some(Mode::ByDraft | Mode::ByTwo))
65                if grant && self.refusal.is_none() =>
66            {
67                Ok(())
68            }
69            (false, None)
70                if !grant && self.refusal.as_deref().is_some_and(|name| !name.is_empty()) =>
71            {
72                Ok(())
73            }
74            _ => Err(Error::CannotAsk("inconsistent live permission answer")),
75        }
76    }
77}
78
79/// Every result at the revision the issuer selected for this batch.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct BatchAnswer {
83    /// The permission revision.
84    pub revision: u64,
85    /// One result per question in exactly the same order.
86    pub results: Vec<CheckAnswer>,
87    /// Degradation affecting the whole reading.
88    pub degraded: Option<Degraded>,
89}
90
91#[cfg(feature = "http")]
92impl Client {
93    /// Ask the live batch route using the caller's pass; never try offline rights on failure.
94    ///
95    /// # Errors
96    ///
97    /// Refuses an empty pass, holder or batch, or an invalid target.
98    /// Transport and response-decoding failures are preserved. A refusal
99    /// status, the wrong result count, a revision below `at_least` or an
100    /// inconsistent permission answer returns [`Error::CannotAsk`].
101    pub async fn check_batch(
102        &self,
103        pass: &str,
104        holder: &str,
105        targets: &[Target],
106        at_least: Option<u64>,
107    ) -> Result<BatchAnswer, Error> {
108        if pass.is_empty() || holder.is_empty() || targets.is_empty() {
109            return Err(Error::Invalid("live check is incomplete"));
110        }
111        for target in targets {
112            Target::new(&target.kind, &target.id, &target.action)?;
113        }
114        let body = Batch {
115            checks: targets
116                .iter()
117                .map(|target| Check {
118                    subject: holder,
119                    target,
120                })
121                .collect(),
122            at_least,
123        };
124        let response = self
125            .http
126            .post(self.api_endpoint("grants/check/batch")?)
127            .bearer_auth(pass)
128            .json(&body)
129            .send()
130            .await
131            .map_err(|error| Error::Transport(Box::new(error)))?;
132        if !response.status().is_success() {
133            return Err(Error::CannotAsk("live check returned a refusal status"));
134        }
135        let answer: BatchAnswer = response
136            .json()
137            .await
138            .map_err(|error| Error::Transport(Box::new(error)))?;
139        if answer.results.len() != targets.len()
140            || at_least.is_some_and(|revision| answer.revision < revision)
141        {
142            return Err(Error::CannotAsk("batch population or revision differs"));
143        }
144        for result in &answer.results {
145            result.validate()?;
146        }
147        Ok(answer)
148    }
149}