mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-29 12:23:56 +02:00
pam multistep auth state machine (#2022)
Himmelblau needs to maintain some data about the state of an authentication across the course of pam exchanges. Signed-off-by: David Mulder <dmulder@samba.org> Co-authored-by: David Mulder <dmulder@samba.org>
This commit is contained in:
parent
2194994ada
commit
da56738dea
Cargo.lock
server
lib/src
testkit/tests
web_ui/src/components
unix_integration
nss_kanidm/src
pam_kanidm
src
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -3366,6 +3366,8 @@ dependencies = [
|
|||
"kanidm_unix_int",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -601,11 +601,11 @@ pub trait IdlSqliteTransaction {
|
|||
#[allow(clippy::let_and_return)]
|
||||
fn verify(&self) -> Vec<Result<(), ConsistencyError>> {
|
||||
let Ok(conn) = self.get_conn() else {
|
||||
return vec![Err(ConsistencyError::SqliteIntegrityFailure)]
|
||||
return vec![Err(ConsistencyError::SqliteIntegrityFailure)];
|
||||
};
|
||||
|
||||
let Ok(mut stmt) = conn.prepare("PRAGMA integrity_check;") else {
|
||||
return vec![Err(ConsistencyError::SqliteIntegrityFailure)]
|
||||
return vec![Err(ConsistencyError::SqliteIntegrityFailure)];
|
||||
};
|
||||
|
||||
// Allow this as it actually extends the life of stmt
|
||||
|
|
|
@ -432,9 +432,7 @@ pub trait BackendTransaction {
|
|||
for f in f_andnot.iter() {
|
||||
f_rem_count -= 1;
|
||||
let FilterResolved::AndNot(f_in, _) = f else {
|
||||
filter_error!(
|
||||
"Invalid server state, a cand filter leaked to andnot set!"
|
||||
);
|
||||
filter_error!("Invalid server state, a cand filter leaked to andnot set!");
|
||||
return Err(OperationError::InvalidState);
|
||||
};
|
||||
let (inter, fp) = self.filter2idl(f_in, thres)?;
|
||||
|
|
|
@ -266,7 +266,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
let other_user_public_key = self.get_other_user_public_key(target, ident)?;
|
||||
let mut shared_key = self.derive_shared_key(self_private, other_user_public_key)?;
|
||||
let Some(self_uuid) = ident.get_uuid() else {
|
||||
return Err(OperationError::NotAuthenticated)
|
||||
return Err(OperationError::NotAuthenticated);
|
||||
};
|
||||
shared_key.extend_from_slice(self_uuid.as_bytes());
|
||||
Ok(shared_key)
|
||||
|
@ -509,8 +509,8 @@ mod test {
|
|||
);
|
||||
|
||||
let Ok(IdentifyUserResponse::ProvideCode { totp, .. }) = res_higher_user else {
|
||||
return assert!(false);
|
||||
};
|
||||
return assert!(false);
|
||||
};
|
||||
|
||||
let res_lower_user_wrong = idms_prox_read.handle_identify_user_submit_code(
|
||||
&IdentifyUserSubmitCodeEvent::new(higher_user_uuid, lower_user.clone(), totp + 1),
|
||||
|
@ -532,9 +532,9 @@ mod test {
|
|||
|
||||
// now we need to get the code from the lower_user and submit it to the higher_user
|
||||
|
||||
let Ok(IdentifyUserResponse::ProvideCode{totp, ..}) = res_lower_user_correct else {
|
||||
return assert!(false);
|
||||
};
|
||||
let Ok(IdentifyUserResponse::ProvideCode { totp, .. }) = res_lower_user_correct else {
|
||||
return assert!(false);
|
||||
};
|
||||
|
||||
let res_higher_user_2_wrong = idms_prox_read.handle_identify_user_submit_code(
|
||||
&IdentifyUserSubmitCodeEvent::new(lower_user_uuid, higher_user.clone(), totp + 1),
|
||||
|
@ -596,9 +596,13 @@ mod test {
|
|||
&IdentifyUserStartEvent::new(lower_user_uuid, higher_user.clone()),
|
||||
);
|
||||
|
||||
let Ok(IdentifyUserResponse::ProvideCode { totp: higher_user_totp, .. }) = res_higher_user else {
|
||||
return assert!(false);
|
||||
};
|
||||
let Ok(IdentifyUserResponse::ProvideCode {
|
||||
totp: higher_user_totp,
|
||||
..
|
||||
}) = res_higher_user
|
||||
else {
|
||||
return assert!(false);
|
||||
};
|
||||
|
||||
// then we get the lower user code
|
||||
|
||||
|
|
|
@ -3770,9 +3770,10 @@ mod tests {
|
|||
.proxy_read()
|
||||
.await
|
||||
.validate_and_parse_token_to_token(Some(&token), ct)
|
||||
.expect("Must not fail") else {
|
||||
.expect("Must not fail")
|
||||
else {
|
||||
panic!("Unexpected auth token type for anonymous auth");
|
||||
};
|
||||
};
|
||||
|
||||
debug!(?uat);
|
||||
|
||||
|
|
|
@ -186,7 +186,7 @@ impl Spn {
|
|||
});
|
||||
|
||||
let Some(domain_name) = domain_name_changed else {
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// IMPORTANT - we have to *pre-emptively reload the domain info here*
|
||||
|
|
|
@ -162,7 +162,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
trace!("internal_migrate_or_create operating on {:?}", e.get_uuid());
|
||||
|
||||
let Some(filt) = e.filter_from_attrs(&[Attribute::Uuid.into()]) else {
|
||||
return Err(OperationError::FilterGeneration)
|
||||
return Err(OperationError::FilterGeneration);
|
||||
};
|
||||
|
||||
trace!("internal_migrate_or_create search {:?}", filt);
|
||||
|
|
|
@ -206,9 +206,9 @@ async fn test_full_identification_flow(rsclient: KanidmClient) {
|
|||
// we check that the user A got a WaitForCode
|
||||
|
||||
let IdentifyUserResponse::ProvideCode { step: _, totp } = higher_user_req_1 else {
|
||||
return assert!(false);
|
||||
// we check that the user B got the code
|
||||
};
|
||||
return assert!(false);
|
||||
// we check that the user B got the code
|
||||
};
|
||||
// we now try to submit the wrong code and we check that we get CodeFailure
|
||||
// we now submit the received totp as the user A
|
||||
|
||||
|
@ -233,8 +233,8 @@ async fn test_full_identification_flow(rsclient: KanidmClient) {
|
|||
.unwrap();
|
||||
// if the totp was correct we must get a ProvideCode
|
||||
let IdentifyUserResponse::ProvideCode { step: _, totp } = lower_user_req_2_right else {
|
||||
return assert!(false)
|
||||
};
|
||||
return assert!(false);
|
||||
};
|
||||
// we now try to do the same thing with user B: we first submit the wrong code expecting CodeFailure,
|
||||
// and then we submit the right one expecting Success
|
||||
|
||||
|
|
|
@ -55,10 +55,9 @@ impl TotpDisplayApp {
|
|||
async fn renew_totp(other_id: String) -> Msg {
|
||||
let uri = format!("/v1/person/{}/_identify_user", other_id);
|
||||
let request = IdentifyUserRequest::DisplayCode;
|
||||
let Ok(state_as_jsvalue) = serde_json::to_string(&request)
|
||||
.map(|s| JsValue::from(&s))
|
||||
else {
|
||||
return Msg::Cancel
|
||||
let Ok(state_as_jsvalue) = serde_json::to_string(&request).map(|s| JsValue::from(&s))
|
||||
else {
|
||||
return Msg::Cancel;
|
||||
};
|
||||
let response = match do_request(&uri, RequestMethod::POST, Some(state_as_jsvalue)).await {
|
||||
Ok((_, _, response, _)) => response,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use kanidm_unix_common::client_sync::call_daemon_blocking;
|
||||
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};
|
||||
|
@ -19,7 +19,17 @@ impl PasswdHooks for KanidmPasswd {
|
|||
}
|
||||
};
|
||||
let req = ClientRequest::NssAccounts;
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout)
|
||||
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(_) => {
|
||||
return Response::Unavail;
|
||||
}
|
||||
};
|
||||
|
||||
daemon_client
|
||||
.call_and_wait(&req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssAccounts(l) => l.into_iter().map(passwd_from_nssuser).collect(),
|
||||
_ => Vec::new(),
|
||||
|
@ -37,7 +47,17 @@ impl PasswdHooks for KanidmPasswd {
|
|||
}
|
||||
};
|
||||
let req = ClientRequest::NssAccountByUid(uid);
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout)
|
||||
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(_) => {
|
||||
return Response::Unavail;
|
||||
}
|
||||
};
|
||||
|
||||
daemon_client
|
||||
.call_and_wait(&req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssAccount(opt) => opt
|
||||
.map(passwd_from_nssuser)
|
||||
|
@ -57,7 +77,16 @@ impl PasswdHooks for KanidmPasswd {
|
|||
}
|
||||
};
|
||||
let req = ClientRequest::NssAccountByName(name);
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout)
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(_) => {
|
||||
return Response::Unavail;
|
||||
}
|
||||
};
|
||||
|
||||
daemon_client
|
||||
.call_and_wait(&req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssAccount(opt) => opt
|
||||
.map(passwd_from_nssuser)
|
||||
|
@ -82,7 +111,16 @@ impl GroupHooks for KanidmGroup {
|
|||
}
|
||||
};
|
||||
let req = ClientRequest::NssGroups;
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout)
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(_) => {
|
||||
return Response::Unavail;
|
||||
}
|
||||
};
|
||||
|
||||
daemon_client
|
||||
.call_and_wait(&req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssGroups(l) => l.into_iter().map(group_from_nssgroup).collect(),
|
||||
_ => Vec::new(),
|
||||
|
@ -100,7 +138,16 @@ impl GroupHooks for KanidmGroup {
|
|||
}
|
||||
};
|
||||
let req = ClientRequest::NssGroupByGid(gid);
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout)
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(_) => {
|
||||
return Response::Unavail;
|
||||
}
|
||||
};
|
||||
|
||||
daemon_client
|
||||
.call_and_wait(&req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssGroup(opt) => opt
|
||||
.map(group_from_nssgroup)
|
||||
|
@ -120,7 +167,16 @@ impl GroupHooks for KanidmGroup {
|
|||
}
|
||||
};
|
||||
let req = ClientRequest::NssGroupByName(name);
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout)
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(_) => {
|
||||
return Response::Unavail;
|
||||
}
|
||||
};
|
||||
|
||||
daemon_client
|
||||
.call_and_wait(&req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssGroup(opt) => opt
|
||||
.map(group_from_nssgroup)
|
||||
|
|
|
@ -18,6 +18,8 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
kanidm_unix_int = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = { workspace = true }
|
||||
|
|
|
@ -47,9 +47,9 @@ pub const _PAM_AUTHTOK_TYPE: PamItemType = 13;
|
|||
|
||||
// Message styles
|
||||
pub const PAM_PROMPT_ECHO_OFF: PamMessageStyle = 1;
|
||||
pub const _PAM_PROMPT_ECHO_ON: PamMessageStyle = 2;
|
||||
pub const _PAM_ERROR_MSG: PamMessageStyle = 3;
|
||||
pub const _PAM_TEXT_INFO: PamMessageStyle = 4;
|
||||
pub const PAM_PROMPT_ECHO_ON: PamMessageStyle = 2;
|
||||
pub const PAM_ERROR_MSG: PamMessageStyle = 3;
|
||||
pub const PAM_TEXT_INFO: PamMessageStyle = 4;
|
||||
/// yes/no/maybe conditionals
|
||||
pub const _PAM_RADIO_TYPE: PamMessageStyle = 5;
|
||||
pub const _PAM_BINARY_PROMPT: PamMessageStyle = 7;
|
||||
|
|
|
@ -35,10 +35,12 @@ use std::collections::BTreeSet;
|
|||
use std::convert::TryFrom;
|
||||
use std::ffi::CStr;
|
||||
|
||||
use kanidm_unix_common::client_sync::call_daemon_blocking;
|
||||
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};
|
||||
use kanidm_unix_common::unix_proto::{
|
||||
ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse,
|
||||
};
|
||||
|
||||
use crate::pam::constants::*;
|
||||
use crate::pam::conv::PamConv;
|
||||
|
@ -46,12 +48,32 @@ use crate::pam::module::{PamHandle, PamHooks};
|
|||
use crate::pam_hooks;
|
||||
use constants::PamResultCode;
|
||||
|
||||
use tracing::{debug, error};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::fmt;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
pub fn get_cfg() -> Result<KanidmUnixdConfig, PamResultCode> {
|
||||
KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
.map_err(|_| PamResultCode::PAM_SERVICE_ERR)
|
||||
}
|
||||
|
||||
fn install_subscriber(debug: bool) {
|
||||
let fmt_layer = fmt::layer().with_target(false);
|
||||
|
||||
let filter_layer = if debug {
|
||||
LevelFilter::DEBUG
|
||||
} else {
|
||||
LevelFilter::ERROR
|
||||
};
|
||||
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(fmt_layer)
|
||||
.try_init();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Options {
|
||||
debug: bool,
|
||||
|
@ -91,22 +113,17 @@ impl PamHooks for PamKanidm {
|
|||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
install_subscriber(opts.debug);
|
||||
|
||||
let tty = pamh.get_tty();
|
||||
let rhost = pamh.get_rhost();
|
||||
|
||||
if opts.debug {
|
||||
println!("acct_mgmt");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
println!("tty -> {:?} rhost -> {:?}", tty, rhost);
|
||||
}
|
||||
debug!(?args, ?opts, ?tty, ?rhost, "acct_mgmt");
|
||||
|
||||
let account_id = match pamh.get_user(None) {
|
||||
Ok(aid) => aid,
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("Error get_user -> {:?}", e);
|
||||
}
|
||||
error!(err = ?e, "get_user");
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
@ -118,46 +135,42 @@ impl PamHooks for PamKanidm {
|
|||
let req = ClientRequest::PamAccountAllowed(account_id);
|
||||
// PamResultCode::PAM_IGNORE
|
||||
|
||||
match call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout) {
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(e) => {
|
||||
error!(err = ?e, "Error DaemonClientBlocking::new()");
|
||||
return PamResultCode::PAM_SERVICE_ERR;
|
||||
}
|
||||
};
|
||||
|
||||
match daemon_client.call_and_wait(&req) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
if opts.debug {
|
||||
println!("PamResultCode::PAM_SUCCESS");
|
||||
}
|
||||
debug!("PamResultCode::PAM_SUCCESS");
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
ClientResponse::PamStatus(Some(false)) => {
|
||||
// println!("PAM_IGNORE");
|
||||
if opts.debug {
|
||||
println!("PamResultCode::PAM_AUTH_ERR");
|
||||
}
|
||||
debug!("PamResultCode::PAM_AUTH_ERR");
|
||||
PamResultCode::PAM_AUTH_ERR
|
||||
}
|
||||
ClientResponse::PamStatus(None) => {
|
||||
if opts.ignore_unknown_user {
|
||||
if opts.debug {
|
||||
println!("PamResultCode::PAM_IGNORE");
|
||||
}
|
||||
debug!("PamResultCode::PAM_IGNORE");
|
||||
PamResultCode::PAM_IGNORE
|
||||
} else {
|
||||
if opts.debug {
|
||||
println!("PamResultCode::PAM_USER_UNKNOWN");
|
||||
}
|
||||
debug!("PamResultCode::PAM_USER_UNKNOWN");
|
||||
PamResultCode::PAM_USER_UNKNOWN
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
if opts.debug {
|
||||
println!("PamResultCode::PAM_IGNORE -> {:?}", r);
|
||||
}
|
||||
error!(err = ?r, "PAM_IGNORE, unexpected resolver response");
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("PamResultCode::PAM_IGNORE -> {:?}", e);
|
||||
}
|
||||
error!(err = ?e, "PamResultCode::PAM_IGNORE");
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
}
|
||||
|
@ -169,113 +182,121 @@ impl PamHooks for PamKanidm {
|
|||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
install_subscriber(opts.debug);
|
||||
|
||||
// This will == "Ok(Some("ssh"))" on remote auth.
|
||||
let tty = pamh.get_tty();
|
||||
let rhost = pamh.get_rhost();
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_authenticate");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
println!("tty -> {:?} rhost -> {:?}", tty, rhost);
|
||||
}
|
||||
debug!(?args, ?opts, ?tty, ?rhost, "sm_authenticate");
|
||||
|
||||
let account_id = match pamh.get_user(None) {
|
||||
Ok(aid) => aid,
|
||||
Err(e) => {
|
||||
println!("Error get_user -> {:?}", e);
|
||||
error!(err = ?e, "get_user");
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
let authtok = match pamh.get_authtok() {
|
||||
Ok(atok) => atok,
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("Error get_authtok -> {:?}", e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
let authtok = match authtok {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
if opts.use_first_pass {
|
||||
if opts.debug {
|
||||
println!("Don't have an authtok, returning PAM_AUTH_ERR");
|
||||
}
|
||||
return PamResultCode::PAM_AUTH_ERR;
|
||||
} else {
|
||||
let conv = match pamh.get_item::<PamConv>() {
|
||||
Ok(conv) => conv,
|
||||
Err(err) => {
|
||||
if opts.debug {
|
||||
println!("Couldn't get pam_conv");
|
||||
}
|
||||
return err;
|
||||
}
|
||||
};
|
||||
match conv.send(PAM_PROMPT_ECHO_OFF, "Password: ") {
|
||||
Ok(password) => match password {
|
||||
Some(pw) => pw,
|
||||
None => {
|
||||
if opts.debug {
|
||||
println!("No password");
|
||||
}
|
||||
return PamResultCode::PAM_CRED_INSUFFICIENT;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
if opts.debug {
|
||||
println!("Couldn't get password");
|
||||
}
|
||||
return err;
|
||||
}
|
||||
}
|
||||
} // end opts.use_first_pass
|
||||
}
|
||||
};
|
||||
|
||||
let cfg = match get_cfg() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let req = ClientRequest::PamAuthenticate(account_id, authtok);
|
||||
|
||||
match call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
// println!("PAM_SUCCESS");
|
||||
PamResultCode::PAM_SUCCESS
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(e) => {
|
||||
error!(err = ?e, "Error DaemonClientBlocking::new()");
|
||||
return PamResultCode::PAM_SERVICE_ERR;
|
||||
}
|
||||
ClientResponse::PamStatus(Some(false)) => {
|
||||
// println!("PAM_AUTH_ERR");
|
||||
PamResultCode::PAM_AUTH_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;
|
||||
}
|
||||
ClientResponse::PamStatus(None) => {
|
||||
// println!("PAM_USER_UNKNOWN");
|
||||
if opts.ignore_unknown_user {
|
||||
PamResultCode::PAM_IGNORE
|
||||
} else {
|
||||
PamResultCode::PAM_USER_UNKNOWN
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
if opts.debug {
|
||||
println!("PAM_IGNORE -> {:?}", r);
|
||||
}
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("PAM_IGNORE -> {:?}", e);
|
||||
}
|
||||
PamResultCode::PAM_IGNORE
|
||||
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);
|
||||
|
||||
loop {
|
||||
match daemon_client.call_and_wait(&req) {
|
||||
Ok(r) => match r {
|
||||
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 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.
|
||||
req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred });
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
error!(err = ?r, "PAM_IGNORE, unexpected resolver response");
|
||||
return PamResultCode::PAM_IGNORE;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!(?err, "PAM_IGNORE");
|
||||
return PamResultCode::PAM_IGNORE;
|
||||
}
|
||||
}
|
||||
} // while true, continue calling PamAuthenticateStep until we get a decision.
|
||||
}
|
||||
|
||||
fn sm_chauthtok(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {
|
||||
|
@ -284,11 +305,10 @@ impl PamHooks for PamKanidm {
|
|||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_chauthtok");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
install_subscriber(opts.debug);
|
||||
|
||||
debug!(?args, ?opts, "sm_chauthtok");
|
||||
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
|
||||
|
@ -298,11 +318,10 @@ impl PamHooks for PamKanidm {
|
|||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_close_session");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
install_subscriber(opts.debug);
|
||||
|
||||
debug!(?args, ?opts, "sm_close_session");
|
||||
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
|
||||
|
@ -312,19 +331,15 @@ impl PamHooks for PamKanidm {
|
|||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_open_session");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
install_subscriber(opts.debug);
|
||||
|
||||
debug!(?args, ?opts, "sm_open_session");
|
||||
|
||||
let account_id = match pamh.get_user(None) {
|
||||
Ok(aid) => aid,
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("Error get_user -> {:?}", e);
|
||||
}
|
||||
return e;
|
||||
Err(err) => {
|
||||
error!(?err, "get_user");
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -334,15 +349,22 @@ impl PamHooks for PamKanidm {
|
|||
};
|
||||
let req = ClientRequest::PamAccountBeginSession(account_id);
|
||||
|
||||
match call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout) {
|
||||
let mut daemon_client =
|
||||
match DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout) {
|
||||
Ok(dc) => dc,
|
||||
Err(e) => {
|
||||
error!(err = ?e, "Error DaemonClientBlocking::new()");
|
||||
return PamResultCode::PAM_SERVICE_ERR;
|
||||
}
|
||||
};
|
||||
|
||||
match daemon_client.call_and_wait(&req) {
|
||||
Ok(ClientResponse::Ok) => {
|
||||
// println!("PAM_SUCCESS");
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
other => {
|
||||
if opts.debug {
|
||||
println!("PAM_IGNORE -> {:?}", other);
|
||||
}
|
||||
debug!(err = ?other, "PAM_IGNORE");
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
}
|
||||
|
@ -354,11 +376,10 @@ impl PamHooks for PamKanidm {
|
|||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_setcred");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
install_subscriber(opts.debug);
|
||||
|
||||
debug!(?args, ?opts, "sm_setcred");
|
||||
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,95 +5,103 @@ use std::time::{Duration, SystemTime};
|
|||
|
||||
use crate::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
pub fn call_daemon_blocking(
|
||||
path: &str,
|
||||
req: &ClientRequest,
|
||||
timeout: u64,
|
||||
) -> Result<ClientResponse, Box<dyn Error>> {
|
||||
let timeout = Duration::from_secs(timeout);
|
||||
pub struct DaemonClientBlocking {
|
||||
timeout: Duration,
|
||||
stream: UnixStream,
|
||||
}
|
||||
|
||||
let mut stream = UnixStream::connect(path)
|
||||
.and_then(|socket| socket.set_read_timeout(Some(timeout)).map(|_| socket))
|
||||
.and_then(|socket| socket.set_write_timeout(Some(timeout)).map(|_| socket))
|
||||
.map_err(|e| {
|
||||
error!("stream setup error -> {:?}", e);
|
||||
e
|
||||
})
|
||||
.map_err(Box::new)?;
|
||||
impl DaemonClientBlocking {
|
||||
pub fn new(path: &str, timeout: u64) -> Result<DaemonClientBlocking, Box<dyn Error>> {
|
||||
let timeout = Duration::from_secs(timeout);
|
||||
|
||||
let data = serde_json::to_vec(&req).map_err(|e| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
Box::new(IoError::new(ErrorKind::Other, "JSON encode error"))
|
||||
})?;
|
||||
// .map_err(Box::new)?;
|
||||
debug!(%path, ?timeout);
|
||||
|
||||
stream
|
||||
.write_all(data.as_slice())
|
||||
.and_then(|_| stream.flush())
|
||||
.map_err(|e| {
|
||||
error!("stream write error -> {:?}", e);
|
||||
e
|
||||
})
|
||||
.map_err(Box::new)?;
|
||||
let stream = UnixStream::connect(path)
|
||||
.and_then(|socket| socket.set_read_timeout(Some(timeout)).map(|_| socket))
|
||||
.and_then(|socket| socket.set_write_timeout(Some(timeout)).map(|_| socket))
|
||||
.map_err(|e| {
|
||||
error!("stream setup error -> {:?}", e);
|
||||
e
|
||||
})
|
||||
.map_err(Box::new)?;
|
||||
|
||||
// Now wait on the response.
|
||||
let start = SystemTime::now();
|
||||
let mut read_started = false;
|
||||
let mut data = Vec::with_capacity(1024);
|
||||
let mut counter = 0;
|
||||
|
||||
loop {
|
||||
let mut buffer = [0; 1024];
|
||||
let durr = SystemTime::now().duration_since(start).map_err(Box::new)?;
|
||||
if durr > timeout {
|
||||
error!("Socket timeout");
|
||||
// timed out, not enough activity.
|
||||
break;
|
||||
}
|
||||
// Would be a lot easier if we had peek ...
|
||||
// https://github.com/rust-lang/rust/issues/76923
|
||||
match stream.read(&mut buffer) {
|
||||
Ok(0) => {
|
||||
if read_started {
|
||||
debug!("read_started true, we have completed");
|
||||
// We're done, no more bytes.
|
||||
break;
|
||||
} else {
|
||||
debug!("Waiting ...");
|
||||
// Still can wait ...
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Ok(count) => {
|
||||
data.extend_from_slice(&buffer);
|
||||
counter += count;
|
||||
if count == 1024 {
|
||||
debug!("Filled 1024 bytes, looping ...");
|
||||
// We have filled the buffer, we need to copy and loop again.
|
||||
read_started = true;
|
||||
continue;
|
||||
} else {
|
||||
debug!("Filled {} bytes, complete", count);
|
||||
// We have a partial read, so we are complete.
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Steam read failure -> {:?}", e);
|
||||
// Failure!
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
Ok(DaemonClientBlocking { timeout, stream })
|
||||
}
|
||||
|
||||
// Extend from slice fills with 0's, so we need to truncate now.
|
||||
data.truncate(counter);
|
||||
pub fn call_and_wait(&mut self, req: &ClientRequest) -> Result<ClientResponse, Box<dyn Error>> {
|
||||
let data = serde_json::to_vec(&req).map_err(|e| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
Box::new(IoError::new(ErrorKind::Other, "JSON encode error"))
|
||||
})?;
|
||||
|
||||
// Now attempt to decode.
|
||||
let cr = serde_json::from_slice::<ClientResponse>(data.as_slice()).map_err(|e| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
Box::new(IoError::new(ErrorKind::Other, "JSON decode error"))
|
||||
})?;
|
||||
self.stream
|
||||
.write_all(data.as_slice())
|
||||
.and_then(|_| self.stream.flush())
|
||||
.map_err(|e| {
|
||||
error!("stream write error -> {:?}", e);
|
||||
e
|
||||
})
|
||||
.map_err(Box::new)?;
|
||||
|
||||
Ok(cr)
|
||||
// Now wait on the response.
|
||||
let start = SystemTime::now();
|
||||
let mut read_started = false;
|
||||
let mut data = Vec::with_capacity(1024);
|
||||
let mut counter = 0;
|
||||
|
||||
loop {
|
||||
let mut buffer = [0; 1024];
|
||||
let durr = SystemTime::now().duration_since(start).map_err(Box::new)?;
|
||||
if durr > self.timeout {
|
||||
error!("Socket timeout");
|
||||
// timed out, not enough activity.
|
||||
break;
|
||||
}
|
||||
// Would be a lot easier if we had peek ...
|
||||
// https://github.com/rust-lang/rust/issues/76923
|
||||
match self.stream.read(&mut buffer) {
|
||||
Ok(0) => {
|
||||
if read_started {
|
||||
debug!("read_started true, we have completed");
|
||||
// We're done, no more bytes.
|
||||
break;
|
||||
} else {
|
||||
debug!("Waiting ...");
|
||||
// Still can wait ...
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Ok(count) => {
|
||||
data.extend_from_slice(&buffer);
|
||||
counter += count;
|
||||
if count == 1024 {
|
||||
debug!("Filled 1024 bytes, looping ...");
|
||||
// We have filled the buffer, we need to copy and loop again.
|
||||
read_started = true;
|
||||
continue;
|
||||
} else {
|
||||
debug!("Filled {} bytes, complete", count);
|
||||
// We have a partial read, so we are complete.
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Steam read failure -> {:?}", e);
|
||||
// Failure!
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extend from slice fills with 0's, so we need to truncate now.
|
||||
data.truncate(counter);
|
||||
|
||||
// Now attempt to decode.
|
||||
let cr = serde_json::from_slice::<ClientResponse>(data.as_slice()).map_err(|e| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
Box::new(IoError::new(ErrorKind::Other, "JSON decode error"))
|
||||
})?;
|
||||
|
||||
Ok(cr)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
|
|||
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
|
||||
use kanidm_unix_common::db::Db;
|
||||
use kanidm_unix_common::idprovider::kanidm::KanidmProvider;
|
||||
// use kanidm_unix_common::idprovider::interface::AuthSession;
|
||||
use kanidm_unix_common::resolver::Resolver;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd};
|
||||
|
@ -189,10 +190,14 @@ async fn handle_client(
|
|||
debug!("Accepted connection");
|
||||
|
||||
let Ok(ucred) = sock.peer_cred() else {
|
||||
return Err(Box::new(IoError::new(ErrorKind::Other, "Unable to verify peer credentials.")));
|
||||
return Err(Box::new(IoError::new(
|
||||
ErrorKind::Other,
|
||||
"Unable to verify peer credentials.",
|
||||
)));
|
||||
};
|
||||
|
||||
let mut reqs = Framed::new(sock, ClientCodec);
|
||||
let mut pam_auth_session_state = None;
|
||||
|
||||
trace!("Waiting for requests ...");
|
||||
while let Some(Ok(req)) = reqs.next().await {
|
||||
|
@ -274,13 +279,44 @@ async fn handle_client(
|
|||
ClientResponse::NssGroup(None)
|
||||
})
|
||||
}
|
||||
ClientRequest::PamAuthenticate(account_id, cred) => {
|
||||
debug!("pam authenticate");
|
||||
cachelayer
|
||||
.pam_account_authenticate(account_id.as_str(), cred.as_str())
|
||||
.await
|
||||
.map(ClientResponse::PamStatus)
|
||||
.unwrap_or(ClientResponse::Error)
|
||||
ClientRequest::PamAuthenticateInit(account_id) => {
|
||||
debug!("pam authenticate init");
|
||||
|
||||
match &pam_auth_session_state {
|
||||
Some(_auth_session) => {
|
||||
// Invalid to init a request twice.
|
||||
warn!("Attempt to init auth session while current session is active");
|
||||
// Clean the former session, something is wrong.
|
||||
pam_auth_session_state = None;
|
||||
ClientResponse::Error
|
||||
}
|
||||
None => {
|
||||
match cachelayer
|
||||
.pam_account_authenticate_init(account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok((auth_session, pam_auth_response)) => {
|
||||
pam_auth_session_state = Some(auth_session);
|
||||
pam_auth_response.into()
|
||||
}
|
||||
Err(_) => ClientResponse::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientRequest::PamAuthenticateStep(pam_next_req) => {
|
||||
debug!("pam authenticate step");
|
||||
match &mut pam_auth_session_state {
|
||||
Some(auth_session) => cachelayer
|
||||
.pam_account_authenticate_step(auth_session, pam_next_req)
|
||||
.await
|
||||
.map(|pam_auth_response| pam_auth_response.into())
|
||||
.unwrap_or(ClientResponse::Error),
|
||||
None => {
|
||||
warn!("Attempt to continue auth session while current session is inactive");
|
||||
ClientResponse::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientRequest::PamAccountAllowed(account_id) => {
|
||||
debug!("pam account allowed");
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::unix_proto::{PamAuthRequest, PamAuthResponse};
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
@ -14,8 +15,8 @@ pub enum IdpError {
|
|||
/// the idp. After returning this error the operation will be retried after a
|
||||
/// successful authentication.
|
||||
ProviderUnauthorised,
|
||||
/// The provider made an invalid request to the idp, and the result is not able to
|
||||
/// be used by the resolver.
|
||||
/// The provider made an invalid or illogical request to the idp, and a result
|
||||
/// is not able to be provided to the resolver.
|
||||
BadRequest,
|
||||
/// The idp has indicated that the requested resource does not exist and should
|
||||
/// be considered deleted, removed, or not present.
|
||||
|
@ -51,21 +52,87 @@ pub struct UserToken {
|
|||
pub valid: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthCredHandler {
|
||||
Password,
|
||||
}
|
||||
|
||||
pub enum AuthRequest {
|
||||
Password,
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<PamAuthResponse> for AuthRequest {
|
||||
fn into(self) -> PamAuthResponse {
|
||||
match self {
|
||||
AuthRequest::Password => PamAuthResponse::Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AuthResult {
|
||||
Success { token: UserToken },
|
||||
Denied,
|
||||
Next(AuthRequest),
|
||||
}
|
||||
|
||||
pub enum AuthCacheAction {
|
||||
None,
|
||||
PasswordHashUpdate { cred: String },
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait IdProvider {
|
||||
async fn provider_authenticate(&self) -> Result<(), IdpError>;
|
||||
|
||||
async fn unix_user_get(
|
||||
&self,
|
||||
id: &Id,
|
||||
old_token: Option<UserToken>,
|
||||
_id: &Id,
|
||||
_token: Option<&UserToken>,
|
||||
) -> Result<UserToken, IdpError>;
|
||||
|
||||
async fn unix_user_authenticate(
|
||||
async fn unix_user_online_auth_init(
|
||||
&self,
|
||||
id: &Id,
|
||||
cred: &str,
|
||||
) -> Result<Option<UserToken>, IdpError>;
|
||||
_account_id: &str,
|
||||
_token: Option<&UserToken>,
|
||||
) -> Result<(AuthRequest, AuthCredHandler), IdpError>;
|
||||
|
||||
async fn unix_user_online_auth_step(
|
||||
&self,
|
||||
_account_id: &str,
|
||||
_cred_handler: &mut AuthCredHandler,
|
||||
_pam_next_req: PamAuthRequest,
|
||||
) -> Result<(AuthResult, AuthCacheAction), IdpError>;
|
||||
|
||||
async fn unix_user_offline_auth_init(
|
||||
&self,
|
||||
_account_id: &str,
|
||||
_token: Option<&UserToken>,
|
||||
) -> Result<(AuthRequest, AuthCredHandler), IdpError>;
|
||||
|
||||
/*
|
||||
// I thought about this part of the interface a lot. we could have the
|
||||
// provider actually need to check the password or credentials, but then
|
||||
// we need to rework the tpm/crypto engine to be an argument to pass here
|
||||
// as well the cached credentials.
|
||||
//
|
||||
// As well, since this is "offline auth" the provider isn't really "doing"
|
||||
// anything special here - when you say you want offline password auth, the
|
||||
// resolver can just do it for you for all the possible implementations.
|
||||
// This is similar for offline ctap2 as well, or even offline totp.
|
||||
//
|
||||
// I think in the future we could reconsider this and let the provider be
|
||||
// involved if there is some "custom logic" or similar that is needed but
|
||||
// for now I think making it generic is a good first step and we can change
|
||||
// it later.
|
||||
async fn unix_user_offline_auth_step(
|
||||
&self,
|
||||
_account_id: &str,
|
||||
_cred_handler: &mut AuthCredHandler,
|
||||
_pam_next_req: PamAuthRequest,
|
||||
_online_at_init: bool,
|
||||
) -> Result<AuthResult, IdpError>;
|
||||
*/
|
||||
|
||||
async fn unix_group_get(&self, id: &Id) -> Result<GroupToken, IdpError>;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,11 @@ use kanidm_client::{ClientError, KanidmClient, StatusCode};
|
|||
use kanidm_proto::v1::{OperationError, UnixGroupToken, UnixUserToken};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::interface::{GroupToken, Id, IdProvider, IdpError, UserToken};
|
||||
use super::interface::{
|
||||
AuthCacheAction, AuthCredHandler, AuthRequest, AuthResult, GroupToken, Id, IdProvider,
|
||||
IdpError, UserToken,
|
||||
};
|
||||
use crate::unix_proto::PamAuthRequest;
|
||||
|
||||
pub struct KanidmProvider {
|
||||
client: RwLock<KanidmClient>,
|
||||
|
@ -68,7 +72,6 @@ impl From<UnixGroupToken> for GroupToken {
|
|||
#[async_trait]
|
||||
impl IdProvider for KanidmProvider {
|
||||
// Needs .read on all types except re-auth.
|
||||
|
||||
async fn provider_authenticate(&self) -> Result<(), IdpError> {
|
||||
match self.client.write().await.auth_anonymous().await {
|
||||
Ok(_uat) => Ok(()),
|
||||
|
@ -82,7 +85,7 @@ impl IdProvider for KanidmProvider {
|
|||
async fn unix_user_get(
|
||||
&self,
|
||||
id: &Id,
|
||||
_old_token: Option<UserToken>,
|
||||
_token: Option<&UserToken>,
|
||||
) -> Result<UserToken, IdpError> {
|
||||
match self
|
||||
.client
|
||||
|
@ -141,70 +144,117 @@ impl IdProvider for KanidmProvider {
|
|||
}
|
||||
}
|
||||
|
||||
async fn unix_user_authenticate(
|
||||
async fn unix_user_online_auth_init(
|
||||
&self,
|
||||
id: &Id,
|
||||
cred: &str,
|
||||
) -> Result<Option<UserToken>, IdpError> {
|
||||
match self
|
||||
.client
|
||||
.read()
|
||||
.await
|
||||
.idm_account_unix_cred_verify(id.to_string().as_str(), cred)
|
||||
.await
|
||||
{
|
||||
Ok(Some(n_tok)) => Ok(Some(UserToken::from(n_tok))),
|
||||
Ok(None) => Ok(None),
|
||||
Err(ClientError::Transport(err)) => {
|
||||
error!(?err);
|
||||
Err(IdpError::Transport)
|
||||
}
|
||||
Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
|
||||
match reason {
|
||||
Some(OperationError::NotAuthenticated) => warn!(
|
||||
"session not authenticated - attempting reauthentication - eventid {}",
|
||||
opid
|
||||
),
|
||||
Some(OperationError::SessionExpired) => warn!(
|
||||
"session expired - attempting reauthentication - eventid {}",
|
||||
opid
|
||||
),
|
||||
e => error!(
|
||||
"authentication error {:?}, moving to offline - eventid {}",
|
||||
e, opid
|
||||
),
|
||||
};
|
||||
Err(IdpError::ProviderUnauthorised)
|
||||
}
|
||||
Err(ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::NoMatchingEntries),
|
||||
opid,
|
||||
))
|
||||
| Err(ClientError::Http(
|
||||
StatusCode::NOT_FOUND,
|
||||
Some(OperationError::NoMatchingEntries),
|
||||
opid,
|
||||
))
|
||||
| Err(ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::InvalidAccountState(_)),
|
||||
opid,
|
||||
)) => {
|
||||
error!(
|
||||
"unknown account or is not a valid posix account - eventid {}",
|
||||
opid
|
||||
);
|
||||
Err(IdpError::NotFound)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "client error");
|
||||
// Some other unknown processing error?
|
||||
Err(IdpError::BadRequest)
|
||||
}
|
||||
_account_id: &str,
|
||||
_token: Option<&UserToken>,
|
||||
) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
|
||||
// Not sure that I need to do much here?
|
||||
Ok((AuthRequest::Password, AuthCredHandler::Password))
|
||||
}
|
||||
|
||||
async fn unix_user_online_auth_step(
|
||||
&self,
|
||||
account_id: &str,
|
||||
cred_handler: &mut AuthCredHandler,
|
||||
pam_next_req: PamAuthRequest,
|
||||
) -> Result<(AuthResult, AuthCacheAction), IdpError> {
|
||||
match (cred_handler, pam_next_req) {
|
||||
(AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
|
||||
match self
|
||||
.client
|
||||
.read()
|
||||
.await
|
||||
.idm_account_unix_cred_verify(account_id, &cred)
|
||||
.await
|
||||
{
|
||||
Ok(Some(n_tok)) => Ok((
|
||||
AuthResult::Success {
|
||||
token: UserToken::from(n_tok),
|
||||
},
|
||||
AuthCacheAction::PasswordHashUpdate { cred },
|
||||
)),
|
||||
Ok(None) => Ok((AuthResult::Denied, AuthCacheAction::None)),
|
||||
Err(ClientError::Transport(err)) => {
|
||||
error!(?err);
|
||||
Err(IdpError::Transport)
|
||||
}
|
||||
Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
|
||||
match reason {
|
||||
Some(OperationError::NotAuthenticated) => warn!(
|
||||
"session not authenticated - attempting reauthentication - eventid {}",
|
||||
opid
|
||||
),
|
||||
Some(OperationError::SessionExpired) => warn!(
|
||||
"session expired - attempting reauthentication - eventid {}",
|
||||
opid
|
||||
),
|
||||
e => error!(
|
||||
"authentication error {:?}, moving to offline - eventid {}",
|
||||
e, opid
|
||||
),
|
||||
};
|
||||
Err(IdpError::ProviderUnauthorised)
|
||||
}
|
||||
Err(ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::NoMatchingEntries),
|
||||
opid,
|
||||
))
|
||||
| Err(ClientError::Http(
|
||||
StatusCode::NOT_FOUND,
|
||||
Some(OperationError::NoMatchingEntries),
|
||||
opid,
|
||||
))
|
||||
| Err(ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::InvalidAccountState(_)),
|
||||
opid,
|
||||
)) => {
|
||||
error!(
|
||||
"unknown account or is not a valid posix account - eventid {}",
|
||||
opid
|
||||
);
|
||||
Err(IdpError::NotFound)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "client error");
|
||||
// Some other unknown processing error?
|
||||
Err(IdpError::BadRequest)
|
||||
}
|
||||
}
|
||||
} // For future when we have different auth combos/types.
|
||||
/*
|
||||
_ => {
|
||||
error!("invalid authentication request state");
|
||||
Err(IdpError::BadRequest)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
async fn unix_user_offline_auth_init(
|
||||
&self,
|
||||
_account_id: &str,
|
||||
_token: Option<&UserToken>,
|
||||
) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
|
||||
// Not sure that I need to do much here?
|
||||
Ok((AuthRequest::Password, AuthCredHandler::Password))
|
||||
}
|
||||
|
||||
/*
|
||||
async fn unix_user_offline_auth_step(
|
||||
&self,
|
||||
_account_id: &str,
|
||||
_cred_handler: &mut AuthCredHandler,
|
||||
_pam_next_req: PamAuthRequest,
|
||||
_online_at_init: bool,
|
||||
) -> Result<AuthResult, IdpError> {
|
||||
// We need any cached credentials here.
|
||||
todo!();
|
||||
}
|
||||
*/
|
||||
|
||||
async fn unix_group_get(&self, id: &Id) -> Result<GroupToken, IdpError> {
|
||||
match self
|
||||
.client
|
||||
|
|
|
@ -28,6 +28,8 @@ pub mod db;
|
|||
#[cfg(target_family = "unix")]
|
||||
pub mod idprovider;
|
||||
#[cfg(target_family = "unix")]
|
||||
pub mod pam_data;
|
||||
#[cfg(target_family = "unix")]
|
||||
pub mod resolver;
|
||||
#[cfg(all(target_family = "unix", feature = "selinux"))]
|
||||
pub mod selinux_util;
|
||||
|
|
7
unix_integration/src/pam_data.rs
Normal file
7
unix_integration/src/pam_data.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/* This is the definition for extra data to be sent along with a pam_prompt
|
||||
* request. It will be sent back to the idprovider to continue an auth attempt.
|
||||
*/
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PamData {}
|
|
@ -12,9 +12,11 @@ use tokio::sync::Mutex;
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::db::{Cache, CacheTxn, Db};
|
||||
use crate::idprovider::interface::{GroupToken, Id, IdProvider, IdpError, UserToken};
|
||||
use crate::idprovider::interface::{
|
||||
AuthCacheAction, AuthCredHandler, AuthResult, GroupToken, Id, IdProvider, IdpError, UserToken,
|
||||
};
|
||||
use crate::unix_config::{HomeAttr, UidAttr};
|
||||
use crate::unix_proto::{HomeDirectoryInfo, NssGroup, NssUser};
|
||||
use crate::unix_proto::{HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse};
|
||||
|
||||
// use crate::unix_passwd::{EtcUser, EtcGroup};
|
||||
|
||||
|
@ -27,10 +29,25 @@ enum CacheState {
|
|||
OfflineNextCheck(SystemTime),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthSession {
|
||||
InProgress {
|
||||
account_id: String,
|
||||
id: Id,
|
||||
token: Option<Box<UserToken>>,
|
||||
online_at_init: bool,
|
||||
// cred_type: AuthCredType,
|
||||
// next_cred: AuthNextCred,
|
||||
cred_handler: AuthCredHandler,
|
||||
},
|
||||
Success,
|
||||
Denied,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Resolver<I>
|
||||
where
|
||||
I: IdProvider,
|
||||
I: IdProvider + Sync,
|
||||
{
|
||||
// Generic / modular types.
|
||||
db: Db,
|
||||
|
@ -61,7 +78,7 @@ impl ToString for Id {
|
|||
|
||||
impl<I> Resolver<I>
|
||||
where
|
||||
I: IdProvider,
|
||||
I: IdProvider + Sync,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn new(
|
||||
|
@ -393,7 +410,7 @@ where
|
|||
account_id: &Id,
|
||||
token: Option<UserToken>,
|
||||
) -> Result<Option<UserToken>, ()> {
|
||||
match self.client.unix_user_get(account_id, token.clone()).await {
|
||||
match self.client.unix_user_get(account_id, token.as_ref()).await {
|
||||
Ok(mut n_tok) => {
|
||||
if self.check_nxset(&n_tok.name, n_tok.gidnumber).await {
|
||||
// Refuse to release the token, it's in the denied set.
|
||||
|
@ -729,31 +746,54 @@ where
|
|||
self.get_nssgroup(Id::Gid(gid)).await
|
||||
}
|
||||
|
||||
/*
|
||||
async fn online_account_authenticate(
|
||||
&self,
|
||||
token: &Option<UserToken>,
|
||||
account_id: &Id,
|
||||
cred: &str,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
cred: Option<PamCred>,
|
||||
data: Option<PamData>,
|
||||
) -> Result<ClientResponse, ()> {
|
||||
debug!("Attempt online password check");
|
||||
// Unwrap the cred for passing to the step function
|
||||
let ucred = match &cred {
|
||||
Some(PamCred::Password(v)) => Some(v.clone()),
|
||||
Some(PamCred::MFACode(v)) => Some(v.clone()),
|
||||
None => None,
|
||||
};
|
||||
// We are online, attempt the pw to the server.
|
||||
match self.client.unix_user_authenticate(account_id, cred).await {
|
||||
Ok(Some(mut n_tok)) => {
|
||||
match self
|
||||
.client
|
||||
.unix_user_authenticate_step(account_id, ucred.as_deref(), data)
|
||||
.await
|
||||
{
|
||||
Ok(ProviderResult::UserToken(Some(mut n_tok))) => {
|
||||
if self.check_nxset(&n_tok.name, n_tok.gidnumber).await {
|
||||
// Refuse to release the token, it's in the denied set.
|
||||
self.delete_cache_usertoken(n_tok.uuid).await?;
|
||||
Ok(None)
|
||||
Ok(ClientResponse::PamStatus(None))
|
||||
} else {
|
||||
debug!("online password check success.");
|
||||
self.set_cache_usertoken(&mut n_tok).await?;
|
||||
self.set_cache_userpassword(n_tok.uuid, cred).await?;
|
||||
Ok(Some(true))
|
||||
match cred {
|
||||
Some(PamCred::Password(cred)) => {
|
||||
// Only cache an actual password (not an MFA token)
|
||||
self.set_cache_userpassword(n_tok.uuid, &cred).await?;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {}
|
||||
}
|
||||
Ok(ClientResponse::PamStatus(Some(true)))
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
Ok(ProviderResult::UserToken(None)) => {
|
||||
error!("incorrect password");
|
||||
// PW failed the check.
|
||||
Ok(Some(false))
|
||||
Ok(ClientResponse::PamStatus(Some(false)))
|
||||
}
|
||||
Ok(ProviderResult::PamPrompt(prompt)) => {
|
||||
debug!("Requesting pam prompt {:?}", prompt);
|
||||
Ok(ClientResponse::PamPrompt(prompt))
|
||||
}
|
||||
Err(IdpError::Transport) => {
|
||||
error!("transport error, moving to offline");
|
||||
|
@ -762,22 +802,33 @@ where
|
|||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
match token.as_ref() {
|
||||
Some(t) => self.check_cache_userpassword(t.uuid, cred).await.map(Some),
|
||||
None => Ok(None),
|
||||
Some(t) => match ucred {
|
||||
Some(cred) => match self.check_cache_userpassword(t.uuid, &cred).await {
|
||||
Ok(res) => Ok(ClientResponse::PamStatus(Some(res))),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
None => Ok(ClientResponse::PamPrompt(PamPrompt::passwd_prompt())),
|
||||
},
|
||||
None => Ok(ClientResponse::PamStatus(None)),
|
||||
}
|
||||
}
|
||||
|
||||
Err(IdpError::ProviderUnauthorised) => {
|
||||
// Something went wrong, mark offline to force a re-auth ASAP.
|
||||
let time = SystemTime::now().sub(Duration::from_secs(1));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
match token.as_ref() {
|
||||
Some(t) => self.check_cache_userpassword(t.uuid, cred).await.map(Some),
|
||||
None => Ok(None),
|
||||
Some(t) => match ucred {
|
||||
Some(cred) => match self.check_cache_userpassword(t.uuid, &cred).await {
|
||||
Ok(res) => Ok(ClientResponse::PamStatus(Some(res))),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
None => Ok(ClientResponse::PamPrompt(PamPrompt::passwd_prompt())),
|
||||
},
|
||||
None => Ok(ClientResponse::PamStatus(None)),
|
||||
}
|
||||
}
|
||||
Err(IdpError::NotFound) => Ok(None),
|
||||
Err(IdpError::NotFound) => Ok(ClientResponse::PamStatus(None)),
|
||||
Err(IdpError::BadRequest) => {
|
||||
// Some other unknown processing error?
|
||||
Err(())
|
||||
|
@ -788,18 +839,29 @@ where
|
|||
async fn offline_account_authenticate(
|
||||
&self,
|
||||
token: &Option<UserToken>,
|
||||
cred: &str,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
cred: Option<PamCred>,
|
||||
) -> Result<ClientResponse, ()> {
|
||||
let cred = match cred {
|
||||
Some(cred) => match cred {
|
||||
PamCred::Password(v) => v.clone(),
|
||||
// We can only authenticate using a password
|
||||
_ => return Ok(ClientResponse::PamStatus(Some(false))),
|
||||
},
|
||||
None => return Ok(ClientResponse::PamPrompt(PamPrompt::passwd_prompt())),
|
||||
};
|
||||
debug!("Attempt offline password check");
|
||||
match token.as_ref() {
|
||||
Some(t) => {
|
||||
if t.valid {
|
||||
self.check_cache_userpassword(t.uuid, cred).await.map(Some)
|
||||
match self.check_cache_userpassword(t.uuid, &cred).await {
|
||||
Ok(res) => Ok(ClientResponse::PamStatus(Some(res))),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Ok(Some(false))
|
||||
Ok(ClientResponse::PamStatus(Some(false)))
|
||||
}
|
||||
}
|
||||
None => Ok(None),
|
||||
None => Ok(ClientResponse::PamStatus(None)),
|
||||
}
|
||||
/*
|
||||
token
|
||||
|
@ -808,6 +870,7 @@ where
|
|||
.transpose()
|
||||
*/
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn pam_account_allowed(&self, account_id: &str) -> Result<Option<bool>, ()> {
|
||||
let token = self.get_usertoken(Id::Name(account_id.to_string())).await?;
|
||||
|
@ -837,31 +900,246 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn pam_account_authenticate_init(
|
||||
&self,
|
||||
account_id: &str,
|
||||
) -> Result<(AuthSession, PamAuthResponse), ()> {
|
||||
// Setup an auth session. If possible bring the resolver online.
|
||||
// Further steps won't attempt to bring the cache online to prevent
|
||||
// weird interactions - they should assume online/offline only for
|
||||
// the duration of their operation. A failure of connectivity during
|
||||
// an online operation will take the cache offline however.
|
||||
|
||||
let id = Id::Name(account_id.to_string());
|
||||
let (_expired, token) = self.get_cached_usertoken(&id).await?;
|
||||
let state = self.get_cachestate().await;
|
||||
|
||||
let online_at_init = if !matches!(state, CacheState::Online) {
|
||||
// Attempt a cache online.
|
||||
self.test_connection().await
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let maybe_err = if online_at_init {
|
||||
self.client
|
||||
.unix_user_online_auth_init(account_id, token.as_ref())
|
||||
.await
|
||||
} else {
|
||||
// Can the auth proceed offline?
|
||||
self.client
|
||||
.unix_user_offline_auth_init(account_id, token.as_ref())
|
||||
.await
|
||||
};
|
||||
|
||||
match maybe_err {
|
||||
Ok((next_req, cred_handler)) => {
|
||||
let auth_session = AuthSession::InProgress {
|
||||
account_id: account_id.to_string(),
|
||||
id,
|
||||
token: token.map(Box::new),
|
||||
online_at_init,
|
||||
cred_handler,
|
||||
};
|
||||
|
||||
// Now identify what credentials are needed next. The auth session tells
|
||||
// us this.
|
||||
|
||||
Ok((auth_session, next_req.into()))
|
||||
}
|
||||
Err(IdpError::NotFound) => Ok((AuthSession::Denied, PamAuthResponse::Unknown)),
|
||||
Err(IdpError::ProviderUnauthorised) | Err(IdpError::Transport) => {
|
||||
error!("transport error, moving to offline");
|
||||
// Something went wrong, mark offline.
|
||||
let time = SystemTime::now().add(Duration::from_secs(15));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
Err(())
|
||||
}
|
||||
Err(IdpError::BadRequest) => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn pam_account_authenticate_step(
|
||||
&self,
|
||||
auth_session: &mut AuthSession,
|
||||
pam_next_req: PamAuthRequest,
|
||||
) -> Result<PamAuthResponse, ()> {
|
||||
let state = self.get_cachestate().await;
|
||||
|
||||
let maybe_err = match (&mut *auth_session, state) {
|
||||
(
|
||||
&mut AuthSession::InProgress {
|
||||
ref account_id,
|
||||
id: _,
|
||||
token: _,
|
||||
online_at_init: true,
|
||||
ref mut cred_handler,
|
||||
},
|
||||
CacheState::Online,
|
||||
) => {
|
||||
let maybe_cache_action = self
|
||||
.client
|
||||
.unix_user_online_auth_step(account_id, cred_handler, pam_next_req)
|
||||
.await;
|
||||
|
||||
match maybe_cache_action {
|
||||
Ok((res, AuthCacheAction::None)) => Ok(res),
|
||||
Ok((
|
||||
AuthResult::Success { token },
|
||||
AuthCacheAction::PasswordHashUpdate { cred },
|
||||
)) => {
|
||||
// Might need a rework with the tpm code.
|
||||
self.set_cache_userpassword(token.uuid, &cred).await?;
|
||||
Ok(AuthResult::Success { token })
|
||||
}
|
||||
// I think this state is actually invalid?
|
||||
Ok((_, AuthCacheAction::PasswordHashUpdate { .. })) => {
|
||||
// Ok(res)
|
||||
error!("provider gave back illogical password hash update with a nonsuccess condition");
|
||||
Err(IdpError::BadRequest)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
/*
|
||||
(
|
||||
&mut AuthSession::InProgress {
|
||||
account_id: _,
|
||||
id: _,
|
||||
token: _,
|
||||
online_at_init: true,
|
||||
cred_handler: _,
|
||||
},
|
||||
_,
|
||||
) => {
|
||||
// Fail, we went offline.
|
||||
error!("Unable to proceed with authentication, resolver has gone offline");
|
||||
Err(IdpError::Transport)
|
||||
}
|
||||
*/
|
||||
(
|
||||
&mut AuthSession::InProgress {
|
||||
account_id: _,
|
||||
id: _,
|
||||
token: Some(ref token),
|
||||
online_at_init: _,
|
||||
ref mut cred_handler,
|
||||
},
|
||||
_,
|
||||
) => {
|
||||
// We are offline, continue. Remember, authsession should have
|
||||
// *everything you need* to proceed here!
|
||||
//
|
||||
// Rather than calling client, should this actually be self
|
||||
// contained to the resolver so that it has generic offline-paths
|
||||
// that are possible?
|
||||
match (cred_handler, pam_next_req) {
|
||||
(AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
|
||||
match self.check_cache_userpassword(token.uuid, &cred).await {
|
||||
Ok(true) => Ok(AuthResult::Success {
|
||||
token: *token.clone(),
|
||||
}),
|
||||
Ok(false) => Ok(AuthResult::Denied),
|
||||
Err(()) => {
|
||||
// We had a genuine backend error of some description.
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
self.client
|
||||
.unix_user_offline_auth_step(
|
||||
&account_id,
|
||||
cred_handler,
|
||||
pam_next_req,
|
||||
online_at_init,
|
||||
)
|
||||
.await
|
||||
*/
|
||||
}
|
||||
(&mut AuthSession::InProgress { token: None, .. }, _) => {
|
||||
// Can't do much with offline auth when there is no token ...
|
||||
warn!("Unable to proceed with offline auth, no token available");
|
||||
Err(IdpError::NotFound)
|
||||
}
|
||||
(&mut AuthSession::Success, _) | (&mut AuthSession::Denied, _) => {
|
||||
Err(IdpError::BadRequest)
|
||||
}
|
||||
};
|
||||
|
||||
match maybe_err {
|
||||
// What did the provider direct us to do next?
|
||||
Ok(AuthResult::Success { mut token }) => {
|
||||
if self.check_nxset(&token.name, token.gidnumber).await {
|
||||
// Refuse to release the token, it's in the denied set.
|
||||
self.delete_cache_usertoken(token.uuid).await?;
|
||||
*auth_session = AuthSession::Denied;
|
||||
|
||||
Ok(PamAuthResponse::Unknown)
|
||||
} else {
|
||||
debug!("provider authentication success.");
|
||||
self.set_cache_usertoken(&mut token).await?;
|
||||
*auth_session = AuthSession::Success;
|
||||
|
||||
Ok(PamAuthResponse::Success)
|
||||
}
|
||||
}
|
||||
Ok(AuthResult::Denied) => {
|
||||
*auth_session = AuthSession::Denied;
|
||||
Ok(PamAuthResponse::Denied)
|
||||
}
|
||||
Ok(AuthResult::Next(req)) => Ok(req.into()),
|
||||
Err(IdpError::NotFound) => Ok(PamAuthResponse::Unknown),
|
||||
Err(IdpError::ProviderUnauthorised) | Err(IdpError::Transport) => {
|
||||
error!("transport error, moving to offline");
|
||||
// Something went wrong, mark offline.
|
||||
let time = SystemTime::now().add(Duration::from_secs(15));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
Err(())
|
||||
}
|
||||
Err(IdpError::BadRequest) => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
// Can this be cfg debug/test?
|
||||
pub async fn pam_account_authenticate(
|
||||
&self,
|
||||
account_id: &str,
|
||||
cred: &str,
|
||||
password: &str,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
let id = Id::Name(account_id.to_string());
|
||||
|
||||
let state = self.get_cachestate().await;
|
||||
let (_expired, token) = self.get_cached_usertoken(&id).await?;
|
||||
|
||||
match state {
|
||||
CacheState::Online => self.online_account_authenticate(&token, &id, cred).await,
|
||||
CacheState::OfflineNextCheck(_time) => {
|
||||
// Always attempt to go online to attempt the authentication.
|
||||
if self.test_connection().await {
|
||||
// Brought ourselves online, lets check.
|
||||
self.online_account_authenticate(&token, &id, cred).await
|
||||
} else {
|
||||
// We are offline, check from the cache if possible.
|
||||
self.offline_account_authenticate(&token, cred).await
|
||||
}
|
||||
let mut auth_session = match self.pam_account_authenticate_init(account_id).await? {
|
||||
(auth_session, PamAuthResponse::Password) => {
|
||||
// Can continue!
|
||||
auth_session
|
||||
}
|
||||
(_, PamAuthResponse::Unknown) => return Ok(None),
|
||||
(_, PamAuthResponse::Denied) => return Ok(Some(false)),
|
||||
(_, PamAuthResponse::Success) => {
|
||||
// Should never get here "off the rip".
|
||||
debug_assert!(false);
|
||||
return Ok(Some(true));
|
||||
}
|
||||
};
|
||||
|
||||
// Now we can make the next step.
|
||||
let pam_next_req = PamAuthRequest::Password {
|
||||
cred: password.to_string(),
|
||||
};
|
||||
match self
|
||||
.pam_account_authenticate_step(&mut auth_session, pam_next_req)
|
||||
.await?
|
||||
{
|
||||
PamAuthResponse::Success => Ok(Some(true)),
|
||||
PamAuthResponse::Denied => Ok(Some(false)),
|
||||
_ => {
|
||||
// We are offline, check from the cache if possible.
|
||||
self.offline_account_authenticate(&token, cred).await
|
||||
// Should not be able to get here, if the user was unknown they should
|
||||
// be out. If it wants more mechanisms, we can't proceed here.
|
||||
// debug_assert!(false);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,10 @@ use clap::Parser;
|
|||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
use kanidm_unix_common::unix_proto::{
|
||||
ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse,
|
||||
};
|
||||
// use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
include!("./opt/tool.rs");
|
||||
|
@ -51,45 +54,63 @@ async fn main() -> ExitCode {
|
|||
} => {
|
||||
debug!("Starting PAM auth tester tool ...");
|
||||
|
||||
let Ok(cfg) = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
else {
|
||||
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
|
||||
return ExitCode::FAILURE
|
||||
};
|
||||
|
||||
let password = match rpassword::prompt_password("Enter Unix password: ") {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!("Problem getting input password: {}", e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
let Ok(cfg) =
|
||||
KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
else {
|
||||
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
|
||||
return ExitCode::FAILURE;
|
||||
};
|
||||
|
||||
let req = ClientRequest::PamAuthenticate(account_id.clone(), password);
|
||||
let mut req = ClientRequest::PamAuthenticateInit(account_id.clone());
|
||||
loop {
|
||||
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => {
|
||||
// ClientResponse::PamStatus(Some(true)) => {
|
||||
println!("auth success!");
|
||||
break;
|
||||
}
|
||||
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Denied) => {
|
||||
// ClientResponse::PamStatus(Some(false)) => {
|
||||
println!("auth failed!");
|
||||
break;
|
||||
}
|
||||
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Unknown) => {
|
||||
// ClientResponse::PamStatus(None) => {
|
||||
println!("auth user unknown");
|
||||
break;
|
||||
}
|
||||
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => {
|
||||
// Prompt for and get the password
|
||||
let cred = match rpassword::prompt_password("Enter Unix password: ") {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!("Problem getting input: {}", e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
// Setup the req for the next loop.
|
||||
req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password {
|
||||
cred,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sereq = ClientRequest::PamAccountAllowed(account_id);
|
||||
|
||||
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
println!("auth success!");
|
||||
}
|
||||
ClientResponse::PamStatus(Some(false)) => {
|
||||
println!("auth failed!");
|
||||
}
|
||||
ClientResponse::PamStatus(None) => {
|
||||
println!("auth user unknown");
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
};
|
||||
|
||||
match call_daemon(cfg.sock_path.as_str(), sereq, cfg.unix_sock_timeout).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
|
|
|
@ -16,6 +16,29 @@ pub struct NssGroup {
|
|||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum PamAuthResponse {
|
||||
Unknown,
|
||||
Success,
|
||||
Denied,
|
||||
Password,
|
||||
/*
|
||||
MFACode {
|
||||
},
|
||||
*/
|
||||
// CTAP2
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum PamAuthRequest {
|
||||
Password { cred: String },
|
||||
/*
|
||||
MFACode {
|
||||
cred: Option<PamCred>
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ClientRequest {
|
||||
SshKey(String),
|
||||
|
@ -25,7 +48,8 @@ pub enum ClientRequest {
|
|||
NssGroups,
|
||||
NssGroupByGid(u32),
|
||||
NssGroupByName(String),
|
||||
PamAuthenticate(String, String),
|
||||
PamAuthenticateInit(String),
|
||||
PamAuthenticateStep(PamAuthRequest),
|
||||
PamAccountAllowed(String),
|
||||
PamAccountBeginSession(String),
|
||||
InvalidateCache,
|
||||
|
@ -40,11 +64,20 @@ pub enum ClientResponse {
|
|||
NssAccount(Option<NssUser>),
|
||||
NssGroups(Vec<NssGroup>),
|
||||
NssGroup(Option<NssGroup>),
|
||||
|
||||
PamStatus(Option<bool>),
|
||||
PamAuthenticateStepResponse(PamAuthResponse),
|
||||
|
||||
Ok,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl From<PamAuthResponse> for ClientResponse {
|
||||
fn from(par: PamAuthResponse) -> Self {
|
||||
ClientResponse::PamAuthenticateStepResponse(par)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct HomeDirectoryInfo {
|
||||
pub gid: u32,
|
||||
|
|
Loading…
Reference in a new issue