//! This is configuration definitions and parser for the various unix integration //! tools and services. This needs to support a number of use cases like pam/nss //! modules parsing the config quickly and the unix daemon which has to connect to //! various backend sources. //! //! To achieve this the configuration has two main sections - the configuration //! specification which will be parsed by the tools, then the configuration as //! relevant to that tool. use std::env; use std::fmt::{Display, Formatter}; use std::fs::File; use std::io::{ErrorKind, Read}; use std::path::{Path, PathBuf}; #[cfg(all(target_family = "unix", feature = "selinux"))] use crate::selinux_util; use crate::unix_passwd::UnixIntegrationError; use crate::constants::*; use serde::Deserialize; #[derive(Debug, Copy, Clone)] pub enum HomeAttr { Uuid, Spn, Name, } impl Display for HomeAttr { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { HomeAttr::Uuid => "UUID", HomeAttr::Spn => "SPN", HomeAttr::Name => "Name", } ) } } #[derive(Debug, Copy, Clone)] pub enum UidAttr { Name, Spn, } impl Display for UidAttr { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { UidAttr::Name => "Name", UidAttr::Spn => "SPN", } ) } } #[derive(Debug, Clone, Default)] pub enum HsmType { #[cfg_attr(not(feature = "tpm"), default)] Soft, #[cfg_attr(feature = "tpm", default)] TpmIfPossible, Tpm, } impl Display for HsmType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { HsmType::Soft => write!(f, "Soft"), HsmType::TpmIfPossible => write!(f, "Tpm if possible"), HsmType::Tpm => write!(f, "Tpm"), } } } // Allowed as the large enum is only short lived at startup to the true config #[allow(clippy::large_enum_variant)] // This bit of magic lets us deserialise the old config and the new versions. #[derive(Debug, Deserialize)] #[serde(untagged)] enum ConfigUntagged { Versioned(ConfigVersion), Legacy(ConfigInt), } #[derive(Debug, Deserialize)] #[serde(tag = "version")] enum ConfigVersion { #[serde(rename = "2")] V2 { #[serde(flatten)] values: ConfigV2, }, } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] /// This is the version 2 of the JSON configuration specification for the unixd suite. struct ConfigV2 { cache_db_path: Option<String>, sock_path: Option<String>, task_sock_path: Option<String>, cache_timeout: Option<u64>, default_shell: Option<String>, home_prefix: Option<String>, home_mount_prefix: Option<String>, home_attr: Option<String>, home_alias: Option<String>, use_etc_skel: Option<bool>, uid_attr_map: Option<String>, gid_attr_map: Option<String>, selinux: Option<bool>, hsm_pin_path: Option<String>, hsm_type: Option<String>, tpm_tcti_name: Option<String>, kanidm: Option<KanidmConfigV2>, } #[derive(Clone, Debug, Deserialize)] pub struct GroupMap { pub local: String, pub with: String, } #[derive(Debug, Deserialize)] struct KanidmConfigV2 { conn_timeout: Option<u64>, request_timeout: Option<u64>, pam_allowed_login_groups: Option<Vec<String>>, #[serde(default)] map_group: Vec<GroupMap>, } #[derive(Debug, Deserialize)] /// This is the version 1 of the JSON configuration specification for the unixd suite. struct ConfigInt { db_path: Option<String>, sock_path: Option<String>, task_sock_path: Option<String>, conn_timeout: Option<u64>, request_timeout: Option<u64>, cache_timeout: Option<u64>, pam_allowed_login_groups: Option<Vec<String>>, default_shell: Option<String>, home_prefix: Option<String>, home_mount_prefix: Option<String>, home_attr: Option<String>, home_alias: Option<String>, use_etc_skel: Option<bool>, uid_attr_map: Option<String>, gid_attr_map: Option<String>, selinux: Option<bool>, #[serde(default)] allow_local_account_override: Vec<String>, hsm_pin_path: Option<String>, hsm_type: Option<String>, tpm_tcti_name: Option<String>, // Detect and warn on values in these places - this is to catch // when someone is using a v2 value on a v1 config. #[serde(default)] cache_db_path: Option<toml::value::Value>, #[serde(default)] kanidm: Option<toml::value::Value>, } // ======================================================================== #[derive(Debug)] /// This is the parsed Kanidm provider configuration that the Unixd resolver /// will use to connect to Kanidm. pub struct KanidmConfig { pub conn_timeout: u64, pub request_timeout: u64, pub pam_allowed_login_groups: Vec<String>, pub map_group: Vec<GroupMap>, } #[derive(Debug)] /// This is the parsed configuration for the Unixd resolver. pub struct UnixdConfig { pub cache_db_path: String, pub sock_path: String, pub task_sock_path: String, pub cache_timeout: u64, pub unix_sock_timeout: u64, pub default_shell: String, pub home_prefix: PathBuf, pub home_mount_prefix: Option<PathBuf>, pub home_attr: HomeAttr, pub home_alias: Option<HomeAttr>, pub use_etc_skel: bool, pub uid_attr_map: UidAttr, pub gid_attr_map: UidAttr, pub selinux: bool, pub hsm_type: HsmType, pub hsm_pin_path: String, pub tpm_tcti_name: String, pub kanidm_config: Option<KanidmConfig>, } impl Default for UnixdConfig { fn default() -> Self { UnixdConfig::new() } } impl Display for UnixdConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "cache_db_path: {}", &self.cache_db_path)?; writeln!(f, "sock_path: {}", self.sock_path)?; writeln!(f, "task_sock_path: {}", self.task_sock_path)?; writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout)?; writeln!(f, "cache_timeout: {}", self.cache_timeout)?; writeln!(f, "default_shell: {}", self.default_shell)?; writeln!(f, "home_prefix: {:?}", self.home_prefix)?; match self.home_mount_prefix.as_deref() { Some(val) => writeln!(f, "home_mount_prefix: {:?}", val)?, None => writeln!(f, "home_mount_prefix: unset")?, } writeln!(f, "home_attr: {}", self.home_attr)?; match self.home_alias { Some(val) => writeln!(f, "home_alias: {}", val)?, None => writeln!(f, "home_alias: unset")?, } writeln!(f, "uid_attr_map: {}", self.uid_attr_map)?; writeln!(f, "gid_attr_map: {}", self.gid_attr_map)?; writeln!(f, "hsm_type: {}", self.hsm_type)?; writeln!(f, "tpm_tcti_name: {}", self.tpm_tcti_name)?; writeln!(f, "selinux: {}", self.selinux)?; if let Some(kconfig) = &self.kanidm_config { writeln!(f, "kanidm: enabled")?; writeln!( f, "kanidm pam_allowed_login_groups: {:#?}", kconfig.pam_allowed_login_groups )?; writeln!(f, "kanidm conn_timeout: {}", kconfig.conn_timeout)?; writeln!(f, "kanidm request_timeout: {}", kconfig.request_timeout)?; } else { writeln!(f, "kanidm: disabled")?; }; Ok(()) } } impl UnixdConfig { pub fn new() -> Self { let cache_db_path = match env::var("KANIDM_CACHE_DB_PATH") { Ok(val) => val, Err(_) => DEFAULT_CACHE_DB_PATH.into(), }; let hsm_pin_path = match env::var("KANIDM_HSM_PIN_PATH") { Ok(val) => val, Err(_) => DEFAULT_HSM_PIN_PATH.into(), }; UnixdConfig { cache_db_path, sock_path: DEFAULT_SOCK_PATH.to_string(), task_sock_path: DEFAULT_TASK_SOCK_PATH.to_string(), unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2, cache_timeout: DEFAULT_CACHE_TIMEOUT, default_shell: DEFAULT_SHELL.to_string(), home_prefix: DEFAULT_HOME_PREFIX.into(), home_mount_prefix: None, home_attr: DEFAULT_HOME_ATTR, home_alias: DEFAULT_HOME_ALIAS, use_etc_skel: DEFAULT_USE_ETC_SKEL, uid_attr_map: DEFAULT_UID_ATTR_MAP, gid_attr_map: DEFAULT_GID_ATTR_MAP, selinux: DEFAULT_SELINUX, hsm_pin_path, hsm_type: HsmType::default(), tpm_tcti_name: DEFAULT_TPM_TCTI_NAME.to_string(), kanidm_config: None, } } pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>( self, config_path: P, ) -> Result<Self, UnixIntegrationError> { debug!("Attempting to load configuration from {:#?}", &config_path); let mut f = match File::open(&config_path) { Ok(f) => { debug!("Successfully opened configuration file {:#?}", &config_path); f } Err(e) => { match e.kind() { ErrorKind::NotFound => { debug!( "Configuration file {:#?} not found, skipping.", &config_path ); } ErrorKind::PermissionDenied => { warn!( "Permission denied loading configuration file {:#?}, skipping.", &config_path ); } _ => { debug!( "Unable to open config file {:#?} [{:?}], skipping ...", &config_path, e ); } }; return Ok(self); } }; let mut contents = String::new(); f.read_to_string(&mut contents).map_err(|e| { error!("{:?}", e); UnixIntegrationError })?; let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| { error!("{:?}", e); UnixIntegrationError })?; match config { ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config), ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => { self.apply_from_config_v2(values) } } } fn apply_from_config_legacy(self, config: ConfigInt) -> Result<Self, UnixIntegrationError> { if config.kanidm.is_some() || config.cache_db_path.is_some() { error!("You are using version=\"2\" options in a legacy config. THESE WILL NOT WORK."); return Err(UnixIntegrationError); } let map_group = config .allow_local_account_override .iter() .map(|name| GroupMap { local: name.clone(), with: name.clone(), }) .collect(); let kanidm_config = Some(KanidmConfig { conn_timeout: config.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT), request_timeout: config.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2), pam_allowed_login_groups: config.pam_allowed_login_groups.unwrap_or_default(), map_group, }); // Now map the values into our config. Ok(UnixdConfig { cache_db_path: config.db_path.unwrap_or(self.cache_db_path), sock_path: config.sock_path.unwrap_or(self.sock_path), task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path), unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2, cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout), default_shell: config.default_shell.unwrap_or(self.default_shell), home_prefix: config .home_prefix .map(|p| p.into()) .unwrap_or(self.home_prefix.clone()), home_mount_prefix: config.home_mount_prefix.map(|p| p.into()), home_attr: config .home_attr .and_then(|v| match v.as_str() { "uuid" => Some(HomeAttr::Uuid), "spn" => Some(HomeAttr::Spn), "name" => Some(HomeAttr::Name), _ => { warn!("Invalid home_attr configured, using default ..."); None } }) .unwrap_or(self.home_attr), home_alias: config .home_alias .and_then(|v| match v.as_str() { "none" => Some(None), "uuid" => Some(Some(HomeAttr::Uuid)), "spn" => Some(Some(HomeAttr::Spn)), "name" => Some(Some(HomeAttr::Name)), _ => { warn!("Invalid home_alias configured, using default ..."); None } }) .unwrap_or(self.home_alias), use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel), uid_attr_map: config .uid_attr_map .and_then(|v| match v.as_str() { "spn" => Some(UidAttr::Spn), "name" => Some(UidAttr::Name), _ => { warn!("Invalid uid_attr_map configured, using default ..."); None } }) .unwrap_or(self.uid_attr_map), gid_attr_map: config .gid_attr_map .and_then(|v| match v.as_str() { "spn" => Some(UidAttr::Spn), "name" => Some(UidAttr::Name), _ => { warn!("Invalid gid_attr_map configured, using default ..."); None } }) .unwrap_or(self.gid_attr_map), selinux: match config.selinux.unwrap_or(self.selinux) { #[cfg(all(target_family = "unix", feature = "selinux"))] true => selinux_util::supported(), _ => false, }, hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path), hsm_type: config .hsm_type .and_then(|v| match v.as_str() { "soft" => Some(HsmType::Soft), "tpm_if_possible" => Some(HsmType::TpmIfPossible), "tpm" => Some(HsmType::Tpm), _ => { warn!("Invalid hsm_type configured, using default ..."); None } }) .unwrap_or(self.hsm_type), tpm_tcti_name: config .tpm_tcti_name .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()), kanidm_config, }) } fn apply_from_config_v2(self, config: ConfigV2) -> Result<Self, UnixIntegrationError> { let kanidm_config = if let Some(kconfig) = config.kanidm { Some(KanidmConfig { conn_timeout: kconfig.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT), request_timeout: kconfig.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2), pam_allowed_login_groups: kconfig.pam_allowed_login_groups.unwrap_or_default(), map_group: kconfig.map_group, }) } else { None }; // Now map the values into our config. Ok(UnixdConfig { cache_db_path: config.cache_db_path.unwrap_or(self.cache_db_path), sock_path: config.sock_path.unwrap_or(self.sock_path), task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path), unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2, cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout), default_shell: config.default_shell.unwrap_or(self.default_shell), home_prefix: config .home_prefix .map(|p| p.into()) .unwrap_or(self.home_prefix.clone()), home_mount_prefix: config.home_mount_prefix.map(|p| p.into()), home_attr: config .home_attr .and_then(|v| match v.as_str() { "uuid" => Some(HomeAttr::Uuid), "spn" => Some(HomeAttr::Spn), "name" => Some(HomeAttr::Name), _ => { warn!("Invalid home_attr configured, using default ..."); None } }) .unwrap_or(self.home_attr), home_alias: config .home_alias .and_then(|v| match v.as_str() { "none" => Some(None), "uuid" => Some(Some(HomeAttr::Uuid)), "spn" => Some(Some(HomeAttr::Spn)), "name" => Some(Some(HomeAttr::Name)), _ => { warn!("Invalid home_alias configured, using default ..."); None } }) .unwrap_or(self.home_alias), use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel), uid_attr_map: config .uid_attr_map .and_then(|v| match v.as_str() { "spn" => Some(UidAttr::Spn), "name" => Some(UidAttr::Name), _ => { warn!("Invalid uid_attr_map configured, using default ..."); None } }) .unwrap_or(self.uid_attr_map), gid_attr_map: config .gid_attr_map .and_then(|v| match v.as_str() { "spn" => Some(UidAttr::Spn), "name" => Some(UidAttr::Name), _ => { warn!("Invalid gid_attr_map configured, using default ..."); None } }) .unwrap_or(self.gid_attr_map), selinux: match config.selinux.unwrap_or(self.selinux) { #[cfg(all(target_family = "unix", feature = "selinux"))] true => selinux_util::supported(), _ => false, }, hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path), hsm_type: config .hsm_type .and_then(|v| match v.as_str() { "soft" => Some(HsmType::Soft), "tpm_if_possible" => Some(HsmType::TpmIfPossible), "tpm" => Some(HsmType::Tpm), _ => { warn!("Invalid hsm_type configured, using default ..."); None } }) .unwrap_or(self.hsm_type), tpm_tcti_name: config .tpm_tcti_name .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()), kanidm_config, }) } } #[derive(Debug)] /// This is the parsed configuration that will be used by pam/nss tools that need fast access to /// only the socket and timeout information related to the resolver. pub struct PamNssConfig { pub sock_path: String, // pub conn_timeout: u64, pub unix_sock_timeout: u64, } impl Default for PamNssConfig { fn default() -> Self { PamNssConfig::new() } } impl Display for PamNssConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "sock_path: {}", self.sock_path)?; writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout) } } impl PamNssConfig { pub fn new() -> Self { PamNssConfig { sock_path: DEFAULT_SOCK_PATH.to_string(), unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2, } } pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>( self, config_path: P, ) -> Result<Self, UnixIntegrationError> { debug!("Attempting to load configuration from {:#?}", &config_path); let mut f = match File::open(&config_path) { Ok(f) => { debug!("Successfully opened configuration file {:#?}", &config_path); f } Err(e) => { match e.kind() { ErrorKind::NotFound => { debug!( "Configuration file {:#?} not found, skipping.", &config_path ); } ErrorKind::PermissionDenied => { warn!( "Permission denied loading configuration file {:#?}, skipping.", &config_path ); } _ => { debug!( "Unable to open config file {:#?} [{:?}], skipping ...", &config_path, e ); } }; return Ok(self); } }; let mut contents = String::new(); f.read_to_string(&mut contents).map_err(|e| { error!("{:?}", e); UnixIntegrationError })?; let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| { error!("{:?}", e); UnixIntegrationError })?; match config { ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config), ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => { self.apply_from_config_v2(values) } } } fn apply_from_config_legacy(self, config: ConfigInt) -> Result<Self, UnixIntegrationError> { let unix_sock_timeout = config .conn_timeout .map(|v| v * 2) .unwrap_or(self.unix_sock_timeout); // Now map the values into our config. Ok(PamNssConfig { sock_path: config.sock_path.unwrap_or(self.sock_path), unix_sock_timeout, }) } fn apply_from_config_v2(self, config: ConfigV2) -> Result<Self, UnixIntegrationError> { let kanidm_conn_timeout = config .kanidm .as_ref() .and_then(|k_config| k_config.conn_timeout) .map(|timeout| timeout * 2); // Now map the values into our config. Ok(PamNssConfig { sock_path: config.sock_path.unwrap_or(self.sock_path), unix_sock_timeout: kanidm_conn_timeout.unwrap_or(self.unix_sock_timeout), }) } } #[cfg(test)] mod tests { use std::path::PathBuf; use super::*; #[test] fn test_load_example_configs() { // Test the various included configs let examples_dir = env!("CARGO_MANIFEST_DIR").to_string() + "/../../examples/"; for file in PathBuf::from(&examples_dir) .canonicalize() .expect(&format!("Can't find examples dir at {}", examples_dir)) .read_dir() .expect("Can't read examples dir!") { let file = file.unwrap(); let filename = file.file_name().into_string().unwrap(); if filename.starts_with("unixd") { print!("Checking that {} parses as a valid config...", filename); UnixdConfig::new() .read_options_from_optional_config(file.path()) .inspect_err(|e| { println!("Failed to parse: {:?}", e); }) .expect("Failed to parse!"); println!("OK"); } } } }