1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! The live Lys check a deliberate route asks (PERMISSIONS-ACROSS-THE-STACK D2, D10).
//!
//! The question goes to Lys's `/api/grants/check/batch` through lys-pass's
//! client: every Lys route a service asks is under `/api` on the issuer.
//! Lys answers the batch check only for an administrator or an app, never a
//! seat's pass, so when haematite's app credential is configured the check
//! is asked as the app, about the connection's holder; without it the
//! connection's pass is the caller. The served connection is
//! synchronous, so the check runs to its end on a runtime owned here. Any
//! failure to reach Lys, or an answer outside the permission contract, is
//! returned as an error: the route is then refused "Lys could not be asked",
//! and the pass is never consulted in its place. An act Lys holds for
//! approval is recorded with `POST /drafts`, the same way.
use std::time::Duration;
use lys_pass::deliberate::CheckAnswer;
use lys_pass::drafts::{DraftRequest, RunError, RunReport};
use lys_pass::{Client, Error, KeySet, Target};
use crate::serve::drafts::Drafts;
use crate::serve::pass::{AUDIENCE, LiveCheck};
use crate::serve::storage::Failure;
/// How long a connection to Lys may take to open.
const CONNECT_WITHIN: Duration = Duration::from_secs(5);
/// How long one question to Lys may take to be answered whole: a Lys that
/// accepts and never answers is then named, never waited on for ever.
const ANSWER_WITHIN: Duration = Duration::from_secs(15);
/// Asks the Lys at one issuer origin.
pub struct LysLive {
client: Client,
runtime: tokio::runtime::Runtime,
/// Haematite's Lys app credential, the caller of every check when set.
asker: Option<String>,
}
impl LysLive {
/// A live check against the Lys at `issuer`, an HTTP origin.
///
/// # Errors
///
/// Returns [`Error`] when `issuer` is not an HTTP origin, or when the
/// check's runtime cannot start.
pub fn new(issuer: &str) -> Result<Self, Error> {
let mut base = url::Url::parse(issuer)?;
if !base.path().ends_with('/') {
base.set_path(&format!("{}/", base.path()));
}
let http = reqwest::Client::builder()
.connect_timeout(CONNECT_WITHIN)
.timeout(ANSWER_WITHIN)
.build()
.map_err(|_| Error::CannotAsk("the live check's client could not start"))?;
// lys-pass 0.3.4 takes the bare origin and mounts its product routes
// under `/api` itself; an origin with a path is `lys_origin_required`.
let client = Client::new(http, base)?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|_| Error::CannotAsk("the live check's runtime could not start"))?;
Ok(Self {
client,
runtime,
asker: None,
})
}
/// The same check, asked with haematite's app `credential` as the caller.
#[must_use]
pub fn asking_as(mut self, credential: String) -> Self {
self.asker = Some(credential);
self
}
/// The verification keys Lys publishes now.
///
/// # Errors
///
/// Returns [`Error`] when Lys cannot be asked or its keyset does not
/// validate whole.
pub fn keys(&self) -> Result<KeySet, Error> {
self.runtime.block_on(self.client.fetch_keys())
}
/// Pull one page of approved drafts with the connector's pass, after
/// `after`, apply each exactly once to `drafts` and acknowledge each
/// outcome to Lys. One explicit pull: nothing here polls or retries.
///
/// # Errors
///
/// Returns where the run stopped. A change applied but not acknowledged
/// keeps its receipt, and the next run answers it again without applying
/// anything twice.
pub fn run_approved(
&self,
connector_pass: &str,
drafts: &mut Drafts<'_>,
after: Option<&str>,
) -> Result<RunReport, Box<RunError<Failure>>> {
self.runtime
.block_on(
self.client
.run_approved(connector_pass, AUDIENCE, after, drafts),
)
.map_err(Box::new)
}
}
impl LiveCheck for LysLive {
fn ask(&self, token: &str, holder: &str, target: &Target) -> Result<CheckAnswer, Error> {
let answer = self.runtime.block_on(self.client.check_batch(
self.asker.as_deref().unwrap_or(token),
holder,
std::slice::from_ref(target),
None,
))?;
answer
.results
.into_iter()
.next()
.ok_or(Error::CannotAsk("Lys answered no result"))
}
fn draft(
&self,
token: &str,
request: &DraftRequest,
judgment: &CheckAnswer,
) -> Result<String, Error> {
let created = self
.runtime
.block_on(self.client.create_draft(token, request, judgment))?;
Ok(created.draft)
}
fn run_drafts(
&self,
connector: &str,
drafts: &mut Drafts<'_>,
after: Option<&str>,
) -> Result<RunReport, Box<RunError<Failure>>> {
self.run_approved(connector, drafts, after)
}
}
#[cfg(test)]
#[path = "live_tests.rs"]
mod tests;