summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs17
-rw-r--r--src/lib.rs1
-rw-r--r--src/umask.rs116
3 files changed, 132 insertions, 2 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 28c2ec8..57c5c07 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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),
}
diff --git a/src/lib.rs b/src/lib.rs
index 0fda855..48572d3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
+}