Skip to main content

signstar_config/config/
credentials.rs

1//! Credentials handling for Signstar configuration.
2
3use std::{fmt::Display, str::FromStr};
4
5use nix::unistd::User;
6use serde::{Deserialize, Serialize};
7use ssh_key::authorized_keys::Entry;
8use zeroize::Zeroize;
9
10use crate::{ConfigError, utils::get_current_system_user};
11
12/// The name of a user on a Unix system
13///
14/// The username may only contain characters in the set of alphanumeric ASCII characters and the
15/// `-`, or `_` character.
16#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Zeroize)]
17#[serde(into = "String", try_from = "String")]
18pub struct SystemUserId(String);
19
20impl SystemUserId {
21    /// Creates a new [`SystemUserId`]
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if `user` contains chars other than alphanumeric ones, `-`, or `_`.
26    ///
27    /// # Examples
28    ///
29    /// ```
30    /// use signstar_config::SystemUserId;
31    ///
32    /// # fn main() -> testresult::TestResult {
33    /// SystemUserId::new("user1".to_string())?;
34    /// SystemUserId::new("User_1".to_string())?;
35    /// assert!(SystemUserId::new("?ser-1".to_string()).is_err());
36    /// # Ok(())
37    /// # }
38    /// ```
39    pub fn new(user: String) -> Result<Self, crate::Error> {
40        if user.is_empty()
41            || !(user
42                .chars()
43                .all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-'))
44        {
45            return Err(ConfigError::InvalidSystemUserName { name: user }.into());
46        }
47        Ok(Self(user))
48    }
49
50    /// Creates a new [`SystemUserId`] from the currently calling Unix user.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if
55    ///
56    /// - the currently calling Unix user cannot be determined
57    /// - the String representation of the currently calling Unix user cannot be used to create a
58    ///   new [`SystemUserId`]
59    pub fn from_current_unix_user() -> Result<Self, crate::Error> {
60        let current_unix_user = get_current_system_user()?;
61        Self::try_from(current_unix_user)
62    }
63}
64
65impl AsRef<str> for SystemUserId {
66    fn as_ref(&self) -> &str {
67        &self.0
68    }
69}
70
71impl Display for SystemUserId {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        self.0.fmt(f)
74    }
75}
76
77impl From<SystemUserId> for String {
78    fn from(value: SystemUserId) -> Self {
79        value.0
80    }
81}
82
83impl FromStr for SystemUserId {
84    type Err = crate::Error;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        Self::new(s.to_string())
88    }
89}
90
91impl TryFrom<String> for SystemUserId {
92    type Error = crate::Error;
93
94    fn try_from(value: String) -> Result<Self, Self::Error> {
95        Self::new(value)
96    }
97}
98
99impl TryFrom<User> for SystemUserId {
100    type Error = crate::Error;
101
102    fn try_from(value: User) -> Result<Self, Self::Error> {
103        Self::new(value.name)
104    }
105}
106
107/// An entry of an authorized_keys file
108///
109/// This type ensures compliance with SSH's [AuhtorizedKeysFile] format.
110///
111/// [AuhtorizedKeysFile]: https://man.archlinux.org/man/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
112#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
113#[serde(into = "String", try_from = "String")]
114pub struct AuthorizedKeyEntry(Entry);
115
116impl AuthorizedKeyEntry {
117    /// Creates a new [`AuthorizedKeyEntry`]
118    ///
119    /// # Errors
120    ///
121    /// Returns an error, if `data` can not be converted to an
122    /// [`ssh_key::authorized_keys::Entry`].
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use signstar_config::AuthorizedKeyEntry;
128    ///
129    /// # fn main() -> testresult::TestResult {
130    /// let auth_key = AuthorizedKeyEntry::new("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".to_string())?;
131    /// assert_eq!("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host", auth_key.to_string());
132    ///
133    /// // this fails because the empty string is not a valid AuthorizedKeyEntry
134    /// assert!(AuthorizedKeyEntry::new("".to_string()).is_err());
135    /// # Ok(())
136    /// # }
137    /// ```
138    pub fn new(entry: String) -> Result<Self, crate::Error> {
139        Ok(Self(Entry::from_str(&entry).map_err(|_source| {
140            ConfigError::InvalidAuthorizedKeyEntry { entry }
141        })?))
142    }
143}
144
145impl AsRef<Entry> for AuthorizedKeyEntry {
146    fn as_ref(&self) -> &Entry {
147        &self.0
148    }
149}
150
151impl Display for AuthorizedKeyEntry {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(f, "{}", self.0.to_string())
154    }
155}
156
157impl From<AuthorizedKeyEntry> for String {
158    fn from(value: AuthorizedKeyEntry) -> Self {
159        value.to_string()
160    }
161}
162
163impl FromStr for AuthorizedKeyEntry {
164    type Err = crate::Error;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        Self::new(s.to_string())
168    }
169}
170
171impl From<&AuthorizedKeyEntry> for Entry {
172    fn from(value: &AuthorizedKeyEntry) -> Self {
173        value.0.clone()
174    }
175}
176
177impl TryFrom<String> for AuthorizedKeyEntry {
178    type Error = crate::Error;
179
180    fn try_from(value: String) -> Result<Self, crate::Error> {
181        Self::new(value)
182    }
183}
184
185impl std::hash::Hash for AuthorizedKeyEntry {
186    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
187        self.0.to_string().hash(state);
188    }
189}
190
191impl Ord for AuthorizedKeyEntry {
192    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
193        self.0.to_string().cmp(&other.0.to_string())
194    }
195}
196
197impl PartialOrd for AuthorizedKeyEntry {
198    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
199        Some(self.cmp(other))
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use testresult::TestResult;
206
207    use super::*;
208
209    #[test]
210    fn system_user_id_new_fails() {
211        assert!(SystemUserId::new("üser".to_string()).is_err());
212    }
213
214    #[test]
215    fn authorized_key_entry_new_fails() {
216        assert!(AuthorizedKeyEntry::new("foo".to_string()).is_err());
217    }
218
219    #[test]
220    fn authorized_key_to_string() -> TestResult {
221        let entry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host";
222        let authorized_key = AuthorizedKeyEntry::new(entry.to_string())?;
223
224        assert_eq!(authorized_key.to_string(), entry);
225        Ok(())
226    }
227}