use super::interface::{ tpm::{self, HmacKey, Tpm}, AuthCredHandler, AuthRequest, AuthResult, GroupToken, GroupTokenState, Id, IdProvider, IdpError, ProviderOrigin, UserToken, UserTokenState, }; use crate::db::KeyStoreTxn; use async_trait::async_trait; use hashbrown::HashMap; use kanidm_client::{ClientError, KanidmClient, StatusCode}; use kanidm_lib_crypto::CryptoPolicy; use kanidm_lib_crypto::DbPasswordV1; use kanidm_lib_crypto::Password; use kanidm_proto::internal::OperationError; use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; use kanidm_unix_common::unix_config::{GroupMap, KanidmConfig}; use kanidm_unix_common::unix_proto::PamAuthRequest; use std::collections::BTreeSet; use std::time::{Duration, SystemTime}; use tokio::sync::{broadcast, Mutex}; const KANIDM_HMAC_KEY: &str = "kanidm-hmac-key"; const KANIDM_PWV1_KEY: &str = "kanidm-pw-v1"; // If the provider is offline, we need to backoff and wait a bit. const OFFLINE_NEXT_CHECK: Duration = Duration::from_secs(60); #[derive(Debug, Clone)] enum CacheState { Online, Offline, OfflineNextCheck(SystemTime), } struct KanidmProviderInternal { state: CacheState, client: KanidmClient, hmac_key: HmacKey, crypto_policy: CryptoPolicy, pam_allow_groups: BTreeSet<String>, } pub struct KanidmProvider { inner: Mutex<KanidmProviderInternal>, // Because this value doesn't change, to support fast // lookup we store the extension map here. map_group: HashMap<String, Id>, } impl KanidmProvider { pub fn new( client: KanidmClient, config: &KanidmConfig, now: SystemTime, keystore: &mut KeyStoreTxn, tpm: &mut tpm::BoxedDynTpm, machine_key: &tpm::MachineKey, ) -> Result<Self, IdpError> { // FUTURE: Randomised jitter on next check at startup. // Initially retrieve our HMAC key. let loadable_hmac_key: Option<tpm::LoadableHmacKey> = keystore .get_tagged_hsm_key(KANIDM_HMAC_KEY) .map_err(|ks_err| { error!(?ks_err); IdpError::KeyStore })?; let loadable_hmac_key = if let Some(loadable_hmac_key) = loadable_hmac_key { loadable_hmac_key } else { let loadable_hmac_key = tpm.hmac_key_create(machine_key).map_err(|tpm_err| { error!(?tpm_err); IdpError::Tpm })?; keystore .insert_tagged_hsm_key(KANIDM_HMAC_KEY, &loadable_hmac_key) .map_err(|ks_err| { error!(?ks_err); IdpError::KeyStore })?; loadable_hmac_key }; let hmac_key = tpm .hmac_key_load(machine_key, &loadable_hmac_key) .map_err(|tpm_err| { error!(?tpm_err); IdpError::Tpm })?; let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250)); let pam_allow_groups = config.pam_allowed_login_groups.iter().cloned().collect(); let map_group = config .map_group .iter() .cloned() .map(|GroupMap { local, with }| (local, Id::Name(with))) .collect(); Ok(KanidmProvider { inner: Mutex::new(KanidmProviderInternal { state: CacheState::OfflineNextCheck(now), client, hmac_key, crypto_policy, pam_allow_groups, }), map_group, }) } } impl From<UnixUserToken> for UserToken { fn from(value: UnixUserToken) -> UserToken { let UnixUserToken { name, spn, displayname, gidnumber, uuid, shell, groups, sshkeys, valid, } = value; let sshkeys = sshkeys.iter().map(|s| s.to_string()).collect(); let groups = groups.into_iter().map(GroupToken::from).collect(); UserToken { provider: ProviderOrigin::Kanidm, name, spn, uuid, gidnumber, displayname, shell, groups, sshkeys, valid, extra_keys: Default::default(), } } } impl From<UnixGroupToken> for GroupToken { fn from(value: UnixGroupToken) -> GroupToken { let UnixGroupToken { name, spn, uuid, gidnumber, } = value; GroupToken { provider: ProviderOrigin::Kanidm, name, spn, uuid, gidnumber, extra_keys: Default::default(), } } } impl UserToken { pub fn kanidm_update_cached_password( &mut self, crypto_policy: &CryptoPolicy, cred: &str, tpm: &mut tpm::BoxedDynTpm, hmac_key: &HmacKey, ) { let pw = match Password::new_argon2id_hsm(crypto_policy, cred, tpm, hmac_key) { Ok(pw) => pw, Err(reason) => { // Clear cached pw. self.extra_keys.remove(KANIDM_PWV1_KEY); warn!( ?reason, "unable to apply kdf to password, clearing cached password." ); return; } }; let pw_value = match serde_json::to_value(pw.to_dbpasswordv1()) { Ok(pw) => pw, Err(reason) => { // Clear cached pw. self.extra_keys.remove(KANIDM_PWV1_KEY); warn!( ?reason, "unable to serialise credential, clearing cached password." ); return; } }; self.extra_keys.insert(KANIDM_PWV1_KEY.into(), pw_value); debug!(spn = %self.spn, "Updated cached pw"); } pub fn kanidm_check_cached_password( &self, cred: &str, tpm: &mut tpm::BoxedDynTpm, hmac_key: &HmacKey, ) -> bool { let pw_value = match self.extra_keys.get(KANIDM_PWV1_KEY) { Some(pw_value) => pw_value, None => { debug!(spn = %self.spn, "no cached pw available"); return false; } }; let dbpw = match serde_json::from_value::<DbPasswordV1>(pw_value.clone()) { Ok(dbpw) => dbpw, Err(reason) => { warn!(spn = %self.spn, ?reason, "unable to deserialise credential"); return false; } }; let pw = match Password::try_from(dbpw) { Ok(pw) => pw, Err(reason) => { warn!(spn = %self.spn, ?reason, "unable to process credential"); return false; } }; pw.verify_ctx(cred, Some((tpm, hmac_key))) .unwrap_or_default() } } impl KanidmProviderInternal { #[instrument(level = "debug", skip_all)] async fn check_online(&mut self, tpm: &mut tpm::BoxedDynTpm, now: SystemTime) -> bool { match self.state { // Proceed CacheState::Online => true, CacheState::OfflineNextCheck(at_time) if now >= at_time => { // Attempt online. If fails, return token. self.attempt_online(tpm, now).await } CacheState::OfflineNextCheck(_) | CacheState::Offline => false, } } #[instrument(level = "debug", skip_all)] async fn attempt_online(&mut self, _tpm: &mut tpm::BoxedDynTpm, now: SystemTime) -> bool { let mut max_attempts = 3; while max_attempts > 0 { max_attempts -= 1; match self.client.auth_anonymous().await { Ok(_uat) => { debug!("provider is now online"); self.state = CacheState::Online; return true; } Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => { error!(?reason, ?opid, "Provider authentication returned unauthorized, {max_attempts} attempts remaining."); // Provider needs to re-auth ASAP. We set this state value here // so that if we exceed max attempts, the next caller knows to check // online immediately. self.state = CacheState::OfflineNextCheck(now); // attempt again immediately!!!! continue; } Err(err) => { error!(?err, "Provider online failed"); self.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); return false; } } } warn!("Exceeded maximum number of attempts to bring provider online"); return false; } } #[async_trait] impl IdProvider for KanidmProvider { fn origin(&self) -> ProviderOrigin { ProviderOrigin::Kanidm } async fn attempt_online(&self, tpm: &mut tpm::BoxedDynTpm, now: SystemTime) -> bool { let mut inner = self.inner.lock().await; inner.check_online(tpm, now).await } async fn mark_next_check(&self, now: SystemTime) { let mut inner = self.inner.lock().await; inner.state = CacheState::OfflineNextCheck(now); } fn has_map_group(&self, local: &str) -> Option<&Id> { self.map_group.get(local) } async fn mark_offline(&self) { let mut inner = self.inner.lock().await; inner.state = CacheState::Offline; } async fn unix_user_get( &self, id: &Id, token: Option<&UserToken>, tpm: &mut tpm::BoxedDynTpm, now: SystemTime, ) -> Result<UserTokenState, IdpError> { let mut inner = self.inner.lock().await; if !inner.check_online(tpm, now).await { // We are offline, return that we should use a cached token. return Ok(UserTokenState::UseCached); } // We are ONLINE, do the get. match inner .client .idm_account_unix_token_get(id.to_string().as_str()) .await { Ok(tok) => { let mut ut = UserToken::from(tok); if let Some(previous_token) = token { ut.extra_keys = previous_token.extra_keys.clone(); } Ok(UserTokenState::Update(ut)) } // Offline? Err(ClientError::Transport(err)) => { error!(?err, "transport error"); inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); Ok(UserTokenState::UseCached) } // Provider session error, need to re-auth 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 ), }; // Provider needs to re-auth ASAP inner.state = CacheState::OfflineNextCheck(now); Ok(UserTokenState::UseCached) } // 404 / Removed. Err(ClientError::Http( StatusCode::BAD_REQUEST, Some(OperationError::NoMatchingEntries), opid, )) | Err(ClientError::Http( StatusCode::NOT_FOUND, Some(OperationError::NoMatchingEntries), opid, )) | Err(ClientError::Http( StatusCode::NOT_FOUND, Some(OperationError::MissingAttribute(_)), opid, )) | Err(ClientError::Http( StatusCode::NOT_FOUND, Some(OperationError::MissingClass(_)), opid, )) | Err(ClientError::Http( StatusCode::BAD_REQUEST, Some(OperationError::InvalidAccountState(_)), opid, )) => { debug!( ?opid, "entry has been removed or is no longer a valid posix account" ); Ok(UserTokenState::NotFound) } // Something is really wrong? We did get a response though, so we are still online. Err(err) => { error!(?err, "client error"); Err(IdpError::BadRequest) } } } async fn unix_user_online_auth_init( &self, _account_id: &str, _token: &UserToken, _tpm: &mut tpm::BoxedDynTpm, _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthRequest, AuthCredHandler), IdpError> { // Not sure that I need to do much here? Ok((AuthRequest::Password, AuthCredHandler::Password)) } async fn unix_unknown_user_online_auth_init( &self, _account_id: &str, _tpm: &mut tpm::BoxedDynTpm, _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<Option<(AuthRequest, AuthCredHandler)>, IdpError> { // We do not support unknown user auth. Ok(None) } async fn unix_user_online_auth_step( &self, account_id: &str, cred_handler: &mut AuthCredHandler, pam_next_req: PamAuthRequest, tpm: &mut tpm::BoxedDynTpm, _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<AuthResult, IdpError> { match (cred_handler, pam_next_req) { (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { let inner = self.inner.lock().await; let auth_result = inner .client .idm_account_unix_cred_verify(account_id, &cred) .await; trace!(?auth_result); match auth_result { Ok(Some(n_tok)) => { let mut token = UserToken::from(n_tok); token.kanidm_update_cached_password( &inner.crypto_policy, cred.as_str(), tpm, &inner.hmac_key, ); Ok(AuthResult::Success { token }) } Ok(None) => { // TODO: i'm not a huge fan of this rn, but currently the way we handle // an expired account is we return Ok(None). // // We can't tell the difference between expired and incorrect password. // So in these cases we have to clear the cached password. :( // // In future once we have domain join, we should be getting the user token // at the start of the auth and checking for account validity instead. Ok(AuthResult::Denied) } Err(ClientError::Transport(err)) => { error!(?err, "A client transport error occured."); 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::NOT_FOUND, Some(OperationError::MissingAttribute(_)), opid, )) | Err(ClientError::Http( StatusCode::NOT_FOUND, Some(OperationError::MissingClass(_)), 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) } } } ( AuthCredHandler::DeviceAuthorizationGrant, PamAuthRequest::DeviceAuthorizationGrant { .. }, ) => { error!("DeviceAuthorizationGrant not implemented!"); Err(IdpError::BadRequest) } _ => { error!("invalid authentication request state"); Err(IdpError::BadRequest) } } } async fn unix_user_offline_auth_init( &self, _token: &UserToken, ) -> Result<(AuthRequest, AuthCredHandler), IdpError> { Ok((AuthRequest::Password, AuthCredHandler::Password)) } async fn unix_user_offline_auth_step( &self, token: &UserToken, cred_handler: &mut AuthCredHandler, pam_next_req: PamAuthRequest, tpm: &mut tpm::BoxedDynTpm, ) -> Result<AuthResult, IdpError> { match (cred_handler, pam_next_req) { (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { let inner = self.inner.lock().await; if token.kanidm_check_cached_password(cred.as_str(), tpm, &inner.hmac_key) { // TODO: We can update the token here and then do lockouts. Ok(AuthResult::Success { token: token.clone(), }) } else { Ok(AuthResult::Denied) } } ( AuthCredHandler::DeviceAuthorizationGrant, PamAuthRequest::DeviceAuthorizationGrant { .. }, ) => { error!("DeviceAuthorizationGrant not implemented!"); Err(IdpError::BadRequest) } _ => { error!("invalid authentication request state"); Err(IdpError::BadRequest) } } } async fn unix_group_get( &self, id: &Id, tpm: &mut tpm::BoxedDynTpm, now: SystemTime, ) -> Result<GroupTokenState, IdpError> { let mut inner = self.inner.lock().await; if !inner.check_online(tpm, now).await { // We are offline, return that we should use a cached token. return Ok(GroupTokenState::UseCached); } match inner .client .idm_group_unix_token_get(id.to_string().as_str()) .await { Ok(tok) => { let gt = GroupToken::from(tok); Ok(GroupTokenState::Update(gt)) } // Offline? Err(ClientError::Transport(err)) => { error!(?err, "transport error"); inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); Ok(GroupTokenState::UseCached) } // Provider session error, need to re-auth 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 ), }; inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); Ok(GroupTokenState::UseCached) } // 404 / Removed. Err(ClientError::Http( StatusCode::BAD_REQUEST, Some(OperationError::NoMatchingEntries), opid, )) | Err(ClientError::Http( StatusCode::NOT_FOUND, Some(OperationError::NoMatchingEntries), opid, )) | Err(ClientError::Http( StatusCode::NOT_FOUND, Some(OperationError::MissingAttribute(_)), opid, )) | Err(ClientError::Http( StatusCode::NOT_FOUND, Some(OperationError::MissingClass(_)), opid, )) | Err(ClientError::Http( StatusCode::BAD_REQUEST, Some(OperationError::InvalidAccountState(_)), opid, )) => { debug!( ?opid, "entry has been removed or is no longer a valid posix account" ); Ok(GroupTokenState::NotFound) } // Something is really wrong? We did get a response though, so we are still online. Err(err) => { error!(?err, "client error"); Err(IdpError::BadRequest) } } } async fn unix_user_authorise(&self, token: &UserToken) -> Result<Option<bool>, IdpError> { let inner = self.inner.lock().await; if inner.pam_allow_groups.is_empty() { // can't allow anything if the group list is zero... warn!("Cannot authenticate users, no allowed groups in configuration!"); Ok(Some(false)) } else { let user_set: BTreeSet<_> = token .groups .iter() .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()]) .collect(); debug!( "Checking if user is in allowed groups ({:?}) -> {:?}", inner.pam_allow_groups, user_set, ); let intersection_count = user_set.intersection(&inner.pam_allow_groups).count(); debug!("Number of intersecting groups: {}", intersection_count); debug!("User token is valid: {}", token.valid); Ok(Some(intersection_count > 0 && token.valid)) } } }