signstar_config/config/
credentials.rs

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