nethsm_config/
credentials.rs

1use std::{collections::HashSet, fmt::Display, str::FromStr};
2
3use nethsm::{Credentials, Passphrase, UserId, UserRole};
4use serde::{Deserialize, Serialize};
5use ssh_key::{PublicKey, authorized_keys::Entry};
6use zeroize::Zeroize;
7
8/// Errors related to credentials
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    /// There is a duplicate SSH public key in a list of SSH authorized keys
12    #[error("The SSH authorized key is used multiple times: {ssh_authorized_key}")]
13    DuplicateAuthorizedKeys {
14        ssh_authorized_key: AuthorizedKeyEntry,
15    },
16
17    /// A system username is invalid
18    #[error("Invalid system user name: {0}")]
19    InvalidSystemUserName(String),
20
21    /// There are no SSH authorized keys
22    #[error("The SSH authorized key is not valid: {entry}")]
23    InvalidAuthorizedKeyEntry { entry: String },
24
25    /// There are no SSH authorized keys
26    #[error("No SSH authorized key provided!")]
27    NoAuthorizedKeys,
28
29    /// An SSH key error
30    #[error("SSH key error: {0}")]
31    SshKey(#[from] ssh_key::Error),
32
33    /// A system-wide [`UserId`] has a namespace
34    #[error("The system-wide User ID has a namespace: {0}")]
35    SystemWideUserIdWithNamespace(UserId),
36
37    /// A [`NetHsm`][`nethsm::NetHsm`] user error
38    #[error("NetHSM user error: {0}")]
39    NetHsmUser(#[from] nethsm::UserError),
40}
41
42/// A set of credentials for a [`NetHsm`][`nethsm::NetHsm`]
43///
44/// Tracks the [`UserRole`], [`UserId`] and optionally the passphrase of the user.
45#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Eq, Serialize, Zeroize)]
46pub struct ConfigCredentials {
47    #[zeroize(skip)]
48    role: UserRole,
49    #[zeroize(skip)]
50    name: UserId,
51    passphrase: Option<String>,
52}
53
54impl ConfigCredentials {
55    /// Creates a new [`ConfigCredentials`]
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use nethsm::UserRole;
61    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity};
62    ///
63    /// # fn main() -> testresult::TestResult {
64    /// // credentials for an Operator user with passphrase
65    /// ConfigCredentials::new(
66    ///     UserRole::Operator,
67    ///     "user1".parse()?,
68    ///     Some("my-passphrase".into()),
69    /// );
70    ///
71    /// // credentials for an Administrator user without passphrase
72    /// ConfigCredentials::new(UserRole::Administrator, "admin1".parse()?, None);
73    /// # Ok(())
74    /// # }
75    /// ```
76    pub fn new(role: UserRole, name: UserId, passphrase: Option<String>) -> Self {
77        Self {
78            role,
79            name,
80            passphrase,
81        }
82    }
83
84    /// Returns the name (a [`UserId`])
85    pub fn get_name(&self) -> UserId {
86        self.name.clone()
87    }
88
89    /// Returns the role (a [`UserRole`])
90    pub fn get_role(&self) -> UserRole {
91        self.role
92    }
93
94    /// Returns the passphrase of the [`ConfigCredentials`]
95    pub fn get_passphrase(&self) -> Option<&str> {
96        self.passphrase.as_deref()
97    }
98
99    /// Sets the passphrase of the [`ConfigCredentials`]
100    pub fn set_passphrase(&mut self, passphrase: String) {
101        self.passphrase = Some(passphrase)
102    }
103
104    /// Returns whether a passphrase is set for the [`ConfigCredentials`]
105    pub fn has_passphrase(&self) -> bool {
106        self.passphrase.is_some()
107    }
108}
109
110impl From<ConfigCredentials> for Credentials {
111    fn from(value: ConfigCredentials) -> Self {
112        Self::new(value.name, value.passphrase.map(Passphrase::new))
113    }
114}
115
116/// The name of a user on a Unix system
117///
118/// The username may only contain characters in the set of alphanumeric characters and the `'_'`
119/// character.
120#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize, Zeroize)]
121#[serde(into = "String", try_from = "String")]
122pub struct SystemUserId(String);
123
124impl SystemUserId {
125    /// Creates a new [`SystemUserId`]
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if `user` contains chars other than alphanumeric ones, `-`, or `_`.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use nethsm_config::SystemUserId;
135    ///
136    /// # fn main() -> testresult::TestResult {
137    /// SystemUserId::new("user1".to_string())?;
138    /// SystemUserId::new("User_1".to_string())?;
139    /// assert!(SystemUserId::new("?ser-1".to_string()).is_err());
140    /// # Ok(())
141    /// # }
142    /// ```
143    pub fn new(user: String) -> Result<Self, Error> {
144        if user.is_empty()
145            || !(user
146                .chars()
147                .all(|char| char.is_alphanumeric() || char == '_' || char == '-'))
148        {
149            return Err(Error::InvalidSystemUserName(user));
150        }
151        Ok(Self(user))
152    }
153}
154
155impl AsRef<str> for SystemUserId {
156    fn as_ref(&self) -> &str {
157        &self.0
158    }
159}
160
161impl Display for SystemUserId {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        self.0.fmt(f)
164    }
165}
166
167impl From<SystemUserId> for String {
168    fn from(value: SystemUserId) -> Self {
169        value.to_string()
170    }
171}
172
173impl FromStr for SystemUserId {
174    type Err = Error;
175
176    fn from_str(s: &str) -> Result<Self, Self::Err> {
177        Self::new(s.to_string())
178    }
179}
180
181impl TryFrom<String> for SystemUserId {
182    type Error = Error;
183
184    fn try_from(value: String) -> Result<Self, Self::Error> {
185        Self::new(value)
186    }
187}
188
189/// An entry of an authorized_keys file
190///
191/// This type ensures compliance with SSH's [AuhtorizedKeysFile] format.
192///
193/// [AuhtorizedKeysFile]: https://man.archlinux.org/man/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
194#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize, Zeroize)]
195#[serde(into = "String", try_from = "String")]
196pub struct AuthorizedKeyEntry(String);
197
198impl AuthorizedKeyEntry {
199    /// Creates a new [`AuthorizedKeyEntry`]
200    ///
201    /// # Errors
202    ///
203    /// Returns an error, if `data` can not be converted to an
204    /// [`ssh_key::authorized_keys::Entry`].
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// use nethsm_config::AuthorizedKeyEntry;
210    ///
211    /// # fn main() -> testresult::TestResult {
212    /// AuthorizedKeyEntry::new("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".to_string())?;
213    ///
214    /// // this fails because the empty string is not a valid AuthorizedKeyEntry
215    /// assert!(AuthorizedKeyEntry::new("".to_string()).is_err());
216    /// # Ok(())
217    /// # }
218    /// ```
219    pub fn new(entry: String) -> Result<Self, Error> {
220        if Entry::from_str(&entry).is_err() {
221            return Err(Error::InvalidAuthorizedKeyEntry { entry });
222        }
223
224        Ok(Self(entry))
225    }
226}
227
228impl AsRef<str> for AuthorizedKeyEntry {
229    fn as_ref(&self) -> &str {
230        &self.0
231    }
232}
233
234impl Display for AuthorizedKeyEntry {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        self.0.fmt(f)
237    }
238}
239
240impl From<AuthorizedKeyEntry> for String {
241    fn from(value: AuthorizedKeyEntry) -> Self {
242        value.to_string()
243    }
244}
245
246impl FromStr for AuthorizedKeyEntry {
247    type Err = Error;
248
249    fn from_str(s: &str) -> Result<Self, Self::Err> {
250        Self::new(s.to_string())
251    }
252}
253
254impl TryFrom<&AuthorizedKeyEntry> for Entry {
255    type Error = Error;
256
257    fn try_from(value: &AuthorizedKeyEntry) -> Result<Self, Error> {
258        Entry::from_str(&value.0).map_err(Error::SshKey)
259    }
260}
261
262impl TryFrom<String> for AuthorizedKeyEntry {
263    type Error = Error;
264
265    fn try_from(value: String) -> Result<Self, Error> {
266        Self::new(value)
267    }
268}
269
270/// A list of [`AuthorizedKeyEntry`]s
271///
272/// The list is guaranteed to contain at least one item and be unique (no duplicate SSH public key
273/// can exist).
274#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)]
275#[serde(into = "Vec<String>", try_from = "Vec<String>")]
276pub struct AuthorizedKeyEntryList(Vec<AuthorizedKeyEntry>);
277
278impl AuthorizedKeyEntryList {
279    /// Creates a new [`AuthorizedKeyEntryList`]
280    ///
281    /// # Errors
282    ///
283    /// Returns an error, if a duplicate SSH public key exists in the provided list of
284    /// [`AuthorizedKeyEntry`] objects or if the list is empty.
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use nethsm_config::AuthorizedKeyEntryList;
290    ///
291    /// # fn main() -> testresult::TestResult {
292    /// AuthorizedKeyEntryList::new(vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINP4nWGVLC7kq4EdwgnJTXCjN0l32GL9ZxII6mx9uGqV user@host".parse()?])?;
293    ///
294    /// // this fails because the AuthorizedKeyEntry are duplicates
295    /// assert!(AuthorizedKeyEntryList::new(vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?]).is_err());
296    ///
297    /// // this fails because there are no SSH authorized keys
298    /// assert!(AuthorizedKeyEntryList::new(vec![]).is_err());
299    /// # Ok(())
300    /// # }
301    /// ```
302    pub fn new(ssh_authorized_keys: Vec<AuthorizedKeyEntry>) -> Result<Self, Error> {
303        if ssh_authorized_keys.is_empty() {
304            return Err(Error::NoAuthorizedKeys);
305        }
306
307        let mut set = HashSet::new();
308        for (ssh_authorized_key, pub_key) in ssh_authorized_keys
309            .iter()
310            .filter_map(|ssh_authorized_key| {
311                if let Ok(entry) = Entry::try_from(ssh_authorized_key) {
312                    Some((ssh_authorized_key.clone(), entry.public_key().clone()))
313                } else {
314                    None
315                }
316            })
317            .collect::<Vec<(AuthorizedKeyEntry, PublicKey)>>()
318        {
319            if !set.insert(pub_key) {
320                return Err(Error::DuplicateAuthorizedKeys { ssh_authorized_key });
321            }
322        }
323
324        Ok(Self(ssh_authorized_keys))
325    }
326}
327
328impl AsRef<[AuthorizedKeyEntry]> for AuthorizedKeyEntryList {
329    fn as_ref(&self) -> &[AuthorizedKeyEntry] {
330        &self.0
331    }
332}
333
334impl From<AuthorizedKeyEntryList> for Vec<String> {
335    fn from(value: AuthorizedKeyEntryList) -> Self {
336        value
337            .0
338            .iter()
339            .map(|authorized_key| authorized_key.to_string())
340            .collect()
341    }
342}
343
344impl From<&AuthorizedKeyEntryList> for Vec<AuthorizedKeyEntry> {
345    fn from(value: &AuthorizedKeyEntryList) -> Self {
346        value.0.to_vec()
347    }
348}
349
350impl TryFrom<Vec<String>> for AuthorizedKeyEntryList {
351    type Error = Error;
352
353    fn try_from(value: Vec<String>) -> Result<Self, Self::Error> {
354        let authorized_keys = {
355            let mut authorized_keys: Vec<AuthorizedKeyEntry> = vec![];
356            for authorized_key in value {
357                authorized_keys.push(AuthorizedKeyEntry::new(authorized_key)?)
358            }
359            authorized_keys
360        };
361
362        Self::new(authorized_keys)
363    }
364}
365
366/// A guaranteed to be system-wide [`NetHsm`][`nethsm::NetHsm`] user
367#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
368#[serde(into = "String", try_from = "String")]
369pub struct SystemWideUserId(UserId);
370
371impl SystemWideUserId {
372    /// Creates a new [`SystemWideUserId`] from an owned string
373    ///
374    /// # Errors
375    ///
376    /// Returns an error, if the provided `user_id` contains a namespace.
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// use nethsm_config::SystemWideUserId;
382    ///
383    /// # fn main() -> testresult::TestResult {
384    /// SystemWideUserId::new("user1".to_string())?;
385    ///
386    /// // this fails because the User ID contains a namespace
387    /// assert!(SystemWideUserId::new("ns1~user1".to_string()).is_err());
388    /// # Ok(())
389    /// # }
390    /// ```
391    pub fn new(user_id: String) -> Result<Self, Error> {
392        let user_id = UserId::new(user_id)?;
393        if user_id.is_namespaced() {
394            return Err(Error::SystemWideUserIdWithNamespace(user_id));
395        }
396        Ok(Self(user_id))
397    }
398}
399
400impl Display for SystemWideUserId {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        self.0.fmt(f)
403    }
404}
405
406impl FromStr for SystemWideUserId {
407    type Err = Error;
408    fn from_str(s: &str) -> Result<Self, Self::Err> {
409        Self::new(s.to_string())
410    }
411}
412
413impl From<SystemWideUserId> for String {
414    fn from(value: SystemWideUserId) -> Self {
415        value.to_string()
416    }
417}
418
419impl From<SystemWideUserId> for UserId {
420    fn from(value: SystemWideUserId) -> Self {
421        value.0
422    }
423}
424
425impl TryFrom<String> for SystemWideUserId {
426    type Error = Error;
427
428    fn try_from(value: String) -> Result<Self, Self::Error> {
429        Self::new(value)
430    }
431}