Skip to main content

signstar_config/config/
credentials.rs

1//! Credentials handling for Signstar configuration.
2
3use std::{collections::HashSet, fmt::Display, fs::read_to_string, path::PathBuf, str::FromStr};
4
5use nix::unistd::User;
6use serde::{Deserialize, Serialize};
7use signstar_common::{ssh::get_ssh_authorized_key_base_dir, system_user::get_home_base_dir_path};
8use ssh_key::authorized_keys::Entry;
9use uzers::all_users;
10use zeroize::Zeroize;
11
12use crate::{
13    config::Error,
14    state::{StateOrigin, StateOriginInfo},
15    utils::get_current_system_user,
16};
17
18/// The name of a user on a Unix system
19///
20/// The username may only contain characters in the set of alphanumeric ASCII characters and the
21/// `-`, or `_` character.
22#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Zeroize)]
23#[serde(into = "String", try_from = "String")]
24pub struct SystemUserId(String);
25
26impl SystemUserId {
27    /// The name of the root user.
28    const ROOT: &str = "root";
29
30    /// Creates a new [`SystemUserId`]
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if `user` contains chars other than alphanumeric ones, `-`, or `_`.
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use signstar_config::config::SystemUserId;
40    ///
41    /// # fn main() -> testresult::TestResult {
42    /// SystemUserId::new("user1".to_string())?;
43    /// SystemUserId::new("User_1".to_string())?;
44    /// assert!(SystemUserId::new("?ser-1".to_string()).is_err());
45    /// # Ok(())
46    /// # }
47    /// ```
48    pub fn new(user: String) -> Result<Self, crate::Error> {
49        if user.is_empty()
50            || !(user
51                .chars()
52                .all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-'))
53        {
54            return Err(Error::InvalidSystemUserName { name: user }.into());
55        }
56        Ok(Self(user))
57    }
58
59    /// Returns the root user.
60    pub fn root() -> Self {
61        Self(Self::ROOT.to_string())
62    }
63
64    /// Creates a new [`SystemUserId`] from the currently calling Unix user.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if
69    ///
70    /// - the currently calling Unix user cannot be determined
71    /// - the String representation of the currently calling Unix user cannot be used to create a
72    ///   new [`SystemUserId`]
73    pub fn from_current_unix_user() -> Result<Self, crate::Error> {
74        let current_unix_user = get_current_system_user()?;
75        Self::try_from(current_unix_user)
76    }
77}
78
79impl AsRef<str> for SystemUserId {
80    fn as_ref(&self) -> &str {
81        &self.0
82    }
83}
84
85impl Display for SystemUserId {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        self.0.fmt(f)
88    }
89}
90
91impl From<SystemUserId> for String {
92    fn from(value: SystemUserId) -> Self {
93        value.0
94    }
95}
96
97impl FromStr for SystemUserId {
98    type Err = crate::Error;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        Self::new(s.to_string())
102    }
103}
104
105impl TryFrom<String> for SystemUserId {
106    type Error = crate::Error;
107
108    fn try_from(value: String) -> Result<Self, Self::Error> {
109        Self::new(value)
110    }
111}
112
113impl TryFrom<&User> for SystemUserId {
114    type Error = crate::Error;
115
116    fn try_from(value: &User) -> Result<Self, Self::Error> {
117        Self::new(value.name.clone())
118    }
119}
120
121impl TryFrom<User> for SystemUserId {
122    type Error = crate::Error;
123
124    fn try_from(value: User) -> Result<Self, Self::Error> {
125        Self::new(value.name)
126    }
127}
128
129/// An entry of an authorized_keys file
130///
131/// This type ensures compliance with SSH's [AuhtorizedKeysFile] format.
132///
133/// [AuhtorizedKeysFile]: https://man.archlinux.org/man/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
134#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
135#[serde(into = "String", try_from = "String")]
136pub struct AuthorizedKeyEntry(Entry);
137
138impl AuthorizedKeyEntry {
139    /// Creates a new [`AuthorizedKeyEntry`]
140    ///
141    /// # Errors
142    ///
143    /// Returns an error, if `data` can not be converted to an
144    /// [`ssh_key::authorized_keys::Entry`].
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use signstar_config::config::AuthorizedKeyEntry;
150    ///
151    /// # fn main() -> testresult::TestResult {
152    /// let auth_key = AuthorizedKeyEntry::new("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".to_string())?;
153    /// assert_eq!("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host", auth_key.to_string());
154    ///
155    /// // this fails because the empty string is not a valid AuthorizedKeyEntry
156    /// assert!(AuthorizedKeyEntry::new("".to_string()).is_err());
157    /// # Ok(())
158    /// # }
159    /// ```
160    pub fn new(entry: String) -> Result<Self, crate::Error> {
161        Ok(Self(Entry::from_str(&entry).map_err(|_source| {
162            Error::InvalidAuthorizedKeyEntry { entry }
163        })?))
164    }
165}
166
167impl AsRef<Entry> for AuthorizedKeyEntry {
168    fn as_ref(&self) -> &Entry {
169        &self.0
170    }
171}
172
173impl Display for AuthorizedKeyEntry {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        write!(f, "{}", self.0.to_string())
176    }
177}
178
179impl From<AuthorizedKeyEntry> for String {
180    fn from(value: AuthorizedKeyEntry) -> Self {
181        value.to_string()
182    }
183}
184
185impl FromStr for AuthorizedKeyEntry {
186    type Err = crate::Error;
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        Self::new(s.to_string())
190    }
191}
192
193impl From<&AuthorizedKeyEntry> for Entry {
194    fn from(value: &AuthorizedKeyEntry) -> Self {
195        value.0.clone()
196    }
197}
198
199impl TryFrom<String> for AuthorizedKeyEntry {
200    type Error = crate::Error;
201
202    fn try_from(value: String) -> Result<Self, crate::Error> {
203        Self::new(value)
204    }
205}
206
207impl std::hash::Hash for AuthorizedKeyEntry {
208    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
209        self.0.to_string().hash(state);
210    }
211}
212
213impl Ord for AuthorizedKeyEntry {
214    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
215        self.0.to_string().cmp(&other.0.to_string())
216    }
217}
218
219impl PartialOrd for AuthorizedKeyEntry {
220    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
221        Some(self.cmp(other))
222    }
223}
224
225/// The available data on system users on a Signstar host or its configuration.
226#[derive(Clone, Debug, Eq, Hash, PartialEq)]
227pub enum SystemUserData<'a> {
228    /// The system user is used to do administrative tasks on a Signstar backend.
229    ///
230    /// # Note
231    ///
232    /// This user must not be setup for remote login.
233    BackendAdmin {
234        /// The system user.
235        system_user: SystemUserId,
236    },
237
238    /// The system user is used to handle backup tasks for a Signstar backend.
239    BackendBackup {
240        /// The system user.
241        system_user: &'a SystemUserId,
242        /// The SSH authorized key for `system_user`.
243        ssh_authorized_key: &'a AuthorizedKeyEntry,
244    },
245
246    /// The system user is used to retrieve certificates from a Signstar backend.
247    BackendCertificateRetrieval {
248        /// The system user.
249        system_user: &'a SystemUserId,
250        /// The SSH authorized key for `system_user`.
251        ssh_authorized_key: &'a AuthorizedKeyEntry,
252    },
253
254    /// The system user is used to deal with the metrics of a Signstar backend.
255    ///
256    /// # Note
257    ///
258    /// This user must not be setup for remote login.
259    BackendHermeticMetrics {
260        /// The system user.
261        system_user: &'a SystemUserId,
262    },
263
264    /// The system user is used to deal with the metrics of a Signstar backend.
265    BackendMetrics {
266        /// The system user.
267        system_user: &'a SystemUserId,
268        /// The SSH authorized key for `system_user`.
269        ssh_authorized_key: &'a AuthorizedKeyEntry,
270    },
271
272    /// The system user is used for signing operations with a Signstar backend.
273    BackendSign {
274        /// The system user.
275        system_user: &'a SystemUserId,
276        /// The SSH authorized key for `system_user`.
277        ssh_authorized_key: &'a AuthorizedKeyEntry,
278    },
279
280    /// The system user is used for the upload of Signstar backend firmware updates.
281    BackendUpdate {
282        /// The system user.
283        system_user: &'a SystemUserId,
284        /// The SSH authorized key for `system_user`.
285        ssh_authorized_key: &'a AuthorizedKeyEntry,
286    },
287
288    /// The system user is used to download network config data.
289    HostDownloadNetworkConfig {
290        /// The system user.
291        system_user: &'a SystemUserId,
292        /// The SSH authorized key for `system_user`.
293        ssh_authorized_key: &'a AuthorizedKeyEntry,
294    },
295
296    /// The system user is used to handle shares of a shared secret.
297    HostShareholder {
298        /// The system user.
299        system_user: &'a SystemUserId,
300        /// The SSH authorized key for `system_user`.
301        ssh_authorized_key: &'a AuthorizedKeyEntry,
302    },
303
304    /// It is not known what the system user is used for.
305    ///
306    /// # Note
307    ///
308    /// This variant is commonly used for all system user information derived from a host.
309    Unknown {
310        /// The system user.
311        system_user: SystemUserId,
312        /// The SSH authorized key for `system_user`.
313        ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
314        /// The home directory of `system_user`.
315        home_dir: PathBuf,
316    },
317}
318
319impl<'a> SystemUserData<'a> {
320    /// Returns a reference to the tracked [`SystemUserId`].
321    pub fn system_user(&'a self) -> &'a SystemUserId {
322        match self {
323            Self::BackendAdmin { system_user } | Self::Unknown { system_user, .. } => system_user,
324            Self::BackendBackup { system_user, .. }
325            | Self::BackendCertificateRetrieval { system_user, .. }
326            | Self::BackendHermeticMetrics { system_user }
327            | Self::BackendMetrics { system_user, .. }
328            | Self::BackendSign { system_user, .. }
329            | Self::BackendUpdate { system_user, .. }
330            | Self::HostDownloadNetworkConfig { system_user, .. }
331            | Self::HostShareholder { system_user, .. } => system_user,
332        }
333    }
334
335    /// Returns a list of references to tracked [`AuthorizedKeyEntry`].
336    pub fn ssh_authorized_keys(&'a self) -> Vec<&'a AuthorizedKeyEntry> {
337        match self {
338            Self::BackendAdmin { .. } | Self::BackendHermeticMetrics { .. } => Vec::new(),
339            Self::BackendBackup {
340                ssh_authorized_key, ..
341            }
342            | Self::BackendCertificateRetrieval {
343                ssh_authorized_key, ..
344            }
345            | Self::BackendMetrics {
346                ssh_authorized_key, ..
347            }
348            | Self::BackendSign {
349                ssh_authorized_key, ..
350            }
351            | Self::HostDownloadNetworkConfig {
352                ssh_authorized_key, ..
353            }
354            | Self::HostShareholder {
355                ssh_authorized_key, ..
356            }
357            | Self::BackendUpdate {
358                ssh_authorized_key, ..
359            } => vec![ssh_authorized_key],
360            Self::Unknown {
361                ssh_authorized_keys,
362                ..
363            } => ssh_authorized_keys.iter().collect::<Vec<_>>(),
364        }
365    }
366}
367
368impl<'a> Display for SystemUserData<'a> {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        write!(f, "system user {} ", self.system_user())?;
371        let ssh_authorized_keys = self.ssh_authorized_keys();
372        if let Self::Unknown { home_dir, .. } = self {
373            write!(f, "in home dir {home_dir:?} ")?;
374        }
375        if !ssh_authorized_keys.is_empty() {
376            write!(
377                f,
378                "with ssh keys {} ",
379                ssh_authorized_keys
380                    .iter()
381                    .map(ToString::to_string)
382                    .collect::<Vec<_>>()
383                    .join(", ")
384            )?;
385        }
386
387        write!(
388            f,
389            "{}",
390            match self {
391                Self::BackendAdmin { .. } => "for backend administration",
392                Self::BackendBackup { .. } => "for backend backups",
393                Self::BackendCertificateRetrieval { .. } => "for backend certificate retrieval",
394                Self::BackendHermeticMetrics { .. } => "for hermetic backend metrics",
395                Self::BackendMetrics { .. } => "for backend metrics",
396                Self::BackendSign { .. } => "for signing using a backend",
397                Self::BackendUpdate { .. } => "for backend updates",
398                Self::HostDownloadNetworkConfig { .. } => "for downloading host network config",
399                Self::HostShareholder { .. } => "for handling shares of a shared secret",
400                Self::Unknown { .. } => "for unknown use",
401            }
402        )
403    }
404}
405
406/// The state of a host.
407#[derive(Debug, Eq, PartialEq)]
408pub struct SystemUserHostState<'a> {
409    pub(crate) system_user_data: HashSet<SystemUserData<'a>>,
410}
411
412impl<'a> SystemUserHostState<'a> {
413    /// The name of the origin for the state.
414    pub const STATE_NAME: &'static str = "system";
415
416    /// Creates a new [`SystemUserHostState`] from system users and associated data on the host.
417    ///
418    /// # Note
419    ///
420    /// The user data collected from the current system is always of the form
421    /// [`SystemUserData::Unknown`], because without further context we cannot know (yet) whether a
422    /// given system user is supposed to be used by the Signstar system or not.
423    ///
424    /// # Safety
425    ///
426    /// Uses `unsafe` functions to retrieve the user data on the current system (see
427    /// [`uzers::all_users`] for details).
428    ///
429    /// # Errors
430    ///
431    /// Returns an error if
432    ///
433    /// - a user record cannot be created from an item in `/etc/passwd`
434    /// - a file with SSH authorized keys cannot be read
435    /// - a file with SSH authorized keys contains an invalid SSH authorized key
436    pub fn new() -> Result<Self, crate::Error> {
437        let user_data = unsafe { all_users() }.collect::<Vec<_>>();
438        let mut system_user_data = HashSet::new();
439
440        for user in user_data {
441            let user = User::from_name(&user.name().to_string_lossy())
442                .map_err(|_source| Error::InvalidSystemUserName {
443                    name: user.name().to_string_lossy().to_string(),
444                })?
445                .ok_or(Error::InvalidSystemUserName {
446                    name: user.name().to_string_lossy().to_string(),
447                })?;
448
449            // Retrieve all SSH authorized keys that can be found for the user.
450            let ssh_authorized_keys = {
451                let mut ssh_authorized_keys = Vec::new();
452
453                let files = [
454                    // The Signstar authorized_keys configuration location.
455                    get_ssh_authorized_key_base_dir()
456                        .join(format!("signstar-user-{}.authorized_keys", user.name)),
457                    // The default SSH authorized_keys configuration location.
458                    get_home_base_dir_path()
459                        .join(&user.name)
460                        .join(".ssh")
461                        .join("authorized_keys"),
462                ];
463
464                for file in files {
465                    if file.is_file() {
466                        for line in read_to_string(&file)
467                            .map_err(|source| crate::Error::IoPath {
468                                path: file,
469                                context: "reading the file to string",
470                                source,
471                            })?
472                            .lines()
473                        {
474                            ssh_authorized_keys.push(AuthorizedKeyEntry::from_str(line)?);
475                        }
476                    }
477                }
478
479                ssh_authorized_keys
480            };
481
482            system_user_data.insert(SystemUserData::Unknown {
483                system_user: SystemUserId::try_from(&user)?,
484                ssh_authorized_keys,
485                home_dir: user.dir,
486            });
487        }
488
489        Ok(Self { system_user_data })
490    }
491
492    /// Returns a reference to the set of [`SystemUserData`].
493    pub fn system_user_data(&self) -> &HashSet<SystemUserData<'a>> {
494        &self.system_user_data
495    }
496}
497
498impl<'a> StateOriginInfo for SystemUserHostState<'a> {
499    fn state_name(&self) -> &str {
500        Self::STATE_NAME
501    }
502
503    fn state_origin(&self) -> StateOrigin {
504        StateOrigin::System
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use rstest::rstest;
511    use testresult::TestResult;
512
513    use super::*;
514
515    #[test]
516    fn system_user_id_new_fails() {
517        assert!(SystemUserId::new("üser".to_string()).is_err());
518    }
519
520    #[test]
521    fn authorized_key_entry_new_fails() {
522        assert!(AuthorizedKeyEntry::new("foo".to_string()).is_err());
523    }
524
525    #[test]
526    fn authorized_key_to_string() -> TestResult {
527        let entry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host";
528        let authorized_key = AuthorizedKeyEntry::new(entry.to_string())?;
529
530        assert_eq!(authorized_key.to_string(), entry);
531        Ok(())
532    }
533
534    #[rstest]
535    #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, SystemUserId::new("root".to_string())?)]
536    #[case::backend_backup(SystemUserData::BackendBackup { system_user: &SystemUserId::new("backup".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("backup".to_string())?)]
537    #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, SystemUserId::new("hermetic-metrics".to_string())?)]
538    #[case::backend_metrics(SystemUserData::BackendMetrics { system_user: &SystemUserId::new("metrics".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("metrics".to_string())?)]
539    #[case::backend_sign(SystemUserData::BackendSign { system_user: &SystemUserId::new("sign".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("sign".to_string())?)]
540    #[case::backend_update(SystemUserData::BackendUpdate { system_user: &SystemUserId::new("update".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("update".to_string())?)]
541    #[case::host_download_network_config(SystemUserData::HostDownloadNetworkConfig { system_user: &SystemUserId::new("network-download".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("network-download".to_string())?)]
542    #[case::host_shareholder(SystemUserData::HostShareholder { system_user: &SystemUserId::new("shareholder".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("shareholder".to_string())?)]
543    #[case::unknown(SystemUserData::Unknown { system_user: SystemUserId::new("someone".to_string())?, ssh_authorized_keys: vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?], home_dir: PathBuf::from("/home/someone") }, SystemUserId::new("someone".to_string())?)]
544    fn system_user_data_system_user<'a>(
545        #[case] system_user_data: SystemUserData<'a>,
546        #[case] system_user: SystemUserId,
547    ) -> TestResult {
548        assert_eq!(system_user_data.system_user(), &system_user);
549        Ok(())
550    }
551
552    #[rstest]
553    #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, Vec::new())]
554    #[case::backend_backup(SystemUserData::BackendBackup { system_user: &SystemUserId::new("backup".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
555    #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, Vec::new())]
556    #[case::backend_metrics(SystemUserData::BackendMetrics { system_user: &SystemUserId::new("metrics".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
557    #[case::backend_sign(SystemUserData::BackendSign { system_user: &SystemUserId::new("sign".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
558    #[case::backend_update(SystemUserData::BackendUpdate { system_user: &SystemUserId::new("update".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
559    #[case::host_download_network_config(SystemUserData::HostDownloadNetworkConfig { system_user: &SystemUserId::new("network-download".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
560    #[case::host_shareholder(SystemUserData::HostShareholder { system_user: &SystemUserId::new("shareholder".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
561    #[case::unknown(SystemUserData::Unknown { system_user: SystemUserId::new("someone".to_string())?, ssh_authorized_keys: vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?], home_dir: PathBuf::from("/home/someone") }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
562    fn system_user_data_ssh_authorized_keys<'a>(
563        #[case] system_user_data: SystemUserData<'a>,
564        #[case] ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
565    ) -> TestResult {
566        assert_eq!(
567            system_user_data.ssh_authorized_keys(),
568            ssh_authorized_keys.iter().collect::<Vec<_>>()
569        );
570        Ok(())
571    }
572
573    #[cfg(target_os = "linux")]
574    #[test]
575    fn system_user_host_state_new_contains_root() -> TestResult {
576        let state = SystemUserHostState::new()?;
577
578        assert!(state.system_user_data.contains(&SystemUserData::Unknown {
579            system_user: SystemUserId::root(),
580            ssh_authorized_keys: Vec::new(),
581            home_dir: PathBuf::from("/root"),
582        }));
583
584        Ok(())
585    }
586}