Skip to main content

signstar_configure_build/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    fs::File,
5    io::Write,
6    path::{Path, PathBuf},
7    process::{Command, ExitStatus, id},
8    str::FromStr,
9};
10
11use log::{debug, info};
12use nix::unistd::User;
13use rand::{RngExt, distr::Alphanumeric, rng};
14use signstar_common::{
15    ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir},
16    system_user::get_home_base_dir_path,
17};
18use signstar_config::config::{
19    AuthorizedKeyEntry,
20    Config,
21    MappingAuthorizedKeyEntry,
22    MappingSystemUserId,
23    SystemUserId,
24    SystemUserMapping,
25};
26#[cfg(feature = "nethsm")]
27use signstar_config::nethsm::NetHsmUserMapping;
28#[cfg(feature = "yubihsm2")]
29use signstar_config::yubihsm2::YubiHsm2UserMapping;
30use sysinfo::{Pid, System};
31
32/// Specific implementations for when any of the HSM backends are compiled in.
33#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
34mod impl_any {
35    use signstar_config::config::{UserBackendConnection, UserBackendConnectionFilter};
36
37    use super::*;
38
39    /// Creates system users and their integration.
40    ///
41    /// Uses the mappings found in a [`Config`] and creates relevant Unix users, if they don't exist
42    /// on the system yet.
43    /// System users are created unlocked, without passphrase, with their homes located in the
44    /// directory returned by [`get_home_base_dir_path`].
45    /// The home directories of users are not created upon user creation, but instead a [tmpfiles.d]
46    /// configuration is added for them to automate their creation upon system boot.
47    ///
48    /// Additionally, if an [`SshForceCommand`] can be derived from a particular mapping in the
49    /// [`Config`] and one or more SSH [authorized_keys] are defined for it, a dedicated SSH
50    /// integration is created for the system user.
51    /// This entails the creation of a dedicated [authorized_keys] file as well as an [sshd_config]
52    /// drop-in in a system-wide location.
53    /// Depending on the mapping in the [`Config`], a specific [ForceCommand] is set for the system
54    /// user, reflecting its role in the system.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if
59    /// - a system user name ([`SystemUserId`]) in the configuration can not be transformed into a
60    ///   valid system user name [`User`]
61    /// - a new user can not be created
62    /// - a newly created user can not be modified
63    /// - the tmpfiles.d integration for a newly created user can not be created
64    /// - the sshd_config drop-in file for a newly created user can not be created
65    ///
66    /// [tmpfiles.d]: https://man.archlinux.org/man/tmpfiles.d.5
67    /// [authorized_keys]: https://man.archlinux.org/man/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
68    /// [sshd_config]: https://man.archlinux.org/man/sshd_config.5
69    /// [ForceCommand]: https://man.archlinux.org/man/sshd_config.5#ForceCommand
70    pub fn create_system_users(config: &Config) -> Result<(), Error> {
71        // Only operate on non-administrative users.
72        for user_backend_connection in config
73            .user_backend_connections(&[UserBackendConnectionFilter::NonAdmin])
74            .iter()
75        {
76            let user = {
77                let user = match user_backend_connection {
78                    #[cfg(feature = "nethsm")]
79                    UserBackendConnection::NetHsm {
80                        admin_secret_handling: _,
81                        non_admin_secret_handling: _,
82                        connections: _,
83                        mapping,
84                    } => mapping.system_user_id(),
85                    #[cfg(feature = "yubihsm2")]
86                    UserBackendConnection::YubiHsm2 {
87                        admin_secret_handling: _,
88                        non_admin_secret_handling: _,
89                        connections: _,
90                        mapping,
91                    } => mapping.system_user_id(),
92                };
93
94                // if there is no system user, there is nothing to do
95                let Some(user) = user else {
96                    continue;
97                };
98                user
99            };
100
101            add_user_and_home(user)?;
102            add_tmpfilesd_integration(user)?;
103
104            let (ssh_force_command, authorized_key_entry) = {
105                match user_backend_connection {
106                    #[cfg(feature = "nethsm")]
107                    UserBackendConnection::NetHsm { mapping, .. } => (
108                        SshForceCommand::try_from(mapping),
109                        mapping.authorized_key_entry(),
110                    ),
111                    #[cfg(feature = "yubihsm2")]
112                    UserBackendConnection::YubiHsm2 { mapping, .. } => (
113                        SshForceCommand::try_from(mapping),
114                        mapping.authorized_key_entry(),
115                    ),
116                }
117            };
118
119            if let Ok(force_command) = ssh_force_command
120                && let Some(authorized_key) = authorized_key_entry
121            {
122                add_ssh_integration(user, authorized_key, &force_command)?;
123            }
124        }
125
126        for mapping in config.system().mappings() {
127            // if there is no system user, there is nothing to do
128            let Some(user) = mapping.system_user_id() else {
129                continue;
130            };
131            add_user_and_home(user)?;
132            add_tmpfilesd_integration(user)?;
133
134            let Some(authorized_key) = mapping.authorized_key_entry() else {
135                continue;
136            };
137            let force_command = SshForceCommand::from(mapping);
138            add_ssh_integration(user, authorized_key, &force_command)?;
139        }
140
141        Ok(())
142    }
143}
144
145/// Specific implementations for when none of the HSM backends are compiled in.
146#[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
147mod impl_none {
148    use super::*;
149
150    /// Creates system users and their integration.
151    ///
152    /// Works on the [`UserMapping`]s of the provided `config` and creates system users for all
153    /// mappings, that define system users, if they don't exist on the system yet.
154    /// System users are created unlocked, without passphrase, with their homes located in the
155    /// directory returned by [`get_home_base_dir_path`].
156    /// The home directories of users are not created upon user creation, but instead a [tmpfiles.d]
157    /// configuration is added for them to automate their creation upon system boot.
158    ///
159    /// Additionally, if an [`SshForceCommand`] can be derived from the particular [`UserMapping`]
160    /// and one or more SSH [authorized_keys] are defined for it, a dedicated SSH integration is
161    /// created for the system user.
162    /// This entails the creation of a dedicated [authorized_keys] file as well as an [sshd_config]
163    /// drop-in in a system-wide location.
164    /// Depending on [`UserMapping`], a specific [ForceCommand] is set for the system user,
165    /// reflecting its role in the system.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if
170    /// - a system user name ([`SystemUserId`]) in the configuration can not be transformed into a
171    ///   valid system user name [`User`]
172    /// - a new user can not be created
173    /// - a newly created user can not be modified
174    /// - the tmpfiles.d integration for a newly created user can not be created
175    /// - the sshd_config drop-in file for a newly created user can not be created
176    ///
177    /// [tmpfiles.d]: https://man.archlinux.org/man/tmpfiles.d.5
178    /// [authorized_keys]: https://man.archlinux.org/man/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
179    /// [sshd_config]: https://man.archlinux.org/man/sshd_config.5
180    /// [ForceCommand]: https://man.archlinux.org/man/sshd_config.5#ForceCommand
181    pub fn create_system_users(config: &Config) -> Result<(), Error> {
182        for mapping in config.system().mappings() {
183            // if there is no system user, there is nothing to do
184            let Some(user) = mapping.system_user_id() else {
185                continue;
186            };
187            add_user_and_home(user)?;
188            add_tmpfilesd_integration(user)?;
189
190            let Some(authorized_key) = mapping.authorized_key_entry() else {
191                continue;
192            };
193            let force_command = SshForceCommand::from(mapping);
194            add_ssh_integration(user, authorized_key, &force_command)?;
195        }
196
197        Ok(())
198    }
199}
200
201#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
202pub use impl_any::create_system_users;
203#[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
204pub use impl_none::create_system_users;
205use yescrypt::{PasswordHasher, Yescrypt};
206
207pub mod cli;
208
209/// The error that may occur when using the "signstar-configure-build" executable.
210#[derive(Debug, thiserror::Error)]
211pub enum Error {
212    /// A config error
213    #[error("Configuration issue: {0}")]
214    Config(#[from] signstar_config::Error),
215
216    /// A [`Command`] exited unsuccessfully
217    #[error(
218        "The command exited with non-zero status code (\"{exit_status}\") and produced the following output on stderr:\n{stderr}"
219    )]
220    CommandNonZero {
221        /// The exit status of the failed command.
222        exit_status: ExitStatus,
223        /// The stderr of the failed command.
224        stderr: String,
225    },
226
227    /// A `u32` value can not be converted to `usize` on the current platform
228    #[error("Unable to convert u32 to usize on this platform.")]
229    FailedU32ToUsizeConversion,
230
231    /// There is no SSH ForceCommand defined for a mapping implementation.
232    #[error(
233        "No SSH ForceCommand defined for user mapping (HSM users: {}{})",
234        backend_users.join(", "),
235        if let Some(system_user) = system_user {
236            format!(", system user: {}", system_user)
237        } else {
238            "".to_string()
239        }
240    )]
241    NoForceCommandForMapping {
242        /// The list of HSM backend users for which no SSH `ForceCommand` is defined.
243        backend_users: Vec<String>,
244        /// The optional system user mapped to `backend_users`.
245        system_user: Option<String>,
246    },
247
248    /// No process information could be retrieved from the current PID
249    #[error("The information on the current process could not be retrieved")]
250    NoProcess,
251
252    /// The application is not run as root
253    #[error("This application must be run as root!")]
254    NotRoot,
255
256    /// No process information could be retrieved from the current PID
257    #[error("No user ID could be retrieved for the current process with PID {0}")]
258    NoUidForProcess(usize),
259
260    /// A password hash error occurred.
261    #[error("Password hash error while {context}: {source}")]
262    PasswordHash {
263        /// The context in which the error occurred.
264        ///
265        /// This is meant to complete the sentence "Password hash error while ".
266        context: String,
267
268        /// The source error.
269        source: yescrypt::password_hash::Error,
270    },
271
272    /// A string could not be converted to a sysinfo::Uid
273    #[error("The string {0} could not be converted to a \"sysinfo::Uid\"")]
274    SysUidFromStr(String),
275
276    /// A `Path` value for a tmpfiles.d integration is not valid.
277    #[error(
278        "The Path value {path} for the tmpfiles.d integration for {user} is not valid:\n{reason}"
279    )]
280    TmpfilesDPath {
281        /// The path that is not valid.
282        path: String,
283        /// The system user for which a `path` is invalid.
284        user: SystemUserId,
285        /// The reason why a path is not valid.
286        ///
287        /// # Note
288        ///
289        /// This is meant to complete the sentence "The Path value {path} for the tmpfiles.d
290        /// integration for {user} is not valid: "
291        reason: &'static str,
292    },
293
294    /// Adding a user failed
295    #[error("Adding user {user} failed:\n{source}")]
296    UserAdd {
297        /// The system user which cannot be added.
298        user: SystemUserId,
299        /// The source error.
300        source: std::io::Error,
301    },
302
303    /// Modifying a user failed
304    #[error("Modifying the user {user} failed:\n{source}")]
305    UserMod {
306        /// The system user which cannot be modified.
307        user: SystemUserId,
308        /// The source error.
309        source: std::io::Error,
310    },
311
312    /// A system user name can not be derived from a configuration user name
313    #[error("Getting a system user for the username {user} failed:\n{source}")]
314    UserNameConversion {
315        /// The system user that only exists in the configuration file.
316        user: SystemUserId,
317        /// The source error.
318        source: nix::Error,
319    },
320
321    /// Writing authorized_keys file for user failed
322    #[error("Writing authorized_keys file for {user} failed:\n{source}")]
323    WriteAuthorizedKeys {
324        /// The system user for which no "authorized_keys" file can be written.
325        user: SystemUserId,
326        /// The source error.
327        source: std::io::Error,
328    },
329
330    /// Writing sshd_config drop-in file for user failed
331    #[error("Writing sshd_config drop-in for {user} failed:\n{source}")]
332    WriteSshdConfig {
333        /// The system user for which an sshd_config drop-in cannot be written.
334        user: SystemUserId,
335        /// The source error.
336        source: std::io::Error,
337    },
338
339    /// Writing tmpfiles.d integration for user failed
340    #[error("Writing tmpfiles.d integration for {user} failed:\n{source}")]
341    WriteTmpfilesD {
342        /// The system user for which a tmpfiles.d file cannot be written.
343        user: SystemUserId,
344        /// The source error.
345        source: std::io::Error,
346    },
347}
348
349/// Adds a specific Unix user and its home, if it does not exist yet.
350///
351/// In addition, the system record for `user` is modified to be unlocked.
352///
353/// # Note
354///
355/// Requires the commands [useradd] and [usermod] to be present on the system.
356///
357/// # Errors
358///
359/// Returns an error, if
360///
361/// - retrieving user information on the system fails
362/// - creation of the user and its home fails
363/// - unlocking of the user fails
364///
365/// [useradd]: https://man.archlinux.org/man/useradd.8
366/// [usermod]: https://man.archlinux.org/man/usermod.8
367fn add_user_and_home(user: &SystemUserId) -> Result<(), Error> {
368    // If the Unix user exists already, we don't have to create it.
369    if User::from_name(user.as_ref())
370        .map_err(|source| Error::UserNameConversion {
371            user: user.clone(),
372            source,
373        })?
374        .is_none()
375    {
376        let home_base_dir = get_home_base_dir_path();
377
378        // add user, but do not create its home
379        info!("Creating user \"{user}\"...");
380        let user_add = Command::new("useradd")
381            .arg("--base-dir")
382            .arg(home_base_dir.as_path())
383            .arg("--user-group")
384            .arg("--groups")
385            .arg("_yubihsm2")
386            .arg("--shell")
387            .arg("/usr/bin/bash")
388            .arg(user.as_ref())
389            .output()
390            .map_err(|error| Error::UserAdd {
391                user: user.clone(),
392                source: error,
393            })?;
394
395        if !user_add.status.success() {
396            return Err(Error::CommandNonZero {
397                exit_status: user_add.status,
398                stderr: String::from_utf8_lossy(&user_add.stderr).into_owned(),
399            });
400        }
401        debug!("{}", String::from_utf8_lossy(&user_add.stdout));
402    } else {
403        debug!("Skipping existing user \"{user}\"...");
404    }
405
406    // Set random 30 char password for the user.
407    let random_passphrase: String = rng()
408        .sample_iter(&Alphanumeric)
409        .take(30)
410        .map(char::from)
411        .collect();
412    let yescrypt = Yescrypt::default();
413    let passphrase_hash = yescrypt
414        .hash_password(random_passphrase.as_bytes())
415        .map_err(|source| Error::PasswordHash {
416            context: format!("creating a passphrase hash for user {user}"),
417            source,
418        })?;
419    let mut command = Command::new("usermod");
420    command.arg("--password");
421    command.arg(passphrase_hash.as_str());
422    command.arg(user.as_ref());
423    let command_output = command.output().map_err(|source| Error::UserMod {
424        user: user.clone(),
425        source,
426    })?;
427
428    if !command_output.status.success() {
429        return Err(Error::CommandNonZero {
430            exit_status: command_output.status,
431            stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
432        });
433    }
434    debug!("{}", String::from_utf8_lossy(&command_output.stdout));
435
436    Ok(())
437}
438
439/// Adds [tmpfiles.d] integration for a `user`.
440///
441/// # Errors
442///
443/// Returns an error, if
444///
445/// - creating the [tmpfiles.d] file for `user` fails
446/// - writing the [tmpfiles.d] file for `user` fails
447///
448/// [tmpfiles.d]: https://man.archlinux.org/man/tmpfiles.d.5
449fn add_tmpfilesd_integration(user: &SystemUserId) -> Result<(), Error> {
450    // add tmpfiles.d integration for the user to create its home directory
451    info!("Adding tmpfiles.d integration for user \"{user}\"...");
452
453    let mut buffer = File::create(format!("/usr/lib/tmpfiles.d/signstar-user-{user}.conf"))
454        .map_err(|source| Error::WriteTmpfilesD {
455            user: user.clone(),
456            source,
457        })?;
458    let home_base_dir = get_home_base_dir_path();
459
460    // ensure that the `Path` component in the tmpfiles.d file
461    // - has whitespace replaced with a c-style escape
462    // - does not contain specifiers
463    let home_dir = {
464        let home_dir = format!("{}/{user}", home_base_dir.to_string_lossy()).replace(" ", "\\x20");
465        if home_dir.contains("%") {
466            return Err(Error::TmpfilesDPath {
467                path: home_dir.clone(),
468                user: user.clone(),
469                reason: "Specifiers (%) are not supported at this point.",
470            });
471        }
472        home_dir
473    };
474
475    buffer
476        .write_all(format!("d {home_dir} 700 {user} {user}\n",).as_bytes())
477        .map_err(|source| Error::WriteTmpfilesD {
478            user: user.clone(),
479            source,
480        })?;
481
482    Ok(())
483}
484
485/// Adds the SSH integration for a specific Unix user.
486///
487/// Sets a single `authorized_key` entry for `user` in the system-wide SSH configuration location.
488/// Sets up a system-wide SSH configuration for `user` in which its `authorized_key` configuration
489/// as well as a specific `force_command` is enforced.
490///
491/// # Errors
492///
493/// Returns an error if
494///
495/// - the `authorized_key` entry for `user` cannot be created
496/// - the sshd configuration file for `user` cannot be created
497fn add_ssh_integration(
498    user: &SystemUserId,
499    authorized_key: &AuthorizedKeyEntry,
500    force_command: &SshForceCommand,
501) -> Result<(), Error> {
502    info!("Adding SSH authorized_keys file for user \"{user}\"...");
503    {
504        let mut buffer = File::create(
505            get_ssh_authorized_key_base_dir().join(format!("signstar-user-{user}.authorized_keys")),
506        )
507        .map_err(|source| Error::WriteAuthorizedKeys {
508            user: user.clone(),
509            source,
510        })?;
511        buffer
512            .write_all(authorized_key.to_string().as_bytes())
513            .map_err(|source| Error::WriteAuthorizedKeys {
514                user: user.clone(),
515                source,
516            })?;
517    }
518
519    // add sshd_config drop-in configuration for user
520    info!("Adding sshd_config drop-in configuration for user \"{user}\"...");
521    {
522        let mut buffer = File::create(
523            get_sshd_config_dropin_dir().join(format!("10-signstar-user-{user}.conf")),
524        )
525        .map_err(|source| Error::WriteSshdConfig {
526            user: user.clone(),
527            source,
528        })?;
529        buffer
530            .write_all(
531                format!(
532                    r#"Match user {user}
533    AuthorizedKeysFile /etc/ssh/signstar-user-{user}.authorized_keys
534    ForceCommand /usr/bin/{force_command}
535"#
536                )
537                .as_bytes(),
538            )
539            .map_err(|source| Error::WriteSshdConfig {
540                user: user.clone(),
541                source,
542            })?;
543    }
544
545    Ok(())
546}
547
548/// The configuration file path for the application.
549#[derive(Clone, Debug)]
550pub struct ConfigPath(PathBuf);
551
552impl ConfigPath {
553    /// Creates a new [`ConfigPath`] from a path.
554    pub fn new(path: PathBuf) -> Self {
555        Self(path)
556    }
557}
558
559impl AsRef<Path> for ConfigPath {
560    fn as_ref(&self) -> &Path {
561        self.0.as_path()
562    }
563}
564
565impl Default for ConfigPath {
566    /// Returns the default [`ConfigPath`].
567    ///
568    /// Uses [`Config::first_existing_system_path`] to find the first usable configuration file
569    /// path, or [`Config::default_system_path`] if none is found.
570    fn default() -> Self {
571        Self(Config::first_existing_system_path().unwrap_or(Config::default_system_path()))
572    }
573}
574
575impl From<PathBuf> for ConfigPath {
576    fn from(value: PathBuf) -> Self {
577        Self(value)
578    }
579}
580
581impl FromStr for ConfigPath {
582    type Err = Error;
583    fn from_str(s: &str) -> Result<Self, Self::Err> {
584        Ok(Self::new(PathBuf::from(s)))
585    }
586}
587
588/// A command enforced for a user connecting over SSH.
589///
590/// Tracks specific executables that are set using [ForceCommand] in an [sshd_config] drop-in
591/// configuration.
592///
593/// [sshd_config]: https://man.archlinux.org/man/sshd_config.5
594/// [ForceCommand]: https://man.archlinux.org/man/sshd_config.5#ForceCommand
595#[derive(strum::AsRefStr, Debug, strum::Display, strum::EnumString, strum::VariantNames)]
596pub enum SshForceCommand {
597    /// Enforce calling signstar-download-backup
598    #[strum(serialize = "signstar-download-backup")]
599    DownloadBackup,
600
601    /// Enforce calling signstar-download-key-certificate
602    #[strum(serialize = "signstar-download-key-certificate")]
603    DownloadKeyCertificate,
604
605    /// Enforce calling signstar-download-metrics
606    #[strum(serialize = "signstar-download-metrics")]
607    DownloadMetrics,
608
609    /// Enforce calling `signstar-shareholder` for handling SSS shares.
610    #[strum(serialize = "signstar-shareholder")]
611    Shareholder,
612
613    /// Enforce calling signstar-download-wireguard
614    #[strum(serialize = "signstar-download-wireguard")]
615    DownloadWireGuard,
616
617    /// Enforce calling `signstar-sign`.
618    #[strum(serialize = "signstar-sign")]
619    Sign,
620
621    /// Enforce calling signstar-upload-backup
622    #[strum(serialize = "signstar-upload-backup")]
623    UploadBackup,
624
625    /// Enforce calling signstar-upload-update
626    #[strum(serialize = "signstar-upload-update")]
627    UploadUpdate,
628}
629
630impl From<&SystemUserMapping> for SshForceCommand {
631    fn from(value: &SystemUserMapping) -> Self {
632        match value {
633            SystemUserMapping::ShareHolder { .. } => SshForceCommand::Shareholder,
634            SystemUserMapping::WireGuardDownload { .. } => SshForceCommand::DownloadWireGuard,
635        }
636    }
637}
638
639#[cfg(feature = "nethsm")]
640impl TryFrom<&NetHsmUserMapping> for SshForceCommand {
641    type Error = Error;
642
643    fn try_from(value: &NetHsmUserMapping) -> Result<Self, Self::Error> {
644        match value {
645            NetHsmUserMapping::Admin(admin) => Err(Error::NoForceCommandForMapping {
646                backend_users: vec![admin.to_string()],
647                system_user: None,
648            }),
649            NetHsmUserMapping::Backup { .. } => Ok(Self::DownloadBackup),
650            NetHsmUserMapping::HermeticMetrics {
651                backend_users,
652                system_user,
653            } => Err(Error::NoForceCommandForMapping {
654                backend_users: backend_users
655                    .get_users()
656                    .iter()
657                    .map(|user| user.to_string())
658                    .collect(),
659                system_user: Some(system_user.to_string()),
660            }),
661            NetHsmUserMapping::Metrics { .. } => Ok(Self::DownloadMetrics),
662            NetHsmUserMapping::Signing { .. } => Ok(SshForceCommand::Sign),
663        }
664    }
665}
666
667#[cfg(feature = "yubihsm2")]
668impl TryFrom<&YubiHsm2UserMapping> for SshForceCommand {
669    type Error = Error;
670
671    fn try_from(value: &YubiHsm2UserMapping) -> Result<Self, Self::Error> {
672        match value {
673            YubiHsm2UserMapping::Admin {
674                authentication_key_id,
675            } => Err(Error::NoForceCommandForMapping {
676                backend_users: vec![authentication_key_id.to_string()],
677                system_user: None,
678            }),
679            YubiHsm2UserMapping::AuditLog { .. } => Ok(SshForceCommand::DownloadMetrics),
680            YubiHsm2UserMapping::Backup { .. } => Ok(SshForceCommand::DownloadBackup),
681            YubiHsm2UserMapping::HermeticAuditLog {
682                authentication_key_id,
683                system_user,
684            } => Err(Error::NoForceCommandForMapping {
685                backend_users: vec![authentication_key_id.to_string()],
686                system_user: Some(system_user.to_string()),
687            }),
688            YubiHsm2UserMapping::Signing { .. } => Ok(SshForceCommand::Sign),
689        }
690    }
691}
692
693/// Checks whether the current process is run by root.
694///
695/// Gets the effective user ID of the current process and checks whether it is `0`.
696///
697/// # Errors
698///
699/// Returns an error if
700/// - conversion of PID to usize `fails`
701/// - the root user ID can not be converted from `"0"`
702/// - no user ID can be retrieved from the current process
703/// - the process is not run by root
704pub fn ensure_root() -> Result<(), Error> {
705    let pid: usize = id()
706        .try_into()
707        .map_err(|_| Error::FailedU32ToUsizeConversion)?;
708
709    let system = System::new_all();
710    let Some(process) = system.process(Pid::from(pid)) else {
711        return Err(Error::NoProcess);
712    };
713
714    let Some(uid) = process.effective_user_id() else {
715        return Err(Error::NoUidForProcess(pid));
716    };
717
718    let root_uid_str = "0";
719    let root_uid = sysinfo::Uid::from_str(root_uid_str)
720        .map_err(|_| Error::SysUidFromStr(root_uid_str.to_string()))?;
721
722    if uid.ne(&root_uid) {
723        return Err(Error::NotRoot);
724    }
725
726    Ok(())
727}