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 deal with the metrics of a Signstar backend.
247    ///
248    /// # Note
249    ///
250    /// This user must not be setup for remote login.
251    BackendHermeticMetrics {
252        /// The system user.
253        system_user: &'a SystemUserId,
254    },
255
256    /// The system user is used to deal with the metrics of a Signstar backend.
257    BackendMetrics {
258        /// The system user.
259        system_user: &'a SystemUserId,
260        /// The SSH authorized key for `system_user`.
261        ssh_authorized_key: &'a AuthorizedKeyEntry,
262    },
263
264    /// The system user is used for signing operations with a Signstar backend.
265    BackendSign {
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 the upload of Signstar backend firmware updates.
273    BackendUpdate {
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 to download network config data.
281    HostDownloadNetworkConfig {
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 handle shares of a shared secret.
289    HostShareholder {
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    /// It is not known what the system user is used for.
297    ///
298    /// # Note
299    ///
300    /// This variant is commonly used for all system user information derived from a host.
301    Unknown {
302        /// The system user.
303        system_user: SystemUserId,
304        /// The SSH authorized key for `system_user`.
305        ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
306        /// The home directory of `system_user`.
307        home_dir: PathBuf,
308    },
309}
310
311impl<'a> SystemUserData<'a> {
312    /// Returns a reference to the tracked [`SystemUserId`].
313    pub fn system_user(&'a self) -> &'a SystemUserId {
314        match self {
315            Self::BackendAdmin { system_user } | Self::Unknown { system_user, .. } => system_user,
316            Self::BackendBackup { system_user, .. }
317            | Self::BackendHermeticMetrics { system_user }
318            | Self::BackendMetrics { system_user, .. }
319            | Self::BackendSign { system_user, .. }
320            | Self::BackendUpdate { system_user, .. }
321            | Self::HostDownloadNetworkConfig { system_user, .. }
322            | Self::HostShareholder { system_user, .. } => system_user,
323        }
324    }
325
326    /// Returns a list of references to tracked [`AuthorizedKeyEntry`].
327    pub fn ssh_authorized_keys(&'a self) -> Vec<&'a AuthorizedKeyEntry> {
328        match self {
329            Self::BackendAdmin { .. } | Self::BackendHermeticMetrics { .. } => Vec::new(),
330            Self::BackendBackup {
331                ssh_authorized_key, ..
332            }
333            | Self::BackendMetrics {
334                ssh_authorized_key, ..
335            }
336            | Self::BackendSign {
337                ssh_authorized_key, ..
338            }
339            | Self::HostDownloadNetworkConfig {
340                ssh_authorized_key, ..
341            }
342            | Self::HostShareholder {
343                ssh_authorized_key, ..
344            }
345            | Self::BackendUpdate {
346                ssh_authorized_key, ..
347            } => vec![ssh_authorized_key],
348            Self::Unknown {
349                ssh_authorized_keys,
350                ..
351            } => ssh_authorized_keys.iter().collect::<Vec<_>>(),
352        }
353    }
354}
355
356impl<'a> Display for SystemUserData<'a> {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        write!(f, "system user {} ", self.system_user())?;
359        let ssh_authorized_keys = self.ssh_authorized_keys();
360        if let Self::Unknown { home_dir, .. } = self {
361            write!(f, "in home dir {home_dir:?} ")?;
362        }
363        if !ssh_authorized_keys.is_empty() {
364            write!(
365                f,
366                "with ssh keys {} ",
367                ssh_authorized_keys
368                    .iter()
369                    .map(ToString::to_string)
370                    .collect::<Vec<_>>()
371                    .join(", ")
372            )?;
373        }
374
375        write!(
376            f,
377            "{}",
378            match self {
379                Self::BackendAdmin { .. } => "for backend administration",
380                Self::BackendBackup { .. } => "for backend backups",
381                Self::BackendHermeticMetrics { .. } => "for hermetic backend metrics",
382                Self::BackendMetrics { .. } => "for backend metrics",
383                Self::BackendSign { .. } => "for signing using a backend",
384                Self::BackendUpdate { .. } => "for backend updates",
385                Self::HostDownloadNetworkConfig { .. } => "for downloading host network config",
386                Self::HostShareholder { .. } => "for handling shares of a shared secret",
387                Self::Unknown { .. } => "for unknown use",
388            }
389        )
390    }
391}
392
393/// The state of a host.
394#[derive(Debug, Eq, PartialEq)]
395pub struct SystemUserHostState<'a> {
396    pub(crate) system_user_data: HashSet<SystemUserData<'a>>,
397}
398
399impl<'a> SystemUserHostState<'a> {
400    /// The name of the origin for the state.
401    pub const STATE_NAME: &'static str = "system";
402
403    /// Creates a new [`SystemUserHostState`] from system users and associated data on the host.
404    ///
405    /// # Note
406    ///
407    /// The user data collected from the current system is always of the form
408    /// [`SystemUserData::Unknown`], because without further context we cannot know (yet) whether a
409    /// given system user is supposed to be used by the Signstar system or not.
410    ///
411    /// # Safety
412    ///
413    /// Uses `unsafe` functions to retrieve the user data on the current system (see
414    /// [`uzers::all_users`] for details).
415    ///
416    /// # Errors
417    ///
418    /// Returns an error if
419    ///
420    /// - a user record cannot be created from an item in `/etc/passwd`
421    /// - a file with SSH authorized keys cannot be read
422    /// - a file with SSH authorized keys contains an invalid SSH authorized key
423    pub fn new() -> Result<Self, crate::Error> {
424        let user_data = unsafe { all_users() }.collect::<Vec<_>>();
425        let mut system_user_data = HashSet::new();
426
427        for user in user_data {
428            let user = User::from_name(&user.name().to_string_lossy())
429                .map_err(|_source| Error::InvalidSystemUserName {
430                    name: user.name().to_string_lossy().to_string(),
431                })?
432                .ok_or(Error::InvalidSystemUserName {
433                    name: user.name().to_string_lossy().to_string(),
434                })?;
435
436            // Retrieve all SSH authorized keys that can be found for the user.
437            let ssh_authorized_keys = {
438                let mut ssh_authorized_keys = Vec::new();
439
440                let files = [
441                    // The Signstar authorized_keys configuration location.
442                    get_ssh_authorized_key_base_dir()
443                        .join(format!("signstar-user-{}.authorized_keys", user.name)),
444                    // The default SSH authorized_keys configuration location.
445                    get_home_base_dir_path()
446                        .join(&user.name)
447                        .join(".ssh")
448                        .join("authorized_keys"),
449                ];
450
451                for file in files {
452                    if file.is_file() {
453                        for line in read_to_string(&file)
454                            .map_err(|source| crate::Error::IoPath {
455                                path: file,
456                                context: "reading the file to string",
457                                source,
458                            })?
459                            .lines()
460                        {
461                            ssh_authorized_keys.push(AuthorizedKeyEntry::from_str(line)?);
462                        }
463                    }
464                }
465
466                ssh_authorized_keys
467            };
468
469            system_user_data.insert(SystemUserData::Unknown {
470                system_user: SystemUserId::try_from(&user)?,
471                ssh_authorized_keys,
472                home_dir: user.dir,
473            });
474        }
475
476        Ok(Self { system_user_data })
477    }
478
479    /// Returns a reference to the set of [`SystemUserData`].
480    pub fn system_user_data(&self) -> &HashSet<SystemUserData<'a>> {
481        &self.system_user_data
482    }
483}
484
485impl<'a> StateOriginInfo for SystemUserHostState<'a> {
486    fn state_name(&self) -> &str {
487        Self::STATE_NAME
488    }
489
490    fn state_origin(&self) -> StateOrigin {
491        StateOrigin::System
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use rstest::rstest;
498    use testresult::TestResult;
499
500    use super::*;
501
502    #[test]
503    fn system_user_id_new_fails() {
504        assert!(SystemUserId::new("üser".to_string()).is_err());
505    }
506
507    #[test]
508    fn authorized_key_entry_new_fails() {
509        assert!(AuthorizedKeyEntry::new("foo".to_string()).is_err());
510    }
511
512    #[test]
513    fn authorized_key_to_string() -> TestResult {
514        let entry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host";
515        let authorized_key = AuthorizedKeyEntry::new(entry.to_string())?;
516
517        assert_eq!(authorized_key.to_string(), entry);
518        Ok(())
519    }
520
521    #[rstest]
522    #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, SystemUserId::new("root".to_string())?)]
523    #[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())?)]
524    #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, SystemUserId::new("hermetic-metrics".to_string())?)]
525    #[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())?)]
526    #[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())?)]
527    #[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())?)]
528    #[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())?)]
529    #[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())?)]
530    #[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())?)]
531    fn system_user_data_system_user<'a>(
532        #[case] system_user_data: SystemUserData<'a>,
533        #[case] system_user: SystemUserId,
534    ) -> TestResult {
535        assert_eq!(system_user_data.system_user(), &system_user);
536        Ok(())
537    }
538
539    #[rstest]
540    #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, Vec::new())]
541    #[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()?])]
542    #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, Vec::new())]
543    #[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()?])]
544    #[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()?])]
545    #[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()?])]
546    #[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()?])]
547    #[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()?])]
548    #[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()?])]
549    fn system_user_data_ssh_authorized_keys<'a>(
550        #[case] system_user_data: SystemUserData<'a>,
551        #[case] ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
552    ) -> TestResult {
553        assert_eq!(
554            system_user_data.ssh_authorized_keys(),
555            ssh_authorized_keys.iter().collect::<Vec<_>>()
556        );
557        Ok(())
558    }
559
560    #[cfg(target_os = "linux")]
561    #[test]
562    fn system_user_host_state_new_contains_root() -> TestResult {
563        let state = SystemUserHostState::new()?;
564
565        assert!(state.system_user_data.contains(&SystemUserData::Unknown {
566            system_user: SystemUserId::root(),
567            ssh_authorized_keys: Vec::new(),
568            home_dir: PathBuf::from("/root"),
569        }));
570
571        Ok(())
572    }
573}