1use crate::constants::*;
11#[cfg(all(target_family = "unix", feature = "selinux"))]
12use crate::selinux_util;
13use crate::unix_passwd::UnixIntegrationError;
14use serde::Deserialize;
15use std::env;
16use std::fmt::{Display, Formatter};
17use std::fs::{read_to_string, File};
18use std::io::{ErrorKind, Read};
19use std::path::{Path, PathBuf};
20
21#[derive(Debug, Copy, Clone)]
22pub enum HomeAttr {
23 Uuid,
24 Spn,
25 Name,
26}
27
28impl Display for HomeAttr {
29 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
30 write!(
31 f,
32 "{}",
33 match self {
34 HomeAttr::Uuid => "UUID",
35 HomeAttr::Spn => "SPN",
36 HomeAttr::Name => "Name",
37 }
38 )
39 }
40}
41
42#[derive(Debug, Copy, Clone)]
43pub enum UidAttr {
44 Name,
45 Spn,
46}
47
48impl Display for UidAttr {
49 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50 write!(
51 f,
52 "{}",
53 match self {
54 UidAttr::Name => "Name",
55 UidAttr::Spn => "SPN",
56 }
57 )
58 }
59}
60
61#[derive(Debug, Clone, Default)]
62pub enum HsmType {
63 #[cfg_attr(not(feature = "tpm"), default)]
64 Soft,
65 #[cfg_attr(feature = "tpm", default)]
66 TpmIfPossible,
67 Tpm,
68}
69
70impl Display for HsmType {
71 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72 match self {
73 HsmType::Soft => write!(f, "Soft"),
74 HsmType::TpmIfPossible => write!(f, "Tpm if possible"),
75 HsmType::Tpm => write!(f, "Tpm"),
76 }
77 }
78}
79
80#[allow(clippy::large_enum_variant)]
82#[derive(Debug, Deserialize)]
84#[serde(untagged)]
85enum ConfigUntagged {
86 Versioned(ConfigVersion),
87 Legacy(ConfigInt),
88}
89
90#[derive(Debug, Deserialize)]
91#[serde(tag = "version")]
92enum ConfigVersion {
93 #[serde(rename = "2")]
94 V2 {
95 #[serde(flatten)]
96 values: ConfigV2,
97 },
98}
99
100#[derive(Debug, Deserialize)]
101#[serde(deny_unknown_fields)]
102struct ConfigV2 {
104 cache_db_path: Option<String>,
105 sock_path: Option<String>,
106 task_sock_path: Option<String>,
107
108 cache_timeout: Option<u64>,
109
110 default_shell: Option<String>,
111 home_prefix: Option<String>,
112 home_mount_prefix: Option<String>,
113 home_attr: Option<String>,
114 home_alias: Option<String>,
115 use_etc_skel: Option<bool>,
116 uid_attr_map: Option<String>,
117 gid_attr_map: Option<String>,
118 selinux: Option<bool>,
119
120 hsm_pin_path: Option<String>,
121 hsm_type: Option<String>,
122 tpm_tcti_name: Option<String>,
123
124 kanidm: Option<KanidmConfigV2>,
125}
126
127#[derive(Clone, Debug, Deserialize)]
128pub struct GroupMap {
129 pub local: String,
130 pub with: String,
131}
132
133#[derive(Debug, Deserialize)]
134struct KanidmConfigV2 {
135 conn_timeout: Option<u64>,
136 request_timeout: Option<u64>,
137 pam_allowed_login_groups: Option<Vec<String>>,
138 #[serde(default)]
139 map_group: Vec<GroupMap>,
140 service_account_token_path: Option<PathBuf>,
141}
142
143#[derive(Debug, Deserialize)]
144struct ConfigInt {
146 db_path: Option<String>,
147 sock_path: Option<String>,
148 task_sock_path: Option<String>,
149 conn_timeout: Option<u64>,
150 request_timeout: Option<u64>,
151 cache_timeout: Option<u64>,
152 pam_allowed_login_groups: Option<Vec<String>>,
153 default_shell: Option<String>,
154 home_prefix: Option<String>,
155 home_mount_prefix: Option<String>,
156 home_attr: Option<String>,
157 home_alias: Option<String>,
158 use_etc_skel: Option<bool>,
159 uid_attr_map: Option<String>,
160 gid_attr_map: Option<String>,
161 selinux: Option<bool>,
162 #[serde(default)]
163 allow_local_account_override: Vec<String>,
164
165 hsm_pin_path: Option<String>,
166 hsm_type: Option<String>,
167 tpm_tcti_name: Option<String>,
168
169 #[serde(default)]
172 cache_db_path: Option<toml::value::Value>,
173 #[serde(default)]
174 kanidm: Option<toml::value::Value>,
175}
176
177#[derive(Debug)]
180pub struct KanidmConfig {
183 pub conn_timeout: u64,
184 pub request_timeout: u64,
185 pub pam_allowed_login_groups: Vec<String>,
186 pub map_group: Vec<GroupMap>,
187 pub service_account_token: Option<String>,
188}
189
190#[derive(Debug)]
191pub struct UnixdConfig {
193 pub cache_db_path: String,
194 pub sock_path: String,
195 pub task_sock_path: String,
196 pub cache_timeout: u64,
197 pub unix_sock_timeout: u64,
198 pub default_shell: String,
199 pub home_prefix: PathBuf,
200 pub home_mount_prefix: Option<PathBuf>,
201 pub home_attr: HomeAttr,
202 pub home_alias: Option<HomeAttr>,
203 pub use_etc_skel: bool,
204 pub uid_attr_map: UidAttr,
205 pub gid_attr_map: UidAttr,
206 pub selinux: bool,
207 pub hsm_type: HsmType,
208 pub hsm_pin_path: String,
209 pub tpm_tcti_name: String,
210 pub kanidm_config: Option<KanidmConfig>,
211}
212
213impl Default for UnixdConfig {
214 fn default() -> Self {
215 UnixdConfig::new()
216 }
217}
218
219impl Display for UnixdConfig {
220 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
221 writeln!(f, "cache_db_path: {}", &self.cache_db_path)?;
222 writeln!(f, "sock_path: {}", self.sock_path)?;
223 writeln!(f, "task_sock_path: {}", self.task_sock_path)?;
224 writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout)?;
225 writeln!(f, "cache_timeout: {}", self.cache_timeout)?;
226 writeln!(f, "default_shell: {}", self.default_shell)?;
227 writeln!(f, "home_prefix: {:?}", self.home_prefix)?;
228 match self.home_mount_prefix.as_deref() {
229 Some(val) => writeln!(f, "home_mount_prefix: {val:?}")?,
230 None => writeln!(f, "home_mount_prefix: unset")?,
231 }
232 writeln!(f, "home_attr: {}", self.home_attr)?;
233 match self.home_alias {
234 Some(val) => writeln!(f, "home_alias: {val}")?,
235 None => writeln!(f, "home_alias: unset")?,
236 }
237
238 writeln!(f, "uid_attr_map: {}", self.uid_attr_map)?;
239 writeln!(f, "gid_attr_map: {}", self.gid_attr_map)?;
240
241 writeln!(f, "hsm_type: {}", self.hsm_type)?;
242 writeln!(f, "tpm_tcti_name: {}", self.tpm_tcti_name)?;
243
244 writeln!(f, "selinux: {}", self.selinux)?;
245
246 if let Some(kconfig) = &self.kanidm_config {
247 writeln!(f, "kanidm: enabled")?;
248 writeln!(
249 f,
250 "kanidm pam_allowed_login_groups: {:#?}",
251 kconfig.pam_allowed_login_groups
252 )?;
253 writeln!(f, "kanidm conn_timeout: {}", kconfig.conn_timeout)?;
254 writeln!(f, "kanidm request_timeout: {}", kconfig.request_timeout)?;
255 } else {
256 writeln!(f, "kanidm: disabled")?;
257 };
258
259 Ok(())
260 }
261}
262
263impl UnixdConfig {
264 pub fn new() -> Self {
265 let cache_db_path = match env::var("KANIDM_CACHE_DB_PATH") {
266 Ok(val) => val,
267 Err(_) => DEFAULT_CACHE_DB_PATH.into(),
268 };
269 let hsm_pin_path = match env::var("KANIDM_HSM_PIN_PATH") {
270 Ok(val) => val,
271 Err(_) => DEFAULT_HSM_PIN_PATH.into(),
272 };
273
274 UnixdConfig {
275 cache_db_path,
276 sock_path: DEFAULT_SOCK_PATH.to_string(),
277 task_sock_path: DEFAULT_TASK_SOCK_PATH.to_string(),
278 unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
279 cache_timeout: DEFAULT_CACHE_TIMEOUT,
280 default_shell: DEFAULT_SHELL.to_string(),
281 home_prefix: DEFAULT_HOME_PREFIX.into(),
282 home_mount_prefix: None,
283 home_attr: DEFAULT_HOME_ATTR,
284 home_alias: DEFAULT_HOME_ALIAS,
285 use_etc_skel: DEFAULT_USE_ETC_SKEL,
286 uid_attr_map: DEFAULT_UID_ATTR_MAP,
287 gid_attr_map: DEFAULT_GID_ATTR_MAP,
288 selinux: DEFAULT_SELINUX,
289 hsm_pin_path,
290 hsm_type: HsmType::default(),
291 tpm_tcti_name: DEFAULT_TPM_TCTI_NAME.to_string(),
292
293 kanidm_config: None,
294 }
295 }
296
297 pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>(
298 self,
299 config_path: P,
300 ) -> Result<Self, UnixIntegrationError> {
301 debug!("Attempting to load configuration from {:#?}", &config_path);
302 let mut f = match File::open(&config_path) {
303 Ok(f) => {
304 debug!("Successfully opened configuration file {:#?}", &config_path);
305 f
306 }
307 Err(e) => {
308 match e.kind() {
309 ErrorKind::NotFound => {
310 debug!(
311 "Configuration file {:#?} not found, skipping.",
312 &config_path
313 );
314 }
315 ErrorKind::PermissionDenied => {
316 warn!(
317 "Permission denied loading configuration file {:#?}, skipping.",
318 &config_path
319 );
320 }
321 _ => {
322 debug!(
323 "Unable to open config file {:#?} [{:?}], skipping ...",
324 &config_path, e
325 );
326 }
327 };
328 return Ok(self);
329 }
330 };
331
332 let mut contents = String::new();
333 f.read_to_string(&mut contents).map_err(|e| {
334 error!("{:?}", e);
335 UnixIntegrationError
336 })?;
337
338 let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| {
339 error!("{:?}", e);
340 UnixIntegrationError
341 })?;
342
343 match config {
344 ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config),
345 ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => {
346 self.apply_from_config_v2(values)
347 }
348 }
349 }
350
351 fn apply_from_config_legacy(self, config: ConfigInt) -> Result<Self, UnixIntegrationError> {
352 if config.kanidm.is_some() || config.cache_db_path.is_some() {
353 error!("You are using version=\"2\" options in a legacy config. THESE WILL NOT WORK.");
354 return Err(UnixIntegrationError);
355 }
356
357 let map_group = config
358 .allow_local_account_override
359 .iter()
360 .map(|name| GroupMap {
361 local: name.clone(),
362 with: name.clone(),
363 })
364 .collect();
365
366 let kanidm_config = Some(KanidmConfig {
367 conn_timeout: config.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT),
368 request_timeout: config.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2),
369 pam_allowed_login_groups: config.pam_allowed_login_groups.unwrap_or_default(),
370 map_group,
371 service_account_token: None,
372 });
373
374 Ok(UnixdConfig {
376 cache_db_path: config.db_path.unwrap_or(self.cache_db_path),
377 sock_path: config.sock_path.unwrap_or(self.sock_path),
378 task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path),
379 unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
380 cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout),
381 default_shell: config.default_shell.unwrap_or(self.default_shell),
382 home_prefix: config
383 .home_prefix
384 .map(|p| p.into())
385 .unwrap_or(self.home_prefix.clone()),
386 home_mount_prefix: config.home_mount_prefix.map(|p| p.into()),
387 home_attr: config
388 .home_attr
389 .and_then(|v| match v.as_str() {
390 "uuid" => Some(HomeAttr::Uuid),
391 "spn" => Some(HomeAttr::Spn),
392 "name" => Some(HomeAttr::Name),
393 _ => {
394 warn!("Invalid home_attr configured, using default ...");
395 None
396 }
397 })
398 .unwrap_or(self.home_attr),
399 home_alias: config
400 .home_alias
401 .and_then(|v| match v.as_str() {
402 "none" => Some(None),
403 "uuid" => Some(Some(HomeAttr::Uuid)),
404 "spn" => Some(Some(HomeAttr::Spn)),
405 "name" => Some(Some(HomeAttr::Name)),
406 _ => {
407 warn!("Invalid home_alias configured, using default ...");
408 None
409 }
410 })
411 .unwrap_or(self.home_alias),
412 use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel),
413 uid_attr_map: config
414 .uid_attr_map
415 .and_then(|v| match v.as_str() {
416 "spn" => Some(UidAttr::Spn),
417 "name" => Some(UidAttr::Name),
418 _ => {
419 warn!("Invalid uid_attr_map configured, using default ...");
420 None
421 }
422 })
423 .unwrap_or(self.uid_attr_map),
424 gid_attr_map: config
425 .gid_attr_map
426 .and_then(|v| match v.as_str() {
427 "spn" => Some(UidAttr::Spn),
428 "name" => Some(UidAttr::Name),
429 _ => {
430 warn!("Invalid gid_attr_map configured, using default ...");
431 None
432 }
433 })
434 .unwrap_or(self.gid_attr_map),
435 selinux: match config.selinux.unwrap_or(self.selinux) {
436 #[cfg(all(target_family = "unix", feature = "selinux"))]
437 true => selinux_util::supported(),
438 _ => false,
439 },
440 hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path),
441 hsm_type: config
442 .hsm_type
443 .and_then(|v| match v.as_str() {
444 "soft" => Some(HsmType::Soft),
445 "tpm_if_possible" => Some(HsmType::TpmIfPossible),
446 "tpm" => Some(HsmType::Tpm),
447 _ => {
448 warn!("Invalid hsm_type configured, using default ...");
449 None
450 }
451 })
452 .unwrap_or(self.hsm_type),
453 tpm_tcti_name: config
454 .tpm_tcti_name
455 .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()),
456 kanidm_config,
457 })
458 }
459
460 fn apply_from_config_v2(self, config: ConfigV2) -> Result<Self, UnixIntegrationError> {
461 let kanidm_config = if let Some(kconfig) = config.kanidm {
462 match &kconfig.pam_allowed_login_groups {
463 None => {
464 error!("You have a 'kanidm' section in the config but an empty pam_allowed_login_groups set. USERS CANNOT AUTH.")
465 }
466 Some(groups) => {
467 if groups.is_empty() {
468 error!("You have a 'kanidm' section in the config but an empty pam_allowed_login_groups set. USERS CANNOT AUTH.");
469 }
470 }
471 }
472
473 let service_account_token_path_env = match env::var("KANIDM_SERVICE_ACCOUNT_TOKEN_PATH")
474 {
475 Ok(val) => val.into(),
476 Err(_) => DEFAULT_KANIDM_SERVICE_ACCOUNT_TOKEN_PATH.into(),
477 };
478
479 let service_account_token_path: PathBuf = kconfig
480 .service_account_token_path
481 .unwrap_or(service_account_token_path_env);
482
483 let service_account_token = if service_account_token_path.exists() {
484 let token_string = read_to_string(&service_account_token_path).map_err(|err| {
485 error!(
486 ?err,
487 "Unable to open and read service account token file '{}'",
488 service_account_token_path.display()
489 );
490 UnixIntegrationError
491 })?;
492
493 let token_string =
494 token_string
495 .lines()
496 .next()
497 .map(String::from)
498 .ok_or_else(|| {
499 error!(
500 "Service account token file '{}' does not contain an api token",
501 service_account_token_path.display()
502 );
503 UnixIntegrationError
504 })?;
505
506 Some(token_string)
507 } else {
508 None
510 };
511
512 Some(KanidmConfig {
513 conn_timeout: kconfig.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT),
514 request_timeout: kconfig.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2),
515 pam_allowed_login_groups: kconfig.pam_allowed_login_groups.unwrap_or_default(),
516 map_group: kconfig.map_group,
517 service_account_token,
518 })
519 } else {
520 error!(
521 "You are using a version 2 config without a 'kanidm' section. USERS CANNOT AUTH."
522 );
523 None
524 };
525
526 Ok(UnixdConfig {
528 cache_db_path: config.cache_db_path.unwrap_or(self.cache_db_path),
529 sock_path: config.sock_path.unwrap_or(self.sock_path),
530 task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path),
531 unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
532 cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout),
533 default_shell: config.default_shell.unwrap_or(self.default_shell),
534 home_prefix: config
535 .home_prefix
536 .map(|p| p.into())
537 .unwrap_or(self.home_prefix.clone()),
538 home_mount_prefix: config.home_mount_prefix.map(|p| p.into()),
539 home_attr: config
540 .home_attr
541 .and_then(|v| match v.as_str() {
542 "uuid" => Some(HomeAttr::Uuid),
543 "spn" => Some(HomeAttr::Spn),
544 "name" => Some(HomeAttr::Name),
545 _ => {
546 warn!("Invalid home_attr configured, using default ...");
547 None
548 }
549 })
550 .unwrap_or(self.home_attr),
551 home_alias: config
552 .home_alias
553 .and_then(|v| match v.as_str() {
554 "none" => Some(None),
555 "uuid" => Some(Some(HomeAttr::Uuid)),
556 "spn" => Some(Some(HomeAttr::Spn)),
557 "name" => Some(Some(HomeAttr::Name)),
558 _ => {
559 warn!("Invalid home_alias configured, using default ...");
560 None
561 }
562 })
563 .unwrap_or(self.home_alias),
564 use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel),
565 uid_attr_map: config
566 .uid_attr_map
567 .and_then(|v| match v.as_str() {
568 "spn" => Some(UidAttr::Spn),
569 "name" => Some(UidAttr::Name),
570 _ => {
571 warn!("Invalid uid_attr_map configured, using default ...");
572 None
573 }
574 })
575 .unwrap_or(self.uid_attr_map),
576 gid_attr_map: config
577 .gid_attr_map
578 .and_then(|v| match v.as_str() {
579 "spn" => Some(UidAttr::Spn),
580 "name" => Some(UidAttr::Name),
581 _ => {
582 warn!("Invalid gid_attr_map configured, using default ...");
583 None
584 }
585 })
586 .unwrap_or(self.gid_attr_map),
587 selinux: match config.selinux.unwrap_or(self.selinux) {
588 #[cfg(all(target_family = "unix", feature = "selinux"))]
589 true => selinux_util::supported(),
590 _ => false,
591 },
592 hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path),
593 hsm_type: config
594 .hsm_type
595 .and_then(|v| match v.as_str() {
596 "soft" => Some(HsmType::Soft),
597 "tpm_if_possible" => Some(HsmType::TpmIfPossible),
598 "tpm" => Some(HsmType::Tpm),
599 _ => {
600 warn!("Invalid hsm_type configured, using default ...");
601 None
602 }
603 })
604 .unwrap_or(self.hsm_type),
605 tpm_tcti_name: config
606 .tpm_tcti_name
607 .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()),
608 kanidm_config,
609 })
610 }
611}
612
613#[derive(Debug)]
614pub struct PamNssConfig {
617 pub sock_path: String,
618 pub unix_sock_timeout: u64,
620}
621
622impl Default for PamNssConfig {
623 fn default() -> Self {
624 PamNssConfig::new()
625 }
626}
627
628impl Display for PamNssConfig {
629 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
630 writeln!(f, "sock_path: {}", self.sock_path)?;
631 writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout)
632 }
633}
634
635impl PamNssConfig {
636 pub fn new() -> Self {
637 PamNssConfig {
638 sock_path: DEFAULT_SOCK_PATH.to_string(),
639 unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
640 }
641 }
642
643 pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>(
644 self,
645 config_path: P,
646 ) -> Result<Self, UnixIntegrationError> {
647 debug!("Attempting to load configuration from {:#?}", &config_path);
648 let mut f = match File::open(&config_path) {
649 Ok(f) => {
650 debug!("Successfully opened configuration file {:#?}", &config_path);
651 f
652 }
653 Err(e) => {
654 match e.kind() {
655 ErrorKind::NotFound => {
656 debug!(
657 "Configuration file {:#?} not found, skipping.",
658 &config_path
659 );
660 }
661 ErrorKind::PermissionDenied => {
662 warn!(
663 "Permission denied loading configuration file {:#?}, skipping.",
664 &config_path
665 );
666 }
667 _ => {
668 debug!(
669 "Unable to open config file {:#?} [{:?}], skipping ...",
670 &config_path, e
671 );
672 }
673 };
674 return Ok(self);
675 }
676 };
677
678 let mut contents = String::new();
679 f.read_to_string(&mut contents).map_err(|e| {
680 error!("{:?}", e);
681 UnixIntegrationError
682 })?;
683
684 let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| {
685 error!("{:?}", e);
686 UnixIntegrationError
687 })?;
688
689 match config {
690 ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config),
691 ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => {
692 self.apply_from_config_v2(values)
693 }
694 }
695 }
696
697 fn apply_from_config_legacy(self, config: ConfigInt) -> Result<Self, UnixIntegrationError> {
698 let unix_sock_timeout = config
699 .conn_timeout
700 .map(|v| v * 2)
701 .unwrap_or(self.unix_sock_timeout);
702
703 Ok(PamNssConfig {
705 sock_path: config.sock_path.unwrap_or(self.sock_path),
706 unix_sock_timeout,
707 })
708 }
709
710 fn apply_from_config_v2(self, config: ConfigV2) -> Result<Self, UnixIntegrationError> {
711 let kanidm_conn_timeout = config
712 .kanidm
713 .as_ref()
714 .and_then(|k_config| k_config.conn_timeout)
715 .map(|timeout| timeout * 2);
716
717 Ok(PamNssConfig {
719 sock_path: config.sock_path.unwrap_or(self.sock_path),
720 unix_sock_timeout: kanidm_conn_timeout.unwrap_or(self.unix_sock_timeout),
721 })
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use std::path::PathBuf;
728
729 use super::*;
730
731 #[test]
732 fn test_load_example_configs() {
733 let examples_dir = env!("CARGO_MANIFEST_DIR").to_string() + "/../../examples/";
736
737 for file in PathBuf::from(&examples_dir)
738 .canonicalize()
739 .unwrap_or_else(|_| panic!("Can't find examples dir at {examples_dir}"))
740 .read_dir()
741 .expect("Can't read examples dir!")
742 {
743 let file = file.unwrap();
744 let filename = file.file_name().into_string().unwrap();
745 if filename.starts_with("unixd") {
746 print!("Checking that {filename} parses as a valid config...");
747
748 UnixdConfig::new()
749 .read_options_from_optional_config(file.path())
750 .inspect_err(|e| {
751 println!("Failed to parse: {e:?}");
752 })
753 .expect("Failed to parse!");
754 println!("OK");
755 }
756 }
757 }
758}