signstar_config/config/
credentials.rs

1//! Credentials handling for [`SignstarConfig`].
2
3use std::{fmt::Display, str::FromStr};
4
5use nethsm::UserId;
6use serde::{Deserialize, Serialize};
7use ssh_key::authorized_keys::Entry;
8use zeroize::Zeroize;
9
10use crate::ConfigError;
11#[cfg(doc)]
12use crate::SignstarConfig;
13
14/// The name of a user on a Unix system
15///
16/// The username may only contain characters in the set of alphanumeric ASCII characters and the
17/// `-`, or `_` character.
18#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Zeroize)]
19#[serde(into = "String", try_from = "String")]
20pub struct SystemUserId(String);
21
22impl SystemUserId {
23    /// Creates a new [`SystemUserId`]
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if `user` contains chars other than alphanumeric ones, `-`, or `_`.
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use signstar_config::SystemUserId;
33    ///
34    /// # fn main() -> testresult::TestResult {
35    /// SystemUserId::new("user1".to_string())?;
36    /// SystemUserId::new("User_1".to_string())?;
37    /// assert!(SystemUserId::new("?ser-1".to_string()).is_err());
38    /// # Ok(())
39    /// # }
40    /// ```
41    pub fn new(user: String) -> Result<Self, crate::Error> {
42        if user.is_empty()
43            || !(user
44                .chars()
45                .all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-'))
46        {
47            return Err(ConfigError::InvalidSystemUserName { name: user }.into());
48        }
49        Ok(Self(user))
50    }
51}
52
53impl AsRef<str> for SystemUserId {
54    fn as_ref(&self) -> &str {
55        &self.0
56    }
57}
58
59impl Display for SystemUserId {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        self.0.fmt(f)
62    }
63}
64
65impl From<SystemUserId> for String {
66    fn from(value: SystemUserId) -> Self {
67        value.0
68    }
69}
70
71impl FromStr for SystemUserId {
72    type Err = crate::Error;
73
74    fn from_str(s: &str) -> Result<Self, Self::Err> {
75        Self::new(s.to_string())
76    }
77}
78
79impl TryFrom<String> for SystemUserId {
80    type Error = crate::Error;
81
82    fn try_from(value: String) -> Result<Self, Self::Error> {
83        Self::new(value)
84    }
85}
86
87/// An entry of an authorized_keys file
88///
89/// This type ensures compliance with SSH's [AuhtorizedKeysFile] format.
90///
91/// [AuhtorizedKeysFile]: https://man.archlinux.org/man/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
92#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Zeroize)]
93#[serde(into = "String", try_from = "String")]
94pub struct AuthorizedKeyEntry(String);
95
96impl AuthorizedKeyEntry {
97    /// Creates a new [`AuthorizedKeyEntry`]
98    ///
99    /// # Errors
100    ///
101    /// Returns an error, if `data` can not be converted to an
102    /// [`ssh_key::authorized_keys::Entry`].
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use signstar_config::AuthorizedKeyEntry;
108    ///
109    /// # fn main() -> testresult::TestResult {
110    /// AuthorizedKeyEntry::new("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".to_string())?;
111    ///
112    /// // this fails because the empty string is not a valid AuthorizedKeyEntry
113    /// assert!(AuthorizedKeyEntry::new("".to_string()).is_err());
114    /// # Ok(())
115    /// # }
116    /// ```
117    pub fn new(entry: String) -> Result<Self, crate::Error> {
118        if Entry::from_str(&entry).is_err() {
119            return Err(ConfigError::InvalidAuthorizedKeyEntry { entry }.into());
120        }
121
122        Ok(Self(entry))
123    }
124}
125
126impl AsRef<str> for AuthorizedKeyEntry {
127    fn as_ref(&self) -> &str {
128        &self.0
129    }
130}
131
132impl Display for AuthorizedKeyEntry {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        self.0.fmt(f)
135    }
136}
137
138impl From<AuthorizedKeyEntry> for String {
139    fn from(value: AuthorizedKeyEntry) -> Self {
140        value.to_string()
141    }
142}
143
144impl FromStr for AuthorizedKeyEntry {
145    type Err = crate::Error;
146
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        Self::new(s.to_string())
149    }
150}
151
152impl TryFrom<&AuthorizedKeyEntry> for Entry {
153    type Error = crate::Error;
154
155    fn try_from(value: &AuthorizedKeyEntry) -> Result<Self, crate::Error> {
156        Entry::from_str(&value.0)
157            .map_err(|source| crate::Error::Config(ConfigError::SshKey(source)))
158    }
159}
160
161impl TryFrom<String> for AuthorizedKeyEntry {
162    type Error = crate::Error;
163
164    fn try_from(value: String) -> Result<Self, crate::Error> {
165        Self::new(value)
166    }
167}
168
169/// A guaranteed to be system-wide [`NetHsm`][`nethsm::NetHsm`] user
170#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
171#[serde(into = "String", try_from = "String")]
172pub struct SystemWideUserId(UserId);
173
174impl SystemWideUserId {
175    /// Creates a new [`SystemWideUserId`] from an owned string
176    ///
177    /// # Errors
178    ///
179    /// Returns an error, if the provided `user_id` contains a namespace.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use signstar_config::SystemWideUserId;
185    ///
186    /// # fn main() -> testresult::TestResult {
187    /// SystemWideUserId::new("user1".to_string())?;
188    ///
189    /// // this fails because the User ID contains a namespace
190    /// assert!(SystemWideUserId::new("ns1~user1".to_string()).is_err());
191    /// # Ok(())
192    /// # }
193    /// ```
194    pub fn new(user_id: String) -> Result<Self, crate::Error> {
195        let user_id = UserId::new(user_id)
196            .map_err(|source| crate::Error::Config(ConfigError::User(source)))?;
197        if user_id.is_namespaced() {
198            return Err(ConfigError::SystemWideUserIdWithNamespace(user_id).into());
199        }
200        Ok(Self(user_id))
201    }
202}
203
204impl Display for SystemWideUserId {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        self.0.fmt(f)
207    }
208}
209
210impl FromStr for SystemWideUserId {
211    type Err = crate::Error;
212    fn from_str(s: &str) -> Result<Self, Self::Err> {
213        Self::new(s.to_string())
214    }
215}
216
217impl From<SystemWideUserId> for String {
218    fn from(value: SystemWideUserId) -> Self {
219        value.to_string()
220    }
221}
222
223impl From<SystemWideUserId> for UserId {
224    fn from(value: SystemWideUserId) -> Self {
225        value.0
226    }
227}
228
229impl TryFrom<String> for SystemWideUserId {
230    type Error = crate::Error;
231
232    fn try_from(value: String) -> Result<Self, Self::Error> {
233        Self::new(value)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use testresult::TestResult;
240
241    use super::*;
242
243    #[test]
244    fn system_user_id_new_fails() {
245        assert!(SystemUserId::new("üser".to_string()).is_err());
246    }
247
248    #[test]
249    fn authorized_key_entry_new_fails() {
250        assert!(AuthorizedKeyEntry::new("foo".to_string()).is_err());
251    }
252
253    #[test]
254    fn authorized_key_as_ref() -> TestResult {
255        let entry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host";
256        let authorized_key = AuthorizedKeyEntry::new(entry.to_string())?;
257
258        assert_eq!(authorized_key.as_ref(), entry);
259        Ok(())
260    }
261
262    #[test]
263    fn system_wide_user_id_new_fails() -> TestResult {
264        assert!(SystemWideUserId::new("ns1~test".to_string()).is_err());
265        Ok(())
266    }
267
268    #[test]
269    fn system_wide_user_id_from_str() -> TestResult {
270        assert!(SystemWideUserId::from_str("ns1~test").is_err());
271        assert!(SystemWideUserId::from_str("test").is_ok());
272        Ok(())
273    }
274}