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>,
shadow: Vec<EtcShadow>,
},
}
enum Source {
Daemon(DaemonClientBlocking),
Fallback {
users: Vec<EtcUser>,
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 shadow = read_etc_shadow_file("/etc/shadow").unwrap_or_default();
Source::Fallback {
users,
shadow,
}
}
}
#[cfg(test)]
RequestOptions::Test {
socket,
users,
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>>;
fn message(&self, prompt: &str) -> PamResult<()>;
fn message_device_grant(&self, data: &DeviceAuthorizationResponse) -> PamResult<()>;
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) => {
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,
}
};
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,
};
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);
}
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPollWait) => {
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;
}
}
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,
}
};
timeout = None;
req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Pin { cred });
continue;
}
ClientResponse::Error(err) => {
error!("Error from kanidm-unixd: {}", err);
return PamResultCode::PAM_AUTH_ERR;
}
ClientResponse::Ok
| ClientResponse::SshKeys(_)
| ClientResponse::NssAccounts(_)
| ClientResponse::NssAccount(_)
| ClientResponse::NssGroups(_)
| ClientResponse::PamStatus(_)
| ClientResponse::ProviderStatus(_)
| ClientResponse::NssGroup(_) => {
debug!("PamResultCode::PAM_AUTH_ERR");
return PamResultCode::PAM_AUTH_ERR;
}
}
} }
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;
}
};
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
}
}
_ => {
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;
}
};
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
}