OXYGENHUB

Coverage / crates/hub/src/setup.rs 89.4%

42 of 47 lines run, 10 of 20 functions · run #470 on main · run missed

1
//! The first administrator (oxygenhub#245). A hub with no accounts gives
2
//! administrator to the first account registered, so that account must
3
//! bring a one-time setup code: the one in `[registration] setup_code`
4
//! (for automation), or else a random one this module keeps in
5
//! `<data>/setup-code` (owner-only) and logs at start. It is never served
6
//! over the web. Registering with it spends it; once there are accounts
7
//! there is no code.
8
9
use crate::db::accounts;
10
use crate::Shared;
11
use std::path::PathBuf;
12
13285×
pub fn path(app: &Shared) -> PathBuf {
14285×
    app.config.data.join("setup-code")
15285×
}
16
17
/// At start: with no accounts, make sure there is a code and say where it
18
/// is; with accounts, make sure none is left lying about.
19191×
pub async fn ensure(app: &Shared) -> anyhow::Result<()> {
20191×
    let file = path(app);
21191×
    if accounts::account_count(&app.db).await? > 0 {
220×
        let _ = std::fs::remove_file(&file);
230×
        return Ok(());
24191×
    }
25191×
    let code = match expected(app) {
26186×
        Some(code) => code,
27
        None => {
285×
            let code = crate::auth::random_hex(12);
295×
            write_private(&file, &code)?;
305×
            code
31
        }
32
    };
33191×
    tracing::warn!(
34
        "this hub has no administrator yet: register the first account at {}/register with setup code {code} (also in {}); it works once",
350×
        app.config.base_url(),
360×
        file.display()
37
    );
38191×
    Ok(())
39191×
}
40
41
/// The code the first registration must bring, if there is one.
42279×
pub fn expected(app: &Shared) -> Option<String> {
43279×
    let configured = app.config.registration.setup_code.trim();
44279×
    if !configured.is_empty() {
45271×
        return Some(configured.to_string());
468×
    }
478×
    std::fs::read_to_string(path(app)).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
48279×
}
49
50
/// Whether `given` is the setup code, compared in constant time.
5188×
pub fn matches(app: &Shared, given: &str) -> bool {
5288×
    match expected(app) {
5388×
        Some(code) => {
5488×
            let (a, b) = (code.as_bytes(), given.trim().as_bytes());
55898×
            a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
56
        }
570×
        None => false,
58
    }
5988×
}
60
61
/// The first administrator exists: the code is spent.
6286×
pub fn spend(app: &Shared) {
6386×
    let _ = std::fs::remove_file(path(app));
6486×
}
65
665×
fn write_private(file: &std::path::Path, code: &str) -> std::io::Result<()> {
67
    use std::io::Write;
685×
    let mut options = std::fs::OpenOptions::new();
695×
    options.write(true).create(true).truncate(true);
70
    #[cfg(unix)]
71
    {
72
        use std::os::unix::fs::OpenOptionsExt;
735×
        options.mode(0o600);
74
    }
755×
    let mut f = options.open(file)?;
765×
    writeln!(f, "{code}")
775×
}