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    pub async fn check_batch(
95        &self,
96        pass: &str,
97        holder: &str,
98        targets: &[Target],
99        at_least: Option<u64>,
100    ) -> Result<BatchAnswer, Error> {
101        if pass.is_empty() || holder.is_empty() || targets.is_empty() {
102            return Err(Error::Invalid("live check is incomplete"));
103        }
104        for target in targets {
105            Target::new(&target.kind, &target.id, &target.action)?;
106        }
107        let body = Batch {
108            checks: targets
109                .iter()
110                .map(|target| Check {
111                    subject: holder,
112                    target,
113                })
114                .collect(),
115            at_least,
116        };
117        let response = self
118            .http
119            .post(self.endpoint("grants/check/batch")?)
120            .bearer_auth(pass)
121            .json(&body)
122            .send()
123            .await
124            .map_err(|error| Error::Transport(Box::new(error)))?;
125        if !response.status().is_success() {
126            return Err(Error::CannotAsk("live check returned a refusal status"));
127        }
128        let answer: BatchAnswer = response
129            .json()
130            .await
131            .map_err(|error| Error::Transport(Box::new(error)))?;
132        if answer.results.len() != targets.len()
133            || at_least.is_some_and(|revision| answer.revision < revision)
134        {
135            return Err(Error::CannotAsk("batch population or revision differs"));
136        }
137        for result in &answer.results {
138            result.validate()?;
139        }
140        Ok(answer)
141    }
142}