diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.rs | 17 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/umask.rs | 116 |
3 files changed, 132 insertions, 2 deletions
@@ -3,7 +3,7 @@ //! This module supports running `pilcrow` as a freestanding program, via the //! [`Args`] struct. -use std::{future, io}; +use std::{future, io, str}; use axum::{ http::header, @@ -14,7 +14,11 @@ use clap::{CommandFactory, Parser}; use sqlx::sqlite::SqlitePool; use tokio::net; -use crate::{app::App, clock, db, routes}; +use crate::{ + app::App, + clock, db, routes, + umask::{self, Umask}, +}; /// Command-line entry point for running the `pilcrow` server. /// @@ -51,6 +55,10 @@ pub struct Args { #[arg(short, long, env, default_value_t = 64209)] port: u16, + /// The umask pilcrow should run under (octal, `inherit`, or `masked`) + #[arg(short = 'U', long, default_value_t = Umask::Masked)] + umask: Umask, + /// Sqlite URL or path for the `pilcrow` database #[arg(short, long, env, default_value = "sqlite://pilcrow.db")] database_url: String, @@ -66,6 +74,7 @@ impl Args { /// /// This will perform the following tasks: /// + /// * Set the process' umask (as specified by `--umask`). /// * Migrate the `pilcrow` database (at `--database-url`). /// * Start an HTTP server (on the interface and port controlled by /// `--address` and `--port`). @@ -78,6 +87,8 @@ impl Args { /// prematurely. The specific [`Error`] variant will expose the cause /// of the failure. pub async fn run(self) -> Result<(), Error> { + self.umask.set(); + let pool = self.pool().await?; let app = App::from(pool); @@ -135,4 +146,6 @@ pub enum Error { Io(#[from] io::Error), /// Failure due to a database initialization error. See [`db::Error`]. Database(#[from] db::Error), + /// Failure due to invalid umask-related options. + Umask(#[from] umask::Error), } @@ -23,4 +23,5 @@ mod setup; mod test; mod token; mod ui; +mod umask; mod user; diff --git a/src/umask.rs b/src/umask.rs new file mode 100644 index 0000000..32a82ad --- /dev/null +++ b/src/umask.rs @@ -0,0 +1,116 @@ +use std::{fmt, str}; + +use nix::{ + libc::mode_t, + sys::stat::{self, Mode}, +}; + +#[derive(Clone, Copy)] +pub enum Umask { + Masked, + Inherit, + Set(Mode), +} + +impl Umask { + pub fn set(self) { + match self { + Self::Masked => Self::masked(), + Self::Inherit => Self::inherit(), + Self::Set(mode) => Self::set_to(mode), + } + } + + fn masked() { + // In the year 2025, this is _still_ the only reasonable way to check the + // process' current umask. This is also why we don't calculate a default + // umask and stick it in Clap's `default_value_t` - I'd prefer not to touch + // the process' umask before we know what we're doing with it, but there's + // no other way to look at the current one. + // + // The choice of `all` here is a complicated compromise. In theory, nothing + // will ever observe this umask, as we immediately overwrite it immediately + // below. As of this writing, there is nothing in this service that could + // create a file in this interval, which could then be affected by the + // temporary umask. However, future code changes _could_ introduce something + // that races with this, such as a signal handler that does more than just + // exiting the process. + // + // Using `all` makes sure that _any_ file created in that interval is + // created with "deny all" file permission bits, which will make it useless + // even for this program. Hopefully, that failure will be immediate enough + // to attract attention. The other alternatives which might work are zero + // (don't mask off any permissions, things might be globally readable) + // or 0o0027 (the permissions we'd want to use on most Linux distros, which + // could then silently hide the above race condition). + let current = stat::umask(Mode::all()); + + // In addition to whatever we inherit, by default, we ensure that files + // created by pilcrow are not world-readable or world-writeable. However, + // we respect the inherited group and user permissions on the assumption + // that the user, or their administrator, has either made a decision about + // group permissions, or are relying on the distro's defaults. + // + // The main thing `pilcrow` creates are its database and its backup + // database, which can contain both confidential information (users' + // conversations) and sensitive information (the service's configuration + // and any API tokens, keys, &c used by the service). + // + // There is no way to tell `sqlite` to restrict the permissions of database + // files directly, which is otherwise what we'd prefer. + let desired = current | Mode::S_IRWXO; + + stat::umask(desired); + } + + fn inherit() { + // Here's a complete list of steps required to inherit the umask from the calling process: + } + + fn set_to(mode: Mode) { + stat::umask(mode); + } +} + +impl str::FromStr for Umask { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let umask = match s { + "masked" => Self::Masked, + "inherit" => Self::Inherit, + octal => { + let mode = mode_t::from_str_radix(octal, 8)?; + let mode = Mode::from_bits(mode).ok_or(Error::UnknownBits)?; + Self::Set(mode) + } + }; + + Ok(umask) + } +} + +impl fmt::Display for Umask { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Masked => "masked".fmt(f), + Self::Inherit => "inherit".fmt(f), + Self::Set(mode) => write!(f, "{mode:o}"), + } + } +} + +/// Errors occurring during umask option parsing. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Failed to parse a umask value from the input. + #[error(transparent)] + Parse(#[from] std::num::ParseIntError), + + /// The provided umask contained invalid bits. (See the constants associated with [`Mode`] for + /// valid umask bits.) + // We dont need to hold onto the actual umask value here - Clap does that for us, and prints + // the value as the user input it, which beats anything we could do here. + #[error("unknown bits in umask")] + UnknownBits, +} |
