mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Add nss testframework and fallback when daemon offline (#3093)
This commit is contained in:
parent
03645c8bf2
commit
50e513b30b
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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<UnixStream> for DaemonClientBlocking {
|
||||
fn from(stream: UnixStream) -> Self {
|
||||
DaemonClientBlocking {
|
||||
stream,
|
||||
default_timeout: DEFAULT_CONN_TIMEOUT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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<u64>,
|
||||
) -> 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| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
|
|
|
@ -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<Vec<EtcUser>, UnixIntegrationErr
|
|||
.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]
|
||||
#[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<i64>,
|
||||
|
@ -63,6 +123,18 @@ pub fn parse_etc_shadow(bytes: &[u8]) -> Result<Vec<EtcShadow>, UnixIntegrationE
|
|||
.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]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct EtcGroup {
|
||||
|
@ -87,6 +159,16 @@ pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, UnixIntegrationErr
|
|||
.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)]
|
||||
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),
|
||||
|
|
328
unix_integration/nss_kanidm/src/core.rs
Normal file
328
unix_integration/nss_kanidm/src/core.rs
Normal 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,
|
||||
}
|
||||
}
|
63
unix_integration/nss_kanidm/src/hooks.rs
Normal file
63
unix_integration/nss_kanidm/src/hooks.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
258
unix_integration/nss_kanidm/src/tests.rs
Normal file
258
unix_integration/nss_kanidm/src/tests.rs
Normal 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!();
|
||||
};
|
||||
}
|
|
@ -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 }
|
||||
|
|
534
unix_integration/pam_kanidm/src/core.rs
Normal file
534
unix_integration/pam_kanidm/src/core.rs
Normal 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
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -55,7 +55,7 @@ impl PamConv {
|
|||
/// styles.
|
||||
pub fn send(&self, style: PamMessageStyle, msg: &str) -> PamResult<Option<String>> {
|
||||
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(),
|
||||
|
|
|
@ -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, PamResultCode> {
|
||||
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<Self, Self::Error> {
|
||||
|
@ -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::<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.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/// <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 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
|
||||
/// <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 res = unsafe {
|
||||
let c_data: Box<PamDataT> = 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
|
||||
/// <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 (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<T: PamItem>(&self) -> PamResult<Option<String>> {
|
||||
fn get_item_string<T: PamItem>(&self) -> PamResult<Option<String>> {
|
||||
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
|
||||
/// <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 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
|
||||
/// <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 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<Option<String>> {
|
||||
fn get_authtok(&self) -> PamResult<Option<String>> {
|
||||
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>()
|
||||
}
|
||||
|
||||
pub fn get_rhost(&self) -> PamResult<Option<String>> {
|
||||
fn get_rhost(&self) -> PamResult<Option<String>> {
|
||||
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>()
|
||||
}
|
||||
|
||||
pub fn get_pam_info(&self) -> PamResult<PamServiceInfo> {
|
||||
fn get_pam_info(&self) -> PamResult<PamServiceInfo> {
|
||||
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::<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
|
||||
|
|
467
unix_integration/pam_kanidm/src/tests.rs
Normal file
467
unix_integration/pam_kanidm/src/tests.rs
Normal 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
|
||||
);
|
||||
}
|
|
@ -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 = [
|
||||
|
|
|
@ -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<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)]
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue