mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
538 lines
18 KiB
Rust
538 lines
18 KiB
Rust
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 occurred, 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::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;
|
|
}
|
|
}
|
|
} // 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
|
|
}
|