summaryrefslogtreecommitdiff
path: root/src/umask.rs
blob: 6aef33a12e30dd9befb2595b7bfa7d12fa883bc7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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}"),
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error(transparent)]
    Parse(#[from] std::num::ParseIntError),

    // 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,
}