pam multistep auth state machine ()

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:
Firstyear 2023-08-28 09:27:29 +10:00 committed by GitHub
parent 2194994ada
commit da56738dea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1012 additions and 427 deletions

2
Cargo.lock generated
View file

@ -3366,6 +3366,8 @@ dependencies = [
"kanidm_unix_int",
"libc",
"pkg-config",
"tracing",
"tracing-subscriber",
]
[[package]]

View file

@ -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

View file

@ -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)?;

View file

@ -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

View file

@ -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);

View file

@ -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*

View file

@ -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);

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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 }

View file

@ -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;

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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");

View file

@ -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>;
}

View file

@ -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

View file

@ -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;

View 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 {}

View file

@ -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)
}
}
}

View file

@ -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)) => {

View file

@ -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,