~goorzhel/radm

radm/src/user.rs -rw-r--r-- 5.6 KiB
d11d708e — Antonio Gurgel 0.6.4 a month ago
                                                                                
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Post-authentication tasks.

use std::{
    fs::{create_dir_all, File},
    os::unix::prelude::CommandExt,
    process::{Command, ExitStatus},
    str::FromStr,
};

use anyhow::{anyhow, Context, Error, Result};
use log::{trace, warn};
use nix::unistd::{Gid, Uid};
use pwd::Passwd;
use xdg::BaseDirectories;

use crate::{cli::Dbus, session::Desktop, system::env_var_or_set_default};

/// An authenticated user.
pub struct User {
    pub name: String,
    pub uid: Uid,
    pub gid: Gid,
    pub dir: String,
    shell: String,
}

impl From<Passwd> for User {
    fn from(passwd_entry: Passwd) -> Self {
        Self {
            name: passwd_entry.name,
            uid: Uid::from_raw(passwd_entry.uid),
            gid: Gid::from_raw(passwd_entry.gid),
            dir: passwd_entry.dir,
            shell: passwd_entry.shell,
        }
    }
}

impl FromStr for User {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let passwd_entry = Passwd::from_name(s)?.unwrap();
        // unwrap: users successfully authed can be assumed to exist in passwd.
        Ok(passwd_entry.into())
    }
}

/// Environment variables to load from the authenticated user's environment.
const USEFUL_ENV_VARS: &[&str] = &[
    "LANG",
    "LC_ALL",
    "XDG_RUNTIME_DIR",
    "XDG_CACHE_HOME",
    "XDG_STATE_HOME",
];

// Many of these function take `xdg: &BaseDirectories` as an argument because
// `User`s are instantiated before their environments have been loaded, and
// trying to determine XDG directories at that point would lead to a catch-22.
impl User {
    /// Loads the user's environment by executing `env` through a login shell.
    ///
    /// The [return value](xdg::BaseDirectories) is useful for later functions to determine
    /// runtime, state, and cache storage. Accordingly, the env var `$XDG_RUNTIME_DIR`
    /// will default to `/run/user/$UID` if unset.
    ///
    /// Known to work with: bash, dash, and zsh.
    pub fn load_environment(&self) -> Result<BaseDirectories> {
        let sh = Command::new(&self.shell)
            .gid(self.gid.into())
            .uid(self.uid.into()) // Ought to fly; `load_environment` runs before `drop_privileges`
            .args(["-lc", "env"])
            .output()
            .context("Couldn't run user's shell")?;
        let output = String::from_utf8(sh.stdout).context("Found invalid UTF-8 in environment")?;

        for pair in output.split_terminator('\n') {
            let mut pair = pair.split('=');
            let (key, value) = (pair.next().unwrap(), pair.next().unwrap());
            // unwrap: output has already been ensured to be KV pairs in valid UTF-8.
            if USEFUL_ENV_VARS.contains(&key) {
                trace!("Loading environment: {}={}", key, value);
                std::env::set_var(key, value);
            }
        }

        // `xdg` crate doesn't assume $XDG_RUNTIME_DIR. `radm` will default to `/run/user/$UID`
        // until there's reason to do otherwise.
        env_var_or_set_default("XDG_RUNTIME_DIR", format!("/run/user/{}", self.uid));

        Ok(BaseDirectories::new()?)
    }

    /// Opens handles on `$XDG_STATE_HOME/${SESSION_NAME}-std{out,err}`, for capturing output from the [session
    /// process](crate::session::Desktop::exec).
    pub fn get_standard_outputs(
        &self,
        session_name: &str,
        xdg: &BaseDirectories,
    ) -> Result<(File, File)> {
        let path = xdg.get_state_home();
        if !path.exists() {
            create_dir_all(&path).context("Couldn't create XDG_STATE_HOME")?;
            warn!("XDG_STATE_HOME ({:?}) nonexistent; created it.", path);
        }

        let open = |file: &str| -> Result<File> {
            let mut path = path.clone();
            path.push(file);
            if path.exists() {
                let mut old = path.clone();
                old.set_extension("old");
                std::fs::rename(&path, old)?;
            }
            let file = std::fs::OpenOptions::new()
                .write(true)
                .create(true)
                .open(&path)
                .with_context(|| anyhow!("Couldn't open {}", &path.to_string_lossy()))?;
            Ok(file)
        };

        let stdout = open(&format!("{}-stdout.log", session_name))?;
        let stderr = open(&format!("{}-stderr.log", session_name))?;
        Ok((stdout, stderr))
    }

    /// Executes the desktop environment.
    pub fn launch_session(
        &self,
        session: &Desktop,
        xdg: &BaseDirectories,
        dbus: Dbus,
    ) -> Result<ExitStatus> {
        let (stdout, stderr) = self
            .get_standard_outputs(&session.name, xdg)
            .context("Couldn't set up stdout/err")?;

        // TODO: This and the Desktop type assume that `Exec` has one and only one argument.
        // Which is the case with `sway` and `i3`, but I don't know about other desktops.
        let mut cmd = match dbus {
            Dbus::Yes => {
                // Can't one-line this (E0716).
                let mut cmd = Command::new("dbus-run-session");
                cmd.arg(&session.exec);
                cmd
            }
            Dbus::No => Command::new(&session.exec),
        };

        trace!("Launching session for {}: {:?}", self.name, &cmd);

        cmd.stdout(stdout)
            .stderr(stderr)
            .status()
            .with_context(|| anyhow!("Couldn't execute {:?}", &cmd))
    }
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use nix::unistd::{Gid, Uid};

    use super::User;

    #[test]
    fn user_fromstr_does_passwd_lookup() {
        let root = User::from_str("root").unwrap();
        assert_eq!(root.uid, Uid::from_raw(0));
        assert_eq!(root.gid, Gid::from_raw(0));
    }
}