diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index a0621bb12..0f227cc6d 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -3181,7 +3181,12 @@ pub(crate) fn route_setup(state: ServerState) -> Router { .route("/v1/group/:id/_unix/_token", get(group_id_unix_token_get)) .route("/v1/group/:id/_unix", post(group_id_unix_post)) .route("/v1/group", get(group_get).post(group_post)) - .route("/v1/group/:id", get(group_id_get).patch(group_id_patch).delete(group_id_delete)) + .route( + "/v1/group/:id", + get(group_id_get) + .patch(group_id_patch) + .delete(group_id_delete), + ) .route( "/v1/group/:id/_attr/:attr", delete(group_id_attr_delete) diff --git a/unix_integration/pam_kanidm/src/pam/mod.rs b/unix_integration/pam_kanidm/src/pam/mod.rs index 1a186e0b3..8dc552598 100755 --- a/unix_integration/pam_kanidm/src/pam/mod.rs +++ b/unix_integration/pam_kanidm/src/pam/mod.rs @@ -53,6 +53,9 @@ use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt; use tracing_subscriber::prelude::*; +use std::thread; +use std::time::Duration; + pub fn get_cfg() -> Result { KanidmUnixdConfig::new() .read_options_from_optional_config(DEFAULT_CONFIG_PATH) @@ -106,6 +109,38 @@ pub struct PamKanidm; pam_hooks!(PamKanidm); +macro_rules! match_sm_auth_client_response { + ($expr:expr, $opts:ident, $($pat:pat => $result:expr),*) => { + match $expr { + Ok(r) => match r { + $($pat => $result),* + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => { + return PamResultCode::PAM_SUCCESS; + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Denied) => { + return PamResultCode::PAM_AUTH_ERR; + } + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Unknown) => { + if $opts.ignore_unknown_user { + return PamResultCode::PAM_IGNORE; + } else { + return PamResultCode::PAM_USER_UNKNOWN; + } + } + _ => { + // unexpected response. + error!(err = ?r, "PAM_IGNORE, unexpected resolver response"); + return PamResultCode::PAM_IGNORE; + } + }, + Err(err) => { + error!(?err, "PAM_IGNORE"); + return PamResultCode::PAM_IGNORE; + } + } + } +} + impl PamHooks for PamKanidm { fn acct_mgmt(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { let opts = match Options::try_from(&args) { @@ -241,85 +276,129 @@ impl PamHooks for PamKanidm { let mut req = ClientRequest::PamAuthenticateInit(account_id); loop { - match daemon_client.call_and_wait(&req, timeout) { - 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; + match_sm_auth_client_response!(daemon_client.call_and_wait(&req, timeout), opts, + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => { + let mut consume_authtok = None; + // Swap the authtok out with a None, so it can only be consumed once. + // If it's already been swapped, we are just swapping two null pointers + // here effectively. + std::mem::swap(&mut authtok, &mut consume_authtok); + let cred = if let Some(cred) = consume_authtok { + cred + } else { + match conv.send(PAM_PROMPT_ECHO_OFF, "Password: ") { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no password"); + return PamResultCode::PAM_CRED_INSUFFICIENT; } - } - }; - - // Now setup the request for the next loop. - timeout = cfg.unix_sock_timeout; - req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred }); - continue; - } - ClientResponse::PamAuthenticateStepResponse( - PamAuthResponse::DeviceAuthorizationGrant { data }, - ) => { - let msg = match &data.message { - Some(msg) => msg.clone(), - None => format!("Using a browser on another device, visit:\n{}\nAnd enter the code:\n{}", - data.verification_uri, data.user_code) - }; - match conv.send(PAM_TEXT_INFO, &msg) { - Ok(_) => {} + }, Err(err) => { - if opts.debug { - println!("Message prompt failed"); - } + debug!("unable to get password"); return err; } } + }; - timeout = u64::from(data.expires_in); - req = ClientRequest::PamAuthenticateStep( - PamAuthRequest::DeviceAuthorizationGrant { data }, - ); - continue; - } - _ => { - // unexpected response. - error!(err = ?r, "PAM_IGNORE, unexpected resolver response"); - return PamResultCode::PAM_IGNORE; - } + // Now setup the request for the next loop. + timeout = cfg.unix_sock_timeout; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred }); + continue; }, - Err(err) => { - error!(?err, "PAM_IGNORE"); - return PamResultCode::PAM_IGNORE; + ClientResponse::PamAuthenticateStepResponse( + PamAuthResponse::DeviceAuthorizationGrant { data }, + ) => { + let msg = match &data.message { + Some(msg) => msg.clone(), + None => format!("Using a browser on another device, visit:\n{}\nAnd enter the code:\n{}", + data.verification_uri, data.user_code) + }; + match conv.send(PAM_TEXT_INFO, &msg) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + + timeout = u64::from(data.expires_in); + req = ClientRequest::PamAuthenticateStep( + PamAuthRequest::DeviceAuthorizationGrant { data }, + ); + continue; + }, + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFACode { + msg, + }) => { + match conv.send(PAM_TEXT_INFO, &msg) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + let cred = match conv.send(PAM_PROMPT_ECHO_OFF, "Code") { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no mfa code"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get mfa code"); + return err; + } + }; + + // Now setup the request for the next loop. + timeout = cfg.unix_sock_timeout; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFACode { + cred, + }); + continue; + }, + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPoll { + msg, + polling_interval, + }) => { + match conv.send(PAM_TEXT_INFO, &msg) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + + loop { + thread::sleep(Duration::from_secs(polling_interval.into())); + timeout = cfg.unix_sock_timeout; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFAPoll); + + // Counter intuitive, but we don't need a max poll attempts here because + // if the resolver goes away, then this will error on the sock and + // will shutdown. This allows the resolver to dynamically extend the + // timeout if needed, and removes logic from the front end. + match_sm_auth_client_response!( + daemon_client.call_and_wait(&req, timeout), opts, + ClientResponse::PamAuthenticateStepResponse( + PamAuthResponse::MFAPollWait, + ) => { + // Continue polling if the daemon says to wait + continue; + } + ); + + } } - } + ); } // while true, continue calling PamAuthenticateStep until we get a decision. } diff --git a/unix_integration/src/daemon.rs b/unix_integration/src/daemon.rs index f85bdce5a..b544e3bd3 100644 --- a/unix_integration/src/daemon.rs +++ b/unix_integration/src/daemon.rs @@ -202,6 +202,10 @@ async fn handle_client( let mut reqs = Framed::new(sock, ClientCodec); let mut pam_auth_session_state = None; + // Setup a broadcast channel so that if we have an unexpected disconnection, we can + // tell consumers to stop work. + let (shutdown_tx, _shutdown_rx) = broadcast::channel(1); + trace!("Waiting for requests ..."); while let Some(Ok(req)) = reqs.next().await { let resp = match req { @@ -295,7 +299,10 @@ async fn handle_client( } None => { match cachelayer - .pam_account_authenticate_init(account_id.as_str()) + .pam_account_authenticate_init( + account_id.as_str(), + shutdown_tx.subscribe(), + ) .await { Ok((auth_session, pam_auth_response)) => { @@ -407,6 +414,14 @@ async fn handle_client( debug!("flushed response!"); } + // Signal any tasks that they need to stop. + if let Err(shutdown_err) = shutdown_tx.send(()) { + warn!( + ?shutdown_err, + "Unable to signal tasks to stop, they will naturally timeout instead." + ) + } + // Disconnect them debug!("Disconnecting client ..."); Ok(()) diff --git a/unix_integration/src/idprovider/interface.rs b/unix_integration/src/idprovider/interface.rs index 100f6fb97..22a79e9dc 100644 --- a/unix_integration/src/idprovider/interface.rs +++ b/unix_integration/src/idprovider/interface.rs @@ -2,6 +2,7 @@ use crate::db::KeyStoreTxn; use crate::unix_proto::{DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast; use uuid::Uuid; pub use kanidm_hsm_crypto as tpm; @@ -63,11 +64,35 @@ pub struct UserToken { pub enum AuthCredHandler { Password, DeviceAuthorizationGrant, + /// Additional data required by the provider to complete the + /// authentication, but not required by PAM + /// + /// Sadly due to how this is passed around we can't make this a + /// generic associated type, else it would have to leak up to the + /// daemon. + /// + /// ⚠️ TODO: Optimally this should actually be a tokio oneshot receiver + /// with the decision from a task that is spawned. + MFA { + data: Vec, + }, } pub enum AuthRequest { Password, - DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, + DeviceAuthorizationGrant { + data: DeviceAuthorizationResponse, + }, + MFACode { + msg: String, + }, + MFAPoll { + /// Message to display to the user. + msg: String, + /// Interval in seconds between poll attemts. + polling_interval: u32, + }, + MFAPollWait, } #[allow(clippy::from_over_into)] @@ -78,6 +103,15 @@ impl Into for AuthRequest { AuthRequest::DeviceAuthorizationGrant { data } => { PamAuthResponse::DeviceAuthorizationGrant { data } } + AuthRequest::MFACode { msg } => PamAuthResponse::MFACode { msg }, + AuthRequest::MFAPoll { + msg, + polling_interval, + } => PamAuthResponse::MFAPoll { + msg, + polling_interval, + }, + AuthRequest::MFAPollWait => PamAuthResponse::MFAPollWait, } } } @@ -133,6 +167,7 @@ pub trait IdProvider { _token: Option<&UserToken>, _tpm: &mut tpm::BoxedDynTpm, _machine_key: &tpm::MachineKey, + _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthRequest, AuthCredHandler), IdpError>; async fn unix_user_online_auth_step( @@ -143,6 +178,7 @@ pub trait IdProvider { _keystore: &mut D, _tpm: &mut tpm::BoxedDynTpm, _machine_key: &tpm::MachineKey, + _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthResult, AuthCacheAction), IdpError>; async fn unix_user_offline_auth_init( diff --git a/unix_integration/src/idprovider/kanidm.rs b/unix_integration/src/idprovider/kanidm.rs index a23ea7694..4ed0aedf5 100644 --- a/unix_integration/src/idprovider/kanidm.rs +++ b/unix_integration/src/idprovider/kanidm.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use kanidm_client::{ClientError, KanidmClient, StatusCode}; use kanidm_proto::internal::OperationError; use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; -use tokio::sync::RwLock; +use tokio::sync::{broadcast, RwLock}; use super::interface::{ // KeyStore, @@ -197,6 +197,7 @@ impl IdProvider for KanidmProvider { _token: Option<&UserToken>, _tpm: &mut tpm::BoxedDynTpm, _machine_key: &tpm::MachineKey, + _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthRequest, AuthCredHandler), IdpError> { // Not sure that I need to do much here? Ok((AuthRequest::Password, AuthCredHandler::Password)) @@ -210,6 +211,7 @@ impl IdProvider for KanidmProvider { _keystore: &mut D, _tpm: &mut tpm::BoxedDynTpm, _machine_key: &tpm::MachineKey, + _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthResult, AuthCacheAction), IdpError> { match (cred_handler, pam_next_req) { (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { diff --git a/unix_integration/src/resolver.rs b/unix_integration/src/resolver.rs index 1bb56699f..271087014 100644 --- a/unix_integration/src/resolver.rs +++ b/unix_integration/src/resolver.rs @@ -28,6 +28,8 @@ use crate::unix_proto::{HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, Pa use kanidm_hsm_crypto::{BoxedDynTpm, HmacKey, MachineKey, Tpm}; +use tokio::sync::broadcast; + const NXCACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(128) }; #[derive(Debug, Clone)] @@ -44,9 +46,12 @@ pub enum AuthSession { id: Id, token: Option>, online_at_init: bool, - // cred_type: AuthCredType, - // next_cred: AuthNextCred, cred_handler: AuthCredHandler, + /// Some authentication operations may need to spawn background tasks. These tasks need + /// to know when to stop as the caller has disconnected. This reciever allows that, so + /// that tasks which .resubscribe() to this channel can then select! on it and be notified + /// when they need to stop. + shutdown_rx: broadcast::Receiver<()>, }, Success, Denied, @@ -867,6 +872,7 @@ where pub async fn pam_account_authenticate_init( &self, account_id: &str, + shutdown_rx: broadcast::Receiver<()>, ) -> 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 @@ -893,6 +899,7 @@ where token.as_ref(), hsm_lock.deref_mut(), &self.machine_key, + &shutdown_rx, ) .await } else { @@ -910,6 +917,7 @@ where token: token.map(Box::new), online_at_init, cred_handler, + shutdown_rx, }; // Now identify what credentials are needed next. The auth session tells @@ -945,6 +953,7 @@ where token: _, online_at_init: true, ref mut cred_handler, + ref shutdown_rx, }, CacheState::Online, ) => { @@ -960,6 +969,7 @@ where &mut dbtxn, hsm_lock.deref_mut(), &self.machine_key, + shutdown_rx, ) .await; @@ -1008,6 +1018,8 @@ where token: Some(ref token), online_at_init: _, ref mut cred_handler, + // Only need in online auth. + shutdown_rx: _, }, _, ) => { @@ -1038,6 +1050,10 @@ where // AuthCredHandler::DeviceAuthorizationGrant is invalid for offline auth return Err(()); } + (AuthCredHandler::MFA { .. }, _) => { + // AuthCredHandler::MFA is invalid for offline auth + return Err(()); + } } /* @@ -1102,7 +1118,12 @@ where account_id: &str, password: &str, ) -> Result, ()> { - let mut auth_session = match self.pam_account_authenticate_init(account_id).await? { + let (_shutdown_tx, shutdown_rx) = broadcast::channel(1); + + let mut auth_session = match self + .pam_account_authenticate_init(account_id, shutdown_rx) + .await? + { (auth_session, PamAuthResponse::Password) => { // Can continue! auth_session @@ -1111,6 +1132,18 @@ where // Can continue! auth_session } + (auth_session, PamAuthResponse::MFACode { .. }) => { + // Can continue! + auth_session + } + (auth_session, PamAuthResponse::MFAPoll { .. }) => { + // Can continue! + auth_session + } + (auth_session, PamAuthResponse::MFAPollWait) => { + // Can continue! + auth_session + } (_, PamAuthResponse::Unknown) => return Ok(None), (_, PamAuthResponse::Denied) => return Ok(Some(false)), (_, PamAuthResponse::Success) => { diff --git a/unix_integration/src/unix_proto.rs b/unix_integration/src/unix_proto.rs index 3e1ab6191..d0fbf3c76 100644 --- a/unix_integration/src/unix_proto.rs +++ b/unix_integration/src/unix_proto.rs @@ -36,22 +36,30 @@ pub enum PamAuthResponse { Success, Denied, Password, - DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, - /* - MFACode { + DeviceAuthorizationGrant { + data: DeviceAuthorizationResponse, }, - */ + /// PAM must prompt for an authentication code + MFACode { + msg: String, + }, + /// PAM will poll for an external response + MFAPoll { + /// Initial message to display as the polling begins. + msg: String, + /// Seconds between polling attempts. + polling_interval: u32, + }, + MFAPollWait, // CTAP2 } #[derive(Serialize, Deserialize, Debug)] pub enum PamAuthRequest { Password { cred: String }, - DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, /* - MFACode { - cred: Option - } - */ + DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, + MFACode { cred: String }, + MFAPoll, } #[derive(Serialize, Deserialize, Debug)]