Skip to main content

haematite_cli/door/
lys.rs

1//! The door's sign-in helper: lys-pass's, run to its end on a runtime owned
2//! here, because the door's connections are synchronous (ACCESS-002 R2).
3//!
4//! The app's issued credential is read once from the file the configuration
5//! names, handed to lys-pass and never printed, logged or sent anywhere but
6//! Lys's token endpoint. A failure to read it says which file, never what
7//! was in it.
8
9use std::io::Read;
10use std::path::Path;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use lys_pass::Error;
14use lys_pass::signin::{Secret, SignIn};
15
16use super::config::{Lys, SignInBounds};
17use super::signin::{Issuer, SignedIn, Started};
18use crate::serve::pass::AUDIENCE;
19
20/// The largest credential file read, in bytes.
21const SECRET_BYTES: u64 = 4096;
22
23/// lys-pass's sign-in helper for one Lys app.
24pub struct LysSignIn {
25    sign_in: SignIn,
26    runtime: tokio::runtime::Runtime,
27    bounds: SignInBounds,
28    assistant: super::assistant_lys::Client,
29}
30
31impl LysSignIn {
32    /// Read the credential `lys` names and the discovery document of its
33    /// issuer.
34    ///
35    /// # Errors
36    ///
37    /// Names the credential file when it cannot be read or is not one line;
38    /// otherwise lys-pass's refusal by name, `lys_could_not_be_asked` when
39    /// the issuer cannot be reached.
40    pub fn connect(lys: &Lys) -> Result<Self, String> {
41        let bounds = SignInBounds::from_env()?;
42        let mut connected = Self::connect_with_timeouts(
43            lys,
44            std::time::Duration::from_millis(bounds.connect_ms.get()),
45            std::time::Duration::from_millis(bounds.total_ms.get()),
46        )?;
47        connected.bounds = bounds;
48        Ok(connected)
49    }
50
51    fn connect_with_timeouts(
52        lys: &Lys,
53        connect: std::time::Duration,
54        total: std::time::Duration,
55    ) -> Result<Self, String> {
56        let secret = read_secret(&lys.secret_file)?;
57        let issuer = url::Url::parse(&lys.issuer)
58            .map_err(|_| format!("the lys issuer {} is not an address", lys.issuer))?;
59        let redirect = url::Url::parse(&lys.redirect)
60            .map_err(|_| format!("the lys redirect {} is not an address", lys.redirect))?;
61        let runtime = tokio::runtime::Builder::new_current_thread()
62            .enable_all()
63            .build()
64            .map_err(|error| format!("the door's sign-in runtime could not start: {error}"))?;
65        let http = reqwest::Client::builder()
66            .connect_timeout(connect)
67            .timeout(total)
68            .redirect(reqwest::redirect::Policy::none())
69            .no_proxy()
70            .build()
71            .map_err(|_| {
72                "lys_could_not_be_asked: the Lys HTTP client could not start".to_owned()
73            })?;
74        let sign_in = runtime
75            .block_on(SignIn::connect(
76                http.clone(),
77                &issuer,
78                AUDIENCE,
79                &lys.client_id,
80                secret,
81                redirect,
82            ))
83            .map_err(|error| format!("{}: the lys issuer {}", error.name(), lys.issuer))?;
84        Ok(Self {
85            sign_in,
86            runtime,
87            bounds: SignInBounds::default(),
88            assistant: super::assistant_lys::Client {
89                issuer: lys.issuer.clone(),
90                http,
91            },
92        })
93    }
94}
95
96impl Issuer for LysSignIn {
97    fn assistant_identity(
98        &self,
99        agent: &str,
100        pass: &str,
101        store: &str,
102    ) -> Result<String, crate::serve::storage::Failure> {
103        self.runtime
104            .block_on(self.assistant.identity(agent, pass, store, now()))
105    }
106    fn assistant_draft(
107        &self,
108        pass: &str,
109        draft: &str,
110        within: std::time::Duration,
111    ) -> Result<serde_json::Value, crate::serve::storage::Failure> {
112        self.runtime
113            .block_on(self.assistant.draft(pass, draft, within))
114    }
115    fn assistant_decide(
116        &self,
117        pass: &str,
118        draft: &serde_json::Value,
119        decision: &str,
120        reason: Option<&str>,
121        within: std::time::Duration,
122    ) -> Result<serde_json::Value, crate::serve::storage::Failure> {
123        self.runtime.block_on(
124            self.assistant
125                .decide_held(pass, draft, decision, reason, within),
126        )
127    }
128    fn assistant_url(&self) -> Result<&str, crate::serve::storage::Failure> {
129        Ok(&self.assistant.issuer)
130    }
131    fn bounds(&self) -> SignInBounds {
132        self.bounds
133    }
134    fn start(&self) -> Result<Started, Error> {
135        self.sign_in.start()
136    }
137
138    fn finish(
139        &self,
140        code: &str,
141        state: &str,
142        browser: &str,
143        started: &Started,
144    ) -> Result<SignedIn, Error> {
145        self.runtime
146            .block_on(self.sign_in.finish(code, state, browser, started, now()))
147    }
148
149    fn refresh(&self, refresh: &str) -> Result<SignedIn, Error> {
150        self.runtime.block_on(self.sign_in.refresh(refresh, now()))
151    }
152}
153
154/// The credential in `path`: its one line, without the newline a file
155/// written by hand ends with.
156fn read_secret(path: &Path) -> Result<Secret, String> {
157    check_secret(path)?;
158    let unreadable = || format!("the lys secret_file {} could not be read", path.display());
159    let file = std::fs::File::open(path).map_err(|_| unreadable())?;
160    let mut text = String::new();
161    file.take(SECRET_BYTES + 1)
162        .read_to_string(&mut text)
163        .map_err(|_| unreadable())?;
164    if u64::try_from(text.len()).unwrap_or(u64::MAX) > SECRET_BYTES {
165        return Err(format!(
166            "the lys secret_file {} is longer than a credential",
167            path.display()
168        ));
169    }
170    let line = text.strip_suffix('\n').unwrap_or(&text).to_owned();
171    Secret::new(line).map_err(|_| {
172        format!(
173            "the lys secret_file {} is not one line of credential",
174            path.display()
175        )
176    })
177}
178
179pub(super) fn check_secret(path: &Path) -> Result<(), String> {
180    use rustix::fs::{Access, AtFlags, CWD, accessat};
181
182    let unreadable = || format!("the lys secret_file {} could not be read", path.display());
183    std::fs::metadata(path).map_err(|_| unreadable())?;
184    accessat(CWD, path, Access::READ_OK, AtFlags::EACCESS).map_err(|_| unreadable())
185}
186
187/// The present instant in Unix seconds; a clock before 1970 reads as 0, at
188/// which every pass is refused as not yet valid.
189fn now() -> u64 {
190    SystemTime::now()
191        .duration_since(UNIX_EPOCH)
192        .map_or(0, |elapsed| elapsed.as_secs())
193}
194
195#[cfg(test)]
196#[path = "lys_tests.rs"]
197mod tests;
198
199#[cfg(test)]
200#[path = "lys_deadline_tests.rs"]
201mod deadline_tests;