Add nss testframework and fallback when daemon offline (#3093)

This commit is contained in:
Firstyear 2024-10-15 14:05:51 +10:00 committed by GitHub
parent 03645c8bf2
commit 50e513b30b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1940 additions and 733 deletions

3
Cargo.lock generated
View file

@ -3370,6 +3370,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
"sha-crypt",
"tokio", "tokio",
"tokio-util", "tokio-util",
"toml", "toml",
@ -3407,7 +3408,6 @@ dependencies = [
"selinux", "selinux",
"serde", "serde",
"serde_json", "serde_json",
"sha-crypt",
"sketching", "sketching",
"time", "time",
"tokio", "tokio",
@ -4548,6 +4548,7 @@ dependencies = [
"kanidm_unix_common", "kanidm_unix_common",
"libc", "libc",
"pkg-config", "pkg-config",
"time",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]

View file

@ -32,6 +32,7 @@ toml = { workspace = true }
tokio = { workspace = true, features = ["time","net","macros"] } tokio = { workspace = true, features = ["time","net","macros"] }
tokio-util = { workspace = true, features = ["codec"] } tokio-util = { workspace = true, features = ["codec"] }
tracing = { workspace = true } tracing = { workspace = true }
sha-crypt = { workspace = true }
[build-dependencies] [build-dependencies]
kanidm_build_profiles = { workspace = true } kanidm_build_profiles = { workspace = true }

View file

@ -1,16 +1,27 @@
use crate::constants::DEFAULT_CONN_TIMEOUT;
use crate::unix_proto::{ClientRequest, ClientResponse};
use std::error::Error; use std::error::Error;
use std::io::{Error as IoError, ErrorKind, Read, Write}; use std::io::{Error as IoError, ErrorKind, Read, Write};
use std::os::unix::net::UnixStream;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use crate::unix_proto::{ClientRequest, ClientResponse}; pub use std::os::unix::net::UnixStream;
pub struct DaemonClientBlocking { pub struct DaemonClientBlocking {
stream: UnixStream, stream: UnixStream,
default_timeout: u64,
}
impl From<UnixStream> for DaemonClientBlocking {
fn from(stream: UnixStream) -> Self {
DaemonClientBlocking {
stream,
default_timeout: DEFAULT_CONN_TIMEOUT,
}
}
} }
impl DaemonClientBlocking { impl DaemonClientBlocking {
pub fn new(path: &str) -> Result<DaemonClientBlocking, Box<dyn Error>> { pub fn new(path: &str, default_timeout: u64) -> Result<DaemonClientBlocking, Box<dyn Error>> {
debug!(%path); debug!(%path);
let stream = UnixStream::connect(path) let stream = UnixStream::connect(path)
@ -23,15 +34,18 @@ impl DaemonClientBlocking {
}) })
.map_err(Box::new)?; .map_err(Box::new)?;
Ok(DaemonClientBlocking { stream }) Ok(DaemonClientBlocking {
stream,
default_timeout,
})
} }
pub fn call_and_wait( pub fn call_and_wait(
&mut self, &mut self,
req: &ClientRequest, req: &ClientRequest,
timeout: u64, timeout: Option<u64>,
) -> Result<ClientResponse, Box<dyn Error>> { ) -> Result<ClientResponse, Box<dyn Error>> {
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| { let data = serde_json::to_vec(&req).map_err(|e| {
error!("socket encoding error -> {:?}", e); error!("socket encoding error -> {:?}", e);

View file

@ -1,7 +1,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::formats::CommaSeparator; use serde_with::formats::CommaSeparator;
use serde_with::{serde_as, DefaultOnNull, StringWithSeparator}; 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)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct EtcUser { pub struct EtcUser {
@ -25,11 +29,67 @@ pub fn parse_etc_passwd(bytes: &[u8]) -> Result<Vec<EtcUser>, UnixIntegrationErr
.collect::<Result<Vec<EtcUser>, UnixIntegrationError>>() .collect::<Result<Vec<EtcUser>, UnixIntegrationError>>()
} }
pub fn read_etc_passwd_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcUser>, 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<Self, Self::Err> {
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] #[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
pub struct EtcShadow { pub struct EtcShadow {
pub name: String, pub name: String,
pub password: String, #[serde_as(as = "serde_with::DisplayFromStr")]
pub password: CryptPw,
// 0 means must change next login. // 0 means must change next login.
// None means all other aging features are disabled // None means all other aging features are disabled
pub epoch_change_days: Option<i64>, pub epoch_change_days: Option<i64>,
@ -63,6 +123,18 @@ pub fn parse_etc_shadow(bytes: &[u8]) -> Result<Vec<EtcShadow>, UnixIntegrationE
.collect::<Result<Vec<EtcShadow>, UnixIntegrationError>>() .collect::<Result<Vec<EtcShadow>, UnixIntegrationError>>()
} }
pub fn read_etc_shadow_file<P: AsRef<Path>>(
path: P,
) -> Result<Vec<EtcShadow>, 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] #[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct EtcGroup { pub struct EtcGroup {
@ -87,6 +159,16 @@ pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, UnixIntegrationErr
.collect::<Result<Vec<EtcGroup>, UnixIntegrationError>>() .collect::<Result<Vec<EtcGroup>, UnixIntegrationError>>()
} }
pub fn read_etc_group_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcGroup>, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -156,7 +238,7 @@ admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePME
shadow[0], shadow[0],
EtcShadow { EtcShadow {
name: "sshd".to_string(), name: "sshd".to_string(),
password: "!".to_string(), password: CryptPw::Invalid,
epoch_change_days: Some(19978), epoch_change_days: Some(19978),
days_min_password_age: 0, days_min_password_age: 0,
days_max_password_age: None, days_max_password_age: None,
@ -171,7 +253,7 @@ admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePME
shadow[1], shadow[1],
EtcShadow { EtcShadow {
name: "tss".to_string(), name: "tss".to_string(),
password: "!".to_string(), password: CryptPw::Invalid,
epoch_change_days: Some(19980), epoch_change_days: Some(19980),
days_min_password_age: 0, days_min_password_age: 0,
days_max_password_age: None, days_max_password_age: None,
@ -184,7 +266,7 @@ admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePME
assert_eq!(shadow[2], EtcShadow { assert_eq!(shadow[2], EtcShadow {
name: "admin".to_string(), 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), epoch_change_days: Some(19980),
days_min_password_age: 0, days_min_password_age: 0,
days_max_password_age: Some(99999), days_max_password_age: Some(99999),

View file

@ -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<UnixStream>,
users: Vec<EtcUser>,
groups: Vec<EtcGroup>,
},
}
enum Source {
Daemon(DaemonClientBlocking),
Fallback {
users: Vec<EtcUser>,
groups: Vec<EtcGroup>,
},
}
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<Vec<Passwd>> {
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<Passwd> {
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<Passwd> {
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<Vec<Group>> {
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<Group> {
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<Group> {
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,
}
}

View file

@ -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<Vec<Passwd>> {
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<Passwd> {
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<Passwd> {
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<Vec<Group>> {
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<Group> {
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<Group> {
let req_opt = RequestOptions::Main {
config_path: DEFAULT_CONFIG_PATH,
};
core::get_group_entry_by_name(name, req_opt)
}
}

View file

@ -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<Vec<Passwd>> {
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<Passwd> {
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<Passwd> {
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<Vec<Group>> {
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<Group> {
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<Group> {
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,
}
}

View file

@ -15,4 +15,10 @@
extern crate libnss; extern crate libnss;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
mod implementation; mod hooks;
#[cfg(target_family = "unix")]
pub(crate) mod core;
#[cfg(test)]
mod tests;

View file

@ -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!();
};
}

View file

@ -21,6 +21,7 @@ kanidm_unix_common = { workspace = true }
libc = { workspace = true } libc = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
time = { workspace = true }
[build-dependencies] [build-dependencies]
pkg-config = { workspace = true } pkg-config = { workspace = true }

View file

@ -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<UnixStream>,
users: Vec<EtcUser>,
// groups: Vec<EtcGroup>,
shadow: Vec<EtcShadow>,
},
}
enum Source {
Daemon(DaemonClientBlocking),
Fallback {
users: Vec<EtcUser>,
// groups: Vec<EtcGroup>,
shadow: Vec<EtcShadow>,
},
}
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<String>;
fn service_info(&self) -> PamResult<PamServiceInfo>;
fn authtok(&self) -> PamResult<Option<String>>;
/// 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<Option<String>>;
fn prompt_for_pin(&self, msg: Option<&str>) -> PamResult<Option<String>>;
fn prompt_for_mfacode(&self) -> PamResult<Option<String>>;
}
pub fn sm_authenticate_connected<P: PamHandler>(
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<u64> = 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<P: PamHandler>(
pamh: &P,
opts: &ModuleOptions,
current_time: OffsetDateTime,
users: Vec<EtcUser>,
shadow: Vec<EtcShadow>,
) -> 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<P: PamHandler>(
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<P: PamHandler>(
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<P: PamHandler>(
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<P: PamHandler>(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode {
PamResultCode::PAM_SUCCESS
}
pub fn sm_chauthtok<P: PamHandler>(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode {
PamResultCode::PAM_IGNORE
}
pub fn sm_setcred<P: PamHandler>(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode {
PamResultCode::PAM_SUCCESS
}

View file

@ -3,8 +3,8 @@
#![deny(clippy::todo)] #![deny(clippy::todo)]
#![deny(clippy::unimplemented)] #![deny(clippy::unimplemented)]
// In this file, we do want to panic on these faults. // In this file, we do want to panic on these faults.
// #![deny(clippy::unwrap_used)] #![deny(clippy::unwrap_used)]
// #![deny(clippy::expect_used)] #![deny(clippy::expect_used)]
#![deny(clippy::panic)] #![deny(clippy::panic)]
#![deny(clippy::unreachable)] #![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)] #![deny(clippy::await_holding_lock)]
@ -14,6 +14,11 @@
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
mod pam; mod pam;
pub(crate) mod core;
// pub use needs to be here so it'll compile and export all the things // pub use needs to be here so it'll compile and export all the things
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub use crate::pam::*; pub use crate::pam::*;
#[cfg(test)]
mod tests;

View file

@ -55,7 +55,7 @@ impl PamConv {
/// styles. /// styles.
pub fn send(&self, style: PamMessageStyle, msg: &str) -> PamResult<Option<String>> { pub fn send(&self, style: PamMessageStyle, msg: &str) -> PamResult<Option<String>> {
let mut resp_ptr: *const PamResponse = ptr::null(); 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 { let msg = PamMessage {
msg_style: style, msg_style: style,
msg: msg_cstr.as_ptr(), msg: msg_cstr.as_ptr(),

View file

@ -35,27 +35,21 @@ use std::collections::BTreeSet;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::ffi::CStr; use std::ffi::CStr;
use kanidm_unix_common::client_sync::DaemonClientBlocking;
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_config::KanidmUnixdConfig; 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::constants::*;
use crate::pam::conv::PamConv;
use crate::pam::module::{PamHandle, PamHooks}; use crate::pam::module::{PamHandle, PamHooks};
use crate::pam_hooks; use crate::pam_hooks;
use constants::PamResultCode; use constants::PamResultCode;
use time::OffsetDateTime;
use tracing::{debug, error}; use tracing::debug;
use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt; use tracing_subscriber::fmt;
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
use std::thread;
use std::time::Duration;
pub fn get_cfg() -> Result<KanidmUnixdConfig, PamResultCode> { pub fn get_cfg() -> Result<KanidmUnixdConfig, PamResultCode> {
KanidmUnixdConfig::new() KanidmUnixdConfig::new()
.read_options_from_optional_config(DEFAULT_CONFIG_PATH) .read_options_from_optional_config(DEFAULT_CONFIG_PATH)
@ -77,14 +71,14 @@ fn install_subscriber(debug: bool) {
.try_init(); .try_init();
} }
#[derive(Debug)] #[derive(Debug, Default)]
struct Options { pub struct ModuleOptions {
debug: bool, pub debug: bool,
use_first_pass: bool, pub use_first_pass: bool,
ignore_unknown_user: bool, pub ignore_unknown_user: bool,
} }
impl TryFrom<&Vec<&CStr>> for Options { impl TryFrom<&Vec<&CStr>> for ModuleOptions {
type Error = (); type Error = ();
fn try_from(args: &Vec<&CStr>) -> Result<Self, Self::Error> { fn try_from(args: &Vec<&CStr>) -> Result<Self, Self::Error> {
@ -97,7 +91,7 @@ impl TryFrom<&Vec<&CStr>> for Options {
} }
}; };
Ok(Options { Ok(ModuleOptions {
debug: gopts.contains("debug"), debug: gopts.contains("debug"),
use_first_pass: gopts.contains("use_first_pass"), use_first_pass: gopts.contains("use_first_pass"),
ignore_unknown_user: gopts.contains("ignore_unknown_user"), ignore_unknown_user: gopts.contains("ignore_unknown_user"),
@ -109,426 +103,47 @@ pub struct PamKanidm;
pam_hooks!(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 { 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 { 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, Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR, Err(_) => return PamResultCode::PAM_SERVICE_ERR,
}; };
install_subscriber(opts.debug); install_subscriber(opts.debug);
let info = match pamh.get_pam_info() { debug!(?args, ?opts, "acct_mgmt");
Ok(info) => info,
Err(e) => { let current_time = OffsetDateTime::now_utc();
error!(err = ?e, "get_pam_info");
return e; let req_opt = RequestOptions::Main {
} config_path: DEFAULT_CONFIG_PATH,
}; };
debug!(?args, ?opts, ?info, "sm_authenticate"); core::sm_authenticate(pamh, &opts, req_opt, current_time)
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::<PamConv>() {
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.
} }
fn sm_chauthtok(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { fn acct_mgmt(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, Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR, Err(_) => return PamResultCode::PAM_SERVICE_ERR,
}; };
install_subscriber(opts.debug); 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 req_opt = RequestOptions::Main {
let opts = match Options::try_from(&args) { config_path: DEFAULT_CONFIG_PATH,
Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
}; };
install_subscriber(opts.debug); core::acct_mgmt(pamh, &opts, req_opt, current_time)
debug!(?args, ?opts, "sm_close_session");
PamResultCode::PAM_SUCCESS
} }
fn sm_open_session(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { 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, Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR, Err(_) => return PamResultCode::PAM_SERVICE_ERR,
}; };
@ -537,42 +152,41 @@ impl PamHooks for PamKanidm {
debug!(?args, ?opts, "sm_open_session"); debug!(?args, ?opts, "sm_open_session");
let account_id = match pamh.get_user(None) { let req_opt = RequestOptions::Main {
Ok(aid) => aid, config_path: DEFAULT_CONFIG_PATH,
Err(err) => {
error!(?err, "get_user");
return err;
}
}; };
let cfg = match get_cfg() { core::sm_open_session(pamh, &opts, req_opt)
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
}
}
} }
fn sm_setcred(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { fn sm_close_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,
};
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, Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR, Err(_) => return PamResultCode::PAM_SERVICE_ERR,
}; };
@ -581,6 +195,6 @@ impl PamHooks for PamKanidm {
debug!(?args, ?opts, "sm_setcred"); debug!(?args, ?opts, "sm_setcred");
PamResultCode::PAM_SUCCESS core::sm_setcred(pamh, &opts)
} }
} }

View file

@ -5,9 +5,14 @@ use std::{mem, ptr};
use libc::c_char; 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::pam::items::{PamAuthTok, PamRHost, PamService, PamTty};
use crate::core::PamHandler;
use kanidm_unix_common::unix_proto::DeviceAuthorizationResponse;
use kanidm_unix_common::unix_proto::PamServiceInfo; use kanidm_unix_common::unix_proto::PamServiceInfo;
/// Opaque type, used as a pointer when making pam API calls. /// Opaque type, used as a pointer when making pam API calls.
@ -26,6 +31,7 @@ pub enum PamDataT {}
#[link(name = "pam")] #[link(name = "pam")]
extern "C" { extern "C" {
/*
fn pam_get_data( fn pam_get_data(
pamh: *const PamHandle, pamh: *const PamHandle,
module_data_name: *const c_char, module_data_name: *const c_char,
@ -42,6 +48,7 @@ extern "C" {
error_status: PamResultCode, error_status: PamResultCode,
), ),
) -> PamResultCode; ) -> PamResultCode;
*/
fn pam_get_item( fn pam_get_item(
pamh: *const PamHandle, pamh: *const PamHandle,
@ -49,8 +56,10 @@ extern "C" {
item: &mut *const PamItemT, item: &mut *const PamItemT,
) -> PamResultCode; ) -> PamResultCode;
/*
fn pam_set_item(pamh: *mut PamHandle, item_type: PamItemType, item: &PamItemT) fn pam_set_item(pamh: *mut PamHandle, item_type: PamItemType, item: &PamItemT)
-> PamResultCode; -> PamResultCode;
*/
fn pam_get_user( fn pam_get_user(
pamh: *const PamHandle, pamh: *const PamHandle,
@ -87,6 +96,7 @@ pub trait PamItem {
} }
impl PamHandle { impl PamHandle {
/*
/// # Safety /// # Safety
/// ///
/// Gets some value, identified by `key`, that has been set by the module /// Gets some value, identified by `key`, that has been set by the module
@ -94,7 +104,7 @@ impl PamHandle {
/// ///
/// See `pam_get_data` in /// See `pam_get_data` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html> /// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
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 c_key = CString::new(key).unwrap();
let mut ptr: *const PamDataT = ptr::null(); let mut ptr: *const PamDataT = ptr::null();
let res = pam_get_data(self, c_key.as_ptr(), &mut ptr); let res = pam_get_data(self, c_key.as_ptr(), &mut ptr);
@ -112,7 +122,7 @@ impl PamHandle {
/// ///
/// See `pam_set_data` in /// See `pam_set_data` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html> /// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub fn set_data<T>(&self, key: &str, data: Box<T>) -> PamResult<()> { fn set_data<T>(&self, key: &str, data: Box<T>) -> PamResult<()> {
let c_key = CString::new(key).unwrap(); let c_key = CString::new(key).unwrap();
let res = unsafe { let res = unsafe {
let c_data: Box<PamDataT> = mem::transmute(data); let c_data: Box<PamDataT> = mem::transmute(data);
@ -125,13 +135,14 @@ impl PamHandle {
Err(res) Err(res)
} }
} }
*/
/// Retrieves a value that has been set, possibly by the pam client. This is /// Retrieves a value that has been set, possibly by the pam client. This is
/// particularly useful for getting a `PamConv` reference. /// particularly useful for getting a `PamConv` reference.
/// ///
/// See `pam_get_item` in /// See `pam_get_item` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html> /// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
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 mut ptr: *const PamItemT = ptr::null();
let (res, item) = unsafe { let (res, item) = unsafe {
let r = pam_get_item(self, T::item_type(), &mut ptr); let r = pam_get_item(self, T::item_type(), &mut ptr);
@ -146,7 +157,7 @@ impl PamHandle {
} }
} }
pub fn get_item_string<T: PamItem>(&self) -> PamResult<Option<String>> { fn get_item_string<T: PamItem>(&self) -> PamResult<Option<String>> {
let mut ptr: *const PamItemT = ptr::null(); let mut ptr: *const PamItemT = ptr::null();
let (res, item) = unsafe { let (res, item) = unsafe {
let r = pam_get_item(self, T::item_type(), &mut ptr); 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 /// Sets a value in the pam context. The value can be retrieved using
/// `get_item`. /// `get_item`.
/// ///
@ -172,7 +184,7 @@ impl PamHandle {
/// ///
/// See `pam_set_item` in /// See `pam_set_item` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html> /// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub fn set_item_str<T: PamItem>(&mut self, item: &str) -> PamResult<()> { fn set_item_str<T: PamItem>(&mut self, item: &str) -> PamResult<()> {
let c_item = CString::new(item).unwrap(); let c_item = CString::new(item).unwrap();
let res = unsafe { let res = unsafe {
@ -190,6 +202,7 @@ impl PamHandle {
Err(res) Err(res)
} }
} }
*/
/// Retrieves the name of the user who is authenticating or logging in. /// Retrieves the name of the user who is authenticating or logging in.
/// ///
@ -197,11 +210,11 @@ impl PamHandle {
/// ///
/// See `pam_get_user` in /// See `pam_get_user` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html> /// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub fn get_user(&self, prompt: Option<&str>) -> PamResult<String> { fn get_user(&self, prompt: Option<&str>) -> PamResult<String> {
let mut ptr: *const c_char = ptr::null_mut(); let mut ptr: *const c_char = ptr::null_mut();
let res = match prompt { let res = match prompt {
Some(p) => { 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()) } unsafe { pam_get_user(self, &mut ptr, c_prompt.as_ptr()) }
} }
None => unsafe { pam_get_user(self, &mut ptr, ptr::null()) }, None => unsafe { pam_get_user(self, &mut ptr, ptr::null()) },
@ -219,23 +232,23 @@ impl PamHandle {
} }
} }
pub fn get_authtok(&self) -> PamResult<Option<String>> { fn get_authtok(&self) -> PamResult<Option<String>> {
self.get_item_string::<PamAuthTok>() self.get_item_string::<PamAuthTok>()
} }
pub fn get_tty(&self) -> PamResult<Option<String>> { fn get_tty(&self) -> PamResult<Option<String>> {
self.get_item_string::<PamTty>() self.get_item_string::<PamTty>()
} }
pub fn get_rhost(&self) -> PamResult<Option<String>> { fn get_rhost(&self) -> PamResult<Option<String>> {
self.get_item_string::<PamRHost>() self.get_item_string::<PamRHost>()
} }
pub fn get_service(&self) -> PamResult<Option<String>> { fn get_service(&self) -> PamResult<Option<String>> {
self.get_item_string::<PamService>() self.get_item_string::<PamService>()
} }
pub fn get_pam_info(&self) -> PamResult<PamServiceInfo> { fn get_pam_info(&self) -> PamResult<PamServiceInfo> {
let maybe_tty = self.get_tty()?; let maybe_tty = self.get_tty()?;
let maybe_rhost = self.get_rhost()?; let maybe_rhost = self.get_rhost()?;
let maybe_service = self.get_service()?; let maybe_service = self.get_service()?;
@ -251,6 +264,57 @@ impl PamHandle {
_ => Err(PamResultCode::PAM_CONV_ERR), _ => Err(PamResultCode::PAM_CONV_ERR),
} }
} }
fn get_conv(&self) -> PamResult<&PamConv> {
self.get_item::<PamConv>()
}
}
impl PamHandler for PamHandle {
fn account_id(&self) -> PamResult<String> {
self.get_user(None)
}
fn service_info(&self) -> PamResult<PamServiceInfo> {
self.get_pam_info()
}
fn authtok(&self) -> PamResult<Option<String>> {
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<Option<String>> {
let conv = self.get_conv()?;
conv.send(PAM_PROMPT_ECHO_OFF, "Password: ")
}
fn prompt_for_mfacode(&self) -> PamResult<Option<String>> {
let conv = self.get_conv()?;
conv.send(PAM_PROMPT_ECHO_OFF, "Code: ")
}
fn prompt_for_pin(&self, msg: Option<&str>) -> PamResult<Option<String>> {
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 /// Provides functions that are invoked by the entrypoints generated by the

View file

@ -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<VecDeque<Event>>,
}
impl Default for TestHandler {
fn default() -> Self {
TestHandler {
response_queue: Default::default(),
}
}
}
impl From<Vec<Event>> for TestHandler {
fn from(v: Vec<Event>) -> 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<String> {
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<PamServiceInfo> {
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<Option<String>> {
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<Option<String>> {
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<Option<String>> {
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<Option<String>> {
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
);
}

View file

@ -75,7 +75,6 @@ selinux = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
sketching = { workspace = true } sketching = { workspace = true }
sha-crypt = { workspace = true }
time = { workspace = true, features = ["std"] } time = { workspace = true, features = ["std"] }
toml = { workspace = true } toml = { workspace = true }
tokio = { workspace = true, features = [ tokio = { workspace = true, features = [

View file

@ -4,7 +4,7 @@ use time::OffsetDateTime;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::interface::{AuthCredHandler, AuthRequest, Id, IdpError}; 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::PamAuthRequest;
use kanidm_unix_common::unix_proto::{NssGroup, NssUser}; use kanidm_unix_common::unix_proto::{NssGroup, NssUser};
@ -43,25 +43,6 @@ pub enum SystemAuthResult {
Next(AuthRequest), Next(AuthRequest),
} }
pub enum CryptPw {
Sha256(String),
Sha512(String),
}
impl TryFrom<String> for CryptPw {
type Error = ();
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.starts_with("$6$") {
Ok(CryptPw::Sha512(value))
} else if value.starts_with("$5$") {
Ok(CryptPw::Sha256(value))
} else {
Err(())
}
}
}
#[allow(dead_code)] #[allow(dead_code)]
struct AgingPolicy { struct AgingPolicy {
last_change: time::OffsetDateTime, last_change: time::OffsetDateTime,
@ -134,12 +115,7 @@ impl Shadow {
) -> SystemAuthResult { ) -> SystemAuthResult {
match (cred_handler, pam_next_req) { match (cred_handler, pam_next_req) {
(AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
let is_valid = match &self.crypt_pw { if self.crypt_pw.check_pw(&cred) {
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 {
SystemAuthResult::Success SystemAuthResult::Success
} else { } else {
SystemAuthResult::Denied SystemAuthResult::Denied
@ -197,33 +173,31 @@ impl SystemProvider {
flag_reserved: _, flag_reserved: _,
} = shadow_entry; } = shadow_entry;
match CryptPw::try_from(password) { if password.is_valid() {
Ok(crypt_pw) => { let aging_policy = epoch_change_days.map(|change_days| {
let aging_policy = epoch_change_days.map(|change_days| { AgingPolicy::new(
AgingPolicy::new( change_days,
change_days, days_min_password_age,
days_min_password_age, days_max_password_age,
days_max_password_age, days_warning_period,
days_warning_period, days_inactivity_period,
days_inactivity_period, )
) });
});
let expiration_date = epoch_expire_date.map(|expire| { let expiration_date = epoch_expire_date
OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire) .map(|expire| OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire));
});
Some(( Some((
name, name,
Arc::new(Shadow { Arc::new(Shadow {
crypt_pw, crypt_pw: password,
aging_policy, aging_policy,
expiration_date, expiration_date,
}), }),
)) ))
} } else {
// No valid pw, don't care. // Invalid password, skip the account
Err(()) => None, None
} }
}); });

View file

@ -12,7 +12,7 @@ use kanidm_unix_common::constants::{
DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX,
DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP, 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::db::{Cache, Db};
use kanidm_unix_resolver::idprovider::interface::Id; use kanidm_unix_resolver::idprovider::interface::Id;
use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider; use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider;
@ -941,7 +941,7 @@ async fn test_cache_authenticate_system_account() {
EtcShadow { EtcShadow {
name: "testaccount1".to_string(), name: "testaccount1".to_string(),
// The very secure password, "a". // 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, epoch_change_days: None,
days_min_password_age: 0, days_min_password_age: 0,
days_max_password_age: Some(1), days_max_password_age: Some(1),
@ -953,7 +953,7 @@ async fn test_cache_authenticate_system_account() {
EtcShadow { EtcShadow {
name: "testaccount2".to_string(), name: "testaccount2".to_string(),
// The very secure password, "a". // 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), epoch_change_days: Some(364),
days_min_password_age: 0, days_min_password_age: 0,
days_max_password_age: Some(2), days_max_password_age: Some(2),