OXYGENHUB

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

42 of 47 lines run, 10 of 20 functions · run #544 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
13314×
pub fn path(app: &Shared) -> PathBuf {
14314×
    app.config.data.join("setup-code")
15314×
}
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.
19208×
pub async fn ensure(app: &Shared) -> anyhow::Result<()> {
20208×
    let file = path(app);
21208×
    if accounts::account_count(&app.db).await? > 0 {
220×
        let _ = std::fs::remove_file(&file);
230×
        return Ok(());
24208×
    }
25208×
    let code = match expected(app) {
26200×
        Some(code) => code,
27
        None => {
288×
            let code = crate::auth::random_hex(12);
298×
            write_private(&file, &code)?;
308×
            code
31
        }
32
    };
33208×
    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
    );
38208×
    Ok(())
39208×
}
40
41
/// The code the first registration must bring, if there is one.
42305×
pub fn expected(app: &Shared) -> Option<String> {
43305×
    let configured = app.config.registration.setup_code.trim();
44305×
    if !configured.is_empty() {
45294×
        return Some(configured.to_string());
4611×
    }
4711×
    std::fs::read_to_string(path(app)).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
48305×
}
49
50
/// Whether `given` is the setup code, compared in constant time.
5197×
pub fn matches(app: &Shared, given: &str) -> bool {
5297×
    match expected(app) {
5397×
        Some(code) => {
5497×
            let (a, b) = (code.as_bytes(), given.trim().as_bytes());
55988×
            a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
56
        }
570×
        None => false,
58
    }
5997×
}
60
61
/// The first administrator exists: the code is spent.
6295×
pub fn spend(app: &Shared) {
6395×
    let _ = std::fs::remove_file(path(app));
6495×
}
65
668×
fn write_private(file: &std::path::Path, code: &str) -> std::io::Result<()> {
67
    use std::io::Write;
688×
    let mut options = std::fs::OpenOptions::new();
698×
    options.write(true).create(true).truncate(true);
70
    #[cfg(unix)]
71
    {
72
        use std::os::unix::fs::OpenOptionsExt;
738×
        options.mode(0o600);
74
    }
758×
    let mut f = options.open(file)?;
768×
    writeln!(f, "{code}")
778×
}