1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct Degraded {
28 pub step: String,
30 pub refusal: String,
32 pub revision: u64,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct CheckAnswer {
40 pub allowed: bool,
42 pub grant: Option<String>,
44 pub mode: Option<Mode>,
46 #[serde(default)]
48 pub path: Vec<String>,
49 pub via: Option<String>,
51 pub refusal: Option<String>,
53 pub reason: Option<String>,
55 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct BatchAnswer {
83 pub revision: u64,
85 pub results: Vec<CheckAnswer>,
87 pub degraded: Option<Degraded>,
89}
90
91#[cfg(feature = "http")]
92impl Client {
93 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}