kanidm_unix_common/
unix_passwd.rs

1use serde::{Deserialize, Serialize};
2use serde_with::formats::CommaSeparator;
3use serde_with::{serde_as, DefaultOnNull, StringWithSeparator};
4use std::fmt;
5use std::fs::File;
6use std::io::Read;
7use std::path::Path;
8use std::str::FromStr;
9
10#[derive(Serialize, Deserialize, Debug)]
11pub struct EtcDb {
12    pub users: Vec<EtcUser>,
13    pub shadow: Vec<EtcShadow>,
14    pub groups: Vec<EtcGroup>,
15}
16
17#[derive(Serialize, Deserialize, Debug, PartialEq)]
18pub struct EtcUser {
19    pub name: String,
20    pub password: String,
21    pub uid: u32,
22    pub gid: u32,
23    pub gecos: String,
24    pub homedir: String,
25    pub shell: String,
26}
27
28pub fn parse_etc_passwd(bytes: &[u8]) -> Result<Vec<EtcUser>, UnixIntegrationError> {
29    use csv::ReaderBuilder;
30    let mut rdr = ReaderBuilder::new()
31        .has_headers(false)
32        .delimiter(b':')
33        .from_reader(bytes);
34    rdr.deserialize()
35        .map(|result| result.map_err(|_e| UnixIntegrationError))
36        .collect::<Result<Vec<EtcUser>, UnixIntegrationError>>()
37}
38
39pub fn read_etc_passwd_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcUser>, UnixIntegrationError> {
40    let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
41
42    let mut contents = vec![];
43    file.read_to_end(&mut contents)
44        .map_err(|_| UnixIntegrationError)?;
45
46    parse_etc_passwd(contents.as_slice()).map_err(|_| UnixIntegrationError)
47}
48
49#[derive(PartialEq, Default)]
50pub enum CryptPw {
51    Sha256(String),
52    Sha512(String),
53    #[default]
54    Invalid,
55}
56
57impl fmt::Display for CryptPw {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            CryptPw::Invalid => write!(f, "x"),
61            CryptPw::Sha256(s) | CryptPw::Sha512(s) => write!(f, "{}", s),
62        }
63    }
64}
65
66impl fmt::Debug for CryptPw {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            CryptPw::Invalid => write!(f, "x"),
70            CryptPw::Sha256(_s) => write!(f, "crypt sha256"),
71            CryptPw::Sha512(_s) => write!(f, "crypt sha512"),
72        }
73    }
74}
75
76impl FromStr for CryptPw {
77    type Err = &'static str;
78
79    fn from_str(value: &str) -> Result<Self, Self::Err> {
80        if value.starts_with("$6$") {
81            Ok(CryptPw::Sha512(value.to_string()))
82        } else if value.starts_with("$5$") {
83            Ok(CryptPw::Sha256(value.to_string()))
84        } else {
85            Ok(CryptPw::Invalid)
86        }
87    }
88}
89
90impl CryptPw {
91    pub fn is_valid(&self) -> bool {
92        !matches!(self, CryptPw::Invalid)
93    }
94
95    pub fn check_pw(&self, cred: &str) -> bool {
96        match &self {
97            CryptPw::Sha256(crypt) => sha_crypt::sha256_check(cred, crypt.as_str()).is_ok(),
98            CryptPw::Sha512(crypt) => sha_crypt::sha512_check(cred, crypt.as_str()).is_ok(),
99            CryptPw::Invalid => false,
100        }
101    }
102}
103
104#[serde_as]
105#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
106pub struct EtcShadow {
107    pub name: String,
108    #[serde_as(as = "serde_with::DisplayFromStr")]
109    pub password: CryptPw,
110    // 0 means must change next login.
111    // None means all other aging features are disabled
112    pub epoch_change_days: Option<i64>,
113    // 0 means no age
114    #[serde_as(deserialize_as = "DefaultOnNull")]
115    pub days_min_password_age: i64,
116    pub days_max_password_age: Option<i64>,
117    // 0 means no warning
118    #[serde_as(deserialize_as = "DefaultOnNull")]
119    pub days_warning_period: i64,
120    // Number of days after max_password_age passes where the password can
121    // still be accepted such that the user can update their password
122    pub days_inactivity_period: Option<i64>,
123    pub epoch_expire_date: Option<i64>,
124    pub flag_reserved: Option<u32>,
125}
126
127pub fn parse_etc_shadow(bytes: &[u8]) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
128    use csv::ReaderBuilder;
129    let mut rdr = ReaderBuilder::new()
130        .has_headers(false)
131        .delimiter(b':')
132        .from_reader(bytes);
133    rdr.deserialize()
134        .map(|result| {
135            result.map_err(|err| {
136                eprintln!("{:?}", err);
137                UnixIntegrationError
138            })
139        })
140        .collect::<Result<Vec<EtcShadow>, UnixIntegrationError>>()
141}
142
143pub fn read_etc_shadow_file<P: AsRef<Path>>(
144    path: P,
145) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
146    let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
147
148    let mut contents = vec![];
149    file.read_to_end(&mut contents)
150        .map_err(|_| UnixIntegrationError)?;
151
152    parse_etc_shadow(contents.as_slice()).map_err(|_| UnixIntegrationError)
153}
154
155#[serde_as]
156#[derive(Serialize, Deserialize, Debug, PartialEq)]
157pub struct EtcGroup {
158    pub name: String,
159    pub password: String,
160    pub gid: u32,
161    #[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")]
162    pub members: Vec<String>,
163}
164
165#[derive(Debug)]
166pub struct UnixIntegrationError;
167
168pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
169    use csv::ReaderBuilder;
170    let mut rdr = ReaderBuilder::new()
171        .has_headers(false)
172        .delimiter(b':')
173        .from_reader(bytes);
174    rdr.deserialize()
175        .map(|result| result.map_err(|_e| UnixIntegrationError))
176        .collect::<Result<Vec<EtcGroup>, UnixIntegrationError>>()
177}
178
179pub fn read_etc_group_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
180    let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
181
182    let mut contents = vec![];
183    file.read_to_end(&mut contents)
184        .map_err(|_| UnixIntegrationError)?;
185
186    parse_etc_group(contents.as_slice()).map_err(|_| UnixIntegrationError)
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    const EXAMPLE_PASSWD: &str = r#"root:x:0:0:root:/root:/bin/bash
194systemd-timesync:x:498:498:systemd Time Synchronization:/:/usr/sbin/nologin
195nobody:x:65534:65534:nobody:/var/lib/nobody:/bin/bash
196"#;
197
198    #[test]
199    fn test_parse_passwd() {
200        let users =
201            parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data");
202
203        assert_eq!(
204            users[0],
205            EtcUser {
206                name: "root".to_string(),
207                password: "x".to_string(),
208                uid: 0,
209                gid: 0,
210                gecos: "root".to_string(),
211                homedir: "/root".to_string(),
212                shell: "/bin/bash".to_string(),
213            }
214        );
215
216        assert_eq!(
217            users[1],
218            EtcUser {
219                name: "systemd-timesync".to_string(),
220                password: "x".to_string(),
221                uid: 498,
222                gid: 498,
223                gecos: "systemd Time Synchronization".to_string(),
224                homedir: "/".to_string(),
225                shell: "/usr/sbin/nologin".to_string(),
226            }
227        );
228
229        assert_eq!(
230            users[2],
231            EtcUser {
232                name: "nobody".to_string(),
233                password: "x".to_string(),
234                uid: 65534,
235                gid: 65534,
236                gecos: "nobody".to_string(),
237                homedir: "/var/lib/nobody".to_string(),
238                shell: "/bin/bash".to_string(),
239            }
240        );
241    }
242
243    // IMPORTANT this is the password "a". Very secure, totes secret.
244    const EXAMPLE_SHADOW: &str = r#"sshd:!:19978::::::
245tss:!:19980::::::
246admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//:19980:0:99999:7:::
247"#;
248
249    #[test]
250    fn test_parse_shadow() {
251        let shadow =
252            parse_etc_shadow(EXAMPLE_SHADOW.as_bytes()).expect("Failed to parse passwd data");
253
254        assert_eq!(
255            shadow[0],
256            EtcShadow {
257                name: "sshd".to_string(),
258                password: CryptPw::Invalid,
259                epoch_change_days: Some(19978),
260                days_min_password_age: 0,
261                days_max_password_age: None,
262                days_warning_period: 0,
263                days_inactivity_period: None,
264                epoch_expire_date: None,
265                flag_reserved: None
266            }
267        );
268
269        assert_eq!(
270            shadow[1],
271            EtcShadow {
272                name: "tss".to_string(),
273                password: CryptPw::Invalid,
274                epoch_change_days: Some(19980),
275                days_min_password_age: 0,
276                days_max_password_age: None,
277                days_warning_period: 0,
278                days_inactivity_period: None,
279                epoch_expire_date: None,
280                flag_reserved: None
281            }
282        );
283
284        assert_eq!(shadow[2], EtcShadow {
285            name: "admin".to_string(),
286            password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
287            epoch_change_days: Some(19980),
288            days_min_password_age: 0,
289            days_max_password_age: Some(99999),
290            days_warning_period: 7,
291            days_inactivity_period: None,
292            epoch_expire_date: None,
293            flag_reserved: None
294        });
295    }
296
297    const EXAMPLE_GROUP: &str = r#"root:x:0:
298wheel:x:481:admin,testuser
299"#;
300
301    #[test]
302    fn test_parse_group() {
303        let groups = parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse groups");
304
305        assert_eq!(
306            groups[0],
307            EtcGroup {
308                name: "root".to_string(),
309                password: "x".to_string(),
310                gid: 0,
311                members: vec![]
312            }
313        );
314
315        assert_eq!(
316            groups[1],
317            EtcGroup {
318                name: "wheel".to_string(),
319                password: "x".to_string(),
320                gid: 481,
321                members: vec!["admin".to_string(), "testuser".to_string(),]
322            }
323        );
324    }
325}