From 50e513b30b828566a432ce914d88441826a63d68 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Tue, 15 Oct 2024 14:05:51 +1000 Subject: [PATCH] Add nss testframework and fallback when daemon offline (#3093) --- Cargo.lock | 3 +- unix_integration/common/Cargo.toml | 1 + unix_integration/common/src/client_sync.rs | 26 +- unix_integration/common/src/unix_passwd.rs | 94 ++- unix_integration/nss_kanidm/src/core.rs | 328 +++++++++++ unix_integration/nss_kanidm/src/hooks.rs | 63 +++ .../nss_kanidm/src/implementation.rs | 204 ------- unix_integration/nss_kanidm/src/lib.rs | 8 +- unix_integration/nss_kanidm/src/tests.rs | 258 +++++++++ unix_integration/pam_kanidm/Cargo.toml | 1 + unix_integration/pam_kanidm/src/core.rs | 534 ++++++++++++++++++ unix_integration/pam_kanidm/src/lib.rs | 9 +- unix_integration/pam_kanidm/src/pam/conv.rs | 2 +- unix_integration/pam_kanidm/src/pam/mod.rs | 502 ++-------------- unix_integration/pam_kanidm/src/pam/module.rs | 90 ++- unix_integration/pam_kanidm/src/tests.rs | 467 +++++++++++++++ unix_integration/resolver/Cargo.toml | 1 - .../resolver/src/idprovider/system.rs | 76 +-- .../resolver/tests/cache_layer_test.rs | 6 +- 19 files changed, 1940 insertions(+), 733 deletions(-) create mode 100644 unix_integration/nss_kanidm/src/core.rs create mode 100644 unix_integration/nss_kanidm/src/hooks.rs delete mode 100644 unix_integration/nss_kanidm/src/implementation.rs create mode 100644 unix_integration/nss_kanidm/src/tests.rs create mode 100644 unix_integration/pam_kanidm/src/core.rs create mode 100644 unix_integration/pam_kanidm/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index ff2deceb9..9d44f1974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3370,6 +3370,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sha-crypt", "tokio", "tokio-util", "toml", @@ -3407,7 +3408,6 @@ dependencies = [ "selinux", "serde", "serde_json", - "sha-crypt", "sketching", "time", "tokio", @@ -4548,6 +4548,7 @@ dependencies = [ "kanidm_unix_common", "libc", "pkg-config", + "time", "tracing", "tracing-subscriber", ] diff --git a/unix_integration/common/Cargo.toml b/unix_integration/common/Cargo.toml index 7bd11d8d5..5c6a5ff0a 100644 --- a/unix_integration/common/Cargo.toml +++ b/unix_integration/common/Cargo.toml @@ -32,6 +32,7 @@ toml = { workspace = true } tokio = { workspace = true, features = ["time","net","macros"] } tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } +sha-crypt = { workspace = true } [build-dependencies] kanidm_build_profiles = { workspace = true } diff --git a/unix_integration/common/src/client_sync.rs b/unix_integration/common/src/client_sync.rs index 190fc4743..9c0473eff 100644 --- a/unix_integration/common/src/client_sync.rs +++ b/unix_integration/common/src/client_sync.rs @@ -1,16 +1,27 @@ +use crate::constants::DEFAULT_CONN_TIMEOUT; +use crate::unix_proto::{ClientRequest, ClientResponse}; use std::error::Error; use std::io::{Error as IoError, ErrorKind, Read, Write}; -use std::os::unix::net::UnixStream; use std::time::{Duration, SystemTime}; -use crate::unix_proto::{ClientRequest, ClientResponse}; +pub use std::os::unix::net::UnixStream; pub struct DaemonClientBlocking { stream: UnixStream, + default_timeout: u64, +} + +impl From for DaemonClientBlocking { + fn from(stream: UnixStream) -> Self { + DaemonClientBlocking { + stream, + default_timeout: DEFAULT_CONN_TIMEOUT, + } + } } impl DaemonClientBlocking { - pub fn new(path: &str) -> Result> { + pub fn new(path: &str, default_timeout: u64) -> Result> { debug!(%path); let stream = UnixStream::connect(path) @@ -23,15 +34,18 @@ impl DaemonClientBlocking { }) .map_err(Box::new)?; - Ok(DaemonClientBlocking { stream }) + Ok(DaemonClientBlocking { + stream, + default_timeout, + }) } pub fn call_and_wait( &mut self, req: &ClientRequest, - timeout: u64, + timeout: Option, ) -> Result> { - let timeout = Duration::from_secs(timeout); + let timeout = Duration::from_secs(timeout.unwrap_or(self.default_timeout)); let data = serde_json::to_vec(&req).map_err(|e| { error!("socket encoding error -> {:?}", e); diff --git a/unix_integration/common/src/unix_passwd.rs b/unix_integration/common/src/unix_passwd.rs index 13f2f3e35..02e265c65 100644 --- a/unix_integration/common/src/unix_passwd.rs +++ b/unix_integration/common/src/unix_passwd.rs @@ -1,7 +1,11 @@ use serde::{Deserialize, Serialize}; - use serde_with::formats::CommaSeparator; use serde_with::{serde_as, DefaultOnNull, StringWithSeparator}; +use std::fmt; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::str::FromStr; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct EtcUser { @@ -25,11 +29,67 @@ pub fn parse_etc_passwd(bytes: &[u8]) -> Result, UnixIntegrationErr .collect::, UnixIntegrationError>>() } +pub fn read_etc_passwd_file>(path: P) -> Result, UnixIntegrationError> { + let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?; + + let mut contents = vec![]; + file.read_to_end(&mut contents) + .map_err(|_| UnixIntegrationError)?; + + parse_etc_passwd(contents.as_slice()).map_err(|_| UnixIntegrationError) +} + +#[derive(Debug, PartialEq, Default)] +pub enum CryptPw { + Sha256(String), + Sha512(String), + #[default] + Invalid, +} + +impl fmt::Display for CryptPw { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CryptPw::Invalid => write!(f, "x"), + CryptPw::Sha256(s) | CryptPw::Sha512(s) => write!(f, "{}", s), + } + } +} + +impl FromStr for CryptPw { + type Err = &'static str; + + fn from_str(value: &str) -> Result { + if value.starts_with("$6$") { + Ok(CryptPw::Sha512(value.to_string())) + } else if value.starts_with("$5$") { + Ok(CryptPw::Sha256(value.to_string())) + } else { + Ok(CryptPw::Invalid) + } + } +} + +impl CryptPw { + pub fn is_valid(&self) -> bool { + !matches!(self, CryptPw::Invalid) + } + + pub fn check_pw(&self, cred: &str) -> bool { + match &self { + CryptPw::Sha256(crypt) => sha_crypt::sha256_check(cred, crypt.as_str()).is_ok(), + CryptPw::Sha512(crypt) => sha_crypt::sha512_check(cred, crypt.as_str()).is_ok(), + CryptPw::Invalid => false, + } + } +} + #[serde_as] -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct EtcShadow { pub name: String, - pub password: String, + #[serde_as(as = "serde_with::DisplayFromStr")] + pub password: CryptPw, // 0 means must change next login. // None means all other aging features are disabled pub epoch_change_days: Option, @@ -63,6 +123,18 @@ pub fn parse_etc_shadow(bytes: &[u8]) -> Result, UnixIntegrationE .collect::, UnixIntegrationError>>() } +pub fn read_etc_shadow_file>( + path: P, +) -> Result, UnixIntegrationError> { + let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?; + + let mut contents = vec![]; + file.read_to_end(&mut contents) + .map_err(|_| UnixIntegrationError)?; + + parse_etc_shadow(contents.as_slice()).map_err(|_| UnixIntegrationError) +} + #[serde_as] #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct EtcGroup { @@ -87,6 +159,16 @@ pub fn parse_etc_group(bytes: &[u8]) -> Result, UnixIntegrationErr .collect::, UnixIntegrationError>>() } +pub fn read_etc_group_file>(path: P) -> Result, UnixIntegrationError> { + let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?; + + let mut contents = vec![]; + file.read_to_end(&mut contents) + .map_err(|_| UnixIntegrationError)?; + + parse_etc_group(contents.as_slice()).map_err(|_| UnixIntegrationError) +} + #[cfg(test)] mod tests { use super::*; @@ -156,7 +238,7 @@ admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePME shadow[0], EtcShadow { name: "sshd".to_string(), - password: "!".to_string(), + password: CryptPw::Invalid, epoch_change_days: Some(19978), days_min_password_age: 0, days_max_password_age: None, @@ -171,7 +253,7 @@ admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePME shadow[1], EtcShadow { name: "tss".to_string(), - password: "!".to_string(), + password: CryptPw::Invalid, epoch_change_days: Some(19980), days_min_password_age: 0, days_max_password_age: None, @@ -184,7 +266,7 @@ admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePME assert_eq!(shadow[2], EtcShadow { name: "admin".to_string(), - password: "$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string(), + password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()), epoch_change_days: Some(19980), days_min_password_age: 0, days_max_password_age: Some(99999), diff --git a/unix_integration/nss_kanidm/src/core.rs b/unix_integration/nss_kanidm/src/core.rs new file mode 100644 index 000000000..d60ba9839 --- /dev/null +++ b/unix_integration/nss_kanidm/src/core.rs @@ -0,0 +1,328 @@ +use kanidm_unix_common::client_sync::DaemonClientBlocking; +use kanidm_unix_common::unix_config::KanidmUnixdConfig; +use kanidm_unix_common::unix_passwd::{ + read_etc_group_file, read_etc_passwd_file, EtcGroup, EtcUser, +}; +use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, NssGroup, NssUser}; + +use libnss::group::Group; +use libnss::interop::Response; +use libnss::passwd::Passwd; + +#[cfg(test)] +use kanidm_unix_common::client_sync::UnixStream; + +pub enum RequestOptions { + Main { + config_path: &'static str, + }, + #[cfg(test)] + Test { + socket: Option, + users: Vec, + groups: Vec, + }, +} + +enum Source { + Daemon(DaemonClientBlocking), + Fallback { + users: Vec, + groups: Vec, + }, +} + +impl RequestOptions { + fn connect_to_daemon(self) -> Source { + match self { + RequestOptions::Main { config_path } => { + let maybe_client = KanidmUnixdConfig::new() + .read_options_from_optional_config(config_path) + .ok() + .and_then(|cfg| { + DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) + .ok() + }); + + if let Some(client) = maybe_client { + Source::Daemon(client) + } else { + let users = read_etc_passwd_file("/etc/passwd").unwrap_or_default(); + + let groups = read_etc_group_file("/etc/group").unwrap_or_default(); + + Source::Fallback { users, groups } + } + } + #[cfg(test)] + RequestOptions::Test { + socket, + users, + groups, + } => { + if let Some(socket) = socket { + Source::Daemon(DaemonClientBlocking::from(socket)) + } else { + Source::Fallback { users, groups } + } + } + } + } +} + +pub fn get_all_user_entries(req_options: RequestOptions) -> Response> { + match req_options.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::NssAccounts; + + daemon_client + .call_and_wait(&req, None) + .map(|r| match r { + ClientResponse::NssAccounts(l) => { + l.into_iter().map(passwd_from_nssuser).collect() + } + _ => Vec::new(), + }) + .map(Response::Success) + .unwrap_or_else(|_| Response::Success(vec![])) + } + Source::Fallback { users, groups: _ } => { + if users.is_empty() { + return Response::Unavail; + } + + let users = users.into_iter().map(passwd_from_etcuser).collect(); + + Response::Success(users) + } + } +} + +pub fn get_user_entry_by_uid(uid: libc::uid_t, req_options: RequestOptions) -> Response { + match req_options.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::NssAccountByUid(uid); + daemon_client + .call_and_wait(&req, None) + .map(|r| match r { + ClientResponse::NssAccount(opt) => opt + .map(passwd_from_nssuser) + .map(Response::Success) + .unwrap_or_else(|| Response::NotFound), + _ => Response::NotFound, + }) + .unwrap_or_else(|_| Response::NotFound) + } + Source::Fallback { users, groups: _ } => { + if users.is_empty() { + return Response::Unavail; + } + + let user = users + .into_iter() + .filter_map(|etcuser| { + if etcuser.uid == uid { + Some(passwd_from_etcuser(etcuser)) + } else { + None + } + }) + .next(); + + if let Some(user) = user { + Response::Success(user) + } else { + Response::NotFound + } + } + } +} + +pub fn get_user_entry_by_name(name: String, req_options: RequestOptions) -> Response { + match req_options.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::NssAccountByName(name); + daemon_client + .call_and_wait(&req, None) + .map(|r| match r { + ClientResponse::NssAccount(opt) => opt + .map(passwd_from_nssuser) + .map(Response::Success) + .unwrap_or_else(|| Response::NotFound), + _ => Response::NotFound, + }) + .unwrap_or_else(|_| Response::NotFound) + } + Source::Fallback { users, groups: _ } => { + if users.is_empty() { + return Response::Unavail; + } + + let user = users + .into_iter() + .filter_map(|etcuser| { + if etcuser.name == name { + Some(passwd_from_etcuser(etcuser)) + } else { + None + } + }) + .next(); + + if let Some(user) = user { + Response::Success(user) + } else { + Response::NotFound + } + } + } +} + +pub fn get_all_group_entries(req_options: RequestOptions) -> Response> { + match req_options.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::NssGroups; + daemon_client + .call_and_wait(&req, None) + .map(|r| match r { + ClientResponse::NssGroups(l) => { + l.into_iter().map(group_from_nssgroup).collect() + } + _ => Vec::new(), + }) + .map(Response::Success) + .unwrap_or_else(|_| Response::Success(vec![])) + } + Source::Fallback { users: _, groups } => { + if groups.is_empty() { + return Response::Unavail; + } + + let groups = groups.into_iter().map(group_from_etcgroup).collect(); + + Response::Success(groups) + } + } +} + +pub fn get_group_entry_by_gid(gid: libc::gid_t, req_options: RequestOptions) -> Response { + match req_options.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::NssGroupByGid(gid); + daemon_client + .call_and_wait(&req, None) + .map(|r| match r { + ClientResponse::NssGroup(opt) => opt + .map(group_from_nssgroup) + .map(Response::Success) + .unwrap_or_else(|| Response::NotFound), + _ => Response::NotFound, + }) + .unwrap_or_else(|_| Response::NotFound) + } + Source::Fallback { users: _, groups } => { + if groups.is_empty() { + return Response::Unavail; + } + + let group = groups + .into_iter() + .filter_map(|etcgroup| { + if etcgroup.gid == gid { + Some(group_from_etcgroup(etcgroup)) + } else { + None + } + }) + .next(); + + if let Some(group) = group { + Response::Success(group) + } else { + Response::NotFound + } + } + } +} + +pub fn get_group_entry_by_name(name: String, req_options: RequestOptions) -> Response { + match req_options.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::NssGroupByName(name); + daemon_client + .call_and_wait(&req, None) + .map(|r| match r { + ClientResponse::NssGroup(opt) => opt + .map(group_from_nssgroup) + .map(Response::Success) + .unwrap_or_else(|| Response::NotFound), + _ => Response::NotFound, + }) + .unwrap_or_else(|_| Response::NotFound) + } + Source::Fallback { users: _, groups } => { + if groups.is_empty() { + return Response::Unavail; + } + + let group = groups + .into_iter() + .filter_map(|etcgroup| { + if etcgroup.name == name { + Some(group_from_etcgroup(etcgroup)) + } else { + None + } + }) + .next(); + + if let Some(group) = group { + Response::Success(group) + } else { + Response::NotFound + } + } + } +} + +fn passwd_from_etcuser(etc: EtcUser) -> Passwd { + Passwd { + name: etc.name, + gecos: etc.gecos, + passwd: "x".to_string(), + uid: etc.uid, + gid: etc.gid, + dir: etc.homedir, + shell: etc.shell, + } +} + +fn passwd_from_nssuser(nu: NssUser) -> Passwd { + Passwd { + name: nu.name, + gecos: nu.gecos, + passwd: "x".to_string(), + uid: nu.uid, + gid: nu.gid, + dir: nu.homedir, + shell: nu.shell, + } +} + +fn group_from_etcgroup(etc: EtcGroup) -> Group { + Group { + name: etc.name, + passwd: "x".to_string(), + gid: etc.gid, + members: etc.members, + } +} + +fn group_from_nssgroup(ng: NssGroup) -> Group { + Group { + name: ng.name, + passwd: "x".to_string(), + gid: ng.gid, + members: ng.members, + } +} diff --git a/unix_integration/nss_kanidm/src/hooks.rs b/unix_integration/nss_kanidm/src/hooks.rs new file mode 100644 index 000000000..62386c987 --- /dev/null +++ b/unix_integration/nss_kanidm/src/hooks.rs @@ -0,0 +1,63 @@ +use crate::core::{self, RequestOptions}; +use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; +use libnss::group::{Group, GroupHooks}; +use libnss::interop::Response; +use libnss::passwd::{Passwd, PasswdHooks}; + +struct KanidmPasswd; +libnss_passwd_hooks!(kanidm, KanidmPasswd); + +impl PasswdHooks for KanidmPasswd { + fn get_all_entries() -> Response> { + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, + }; + + core::get_all_user_entries(req_opt) + } + + fn get_entry_by_uid(uid: libc::uid_t) -> Response { + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, + }; + + core::get_user_entry_by_uid(uid, req_opt) + } + + fn get_entry_by_name(name: String) -> Response { + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, + }; + + core::get_user_entry_by_name(name, req_opt) + } +} + +struct KanidmGroup; +libnss_group_hooks!(kanidm, KanidmGroup); + +impl GroupHooks for KanidmGroup { + fn get_all_entries() -> Response> { + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, + }; + + core::get_all_group_entries(req_opt) + } + + fn get_entry_by_gid(gid: libc::gid_t) -> Response { + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, + }; + + core::get_group_entry_by_gid(gid, req_opt) + } + + fn get_entry_by_name(name: String) -> Response { + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, + }; + + core::get_group_entry_by_name(name, req_opt) + } +} diff --git a/unix_integration/nss_kanidm/src/implementation.rs b/unix_integration/nss_kanidm/src/implementation.rs deleted file mode 100644 index 5774a8bf6..000000000 --- a/unix_integration/nss_kanidm/src/implementation.rs +++ /dev/null @@ -1,204 +0,0 @@ -use kanidm_unix_common::client_sync::DaemonClientBlocking; -use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; -use kanidm_unix_common::unix_config::KanidmUnixdConfig; -use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, NssGroup, NssUser}; -use libnss::group::{Group, GroupHooks}; -use libnss::interop::Response; -use libnss::passwd::{Passwd, PasswdHooks}; - -struct KanidmPasswd; -libnss_passwd_hooks!(kanidm, KanidmPasswd); - -impl PasswdHooks for KanidmPasswd { - fn get_all_entries() -> Response> { - let cfg = - match KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH) { - Ok(c) => c, - Err(_) => { - return Response::Unavail; - } - }; - let req = ClientRequest::NssAccounts; - - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; - - daemon_client - .call_and_wait(&req, cfg.unix_sock_timeout) - .map(|r| match r { - ClientResponse::NssAccounts(l) => l.into_iter().map(passwd_from_nssuser).collect(), - _ => Vec::new(), - }) - .map(Response::Success) - .unwrap_or_else(|_| Response::Success(vec![])) - } - - fn get_entry_by_uid(uid: libc::uid_t) -> Response { - let cfg = - match KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH) { - Ok(c) => c, - Err(_) => { - return Response::Unavail; - } - }; - let req = ClientRequest::NssAccountByUid(uid); - - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; - - daemon_client - .call_and_wait(&req, cfg.unix_sock_timeout) - .map(|r| match r { - ClientResponse::NssAccount(opt) => opt - .map(passwd_from_nssuser) - .map(Response::Success) - .unwrap_or_else(|| Response::NotFound), - _ => Response::NotFound, - }) - .unwrap_or_else(|_| Response::NotFound) - } - - fn get_entry_by_name(name: String) -> Response { - let cfg = - match KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH) { - Ok(c) => c, - Err(_) => { - return Response::Unavail; - } - }; - let req = ClientRequest::NssAccountByName(name); - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; - - daemon_client - .call_and_wait(&req, cfg.unix_sock_timeout) - .map(|r| match r { - ClientResponse::NssAccount(opt) => opt - .map(passwd_from_nssuser) - .map(Response::Success) - .unwrap_or_else(|| Response::NotFound), - _ => Response::NotFound, - }) - .unwrap_or_else(|_| Response::NotFound) - } -} - -struct KanidmGroup; -libnss_group_hooks!(kanidm, KanidmGroup); - -impl GroupHooks for KanidmGroup { - fn get_all_entries() -> Response> { - let cfg = - match KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH) { - Ok(c) => c, - Err(_) => { - return Response::Unavail; - } - }; - let req = ClientRequest::NssGroups; - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; - - daemon_client - .call_and_wait(&req, cfg.unix_sock_timeout) - .map(|r| match r { - ClientResponse::NssGroups(l) => l.into_iter().map(group_from_nssgroup).collect(), - _ => Vec::new(), - }) - .map(Response::Success) - .unwrap_or_else(|_| Response::Success(vec![])) - } - - fn get_entry_by_gid(gid: libc::gid_t) -> Response { - let cfg = - match KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH) { - Ok(c) => c, - Err(_) => { - return Response::Unavail; - } - }; - let req = ClientRequest::NssGroupByGid(gid); - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; - - daemon_client - .call_and_wait(&req, cfg.unix_sock_timeout) - .map(|r| match r { - ClientResponse::NssGroup(opt) => opt - .map(group_from_nssgroup) - .map(Response::Success) - .unwrap_or_else(|| Response::NotFound), - _ => Response::NotFound, - }) - .unwrap_or_else(|_| Response::NotFound) - } - - fn get_entry_by_name(name: String) -> Response { - let cfg = - match KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH) { - Ok(c) => c, - Err(_) => { - return Response::Unavail; - } - }; - let req = ClientRequest::NssGroupByName(name); - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(_) => { - return Response::Unavail; - } - }; - - daemon_client - .call_and_wait(&req, cfg.unix_sock_timeout) - .map(|r| match r { - ClientResponse::NssGroup(opt) => opt - .map(group_from_nssgroup) - .map(Response::Success) - .unwrap_or_else(|| Response::NotFound), - _ => Response::NotFound, - }) - .unwrap_or_else(|_| Response::NotFound) - } -} - -fn passwd_from_nssuser(nu: NssUser) -> Passwd { - Passwd { - name: nu.name, - gecos: nu.gecos, - passwd: "x".to_string(), - uid: nu.gid, - gid: nu.gid, - dir: nu.homedir, - shell: nu.shell, - } -} - -fn group_from_nssgroup(ng: NssGroup) -> Group { - Group { - name: ng.name, - passwd: "x".to_string(), - gid: ng.gid, - members: ng.members, - } -} diff --git a/unix_integration/nss_kanidm/src/lib.rs b/unix_integration/nss_kanidm/src/lib.rs index e3eeba76b..0ac5c7cbc 100644 --- a/unix_integration/nss_kanidm/src/lib.rs +++ b/unix_integration/nss_kanidm/src/lib.rs @@ -15,4 +15,10 @@ extern crate libnss; #[cfg(target_family = "unix")] -mod implementation; +mod hooks; + +#[cfg(target_family = "unix")] +pub(crate) mod core; + +#[cfg(test)] +mod tests; diff --git a/unix_integration/nss_kanidm/src/tests.rs b/unix_integration/nss_kanidm/src/tests.rs new file mode 100644 index 000000000..59629026e --- /dev/null +++ b/unix_integration/nss_kanidm/src/tests.rs @@ -0,0 +1,258 @@ +use crate::core::{self, RequestOptions}; +use kanidm_unix_common::unix_passwd::{EtcGroup, EtcUser}; +use libnss::interop::Response; + +impl RequestOptions { + fn fallback_fixture() -> Self { + RequestOptions::Test { + socket: None, + users: vec![ + EtcUser { + name: "root".to_string(), + password: "a".to_string(), + uid: 0, + gid: 0, + gecos: "Root".to_string(), + homedir: "/root".to_string(), + shell: "/bin/bash".to_string(), + }, + EtcUser { + name: "tobias".to_string(), + password: "a".to_string(), + uid: 1000, + gid: 1000, + gecos: "Tobias".to_string(), + homedir: "/home/tobias".to_string(), + shell: "/bin/zsh".to_string(), + }, + EtcUser { + name: "ellie".to_string(), + password: "a".to_string(), + uid: 1001, + gid: 1001, + gecos: "Ellie".to_string(), + homedir: "/home/ellie".to_string(), + shell: "/bin/tcsh".to_string(), + }, + ], + groups: vec![ + EtcGroup { + name: "root".to_string(), + password: "a".to_string(), + gid: 0, + members: vec!["root".to_string()], + }, + EtcGroup { + name: "tobias".to_string(), + password: "a".to_string(), + gid: 1000, + members: vec!["tobias".to_string()], + }, + EtcGroup { + name: "ellie".to_string(), + password: "a".to_string(), + gid: 1001, + members: vec!["ellie".to_string()], + }, + ], + } + } + + fn fallback_unavail() -> Self { + RequestOptions::Test { + socket: None, + users: vec![], + groups: vec![], + } + } +} + +#[test] +fn nss_fallback_unavail() { + let req_opt = RequestOptions::fallback_unavail(); + let Response::Unavail = core::get_all_user_entries(req_opt) else { + unreachable!(); + }; + + let req_opt = RequestOptions::fallback_unavail(); + let Response::Unavail = core::get_user_entry_by_uid(0, req_opt) else { + unreachable!(); + }; + + let req_opt = RequestOptions::fallback_unavail(); + let Response::Unavail = core::get_user_entry_by_name("root".to_string(), req_opt) else { + unreachable!(); + }; + + let req_opt = RequestOptions::fallback_unavail(); + let Response::Unavail = core::get_all_group_entries(req_opt) else { + unreachable!(); + }; + + let req_opt = RequestOptions::fallback_unavail(); + let Response::Unavail = core::get_group_entry_by_gid(0, req_opt) else { + unreachable!(); + }; + + let req_opt = RequestOptions::fallback_unavail(); + let Response::Unavail = core::get_group_entry_by_name("root".to_string(), req_opt) else { + unreachable!(); + }; +} + +#[test] +fn nss_fallback_all_user_entries() { + let req_opt = RequestOptions::fallback_fixture(); + + let Response::Success(users) = core::get_all_user_entries(req_opt) else { + unreachable!(); + }; + + assert_eq!(users.len(), 3); + assert_eq!(users[0].name, "root"); + assert_eq!(users[0].passwd, "x"); + assert_eq!(users[0].uid, 0); + assert_eq!(users[0].gid, 0); + + assert_eq!(users[1].name, "tobias"); + assert_eq!(users[1].passwd, "x"); + assert_eq!(users[1].uid, 1000); + assert_eq!(users[1].gid, 1000); + + assert_eq!(users[2].name, "ellie"); + assert_eq!(users[2].passwd, "x"); + assert_eq!(users[2].uid, 1001); + assert_eq!(users[2].gid, 1001); +} + +#[test] +fn nss_fallback_user_entry_by_uid() { + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(user) = core::get_user_entry_by_uid(0, req_opt) else { + unreachable!(); + }; + + assert_eq!(user.name, "root"); + assert_eq!(user.passwd, "x"); + assert_eq!(user.uid, 0); + assert_eq!(user.gid, 0); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(user) = core::get_user_entry_by_uid(1000, req_opt) else { + unreachable!(); + }; + + assert_eq!(user.name, "tobias"); + assert_eq!(user.passwd, "x"); + assert_eq!(user.uid, 1000); + assert_eq!(user.gid, 1000); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::NotFound = core::get_user_entry_by_uid(10, req_opt) else { + unreachable!(); + }; +} + +#[test] +fn nss_fallback_user_entry_by_name() { + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(user) = core::get_user_entry_by_name("root".to_string(), req_opt) else { + unreachable!(); + }; + + assert_eq!(user.name, "root"); + assert_eq!(user.passwd, "x"); + assert_eq!(user.uid, 0); + assert_eq!(user.gid, 0); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(user) = core::get_user_entry_by_name("ellie".to_string(), req_opt) else { + unreachable!(); + }; + + assert_eq!(user.name, "ellie"); + assert_eq!(user.passwd, "x"); + assert_eq!(user.uid, 1001); + assert_eq!(user.gid, 1001); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::NotFound = core::get_user_entry_by_name("william".to_string(), req_opt) else { + unreachable!(); + }; +} + +#[test] +fn nss_fallback_all_group_entries() { + let req_opt = RequestOptions::fallback_fixture(); + + let Response::Success(groups) = core::get_all_group_entries(req_opt) else { + unreachable!(); + }; + + assert_eq!(groups.len(), 3); + assert_eq!(groups[0].name, "root"); + assert_eq!(groups[0].passwd, "x"); + assert_eq!(groups[0].gid, 0); + + assert_eq!(groups[1].name, "tobias"); + assert_eq!(groups[1].passwd, "x"); + assert_eq!(groups[1].gid, 1000); + + assert_eq!(groups[2].name, "ellie"); + assert_eq!(groups[2].passwd, "x"); + assert_eq!(groups[2].gid, 1001); +} + +#[test] +fn nss_fallback_group_entry_by_uid() { + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(group) = core::get_group_entry_by_gid(0, req_opt) else { + unreachable!(); + }; + + assert_eq!(group.name, "root"); + assert_eq!(group.passwd, "x"); + assert_eq!(group.gid, 0); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(group) = core::get_group_entry_by_gid(1000, req_opt) else { + unreachable!(); + }; + + assert_eq!(group.name, "tobias"); + assert_eq!(group.passwd, "x"); + assert_eq!(group.gid, 1000); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::NotFound = core::get_group_entry_by_gid(10, req_opt) else { + unreachable!(); + }; +} + +#[test] +fn nss_fallback_group_entry_by_name() { + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(group) = core::get_group_entry_by_name("root".to_string(), req_opt) + else { + unreachable!(); + }; + + assert_eq!(group.name, "root"); + assert_eq!(group.passwd, "x"); + assert_eq!(group.gid, 0); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::Success(group) = core::get_group_entry_by_name("ellie".to_string(), req_opt) + else { + unreachable!(); + }; + + assert_eq!(group.name, "ellie"); + assert_eq!(group.passwd, "x"); + assert_eq!(group.gid, 1001); + + let req_opt = RequestOptions::fallback_fixture(); + let Response::NotFound = core::get_group_entry_by_name("william".to_string(), req_opt) else { + unreachable!(); + }; +} diff --git a/unix_integration/pam_kanidm/Cargo.toml b/unix_integration/pam_kanidm/Cargo.toml index 9d32d528d..984a6b703 100644 --- a/unix_integration/pam_kanidm/Cargo.toml +++ b/unix_integration/pam_kanidm/Cargo.toml @@ -21,6 +21,7 @@ kanidm_unix_common = { workspace = true } libc = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } +time = { workspace = true } [build-dependencies] pkg-config = { workspace = true } diff --git a/unix_integration/pam_kanidm/src/core.rs b/unix_integration/pam_kanidm/src/core.rs new file mode 100644 index 000000000..b30331363 --- /dev/null +++ b/unix_integration/pam_kanidm/src/core.rs @@ -0,0 +1,534 @@ +use crate::constants::PamResultCode; +use crate::module::PamResult; +use crate::pam::ModuleOptions; +use kanidm_unix_common::client_sync::DaemonClientBlocking; +use kanidm_unix_common::unix_config::KanidmUnixdConfig; +use kanidm_unix_common::unix_passwd::{ + read_etc_passwd_file, read_etc_shadow_file, EtcShadow, EtcUser, +}; +use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse}; +use kanidm_unix_common::unix_proto::{ + DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse, PamServiceInfo, +}; +use std::time::Duration; +use time::OffsetDateTime; + +use tracing::{debug, error}; + +#[cfg(test)] +use kanidm_unix_common::client_sync::UnixStream; + +pub enum RequestOptions { + Main { + config_path: &'static str, + }, + #[cfg(test)] + Test { + socket: Option, + users: Vec, + // groups: Vec, + shadow: Vec, + }, +} + +enum Source { + Daemon(DaemonClientBlocking), + Fallback { + users: Vec, + // groups: Vec, + shadow: Vec, + }, +} + +impl RequestOptions { + fn connect_to_daemon(self) -> Source { + match self { + RequestOptions::Main { config_path } => { + let maybe_client = KanidmUnixdConfig::new() + .read_options_from_optional_config(config_path) + .ok() + .and_then(|cfg| { + DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) + .ok() + }); + + if let Some(client) = maybe_client { + Source::Daemon(client) + } else { + let users = read_etc_passwd_file("/etc/passwd").unwrap_or_default(); + // let groups = read_etc_group_file("/etc/group").unwrap_or_default(); + let shadow = read_etc_shadow_file("/etc/shadow").unwrap_or_default(); + Source::Fallback { + users, + // groups, + shadow, + } + } + } + #[cfg(test)] + RequestOptions::Test { + socket, + users, + // groups, + shadow, + } => { + if let Some(socket) = socket { + Source::Daemon(DaemonClientBlocking::from(socket)) + } else { + Source::Fallback { users, shadow } + } + } + } + } +} + +pub trait PamHandler { + fn account_id(&self) -> PamResult; + + fn service_info(&self) -> PamResult; + + fn authtok(&self) -> PamResult>; + + /// Display a message to the user. + fn message(&self, prompt: &str) -> PamResult<()>; + + /// Display a device grant request to the user. + fn message_device_grant(&self, data: &DeviceAuthorizationResponse) -> PamResult<()>; + + /// Request a password from the user. + fn prompt_for_password(&self) -> PamResult>; + + fn prompt_for_pin(&self, msg: Option<&str>) -> PamResult>; + + fn prompt_for_mfacode(&self) -> PamResult>; +} + +pub fn sm_authenticate_connected( + pamh: &P, + opts: &ModuleOptions, + _current_time: OffsetDateTime, + mut daemon_client: DaemonClientBlocking, +) -> PamResultCode { + let info = match pamh.service_info() { + Ok(info) => info, + Err(e) => { + error!(err = ?e, "get_pam_info"); + return e; + } + }; + + let account_id = match pamh.account_id() { + Ok(acc) => acc, + Err(err) => return err, + }; + + let mut timeout: Option = None; + let mut active_polling_interval = Duration::from_secs(1); + + let mut stacked_authtok = if opts.use_first_pass { + match pamh.authtok() { + Ok(authtok) => authtok, + Err(err) => return err, + } + } else { + None + }; + + let mut req = ClientRequest::PamAuthenticateInit { account_id, info }; + + loop { + let client_response = match daemon_client.call_and_wait(&req, timeout) { + Ok(r) => r, + Err(err) => { + // Something unrecoverable occured, bail and stop everything + error!(?err, "PAM_AUTH_ERR"); + return PamResultCode::PAM_AUTH_ERR; + } + }; + + match client_response { + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => { + return PamResultCode::PAM_SUCCESS; + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Denied) => { + return PamResultCode::PAM_AUTH_ERR; + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Unknown) => { + if opts.ignore_unknown_user { + return PamResultCode::PAM_IGNORE; + } else { + return PamResultCode::PAM_USER_UNKNOWN; + } + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => { + let mut authtok = None; + std::mem::swap(&mut authtok, &mut stacked_authtok); + + let cred = if let Some(cred) = authtok { + cred + } else { + match pamh.prompt_for_password() { + Ok(Some(cred)) => cred, + Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT, + Err(err) => return err, + } + }; + + // Now setup the request for the next loop. + timeout = None; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred }); + continue; + } + ClientResponse::PamAuthenticateStepResponse( + PamAuthResponse::DeviceAuthorizationGrant { data }, + ) => { + if let Err(err) = pamh.message_device_grant(&data) { + return err; + }; + + timeout = Some(u64::from(data.expires_in)); + req = + ClientRequest::PamAuthenticateStep(PamAuthRequest::DeviceAuthorizationGrant { + data, + }); + continue; + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFACode { msg: _ }) => { + let cred = match pamh.prompt_for_mfacode() { + Ok(Some(cred)) => cred, + Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT, + Err(err) => return err, + }; + + // Now setup the request for the next loop. + timeout = None; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFACode { cred }); + continue; + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPoll { + msg, + polling_interval, + }) => { + if let Err(err) = pamh.message(msg.as_str()) { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + + active_polling_interval = Duration::from_secs(polling_interval.into()); + + timeout = None; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFAPoll); + // We don't need to actually sleep here as we immediately will poll and then go + // into the MFAPollWait response below. + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPollWait) => { + // Counter intuitive, but we don't need a max poll attempts here because + // if the resolver goes away, then this will error on the sock and + // will shutdown. This allows the resolver to dynamically extend the + // timeout if needed, and removes logic from the front end. + std::thread::sleep(active_polling_interval); + timeout = None; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFAPoll); + } + + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::SetupPin { msg }) => { + if let Err(err) = pamh.message(msg.as_str()) { + return err; + } + + let mut pin; + let mut confirm; + + loop { + pin = match pamh.prompt_for_pin(Some("New PIN: ")) { + Ok(Some(p)) => p, + Ok(None) => { + debug!("no pin"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + Err(err) => { + debug!("unable to get pin"); + return err; + } + }; + + confirm = match pamh.prompt_for_pin(Some("Confirm PIN: ")) { + Ok(Some(p)) => p, + Ok(None) => { + debug!("no pin"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + Err(err) => { + debug!("unable to get pin"); + return err; + } + }; + + if pin == confirm { + break; + } else if let Err(err) = pamh.message("Inputs did not match. Try again.") { + return err; + } + } + + // Now setup the request for the next loop. + timeout = None; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::SetupPin { pin }); + continue; + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Pin) => { + let mut authtok = None; + std::mem::swap(&mut authtok, &mut stacked_authtok); + + let cred = if let Some(cred) = authtok { + cred + } else { + match pamh.prompt_for_pin(None) { + Ok(Some(cred)) => cred, + Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT, + Err(err) => return err, + } + }; + + // Now setup the request for the next loop. + timeout = None; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Pin { cred }); + continue; + } + + ClientResponse::Ok + | ClientResponse::Error + | ClientResponse::SshKeys(_) + | ClientResponse::NssAccounts(_) + | ClientResponse::NssAccount(_) + | ClientResponse::NssGroups(_) + | ClientResponse::PamStatus(_) + | ClientResponse::ProviderStatus(_) + | ClientResponse::NssGroup(_) => { + debug!("PamResultCode::PAM_AUTH_ERR"); + return PamResultCode::PAM_AUTH_ERR; + } + } + } // while true, continue calling PamAuthenticateStep until we get a decision. +} + +pub fn sm_authenticate_fallback( + pamh: &P, + opts: &ModuleOptions, + current_time: OffsetDateTime, + users: Vec, + shadow: Vec, +) -> PamResultCode { + let account_id = match pamh.account_id() { + Ok(acc) => acc, + Err(err) => return err, + }; + + let user = users.into_iter().find(|etcuser| etcuser.name == account_id); + + let shadow = shadow + .into_iter() + .find(|etcshadow| etcshadow.name == account_id); + + let (_user, shadow) = match (user, shadow) { + (Some(user), Some(shadow)) => (user, shadow), + _ => { + if opts.ignore_unknown_user { + debug!("PamResultCode::PAM_IGNORE"); + return PamResultCode::PAM_IGNORE; + } else { + debug!("PamResultCode::PAM_USER_UNKNOWN"); + return PamResultCode::PAM_USER_UNKNOWN; + } + } + }; + + let expiration_date = shadow + .epoch_expire_date + .map(|expire| OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire)); + + if let Some(expire) = expiration_date { + if current_time >= expire { + debug!("PamResultCode::PAM_ACCT_EXPIRED"); + return PamResultCode::PAM_ACCT_EXPIRED; + } + }; + + // All checks passed! We can now proceed to authenticate the account. + let mut stacked_authtok = if opts.use_first_pass { + match pamh.authtok() { + Ok(authtok) => authtok, + Err(err) => return err, + } + } else { + None + }; + + let mut authtok = None; + std::mem::swap(&mut authtok, &mut stacked_authtok); + + let cred = if let Some(cred) = authtok { + cred + } else { + match pamh.prompt_for_password() { + Ok(Some(cred)) => cred, + Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT, + Err(err) => return err, + } + }; + + if shadow.password.check_pw(cred.as_str()) { + PamResultCode::PAM_SUCCESS + } else { + PamResultCode::PAM_AUTH_ERR + } +} + +pub fn sm_authenticate( + pamh: &P, + opts: &ModuleOptions, + req_opt: RequestOptions, + current_time: OffsetDateTime, +) -> PamResultCode { + match req_opt.connect_to_daemon() { + Source::Daemon(daemon_client) => { + sm_authenticate_connected(pamh, opts, current_time, daemon_client) + } + Source::Fallback { users, shadow } => { + sm_authenticate_fallback(pamh, opts, current_time, users, shadow) + } + } +} + +pub fn acct_mgmt( + pamh: &P, + opts: &ModuleOptions, + req_opt: RequestOptions, + current_time: OffsetDateTime, +) -> PamResultCode { + let account_id = match pamh.account_id() { + Ok(acc) => acc, + Err(err) => return err, + }; + + match req_opt.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::PamAccountAllowed(account_id); + match daemon_client.call_and_wait(&req, None) { + Ok(r) => match r { + ClientResponse::PamStatus(Some(true)) => { + debug!("PamResultCode::PAM_SUCCESS"); + PamResultCode::PAM_SUCCESS + } + ClientResponse::PamStatus(Some(false)) => { + debug!("PamResultCode::PAM_AUTH_ERR"); + PamResultCode::PAM_AUTH_ERR + } + ClientResponse::PamStatus(None) => { + if opts.ignore_unknown_user { + debug!("PamResultCode::PAM_IGNORE"); + PamResultCode::PAM_IGNORE + } else { + debug!("PamResultCode::PAM_USER_UNKNOWN"); + PamResultCode::PAM_USER_UNKNOWN + } + } + _ => { + // unexpected response. + error!(err = ?r, "PAM_IGNORE, unexpected resolver response"); + PamResultCode::PAM_IGNORE + } + }, + Err(e) => { + error!(err = ?e, "PamResultCode::PAM_IGNORE"); + PamResultCode::PAM_IGNORE + } + } + } + Source::Fallback { users, shadow } => { + let user = users.into_iter().find(|etcuser| etcuser.name == account_id); + + let shadow = shadow + .into_iter() + .find(|etcshadow| etcshadow.name == account_id); + + let (_user, shadow) = match (user, shadow) { + (Some(user), Some(shadow)) => (user, shadow), + _ => { + if opts.ignore_unknown_user { + debug!("PamResultCode::PAM_IGNORE"); + return PamResultCode::PAM_IGNORE; + } else { + debug!("PamResultCode::PAM_USER_UNKNOWN"); + return PamResultCode::PAM_USER_UNKNOWN; + } + } + }; + + let expiration_date = shadow + .epoch_expire_date + .map(|expire| OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire)); + + if let Some(expire) = expiration_date { + if current_time >= expire { + debug!("PamResultCode::PAM_ACCT_EXPIRED"); + return PamResultCode::PAM_ACCT_EXPIRED; + } + }; + + // All checks passed! + + debug!("PAM_SUCCESS"); + PamResultCode::PAM_SUCCESS + } + } +} + +pub fn sm_open_session( + pamh: &P, + _opts: &ModuleOptions, + req_opt: RequestOptions, +) -> PamResultCode { + let account_id = match pamh.account_id() { + Ok(acc) => acc, + Err(err) => return err, + }; + + match req_opt.connect_to_daemon() { + Source::Daemon(mut daemon_client) => { + let req = ClientRequest::PamAccountBeginSession(account_id); + + match daemon_client.call_and_wait(&req, None) { + Ok(ClientResponse::Ok) => { + debug!("PAM_SUCCESS"); + PamResultCode::PAM_SUCCESS + } + other => { + debug!(err = ?other, "PAM_IGNORE"); + PamResultCode::PAM_IGNORE + } + } + } + Source::Fallback { + users: _, + shadow: _, + } => { + debug!("PAM_SUCCESS"); + PamResultCode::PAM_SUCCESS + } + } +} + +pub fn sm_close_session(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode { + PamResultCode::PAM_SUCCESS +} + +pub fn sm_chauthtok(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode { + PamResultCode::PAM_IGNORE +} + +pub fn sm_setcred(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode { + PamResultCode::PAM_SUCCESS +} diff --git a/unix_integration/pam_kanidm/src/lib.rs b/unix_integration/pam_kanidm/src/lib.rs index 9faf11f75..797283b3f 100644 --- a/unix_integration/pam_kanidm/src/lib.rs +++ b/unix_integration/pam_kanidm/src/lib.rs @@ -3,8 +3,8 @@ #![deny(clippy::todo)] #![deny(clippy::unimplemented)] // In this file, we do want to panic on these faults. -// #![deny(clippy::unwrap_used)] -// #![deny(clippy::expect_used)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] #![deny(clippy::panic)] #![deny(clippy::unreachable)] #![deny(clippy::await_holding_lock)] @@ -14,6 +14,11 @@ #[cfg(target_family = "unix")] mod pam; +pub(crate) mod core; + // pub use needs to be here so it'll compile and export all the things #[cfg(target_family = "unix")] pub use crate::pam::*; + +#[cfg(test)] +mod tests; diff --git a/unix_integration/pam_kanidm/src/pam/conv.rs b/unix_integration/pam_kanidm/src/pam/conv.rs index 166fdf266..6c0b970ce 100644 --- a/unix_integration/pam_kanidm/src/pam/conv.rs +++ b/unix_integration/pam_kanidm/src/pam/conv.rs @@ -55,7 +55,7 @@ impl PamConv { /// styles. pub fn send(&self, style: PamMessageStyle, msg: &str) -> PamResult> { let mut resp_ptr: *const PamResponse = ptr::null(); - let msg_cstr = CString::new(msg).unwrap(); + let msg_cstr = CString::new(msg).map_err(|_| PamResultCode::PAM_CONV_ERR)?; let msg = PamMessage { msg_style: style, msg: msg_cstr.as_ptr(), diff --git a/unix_integration/pam_kanidm/src/pam/mod.rs b/unix_integration/pam_kanidm/src/pam/mod.rs index 6a76c6114..0a40b1c25 100755 --- a/unix_integration/pam_kanidm/src/pam/mod.rs +++ b/unix_integration/pam_kanidm/src/pam/mod.rs @@ -35,27 +35,21 @@ use std::collections::BTreeSet; use std::convert::TryFrom; use std::ffi::CStr; -use kanidm_unix_common::client_sync::DaemonClientBlocking; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::unix_config::KanidmUnixdConfig; -use kanidm_unix_common::unix_proto::{ - ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, -}; +use crate::core::{self, RequestOptions}; use crate::pam::constants::*; -use crate::pam::conv::PamConv; use crate::pam::module::{PamHandle, PamHooks}; use crate::pam_hooks; use constants::PamResultCode; +use time::OffsetDateTime; -use tracing::{debug, error}; +use tracing::debug; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt; use tracing_subscriber::prelude::*; -use std::thread; -use std::time::Duration; - pub fn get_cfg() -> Result { KanidmUnixdConfig::new() .read_options_from_optional_config(DEFAULT_CONFIG_PATH) @@ -77,14 +71,14 @@ fn install_subscriber(debug: bool) { .try_init(); } -#[derive(Debug)] -struct Options { - debug: bool, - use_first_pass: bool, - ignore_unknown_user: bool, +#[derive(Debug, Default)] +pub struct ModuleOptions { + pub debug: bool, + pub use_first_pass: bool, + pub ignore_unknown_user: bool, } -impl TryFrom<&Vec<&CStr>> for Options { +impl TryFrom<&Vec<&CStr>> for ModuleOptions { type Error = (); fn try_from(args: &Vec<&CStr>) -> Result { @@ -97,7 +91,7 @@ impl TryFrom<&Vec<&CStr>> for Options { } }; - Ok(Options { + Ok(ModuleOptions { debug: gopts.contains("debug"), use_first_pass: gopts.contains("use_first_pass"), ignore_unknown_user: gopts.contains("ignore_unknown_user"), @@ -109,426 +103,47 @@ pub struct PamKanidm; pam_hooks!(PamKanidm); -macro_rules! match_sm_auth_client_response { - ($expr:expr, $opts:ident, $($pat:pat => $result:expr),*) => { - match $expr { - Ok(r) => match r { - $($pat => $result),* - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => { - return PamResultCode::PAM_SUCCESS; - } - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Denied) => { - return PamResultCode::PAM_AUTH_ERR; - } - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Unknown) => { - if $opts.ignore_unknown_user { - return PamResultCode::PAM_IGNORE; - } else { - return PamResultCode::PAM_USER_UNKNOWN; - } - } - _ => { - // unexpected response. - error!(err = ?r, "PAM_IGNORE, unexpected resolver response"); - return PamResultCode::PAM_IGNORE; - } - }, - Err(err) => { - error!(?err, "PAM_IGNORE"); - return PamResultCode::PAM_IGNORE; - } - } - } -} - impl PamHooks for PamKanidm { - fn acct_mgmt(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { - let opts = match Options::try_from(&args) { - Ok(o) => o, - Err(_) => return PamResultCode::PAM_SERVICE_ERR, - }; - - install_subscriber(opts.debug); - - let tty = pamh.get_tty(); - let rhost = pamh.get_rhost(); - - debug!(?args, ?opts, ?tty, ?rhost, "acct_mgmt"); - - let account_id = match pamh.get_user(None) { - Ok(aid) => aid, - Err(e) => { - error!(err = ?e, "get_user"); - return e; - } - }; - - let cfg = match get_cfg() { - Ok(cfg) => cfg, - Err(e) => return e, - }; - let req = ClientRequest::PamAccountAllowed(account_id); - // PamResultCode::PAM_IGNORE - - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(e) => { - error!(err = ?e, "Error DaemonClientBlocking::new()"); - return PamResultCode::PAM_SERVICE_ERR; - } - }; - - match daemon_client.call_and_wait(&req, cfg.unix_sock_timeout) { - Ok(r) => match r { - ClientResponse::PamStatus(Some(true)) => { - debug!("PamResultCode::PAM_SUCCESS"); - PamResultCode::PAM_SUCCESS - } - ClientResponse::PamStatus(Some(false)) => { - debug!("PamResultCode::PAM_AUTH_ERR"); - PamResultCode::PAM_AUTH_ERR - } - ClientResponse::PamStatus(None) => { - if opts.ignore_unknown_user { - debug!("PamResultCode::PAM_IGNORE"); - PamResultCode::PAM_IGNORE - } else { - debug!("PamResultCode::PAM_USER_UNKNOWN"); - PamResultCode::PAM_USER_UNKNOWN - } - } - _ => { - // unexpected response. - error!(err = ?r, "PAM_IGNORE, unexpected resolver response"); - PamResultCode::PAM_IGNORE - } - }, - Err(e) => { - error!(err = ?e, "PamResultCode::PAM_IGNORE"); - PamResultCode::PAM_IGNORE - } - } - } - fn sm_authenticate(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { - let opts = match Options::try_from(&args) { + let opts = match ModuleOptions::try_from(&args) { Ok(o) => o, Err(_) => return PamResultCode::PAM_SERVICE_ERR, }; install_subscriber(opts.debug); - let info = match pamh.get_pam_info() { - Ok(info) => info, - Err(e) => { - error!(err = ?e, "get_pam_info"); - return e; - } + debug!(?args, ?opts, "acct_mgmt"); + + let current_time = OffsetDateTime::now_utc(); + + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, }; - debug!(?args, ?opts, ?info, "sm_authenticate"); - - let account_id = match pamh.get_user(None) { - Ok(aid) => aid, - Err(e) => { - error!(err = ?e, "get_user"); - return e; - } - }; - - let cfg = match get_cfg() { - Ok(cfg) => cfg, - Err(e) => return e, - }; - - let mut timeout = cfg.unix_sock_timeout; - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(e) => { - error!(err = ?e, "Error DaemonClientBlocking::new()"); - return PamResultCode::PAM_SERVICE_ERR; - } - }; - - // Later we may need to move this to a function and call it as a oneshot for auth methods - // that don't require any authtoks at all. For example, imagine a user authed and they - // needed to follow a URL to continue. In that case, they would fail here because they - // didn't enter an authtok that they didn't need! - let mut authtok = match pamh.get_authtok() { - Ok(Some(v)) => Some(v), - Ok(None) => { - if opts.use_first_pass { - debug!("Don't have an authtok, returning PAM_AUTH_ERR"); - return PamResultCode::PAM_AUTH_ERR; - } - None - } - Err(e) => { - error!(err = ?e, "get_authtok"); - return e; - } - }; - - let conv = match pamh.get_item::() { - Ok(conv) => conv, - Err(err) => { - error!(?err, "pam_conv"); - return err; - } - }; - - let mut req = ClientRequest::PamAuthenticateInit { account_id, info }; - - loop { - match_sm_auth_client_response!(daemon_client.call_and_wait(&req, timeout), opts, - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => { - let mut consume_authtok = None; - // Swap the authtok out with a None, so it can only be consumed once. - // If it's already been swapped, we are just swapping two null pointers - // here effectively. - std::mem::swap(&mut authtok, &mut consume_authtok); - let cred = if let Some(cred) = consume_authtok { - cred - } else { - match conv.send(PAM_PROMPT_ECHO_OFF, "Password: ") { - Ok(password) => match password { - Some(cred) => cred, - None => { - debug!("no password"); - return PamResultCode::PAM_CRED_INSUFFICIENT; - } - }, - Err(err) => { - debug!("unable to get password"); - return err; - } - } - }; - - // Now setup the request for the next loop. - timeout = cfg.unix_sock_timeout; - req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred }); - continue; - }, - ClientResponse::PamAuthenticateStepResponse( - PamAuthResponse::DeviceAuthorizationGrant { data }, - ) => { - let msg = match &data.message { - Some(msg) => msg.clone(), - None => format!("Using a browser on another device, visit:\n{}\nAnd enter the code:\n{}", - data.verification_uri, data.user_code) - }; - match conv.send(PAM_TEXT_INFO, &msg) { - Ok(_) => {} - Err(err) => { - if opts.debug { - println!("Message prompt failed"); - } - return err; - } - } - - timeout = u64::from(data.expires_in); - req = ClientRequest::PamAuthenticateStep( - PamAuthRequest::DeviceAuthorizationGrant { data }, - ); - continue; - }, - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFACode { - msg, - }) => { - match conv.send(PAM_TEXT_INFO, &msg) { - Ok(_) => {} - Err(err) => { - if opts.debug { - println!("Message prompt failed"); - } - return err; - } - } - let cred = match conv.send(PAM_PROMPT_ECHO_OFF, "Code: ") { - Ok(password) => match password { - Some(cred) => cred, - None => { - debug!("no mfa code"); - return PamResultCode::PAM_CRED_INSUFFICIENT; - } - }, - Err(err) => { - debug!("unable to get mfa code"); - return err; - } - }; - - // Now setup the request for the next loop. - timeout = cfg.unix_sock_timeout; - req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFACode { - cred, - }); - continue; - }, - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPoll { - msg, - polling_interval, - }) => { - match conv.send(PAM_TEXT_INFO, &msg) { - Ok(_) => {} - Err(err) => { - if opts.debug { - println!("Message prompt failed"); - } - return err; - } - } - - loop { - thread::sleep(Duration::from_secs(polling_interval.into())); - timeout = cfg.unix_sock_timeout; - req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFAPoll); - - // Counter intuitive, but we don't need a max poll attempts here because - // if the resolver goes away, then this will error on the sock and - // will shutdown. This allows the resolver to dynamically extend the - // timeout if needed, and removes logic from the front end. - match_sm_auth_client_response!( - daemon_client.call_and_wait(&req, timeout), opts, - ClientResponse::PamAuthenticateStepResponse( - PamAuthResponse::MFAPollWait, - ) => { - // Continue polling if the daemon says to wait - continue; - } - ); - - } - }, - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::SetupPin { - msg, - }) => { - match conv.send(PAM_TEXT_INFO, &msg) { - Ok(_) => {} - Err(err) => { - if opts.debug { - println!("Message prompt failed"); - } - return err; - } - } - - let mut pin; - let mut confirm; - loop { - pin = match conv.send(PAM_PROMPT_ECHO_OFF, "New PIN: ") { - Ok(password) => match password { - Some(cred) => cred, - None => { - debug!("no pin"); - return PamResultCode::PAM_CRED_INSUFFICIENT; - } - }, - Err(err) => { - debug!("unable to get pin"); - return err; - } - }; - - confirm = match conv.send(PAM_PROMPT_ECHO_OFF, "Confirm PIN: ") { - Ok(password) => match password { - Some(cred) => cred, - None => { - debug!("no confirmation pin"); - return PamResultCode::PAM_CRED_INSUFFICIENT; - } - }, - Err(err) => { - debug!("unable to get confirmation pin"); - return err; - } - }; - - if pin == confirm { - break; - } else { - match conv.send(PAM_TEXT_INFO, "Inputs did not match. Try again.") { - Ok(_) => {} - Err(err) => { - if opts.debug { - println!("Message prompt failed"); - } - return err; - } - } - } - } - - // Now setup the request for the next loop. - timeout = cfg.unix_sock_timeout; - req = ClientRequest::PamAuthenticateStep(PamAuthRequest::SetupPin { - pin, - }); - continue; - }, - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Pin) => { - let mut consume_authtok = None; - // Swap the authtok out with a None, so it can only be consumed once. - // If it's already been swapped, we are just swapping two null pointers - // here effectively. - std::mem::swap(&mut authtok, &mut consume_authtok); - let cred = if let Some(cred) = consume_authtok { - cred - } else { - match conv.send(PAM_PROMPT_ECHO_OFF, "PIN: ") { - Ok(password) => match password { - Some(cred) => cred, - None => { - debug!("no pin"); - return PamResultCode::PAM_CRED_INSUFFICIENT; - } - }, - Err(err) => { - debug!("unable to get pin"); - return err; - } - } - }; - - // Now setup the request for the next loop. - timeout = cfg.unix_sock_timeout; - req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Pin { cred }); - continue; - } - ); - } // while true, continue calling PamAuthenticateStep until we get a decision. + core::sm_authenticate(pamh, &opts, req_opt, current_time) } - fn sm_chauthtok(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { - let opts = match Options::try_from(&args) { + fn acct_mgmt(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + let opts = match ModuleOptions::try_from(&args) { Ok(o) => o, Err(_) => return PamResultCode::PAM_SERVICE_ERR, }; install_subscriber(opts.debug); - debug!(?args, ?opts, "sm_chauthtok"); + debug!(?args, ?opts, "acct_mgmt"); - PamResultCode::PAM_IGNORE - } + let current_time = OffsetDateTime::now_utc(); - fn sm_close_session(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { - let opts = match Options::try_from(&args) { - Ok(o) => o, - Err(_) => return PamResultCode::PAM_SERVICE_ERR, + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, }; - install_subscriber(opts.debug); - - debug!(?args, ?opts, "sm_close_session"); - - PamResultCode::PAM_SUCCESS + core::acct_mgmt(pamh, &opts, req_opt, current_time) } fn sm_open_session(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { - let opts = match Options::try_from(&args) { + let opts = match ModuleOptions::try_from(&args) { Ok(o) => o, Err(_) => return PamResultCode::PAM_SERVICE_ERR, }; @@ -537,42 +152,41 @@ impl PamHooks for PamKanidm { debug!(?args, ?opts, "sm_open_session"); - let account_id = match pamh.get_user(None) { - Ok(aid) => aid, - Err(err) => { - error!(?err, "get_user"); - return err; - } + let req_opt = RequestOptions::Main { + config_path: DEFAULT_CONFIG_PATH, }; - let cfg = match get_cfg() { - Ok(cfg) => cfg, - Err(e) => return e, - }; - let req = ClientRequest::PamAccountBeginSession(account_id); - - let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) { - Ok(dc) => dc, - Err(e) => { - error!(err = ?e, "Error DaemonClientBlocking::new()"); - return PamResultCode::PAM_SERVICE_ERR; - } - }; - - match daemon_client.call_and_wait(&req, cfg.unix_sock_timeout) { - Ok(ClientResponse::Ok) => { - // println!("PAM_SUCCESS"); - PamResultCode::PAM_SUCCESS - } - other => { - debug!(err = ?other, "PAM_IGNORE"); - PamResultCode::PAM_IGNORE - } - } + core::sm_open_session(pamh, &opts, req_opt) } - fn sm_setcred(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { - let opts = match Options::try_from(&args) { + fn sm_close_session(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + let opts = match ModuleOptions::try_from(&args) { + Ok(o) => o, + Err(_) => return PamResultCode::PAM_SERVICE_ERR, + }; + + install_subscriber(opts.debug); + + debug!(?args, ?opts, "sm_close_session"); + + core::sm_close_session(pamh, &opts) + } + + fn sm_chauthtok(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + let opts = match ModuleOptions::try_from(&args) { + Ok(o) => o, + Err(_) => return PamResultCode::PAM_SERVICE_ERR, + }; + + install_subscriber(opts.debug); + + debug!(?args, ?opts, "sm_chauthtok"); + + core::sm_chauthtok(pamh, &opts) + } + + fn sm_setcred(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + let opts = match ModuleOptions::try_from(&args) { Ok(o) => o, Err(_) => return PamResultCode::PAM_SERVICE_ERR, }; @@ -581,6 +195,6 @@ impl PamHooks for PamKanidm { debug!(?args, ?opts, "sm_setcred"); - PamResultCode::PAM_SUCCESS + core::sm_setcred(pamh, &opts) } } diff --git a/unix_integration/pam_kanidm/src/pam/module.rs b/unix_integration/pam_kanidm/src/pam/module.rs index 8cfcdbb11..95404b6a9 100755 --- a/unix_integration/pam_kanidm/src/pam/module.rs +++ b/unix_integration/pam_kanidm/src/pam/module.rs @@ -5,9 +5,14 @@ use std::{mem, ptr}; use libc::c_char; -use crate::pam::constants::{PamFlag, PamItemType, PamResultCode}; +use crate::pam::constants::{ + PamFlag, PamItemType, PamResultCode, PAM_PROMPT_ECHO_OFF, PAM_TEXT_INFO, +}; +use crate::pam::conv::PamConv; use crate::pam::items::{PamAuthTok, PamRHost, PamService, PamTty}; +use crate::core::PamHandler; +use kanidm_unix_common::unix_proto::DeviceAuthorizationResponse; use kanidm_unix_common::unix_proto::PamServiceInfo; /// Opaque type, used as a pointer when making pam API calls. @@ -26,6 +31,7 @@ pub enum PamDataT {} #[link(name = "pam")] extern "C" { + /* fn pam_get_data( pamh: *const PamHandle, module_data_name: *const c_char, @@ -42,6 +48,7 @@ extern "C" { error_status: PamResultCode, ), ) -> PamResultCode; + */ fn pam_get_item( pamh: *const PamHandle, @@ -49,8 +56,10 @@ extern "C" { item: &mut *const PamItemT, ) -> PamResultCode; + /* fn pam_set_item(pamh: *mut PamHandle, item_type: PamItemType, item: &PamItemT) -> PamResultCode; + */ fn pam_get_user( pamh: *const PamHandle, @@ -87,6 +96,7 @@ pub trait PamItem { } impl PamHandle { + /* /// # Safety /// /// Gets some value, identified by `key`, that has been set by the module @@ -94,7 +104,7 @@ impl PamHandle { /// /// See `pam_get_data` in /// - pub unsafe fn get_data<'a, T>(&'a self, key: &str) -> PamResult<&'a T> { + unsafe fn get_data<'a, T>(&'a self, key: &str) -> PamResult<&'a T> { let c_key = CString::new(key).unwrap(); let mut ptr: *const PamDataT = ptr::null(); let res = pam_get_data(self, c_key.as_ptr(), &mut ptr); @@ -112,7 +122,7 @@ impl PamHandle { /// /// See `pam_set_data` in /// - pub fn set_data(&self, key: &str, data: Box) -> PamResult<()> { + fn set_data(&self, key: &str, data: Box) -> PamResult<()> { let c_key = CString::new(key).unwrap(); let res = unsafe { let c_data: Box = mem::transmute(data); @@ -125,13 +135,14 @@ impl PamHandle { Err(res) } } + */ /// Retrieves a value that has been set, possibly by the pam client. This is /// particularly useful for getting a `PamConv` reference. /// /// See `pam_get_item` in /// - pub fn get_item<'a, T: PamItem>(&self) -> PamResult<&'a T> { + fn get_item<'a, T: PamItem>(&self) -> PamResult<&'a T> { let mut ptr: *const PamItemT = ptr::null(); let (res, item) = unsafe { let r = pam_get_item(self, T::item_type(), &mut ptr); @@ -146,7 +157,7 @@ impl PamHandle { } } - pub fn get_item_string(&self) -> PamResult> { + fn get_item_string(&self) -> PamResult> { let mut ptr: *const PamItemT = ptr::null(); let (res, item) = unsafe { let r = pam_get_item(self, T::item_type(), &mut ptr); @@ -165,6 +176,7 @@ impl PamHandle { } } + /* /// Sets a value in the pam context. The value can be retrieved using /// `get_item`. /// @@ -172,7 +184,7 @@ impl PamHandle { /// /// See `pam_set_item` in /// - pub fn set_item_str(&mut self, item: &str) -> PamResult<()> { + fn set_item_str(&mut self, item: &str) -> PamResult<()> { let c_item = CString::new(item).unwrap(); let res = unsafe { @@ -190,6 +202,7 @@ impl PamHandle { Err(res) } } + */ /// Retrieves the name of the user who is authenticating or logging in. /// @@ -197,11 +210,11 @@ impl PamHandle { /// /// See `pam_get_user` in /// - pub fn get_user(&self, prompt: Option<&str>) -> PamResult { + fn get_user(&self, prompt: Option<&str>) -> PamResult { let mut ptr: *const c_char = ptr::null_mut(); let res = match prompt { Some(p) => { - let c_prompt = CString::new(p).unwrap(); + let c_prompt = CString::new(p).map_err(|_| PamResultCode::PAM_CONV_ERR)?; unsafe { pam_get_user(self, &mut ptr, c_prompt.as_ptr()) } } None => unsafe { pam_get_user(self, &mut ptr, ptr::null()) }, @@ -219,23 +232,23 @@ impl PamHandle { } } - pub fn get_authtok(&self) -> PamResult> { + fn get_authtok(&self) -> PamResult> { self.get_item_string::() } - pub fn get_tty(&self) -> PamResult> { + fn get_tty(&self) -> PamResult> { self.get_item_string::() } - pub fn get_rhost(&self) -> PamResult> { + fn get_rhost(&self) -> PamResult> { self.get_item_string::() } - pub fn get_service(&self) -> PamResult> { + fn get_service(&self) -> PamResult> { self.get_item_string::() } - pub fn get_pam_info(&self) -> PamResult { + fn get_pam_info(&self) -> PamResult { let maybe_tty = self.get_tty()?; let maybe_rhost = self.get_rhost()?; let maybe_service = self.get_service()?; @@ -251,6 +264,57 @@ impl PamHandle { _ => Err(PamResultCode::PAM_CONV_ERR), } } + + fn get_conv(&self) -> PamResult<&PamConv> { + self.get_item::() + } +} + +impl PamHandler for PamHandle { + fn account_id(&self) -> PamResult { + self.get_user(None) + } + + fn service_info(&self) -> PamResult { + self.get_pam_info() + } + + fn authtok(&self) -> PamResult> { + self.get_authtok() + } + + fn message(&self, msg: &str) -> PamResult<()> { + let conv = self.get_conv()?; + conv.send(PAM_TEXT_INFO, msg).map(|_| ()) + } + + fn prompt_for_password(&self) -> PamResult> { + let conv = self.get_conv()?; + conv.send(PAM_PROMPT_ECHO_OFF, "Password: ") + } + + fn prompt_for_mfacode(&self) -> PamResult> { + let conv = self.get_conv()?; + conv.send(PAM_PROMPT_ECHO_OFF, "Code: ") + } + + fn prompt_for_pin(&self, msg: Option<&str>) -> PamResult> { + let conv = self.get_conv()?; + let msg = msg.unwrap_or("PIN: "); + conv.send(PAM_PROMPT_ECHO_OFF, msg) + } + + fn message_device_grant(&self, data: &DeviceAuthorizationResponse) -> PamResult<()> { + let conv = self.get_conv()?; + let msg = match &data.message { + Some(msg) => msg.clone(), + None => format!( + "Using a browser on another device, visit:\n{}\nAnd enter the code:\n{}", + data.verification_uri, data.user_code + ), + }; + conv.send(PAM_TEXT_INFO, &msg).map(|_| ()) + } } /// Provides functions that are invoked by the entrypoints generated by the diff --git a/unix_integration/pam_kanidm/src/tests.rs b/unix_integration/pam_kanidm/src/tests.rs new file mode 100644 index 000000000..2691c6d6f --- /dev/null +++ b/unix_integration/pam_kanidm/src/tests.rs @@ -0,0 +1,467 @@ +use crate::constants::PamResultCode; +use crate::core::PamHandler; +use crate::core::{self, RequestOptions}; +use crate::module::PamResult; +use crate::pam::ModuleOptions; +use kanidm_unix_common::unix_passwd::{CryptPw, EtcShadow, EtcUser}; +use kanidm_unix_common::unix_proto::{DeviceAuthorizationResponse, PamServiceInfo}; +use std::collections::VecDeque; +use std::str::FromStr; +use std::sync::Mutex; +use time::OffsetDateTime; + +impl RequestOptions { + fn fallback_fixture() -> Self { + RequestOptions::Test { + socket: None, + users: vec![ + EtcUser { + name: "root".to_string(), + password: "x".to_string(), + uid: 0, + gid: 0, + gecos: "Root".to_string(), + homedir: "/root".to_string(), + shell: "/bin/bash".to_string(), + }, + EtcUser { + name: "tobias".to_string(), + password: "x".to_string(), + uid: 1000, + gid: 1000, + gecos: "Tobias".to_string(), + homedir: "/home/tobias".to_string(), + shell: "/bin/zsh".to_string(), + }, + ], + // groups: vec![], + shadow: vec![ + EtcShadow { + name: "root".to_string(), + // The very secure password, 'a' + password: CryptPw::from_str("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//").unwrap(), + ..Default::default() + }, + EtcShadow { + name: "tobias".to_string(), + // The very secure password, 'a' + password: CryptPw::from_str("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//").unwrap(), + epoch_expire_date: Some(10), + ..Default::default() + }, + ], + } + } +} + +#[allow(dead_code)] +#[derive(Debug)] +enum Event { + Account(&'static str), + ServiceInfo(PamServiceInfo), + PromptPassword(&'static str), + StackedAuthtok(Option<&'static str>), +} + +struct TestHandler { + response_queue: Mutex>, +} + +impl Default for TestHandler { + fn default() -> Self { + TestHandler { + response_queue: Default::default(), + } + } +} + +impl From> for TestHandler { + fn from(v: Vec) -> Self { + TestHandler { + response_queue: Mutex::new(v.into_iter().collect()), + } + } +} + +impl Drop for TestHandler { + fn drop(&mut self) { + let q = self.response_queue.lock().unwrap(); + assert!(q.is_empty()); + } +} + +impl PamHandler for TestHandler { + fn account_id(&self) -> PamResult { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + Some(Event::Account(name)) => Ok(name.to_string()), + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } + + fn service_info(&self) -> PamResult { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + Some(Event::ServiceInfo(info)) => Ok(info), + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } + + fn authtok(&self) -> PamResult> { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + Some(Event::StackedAuthtok(Some(v))) => Ok(Some(v.to_string())), + Some(Event::StackedAuthtok(None)) => Ok(None), + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } + + /// Display a message to the user. + fn message(&self, _prompt: &str) -> PamResult<()> { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } + + /// Display a device grant request to the user. + fn message_device_grant(&self, _data: &DeviceAuthorizationResponse) -> PamResult<()> { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } + + /// Request a password from the user. + fn prompt_for_password(&self) -> PamResult> { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + Some(Event::PromptPassword(value)) => Ok(Some(value.to_string())), + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } + + fn prompt_for_pin(&self, _msg: Option<&str>) -> PamResult> { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } + + fn prompt_for_mfacode(&self) -> PamResult> { + let mut q = self.response_queue.lock().unwrap(); + match q.pop_front() { + e => { + eprintln!("{:?}", e); + panic!("Invalid event transition"); + } + } + } +} + +/// Show that a user can authenticate with the correct password +#[test] +fn pam_fallback_sm_authenticate_default() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("tobias"), Event::PromptPassword("a")]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_SUCCESS + ); +} + +/// Show that incorrect pw fails +#[test] +fn pam_fallback_sm_authenticate_incorrect_pw() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![ + Event::Account("tobias"), + Event::PromptPassword("wrong"), + ]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_AUTH_ERR + ); +} + +/// Show that root can authenticate with the correct password +#[test] +fn pam_fallback_sm_authenticate_root() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("root"), Event::PromptPassword("a")]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_SUCCESS + ); +} + +/// Show that incorrect root pw fails +#[test] +fn pam_fallback_sm_authenticate_root_incorrect_pw() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("root"), Event::PromptPassword("wrong")]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_AUTH_ERR + ); +} + +/// Show that an expired account does not prompt for pw at all. +#[test] +fn pam_fallback_sm_authenticate_expired() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(16); + + let pamh = TestHandler::from(vec![Event::Account("tobias")]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_ACCT_EXPIRED + ); +} + +/// Show that unknown users are denied +#[test] +fn pam_fallback_sm_authenticate_unknown_denied() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("nonexist")]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_USER_UNKNOWN + ); +} + +/// Show that unknown users are ignored when the setting is enabled. +#[test] +fn pam_fallback_sm_authenticate_unknown_ignore() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions { + ignore_unknown_user: true, + ..Default::default() + }; + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("nonexist")]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_IGNORE + ); +} + +/// If there is a stacked cred and use_first_pass is set, it is consumed. +#[test] +fn pam_fallback_sm_authenticate_stacked_cred() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions { + use_first_pass: true, + ..Default::default() + }; + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![ + Event::Account("tobias"), + Event::StackedAuthtok(Some("a")), + ]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_SUCCESS + ); +} + +/// If there is no stacked credential in pam, then one is prompted for +#[test] +fn pam_fallback_sm_authenticate_no_stacked_cred() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions { + use_first_pass: true, + ..Default::default() + }; + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![ + Event::Account("tobias"), + Event::StackedAuthtok(None), + Event::PromptPassword("a"), + ]); + + assert_eq!( + core::sm_authenticate(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_SUCCESS + ); +} + +/// Show that by default, the account "tobias" can login during +/// fallback mode (matching the behaviour of the daemon) +#[test] +fn pam_fallback_acct_mgmt_default() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("tobias")]); + + assert_eq!( + core::acct_mgmt(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_SUCCESS + ); +} + +/// Test that root can always access the system +#[test] +fn pam_fallback_acct_mgmt_root() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("root")]); + + assert_eq!( + core::acct_mgmt(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_SUCCESS + ); +} + +/// Unknown accounts are denied access +#[test] +fn pam_fallback_acct_mgmt_deny_unknown() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("nonexist")]); + + assert_eq!( + core::acct_mgmt(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_USER_UNKNOWN + ); +} + +/// Unknown account returns 'ignore' when this option is set +#[test] +fn pam_fallback_acct_mgmt_ignore_unknown() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions { + ignore_unknown_user: true, + ..Default::default() + }; + let test_time = OffsetDateTime::UNIX_EPOCH; + + let pamh = TestHandler::from(vec![Event::Account("nonexist")]); + + assert_eq!( + core::acct_mgmt(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_IGNORE + ); +} + +/// Exipired accounts are denied +#[test] +fn pam_fallback_acct_mgmt_expired() { + // Show that an expired account is unable to login. + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + let test_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(16); + + let pamh = TestHandler::from(vec![Event::Account("tobias")]); + + assert_eq!( + core::acct_mgmt(&pamh, &mod_opts, req_opt, test_time), + PamResultCode::PAM_ACCT_EXPIRED + ); +} + +#[test] +fn pam_fallback_sm_open_session() { + let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + + let pamh = TestHandler::from(vec![Event::Account("tobias")]); + + assert_eq!( + core::sm_open_session(&pamh, &mod_opts, req_opt), + PamResultCode::PAM_SUCCESS + ); +} + +#[test] +fn pam_fallback_sm_close_session() { + // let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + + let pamh = TestHandler::default(); + + assert_eq!( + core::sm_close_session(&pamh, &mod_opts), + PamResultCode::PAM_SUCCESS + ); +} + +#[test] +fn pam_fallback_sm_chauthtok() { + // let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + + let pamh = TestHandler::default(); + + assert_eq!( + core::sm_chauthtok(&pamh, &mod_opts), + PamResultCode::PAM_IGNORE + ); +} + +#[test] +fn pam_fallback_sm_setcred() { + // let req_opt = RequestOptions::fallback_fixture(); + let mod_opts = ModuleOptions::default(); + + let pamh = TestHandler::default(); + + assert_eq!( + core::sm_setcred(&pamh, &mod_opts), + PamResultCode::PAM_SUCCESS + ); +} diff --git a/unix_integration/resolver/Cargo.toml b/unix_integration/resolver/Cargo.toml index 75e7b2df9..51aa69838 100644 --- a/unix_integration/resolver/Cargo.toml +++ b/unix_integration/resolver/Cargo.toml @@ -75,7 +75,6 @@ selinux = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sketching = { workspace = true } -sha-crypt = { workspace = true } time = { workspace = true, features = ["std"] } toml = { workspace = true } tokio = { workspace = true, features = [ diff --git a/unix_integration/resolver/src/idprovider/system.rs b/unix_integration/resolver/src/idprovider/system.rs index 1789e0902..3c11c3aff 100644 --- a/unix_integration/resolver/src/idprovider/system.rs +++ b/unix_integration/resolver/src/idprovider/system.rs @@ -4,7 +4,7 @@ use time::OffsetDateTime; use tokio::sync::Mutex; use super::interface::{AuthCredHandler, AuthRequest, Id, IdpError}; -use kanidm_unix_common::unix_passwd::{EtcGroup, EtcShadow, EtcUser}; +use kanidm_unix_common::unix_passwd::{CryptPw, EtcGroup, EtcShadow, EtcUser}; use kanidm_unix_common::unix_proto::PamAuthRequest; use kanidm_unix_common::unix_proto::{NssGroup, NssUser}; @@ -43,25 +43,6 @@ pub enum SystemAuthResult { Next(AuthRequest), } -pub enum CryptPw { - Sha256(String), - Sha512(String), -} - -impl TryFrom for CryptPw { - type Error = (); - - fn try_from(value: String) -> Result { - if value.starts_with("$6$") { - Ok(CryptPw::Sha512(value)) - } else if value.starts_with("$5$") { - Ok(CryptPw::Sha256(value)) - } else { - Err(()) - } - } -} - #[allow(dead_code)] struct AgingPolicy { last_change: time::OffsetDateTime, @@ -134,12 +115,7 @@ impl Shadow { ) -> SystemAuthResult { match (cred_handler, pam_next_req) { (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { - let is_valid = match &self.crypt_pw { - CryptPw::Sha256(crypt) => sha_crypt::sha256_check(&cred, crypt).is_ok(), - CryptPw::Sha512(crypt) => sha_crypt::sha512_check(&cred, crypt).is_ok(), - }; - - if is_valid { + if self.crypt_pw.check_pw(&cred) { SystemAuthResult::Success } else { SystemAuthResult::Denied @@ -197,33 +173,31 @@ impl SystemProvider { flag_reserved: _, } = shadow_entry; - match CryptPw::try_from(password) { - Ok(crypt_pw) => { - let aging_policy = epoch_change_days.map(|change_days| { - AgingPolicy::new( - change_days, - days_min_password_age, - days_max_password_age, - days_warning_period, - days_inactivity_period, - ) - }); + if password.is_valid() { + let aging_policy = epoch_change_days.map(|change_days| { + AgingPolicy::new( + change_days, + days_min_password_age, + days_max_password_age, + days_warning_period, + days_inactivity_period, + ) + }); - let expiration_date = epoch_expire_date.map(|expire| { - OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire) - }); + let expiration_date = epoch_expire_date + .map(|expire| OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire)); - Some(( - name, - Arc::new(Shadow { - crypt_pw, - aging_policy, - expiration_date, - }), - )) - } - // No valid pw, don't care. - Err(()) => None, + Some(( + name, + Arc::new(Shadow { + crypt_pw: password, + aging_policy, + expiration_date, + }), + )) + } else { + // Invalid password, skip the account + None } }); diff --git a/unix_integration/resolver/tests/cache_layer_test.rs b/unix_integration/resolver/tests/cache_layer_test.rs index b839056e7..76bbb7cda 100644 --- a/unix_integration/resolver/tests/cache_layer_test.rs +++ b/unix_integration/resolver/tests/cache_layer_test.rs @@ -12,7 +12,7 @@ use kanidm_unix_common::constants::{ DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP, }; -use kanidm_unix_common::unix_passwd::{EtcGroup, EtcShadow, EtcUser}; +use kanidm_unix_common::unix_passwd::{CryptPw, EtcGroup, EtcShadow, EtcUser}; use kanidm_unix_resolver::db::{Cache, Db}; use kanidm_unix_resolver::idprovider::interface::Id; use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider; @@ -941,7 +941,7 @@ async fn test_cache_authenticate_system_account() { EtcShadow { name: "testaccount1".to_string(), // The very secure password, "a". - password: "$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string(), + password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()), epoch_change_days: None, days_min_password_age: 0, days_max_password_age: Some(1), @@ -953,7 +953,7 @@ async fn test_cache_authenticate_system_account() { EtcShadow { name: "testaccount2".to_string(), // The very secure password, "a". - password: "$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string(), + password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()), epoch_change_days: Some(364), days_min_password_age: 0, days_max_password_age: Some(2),