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(
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}