Skip to main content

fungus/sys/
user.rs

1use crate::{
2    errors::*,
3    sys::{self, PathExt},
4};
5use std::{io, iter, mem, path::PathBuf, ptr};
6
7// Implementation in Rust for the XDB Base Directory Specification
8// https://wiki.archlinux.org/index.php/XDG_Base_Directory
9// -------------------------------------------------------------------------------------------------
10
11/// Returns the full path to the current user's home directory.
12///
13/// Alternate implementation as the Rust std::env::home_dir implementation which has be deprecated
14/// https://doc.rust-lang.org/std/env/fn.home_dir.html
15///
16/// ### Examples
17/// ```
18/// use fungus::prelude::*;
19///
20/// assert!(user::home_dir().is_ok());
21/// ```
22pub fn home_dir() -> FuResult<PathBuf> {
23    let home = sys::var("HOME")?;
24    let dir = PathBuf::from(home);
25    Ok(dir)
26}
27
28/// Returns the full path to the current user's config directory.
29/// Where user-specific configurations should be written (analogous to /etc).
30/// Defaults to $HOME/.config.
31///
32/// ### Examples
33/// ```
34/// use fungus::prelude::*;
35///
36/// assert!(user::config_dir().is_ok());
37/// ```
38pub fn config_dir() -> FuResult<PathBuf> {
39    Ok(match sys::var("XDG_CONFIG_HOME") {
40        Ok(x) => PathBuf::from(x),
41        Err(_) => home_dir()?.mash(".config"),
42    })
43}
44
45/// Returns the full path to the current user's cache directory.
46/// Where user-specific non-essential (cached) data should be written (analogous to /var/cache).
47/// Defaults to $HOME/.cache.
48///
49/// ### Examples
50/// ```
51/// use fungus::prelude::*;
52///
53/// assert!(user::cache_dir().is_ok());
54/// ```
55pub fn cache_dir() -> FuResult<PathBuf> {
56    Ok(match sys::var("XDG_CACHE_HOME") {
57        Ok(x) => PathBuf::from(x),
58        Err(_) => home_dir()?.mash(".cache"),
59    })
60}
61
62/// Returns the full path to the current user's data directory.
63/// Where user-specific data files should be written (analogous to /usr/share).
64/// Defaults to $HOME/.local/share
65///
66/// ### Examples
67/// ```
68/// use fungus::prelude::*;
69///
70/// assert!(user::data_dir().is_ok());
71/// ```
72pub fn data_dir() -> FuResult<PathBuf> {
73    Ok(match sys::var("XDG_DATA_HOME") {
74        Ok(x) => PathBuf::from(x),
75        Err(_) => home_dir()?.mash(".local/share"),
76    })
77}
78
79/// Returns the full path to the current user's runtime directory.
80/// Used for non-essential, user-specific data files such as sockets, named pipes, etc.
81/// Must be owned by the user with an access mode of 0700.
82/// Filesystem fully featured by standards of OS.
83/// Must be on the local filesystem.
84/// May be subject to periodic cleanup.
85/// Modified every 6 hours or set sticky bit if persistence is desired.
86/// Can only exist for the duration of the user's login.
87/// Should not store large files as it may be mounted as a tmpfs.
88///
89/// Defaults to /tmp if $XDG_RUNTIME_DIR is not set
90///
91/// ### Examples
92/// ```
93/// use fungus::prelude::*;
94///
95/// println!("runtime directory of the current user: {:?}", user::runtime_dir());
96/// ```
97pub fn runtime_dir() -> PathBuf {
98    match sys::var("XDG_RUNTIME_DIR") {
99        Ok(x) => PathBuf::from(x),
100        Err(_) => PathBuf::from("/tmp"),
101    }
102}
103
104/// Returns the full path to a newly created directory in `/tmp` that can be used for temporary
105/// work. The returned path will be checked for uniqueness and created with a random suffix and
106/// the given `prefix`. It is up to the calling code to ensure the directory returned is
107/// properly cleaned up when done with.
108///
109/// ### Examples
110/// ```
111/// use fungus::prelude::*;
112///
113/// let tmpdir = user::temp_dir("foo").unwrap();
114/// assert_eq!(tmpdir.exists(), true);
115/// {
116///     let _defer = defer(|| sys::remove_all(&tmpdir).unwrap());
117/// }
118/// assert_eq!(tmpdir.exists(), false);
119/// ```
120pub fn temp_dir<T: AsRef<str>>(prefix: T) -> FuResult<PathBuf> {
121    loop {
122        let suffix: String = iter::repeat_with(fastrand::alphanumeric).take(8).collect();
123        let dir = PathBuf::from(format!("/tmp/{}-{}", prefix.as_ref(), suffix));
124        if !dir.exists() {
125            return sys::mkdir(&dir);
126        }
127    }
128}
129
130/// Returns the current user's data directories.
131/// List of directories seperated by : (analogous to PATH).
132/// Defaults to /usr/local/share:/usr/share.
133///
134/// ### Examples
135/// ```
136/// use fungus::prelude::*;
137///
138/// assert!(user::data_dirs().is_ok());
139/// ```
140pub fn data_dirs() -> FuResult<Vec<PathBuf>> {
141    Ok(match sys::var("XDG_DATA_DIRS") {
142        Ok(x) => sys::parse_paths(x)?,
143        Err(_) => vec![PathBuf::from("/usr/local/share:/usr/share")],
144    })
145}
146
147/// Returns the current user's config directories.
148/// List of directories seperated by : (analogous to PATH).
149/// Defaults to /etc/xdg
150///
151/// ### Examples
152/// ```
153/// use fungus::prelude::*;
154///
155/// assert!(user::config_dirs().is_ok());
156/// ```
157pub fn config_dirs() -> FuResult<Vec<PathBuf>> {
158    Ok(match sys::var("XDG_CONFIG_DIRS") {
159        Ok(x) => sys::parse_paths(x)?,
160        Err(_) => vec![PathBuf::from("/etc/xdg")],
161    })
162}
163
164/// Returns the current user's path directories.
165/// List of directories seperated by :
166///
167/// ### Examples
168/// ```
169/// use fungus::prelude::*;
170///
171/// assert!(user::path_dirs().is_ok());
172/// ```
173pub fn path_dirs() -> FuResult<Vec<PathBuf>> {
174    sys::parse_paths(sys::var("PATH")?)
175}
176
177// User functions
178// -------------------------------------------------------------------------------------------------
179
180/// User provides options for a specific user.
181#[derive(Debug, Clone, Default)]
182pub struct User {
183    pub uid: u32,           // user id
184    pub gid: u32,           // user group id
185    pub name: String,       // user name
186    pub home: PathBuf,      // user home
187    pub shell: PathBuf,     // user shell
188    pub ruid: u32,          // real user id behind sudo
189    pub rgid: u32,          // real user group id behind sudo
190    pub realname: String,   // real user name behind sudo
191    pub realhome: PathBuf,  // real user home behind sudo
192    pub realshell: PathBuf, // real user shell behind sudo
193}
194
195impl User {
196    /// Returns true if the user is root
197    ///
198    /// ### Examples
199    /// ```
200    /// use fungus::prelude::*;
201    ///
202    /// assert_eq!(user::current().unwrap().is_root(), false);
203    /// ```
204    pub fn is_root(&self) -> bool {
205        self.uid == 0
206    }
207}
208
209/// Get the current user
210///
211/// ### Examples
212/// ```
213/// use fungus::prelude::*;
214///
215/// assert!(user::current().is_ok());
216/// ```
217pub fn current() -> FuResult<User> {
218    let user = lookup(unsafe { libc::getuid() })?;
219    Ok(user)
220}
221
222/// Switches back to the original user under the sudo mask with no way to go back.
223///
224/// ### Examples
225/// ```
226/// use fungus::prelude::*;
227///
228/// assert!(user::drop_sudo().is_ok());
229/// ```
230pub fn drop_sudo() -> FuResult<()> {
231    match getuid() {
232        0 => {
233            let (ruid, rgid) = getrids(0, 0);
234            switchuser(ruid, ruid, ruid, rgid, rgid, rgid)
235        },
236        _ => Ok(()),
237    }
238}
239
240/// Returns the user ID for the current user.
241///
242/// ### Examples
243/// ```
244/// use fungus::prelude::*;
245///
246/// assert!(user::getuid() != 0);
247/// ```
248pub fn getuid() -> u32 {
249    unsafe { libc::getuid() }
250}
251
252/// Returns the group ID for the current user.
253///
254/// ### Examples
255/// ```
256/// use fungus::prelude::*;
257///
258/// assert!(user::getgid() != 0);
259/// ```
260pub fn getgid() -> u32 {
261    unsafe { libc::getgid() }
262}
263
264/// Returns the user effective ID for the current user.
265///
266/// ### Examples
267/// ```
268/// use fungus::prelude::*;
269///
270/// assert!(user::geteuid() != 0);
271/// ```
272pub fn geteuid() -> u32 {
273    unsafe { libc::geteuid() }
274}
275
276/// Returns the group effective ID for the current user.
277///
278/// ### Examples
279/// ```
280/// use fungus::prelude::*;
281///
282/// assert!(user::getegid() != 0);
283/// ```
284pub fn getegid() -> u32 {
285    unsafe { libc::getegid() }
286}
287
288/// Returns the real IDs for the given user.
289///
290/// ### Examples
291/// ```
292/// use fungus::prelude::*;
293///
294/// assert_eq!(user::getrids(user::getuid(), user::getgid()), (user::getuid(), user::getgid()));
295/// ```
296pub fn getrids(uid: u32, gid: u32) -> (u32, u32) {
297    match uid {
298        0 => match (sys::var("SUDO_UID"), sys::var("SUDO_GID")) {
299            (Ok(u), Ok(g)) => match (u.parse::<u32>(), g.parse::<u32>()) {
300                (Ok(u), Ok(g)) => (u, g),
301                _ => (uid, gid),
302            },
303            _ => (uid, gid),
304        },
305        _ => (uid, gid),
306    }
307}
308
309/// Return true if the current user is the root user.
310///
311/// ### Examples
312/// ```
313/// use fungus::prelude::*;
314///
315/// assert_eq!(user::is_root(), false);
316/// ```
317pub fn is_root() -> bool {
318    getuid() == 0
319}
320
321/// Lookup a user by user id
322///
323/// ### Examples
324/// ```
325/// use fungus::prelude::*;
326///
327/// assert!(user::lookup(user::getuid()).is_ok());
328/// ```
329pub fn lookup(uid: u32) -> FuResult<User> {
330    // Get the libc::passwd by user id
331    let mut buf = vec![0; 2048];
332    let mut res = ptr::null_mut::<libc::passwd>();
333    let mut passwd = unsafe { mem::zeroed::<libc::passwd>() };
334    unsafe {
335        libc::getpwuid_r(uid, &mut passwd, buf.as_mut_ptr(), buf.len(), &mut res);
336    }
337    if res.is_null() || res != &mut passwd {
338        return Err(UserError::does_not_exist_by_id(uid).into());
339    }
340
341    // Convert libc::passwd object into a User object
342    //----------------------------------------------------------------------------------------------
343    let gid = passwd.pw_gid;
344
345    // User name for the lookedup user. We always want this and it should always exist.
346    let username = unsafe { sys::libc::to_string(passwd.pw_name)? };
347
348    // Will almost always be a single 'x' as the passwd is in the shadow database
349    // let userpwd = unsafe { crate::sys::libc::to_string(passwd.pw_passwd)? };
350
351    // User home directory e.g. '/home/<user>'. Might be a null pointer indicating the system default
352    // should be used
353    let userhome = unsafe { sys::libc::to_string(passwd.pw_dir) }.unwrap_or_default();
354
355    // User shell e.g. '/bin/bash'. Might be a null pointer indicating the system default should be used
356    let usershell = unsafe { sys::libc::to_string(passwd.pw_shell) }.unwrap_or_default();
357
358    // A string container user contextual information, possibly real name or phone number.
359    // let usergecos = unsafe { crate::sys::libc::to_string(passwd.pw_gecos)? };
360
361    // Get the user's real ids as well if applicable
362    let (ruid, rgid) = getrids(uid, gid);
363    let realuser = if uid != ruid {
364        lookup(ruid)?
365    } else {
366        User {
367            uid,
368            gid,
369            name: username.to_string(),
370            home: PathBuf::from(&userhome),
371            shell: PathBuf::from(&usershell),
372            ..Default::default()
373        }
374    };
375    Ok(User {
376        uid,
377        gid,
378        name: username,
379        home: PathBuf::from(&userhome),
380        shell: PathBuf::from(&usershell),
381        ruid,
382        rgid,
383        realname: realuser.name,
384        realhome: realuser.home,
385        realshell: realuser.shell,
386    })
387}
388
389/// Returns the current user's name.
390///
391/// ### Examples
392/// ```
393/// use fungus::prelude::*;
394///
395/// println!("current user name: {:?}", user::name().unwrap());
396/// ```
397pub fn name() -> FuResult<String> {
398    Ok(current()?.name)
399}
400
401/// Switches back to the original user under the sudo mask. Preserves the ability to raise sudo
402/// again.
403///
404/// ### Examples
405/// ```ignore
406/// use fungus::prelude::*;
407///
408/// assert!(user::pause_sudo().is_ok());
409/// ```
410pub fn pause_sudo() -> FuResult<()> {
411    match getuid() {
412        0 => {
413            let (ruid, rgid) = getrids(0, 0);
414            switchuser(ruid, ruid, 0, rgid, rgid, 0)
415        },
416        _ => Ok(()),
417    }
418}
419
420/// Set the user ID for the current user.
421///
422/// ### Examples
423/// ```ignore
424/// use fungus::prelude::*;
425///
426/// assert!(user::setuid(user::getuid()).is_ok());
427/// ```
428pub fn setuid(uid: u32) -> FuResult<()> {
429    match unsafe { libc::setuid(uid) } {
430        0 => Ok(()),
431        _ => Err(io::Error::last_os_error().into()),
432    }
433}
434
435/// Set the user effective ID for the current user.
436///
437/// ### Examples
438/// ```ignore
439/// use fungus::prelude::*;
440///
441/// assert!(user::seteuid(user::geteuid()).is_ok());
442/// ```
443pub fn seteuid(euid: u32) -> FuResult<()> {
444    match unsafe { libc::seteuid(euid) } {
445        0 => Ok(()),
446        _ => Err(io::Error::last_os_error().into()),
447    }
448}
449
450/// Set the group ID for the current user.
451///
452/// ### Examples
453/// ```ignore
454/// use fungus::prelude::*;
455///
456/// assert!(user::setgid(user::getgid()).is_ok());
457/// ```
458pub fn setgid(gid: u32) -> FuResult<()> {
459    match unsafe { libc::setgid(gid) } {
460        0 => Ok(()),
461        _ => Err(io::Error::last_os_error().into()),
462    }
463}
464
465/// Set the group effective ID for the current user.
466///
467/// ### Examples
468/// ```ignore
469/// use fungus::prelude::*;
470///
471/// assert!(user::setegid(user::getegid()).is_ok());
472/// ```
473pub fn setegid(egid: u32) -> FuResult<()> {
474    match unsafe { libc::setegid(egid) } {
475        0 => Ok(()),
476        _ => Err(io::Error::last_os_error().into()),
477    }
478}
479
480/// Switches back to sudo root. Returns and error if not allowed.
481///
482/// ### Examples
483/// ```ignore
484/// use fungus::prelude::*;
485///
486/// user:sudo().unwrap();
487/// ```
488pub fn sudo() -> FuResult<()> {
489    switchuser(0, 0, 0, 0, 0, 0)
490}
491
492/// Switches to another use by setting the real, effective and saved user and group ids.
493///
494/// ### Examples
495/// ```ignore
496/// use fungus::prelude::*;
497///
498/// // Switch to user 1000 but preserve root priviledeges to switch again
499/// user::switchuser(1000, 1000, 0, 1000, 1000, 0);
500///
501/// // Switch to user 1000 and drop root priviledges permanantely
502/// user::switchuser(1000, 1000, 1000, 1000, 1000, 1000);
503/// ```
504pub fn switchuser(ruid: u32, euid: u32, suid: u32, rgid: u32, egid: u32, sgid: u32) -> FuResult<()> {
505    // Best practice to drop the group first
506    match unsafe { libc::setresgid(rgid, egid, sgid) } {
507        0 => match unsafe { libc::setresuid(ruid, euid, suid) } {
508            0 => Ok(()),
509            _ => Err(io::Error::last_os_error().into()),
510        },
511        _ => Err(io::Error::last_os_error().into()),
512    }
513}
514
515// Unit tests
516// -------------------------------------------------------------------------------------------------
517#[cfg(test)]
518mod tests {
519    use crate::prelude::*;
520
521    #[test]
522    fn test_user_home() {
523        let home_str = sys::var("HOME").unwrap();
524        let home_path = PathBuf::from(home_str);
525        let home_dir = home_path.parent().unwrap();
526        assert_eq!(home_dir.to_path_buf(), user::home_dir().unwrap().dir().unwrap());
527    }
528
529    #[test]
530    fn test_user_libc() {
531        assert!(user::pause_sudo().is_ok());
532        assert!(user::drop_sudo().is_ok());
533        assert!(user::getuid() != 0);
534        assert!(user::getgid() != 0);
535        assert!(user::geteuid() != 0);
536        assert!(user::getegid() != 0);
537        assert_eq!(user::getrids(user::getuid(), user::getgid()), (user::getuid(), user::getgid()));
538        assert_eq!(user::is_root(), false);
539        assert!(user::lookup(user::getuid()).is_ok());
540        assert!(user::name().unwrap() != "".to_string());
541        assert!(user::current().is_ok());
542        assert_eq!(user::current().unwrap().is_root(), false);
543        // assert!(user::sudo().is_err());
544        // assert!(user::setegid(user::getegid()).is_ok());
545        // assert!(user::setgid(user::getgid()).is_ok());
546        // assert!(user::seteuid(user::geteuid()).is_ok());
547        // assert!(user::setuid(user::getuid()).is_ok());
548    }
549
550    #[test]
551    fn test_user_dirs() {
552        assert!(user::home_dir().is_ok());
553        assert!(user::config_dir().is_ok());
554        assert!(user::cache_dir().is_ok());
555        assert!(user::data_dir().is_ok());
556        user::runtime_dir();
557        assert!(user::data_dirs().is_ok());
558        assert!(user::config_dirs().is_ok());
559        assert!(user::path_dirs().is_ok());
560
561        let tmpdir = user::temp_dir("test_user_dirs").unwrap();
562        assert_eq!(tmpdir.exists(), true);
563        {
564            let _defer = defer(|| sys::remove_all(&tmpdir).unwrap());
565        }
566        assert_eq!(tmpdir.exists(), false);
567    }
568
569    #[test]
570    fn test_temp_dir() {
571        let tmpdir = user::temp_dir("foo").unwrap();
572        assert_eq!(tmpdir.exists(), true);
573        assert!(sys::remove_all(&tmpdir).is_ok());
574        assert_eq!(tmpdir.exists(), false);
575    }
576}