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::{BufRead, Read};
7use std::path::Path;
8use std::str::FromStr;
9
10#[derive(Serialize, Deserialize, Debug, Default, Clone)]
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, Clone)]
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
31 let filecontents = strip_comments(bytes);
32
33 let mut rdr = ReaderBuilder::new()
34 .has_headers(false)
35 .delimiter(b':')
36 .from_reader(filecontents.as_bytes());
37
38 rdr.deserialize()
39 .map(|result| result.map_err(|_e| UnixIntegrationError))
40 .collect::<Result<Vec<EtcUser>, UnixIntegrationError>>()
41}
42
43pub fn read_etc_passwd_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcUser>, UnixIntegrationError> {
44 let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
45
46 let mut contents = vec![];
47 file.read_to_end(&mut contents)
48 .map_err(|_| UnixIntegrationError)?;
49
50 parse_etc_passwd(contents.as_slice()).map_err(|_| UnixIntegrationError)
51}
52
53#[derive(PartialEq, Default, Clone)]
54pub enum CryptPw {
55 Sha256(String),
56 Sha512(String),
57 #[default]
58 Invalid,
59}
60
61impl fmt::Display for CryptPw {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 CryptPw::Invalid => write!(f, "x"),
65 CryptPw::Sha256(s) | CryptPw::Sha512(s) => write!(f, "{}", s),
66 }
67 }
68}
69
70impl fmt::Debug for CryptPw {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 CryptPw::Invalid => write!(f, "x"),
74 CryptPw::Sha256(_s) => write!(f, "crypt sha256"),
75 CryptPw::Sha512(_s) => write!(f, "crypt sha512"),
76 }
77 }
78}
79
80impl FromStr for CryptPw {
81 type Err = &'static str;
82
83 fn from_str(value: &str) -> Result<Self, Self::Err> {
84 if value.starts_with("$6$") {
85 Ok(CryptPw::Sha512(value.to_string()))
86 } else if value.starts_with("$5$") {
87 Ok(CryptPw::Sha256(value.to_string()))
88 } else {
89 Ok(CryptPw::Invalid)
90 }
91 }
92}
93
94impl CryptPw {
95 pub fn is_valid(&self) -> bool {
96 !matches!(self, CryptPw::Invalid)
97 }
98
99 pub fn check_pw(&self, cred: &str) -> bool {
100 match &self {
101 CryptPw::Sha256(crypt) => sha_crypt::sha256_check(cred, crypt.as_str()).is_ok(),
102 CryptPw::Sha512(crypt) => sha_crypt::sha512_check(cred, crypt.as_str()).is_ok(),
103 CryptPw::Invalid => false,
104 }
105 }
106}
107
108#[serde_as]
109#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)]
110pub struct EtcShadow {
111 pub name: String,
112 #[serde_as(as = "serde_with::DisplayFromStr")]
113 pub password: CryptPw,
114 pub epoch_change_days: Option<i64>,
117 #[serde_as(deserialize_as = "DefaultOnNull")]
119 pub days_min_password_age: i64,
120 pub days_max_password_age: Option<i64>,
121 #[serde_as(deserialize_as = "DefaultOnNull")]
123 pub days_warning_period: i64,
124 pub days_inactivity_period: Option<i64>,
127 pub epoch_expire_date: Option<i64>,
128 pub flag_reserved: Option<u32>,
129}
130
131pub fn parse_etc_shadow(bytes: &[u8]) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
132 use csv::ReaderBuilder;
133
134 let filecontents = strip_comments(bytes);
135
136 let mut rdr = ReaderBuilder::new()
137 .has_headers(false)
138 .delimiter(b':')
139 .from_reader(filecontents.as_bytes());
140 rdr.deserialize()
141 .map(|result| {
142 result.map_err(|err| {
143 eprintln!("{:?}", err);
144 UnixIntegrationError
145 })
146 })
147 .collect::<Result<Vec<EtcShadow>, UnixIntegrationError>>()
148}
149
150pub fn read_etc_shadow_file<P: AsRef<Path>>(
151 path: P,
152) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
153 let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
154
155 let mut contents = vec![];
156 file.read_to_end(&mut contents)
157 .map_err(|_| UnixIntegrationError)?;
158
159 parse_etc_shadow(contents.as_slice()).map_err(|_| UnixIntegrationError)
160}
161
162#[serde_as]
163#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
164pub struct EtcGroup {
165 pub name: String,
166 pub password: String,
167 pub gid: u32,
168 #[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")]
169 pub members: Vec<String>,
170}
171
172#[derive(Debug)]
173pub struct UnixIntegrationError;
174
175pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
176 use csv::ReaderBuilder;
177
178 let filecontents = strip_comments(bytes);
179
180 let mut rdr = ReaderBuilder::new()
181 .has_headers(false)
182 .delimiter(b':')
183 .from_reader(filecontents.as_bytes());
184 rdr.deserialize()
185 .map(|result| result.map_err(|_e| UnixIntegrationError))
186 .collect::<Result<Vec<EtcGroup>, UnixIntegrationError>>()
187}
188
189pub fn read_etc_group_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
190 let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
191
192 let mut contents = vec![];
193 file.read_to_end(&mut contents)
194 .map_err(|_| UnixIntegrationError)?;
195
196 parse_etc_group(contents.as_slice()).map_err(|_| UnixIntegrationError)
197}
198
199fn strip_comments(bytes: &[u8]) -> String {
200 bytes
201 .lines()
202 .filter_map(|line| {
203 let line = line.ok()?;
204 let trimmed = line.trim();
205 if !trimmed.is_empty() && !trimmed.starts_with('#') {
206 Some(line)
207 } else {
208 None
209 }
210 })
211 .collect::<Vec<_>>()
212 .join("\n")
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 const EXAMPLE_PASSWD: &str = r#"root:x:0:0:root:/root:/bin/bash
220systemd-timesync:x:498:498:systemd Time Synchronization:/:/usr/sbin/nologin
221nobody:x:65534:65534:nobody:/var/lib/nobody:/bin/bash
222"#;
223
224 #[test]
225 fn test_parse_passwd() {
226 let users =
227 parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data");
228
229 assert_eq!(
230 users[0],
231 EtcUser {
232 name: "root".to_string(),
233 password: "x".to_string(),
234 uid: 0,
235 gid: 0,
236 gecos: "root".to_string(),
237 homedir: "/root".to_string(),
238 shell: "/bin/bash".to_string(),
239 }
240 );
241
242 assert_eq!(
243 users[1],
244 EtcUser {
245 name: "systemd-timesync".to_string(),
246 password: "x".to_string(),
247 uid: 498,
248 gid: 498,
249 gecos: "systemd Time Synchronization".to_string(),
250 homedir: "/".to_string(),
251 shell: "/usr/sbin/nologin".to_string(),
252 }
253 );
254
255 assert_eq!(
256 users[2],
257 EtcUser {
258 name: "nobody".to_string(),
259 password: "x".to_string(),
260 uid: 65534,
261 gid: 65534,
262 gecos: "nobody".to_string(),
263 homedir: "/var/lib/nobody".to_string(),
264 shell: "/bin/bash".to_string(),
265 }
266 );
267 }
268
269 const EXAMPLE_SHADOW: &str = r#"sshd:!:19978::::::
271tss:!:19980::::::
272admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//:19980:0:99999:7:::
273"#;
274
275 #[test]
276 fn test_parse_shadow() {
277 let shadow =
278 parse_etc_shadow(EXAMPLE_SHADOW.as_bytes()).expect("Failed to parse passwd data");
279
280 assert_eq!(
281 shadow[0],
282 EtcShadow {
283 name: "sshd".to_string(),
284 password: CryptPw::Invalid,
285 epoch_change_days: Some(19978),
286 days_min_password_age: 0,
287 days_max_password_age: None,
288 days_warning_period: 0,
289 days_inactivity_period: None,
290 epoch_expire_date: None,
291 flag_reserved: None
292 }
293 );
294
295 assert_eq!(
296 shadow[1],
297 EtcShadow {
298 name: "tss".to_string(),
299 password: CryptPw::Invalid,
300 epoch_change_days: Some(19980),
301 days_min_password_age: 0,
302 days_max_password_age: None,
303 days_warning_period: 0,
304 days_inactivity_period: None,
305 epoch_expire_date: None,
306 flag_reserved: None
307 }
308 );
309
310 assert_eq!(shadow[2], EtcShadow {
311 name: "admin".to_string(),
312 password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
313 epoch_change_days: Some(19980),
314 days_min_password_age: 0,
315 days_max_password_age: Some(99999),
316 days_warning_period: 7,
317 days_inactivity_period: None,
318 epoch_expire_date: None,
319 flag_reserved: None
320 });
321 }
322
323 const EXAMPLE_GROUP: &str = r#"root:x:0:
324wheel:x:481:admin,testuser
325"#;
326
327 #[test]
328 fn test_parse_group() {
329 let groups = parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse groups");
330
331 assert_eq!(
332 groups[0],
333 EtcGroup {
334 name: "root".to_string(),
335 password: "x".to_string(),
336 gid: 0,
337 members: vec![]
338 }
339 );
340
341 assert_eq!(
342 groups[1],
343 EtcGroup {
344 name: "wheel".to_string(),
345 password: "x".to_string(),
346 gid: 481,
347 members: vec!["admin".to_string(), "testuser".to_string(),]
348 }
349 );
350 }
351
352 #[test]
353 fn test_parse_group_freebsd() {
354 let group_data = r#"wheel:*:0:root,testuser,kanidm"#;
355 let groups = parse_etc_group(group_data.as_bytes()).expect("Failed to parse groups");
356 assert_eq!(
357 groups[0],
358 EtcGroup {
359 name: "wheel".to_string(),
360 password: "*".to_string(),
361 gid: 0,
362 members: vec![
363 "root".to_string(),
364 "testuser".to_string(),
365 "kanidm".to_string()
366 ]
367 }
368 );
369 let group_data = r#"
371 # $FreeBSD$
372#
373wheel:*:0:"#;
374 let groups = parse_etc_group(group_data.as_bytes()).expect("Failed to parse groups");
375 assert_eq!(
376 groups[0],
377 EtcGroup {
378 name: "wheel".to_string(),
379 password: "*".to_string(),
380 gid: 0,
381 members: vec![]
382 }
383 );
384 }
385}