mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
kanidm unixd mfa capabilities (#2672)
Improve the support for the resolver to support MFA options with pam. This enables async task spawning and cancelation via the resolver backend as well. Co-authored-by: David Mulder <dmulder@samba.org>
This commit is contained in:
parent
03ce2a0c32
commit
c09daa4643
|
@ -3181,7 +3181,12 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
|
|||
.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)
|
||||
|
|
|
@ -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, PamResultCode> {
|
||||
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,21 +276,7 @@ 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;
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
@ -284,7 +305,7 @@ impl PamHooks for PamKanidm {
|
|||
timeout = cfg.unix_sock_timeout;
|
||||
req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred });
|
||||
continue;
|
||||
}
|
||||
},
|
||||
ClientResponse::PamAuthenticateStepResponse(
|
||||
PamAuthResponse::DeviceAuthorizationGrant { data },
|
||||
) => {
|
||||
|
@ -308,18 +329,76 @@ impl PamHooks for PamKanidm {
|
|||
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");
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
error!(err = ?r, "PAM_IGNORE, unexpected resolver response");
|
||||
return PamResultCode::PAM_IGNORE;
|
||||
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) => {
|
||||
error!(?err, "PAM_IGNORE");
|
||||
return PamResultCode::PAM_IGNORE;
|
||||
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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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<String>,
|
||||
},
|
||||
}
|
||||
|
||||
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<PamAuthResponse> 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<D: KeyStoreTxn + Send>(
|
||||
|
@ -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(
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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<Box<UserToken>>,
|
||||
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<Option<bool>, ()> {
|
||||
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) => {
|
||||
|
|
|
@ -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<PamCred>
|
||||
}
|
||||
*/
|
||||
DeviceAuthorizationGrant { data: DeviceAuthorizationResponse },
|
||||
MFACode { cred: String },
|
||||
MFAPoll,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
Loading…
Reference in a new issue