OXYGENHUB
◆ api-unknown-fields oxygenhub / crates / hub / src / setup.rs
2.6 KiB · Rust · last changed A new hub gives administrator only to whoever brings its setup code 19 days ago source blame history raw
c8684d44 Andy McCall 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
13
pub fn path(app: &Shared) -> PathBuf {
14
    app.config.data.join("setup-code")
15
}
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.
19
pub async fn ensure(app: &Shared) -> anyhow::Result<()> {
20
    let file = path(app);
21
    if accounts::account_count(&app.db).await? > 0 {
22
        let _ = std::fs::remove_file(&file);
23
        return Ok(());
24
    }
25
    let code = match expected(app) {
26
        Some(code) => code,
27
        None => {
28
            let code = crate::auth::random_hex(12);
29
            write_private(&file, &code)?;
30
            code
31
        }
32
    };
33
    tracing::warn!(
34
        "this hub has no administrator yet: register the first account at {}/register with setup code {code} (also in {}); it works once",
35
        app.config.base_url(),
36
        file.display()
37
    );
38
    Ok(())
39
}
40
41
/// The code the first registration must bring, if there is one.
42
pub fn expected(app: &Shared) -> Option<String> {
43
    let configured = app.config.registration.setup_code.trim();
44
    if !configured.is_empty() {
45
        return Some(configured.to_string());
46
    }
47
    std::fs::read_to_string(path(app)).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
48
}
49
50
/// Whether `given` is the setup code, compared in constant time.
51
pub fn matches(app: &Shared, given: &str) -> bool {
52
    match expected(app) {
53
        Some(code) => {
54
            let (a, b) = (code.as_bytes(), given.trim().as_bytes());
55
            a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
56
        }
57
        None => false,
58
    }
59
}
60
61
/// The first administrator exists: the code is spent.
62
pub fn spend(app: &Shared) {
63
    let _ = std::fs::remove_file(path(app));
64
}
65
66
fn write_private(file: &std::path::Path, code: &str) -> std::io::Result<()> {
67
    use std::io::Write;
68
    let mut options = std::fs::OpenOptions::new();
69
    options.write(true).create(true).truncate(true);
70
    #[cfg(unix)]
71
    {
72
        use std::os::unix::fs::OpenOptionsExt;
73
        options.mode(0o600);
74
    }
75
    let mut f = options.open(file)?;
76
    writeln!(f, "{code}")
77
}