Skip to main content

urma_node/platform/
defaults.rs

1//! First-install state (URMA_NODE_INSTALL.md ยง3): every item is created only
2//! when absent, nothing existing is rewritten, and nothing existing is adopted
3//! unless an administrator owns it and only administrators can write it.
4use super::{Exposure, Paths, ServiceSettings, os};
5use crate::{Error, config, service};
6use std::{
7    fs::File,
8    io::{self, Write},
9    path::{Path, PathBuf},
10};
11
12type Writer<'a> = &'a dyn Fn(&mut File) -> Result<(), Error>;
13type Create = fn(&Path) -> Result<(), io::Error>;
14
15/// Whether an existing item must pass `urma_io::check_admin` before it is
16/// adopted. The Linux state directory and everything below it belong to the
17/// service account after install, so only Windows guards the token there.
18#[derive(Clone, Copy, PartialEq, Eq)]
19enum Adopt {
20    AdminOnly,
21    Owned,
22}
23
24impl Adopt {
25    const STATE: Self = match cfg!(windows) {
26        true => Self::AdminOnly,
27        false => Self::Owned,
28    };
29}
30
31/// Creates what is missing below `paths` and returns what it created. The
32/// caller (install) owns the account and, on Linux, the chown to it.
33pub fn ensure_defaults(paths: &Paths) -> Result<Vec<PathBuf>, Error> {
34    let mut created = Vec::new();
35    let directories: [(&Path, Create, Adopt); 3] = [
36        (
37            &paths.settings_dir,
38            os::create_settings_directory,
39            Adopt::AdminOnly,
40        ),
41        (&paths.state_dir, os::create_state_directory, Adopt::STATE),
42        (&paths.data, os::create_data_directory, Adopt::Owned),
43    ];
44    for (path, create, adopt) in directories {
45        if absent_directory(path, adopt)? {
46            create(path)?;
47            created.push(path.to_path_buf());
48        }
49    }
50    let config = serde_json::to_vec_pretty(&config::service_archive_config()?)?;
51    let settings = serde_json::to_vec_pretty(&ServiceSettings::defaults(paths))?;
52    let files: [(&Path, Exposure, Adopt, Writer); 4] = [
53        (&paths.config, Exposure::Public, Adopt::AdminOnly, &|file| {
54            Ok(file.write_all(&config)?)
55        }),
56        (
57            &paths.settings,
58            Exposure::Public,
59            Adopt::AdminOnly,
60            &|file| Ok(file.write_all(&settings)?),
61        ),
62        (
63            &paths.peer_key,
64            Exposure::Private,
65            Adopt::Owned,
66            &write_peer_key,
67        ),
68        (
69            &paths.token,
70            Exposure::Readers,
71            Adopt::STATE,
72            &service::write_token,
73        ),
74    ];
75    for (path, exposure, adopt, write) in files {
76        if fill(path, exposure, adopt, write)? {
77            created.push(path.to_path_buf());
78        }
79    }
80    Ok(created)
81}
82
83/// A fresh Ed25519 transport identity, never a wallet or decryption key.
84pub fn write_peer_key(file: &mut File) -> Result<(), Error> {
85    let mut bytes = peer_key_bytes()?;
86    let written = file.write_all(&bytes);
87    bytes.fill(0);
88    Ok(written?)
89}
90
91/// A private per-user transport identity at `path`; an existing file stays.
92pub fn create_peer_key(path: &Path) -> Result<(), Error> {
93    let mut bytes = peer_key_bytes()?;
94    let written = urma_io::write_new(path, &bytes);
95    bytes.fill(0);
96    written.map_err(|error| Error::Invalid(error.to_string()))
97}
98
99fn peer_key_bytes() -> Result<Vec<u8>, Error> {
100    libp2p::identity::Keypair::generate_ed25519()
101        .to_protobuf_encoding()
102        .map_err(|error| Error::Invalid(error.to_string()))
103}
104
105fn adoptable(path: &Path, adopt: Adopt) -> Result<(), Error> {
106    match adopt {
107        Adopt::Owned => Ok(()),
108        Adopt::AdminOnly => match urma_io::check_admin(path) {
109            Ok(()) => Ok(()),
110            Err(error) => Err(Error::Invalid(format!(
111                "refusing to adopt {}: it must be owned by and writable only by administrators \
112                 ({error}); remove it or let `urma-node service install` reset it",
113                path.display()
114            ))),
115        },
116    }
117}
118
119fn absent_directory(path: &Path, adopt: Adopt) -> Result<bool, Error> {
120    match std::fs::symlink_metadata(path) {
121        Ok(metadata) if metadata.is_dir() => {
122            adoptable(path, adopt)?;
123            Ok(false)
124        }
125        Ok(metadata) => Err(Error::Invalid(format!(
126            "{} exists but is a {:?}, not a directory",
127            path.display(),
128            metadata.file_type()
129        ))),
130        Err(cause) if cause.kind() == io::ErrorKind::NotFound => Ok(true),
131        Err(cause) => Err(cause.into()),
132    }
133}
134
135fn fill(path: &Path, exposure: Exposure, adopt: Adopt, write: Writer) -> Result<bool, Error> {
136    let mut file = match os::create_file(path, exposure) {
137        Ok(file) => file,
138        Err(cause) if cause.kind() == io::ErrorKind::AlreadyExists => {
139            if !std::fs::symlink_metadata(path)?.is_file() {
140                return Err(Error::Invalid(format!(
141                    "{} exists but is not a regular file: {cause}",
142                    path.display()
143                )));
144            }
145            adoptable(path, adopt)?;
146            return Ok(false);
147        }
148        Err(cause) => return Err(cause.into()),
149    };
150    let outcome = write(&mut file).and_then(|()| Ok(file.sync_all()?));
151    drop(file);
152    match outcome {
153        Ok(()) => Ok(true),
154        Err(cause) => {
155            std::fs::remove_file(path)?;
156            Err(cause)
157        }
158    }
159}