1use super::interface::{
2 tpm::{
3 provider::{BoxedDynTpm, TpmHmacS256},
4 structures::{HmacS256Key, LoadableHmacS256Key, StorageKey},
5 },
6 AuthCredHandler, AuthRequest, AuthResult, GroupToken, GroupTokenState, Id, IdProvider,
7 IdpError, ProviderOrigin, UserToken, UserTokenState,
8};
9use crate::db::KeyStoreTxn;
10use async_trait::async_trait;
11use hashbrown::HashMap;
12use kanidm_client::{ClientError, KanidmClient, StatusCode};
13use kanidm_lib_crypto::CryptoPolicy;
14use kanidm_lib_crypto::DbPasswordV1;
15use kanidm_lib_crypto::Password;
16use kanidm_proto::internal::OperationError;
17use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
18use kanidm_unix_common::constants::{
19 DEFAULT_CACHE_TIMEOUT_JITTER_MS, DEFAULT_OFFLINE_PROVIDER_CHECK_TIME,
20};
21use kanidm_unix_common::unix_config::{GroupMap, KanidmConfig};
22use kanidm_unix_common::unix_proto::PamAuthRequest;
23use std::collections::BTreeSet;
24use std::time::{Duration, SystemTime};
25use tokio::sync::{broadcast, Mutex};
26
27const KANIDM_HMAC_KEY: &str = "kanidm-hmac-key-v2";
28const KANIDM_PWV1_KEY: &str = "kanidm-pw-v1";
29
30fn next_offline_check(now: SystemTime) -> SystemTime {
31 let jitter = rand::random_range(0..DEFAULT_CACHE_TIMEOUT_JITTER_MS);
32 now + (Duration::from_secs(DEFAULT_OFFLINE_PROVIDER_CHECK_TIME) - Duration::from_millis(jitter))
33}
34
35#[derive(Debug, Clone)]
36enum CacheState {
37 Online,
38 Offline,
39 OfflineNextCheck(SystemTime),
40}
41
42struct KanidmProviderInternal {
43 state: CacheState,
44 client: KanidmClient,
45 hmac_key: HmacS256Key,
46 crypto_policy: CryptoPolicy,
47 pam_allow_groups: BTreeSet<String>,
48 bearer_token_set: bool,
49}
50
51pub struct KanidmProvider {
52 inner: Mutex<KanidmProviderInternal>,
53 map_group: HashMap<String, Id>,
56}
57
58impl KanidmProvider {
59 pub async fn new<'a, 'b>(
60 client: KanidmClient,
61 config: &KanidmConfig,
62 now: SystemTime,
63 keystore: &mut KeyStoreTxn<'a, 'b>,
64 tpm: &mut BoxedDynTpm,
65 machine_key: &StorageKey,
66 ) -> Result<Self, IdpError> {
67 let tpm_ctx: &mut dyn TpmHmacS256 = &mut **tpm;
68
69 let loadable_hmac_key: Option<LoadableHmacS256Key> = keystore
71 .get_tagged_hsm_key(KANIDM_HMAC_KEY)
72 .map_err(|ks_err| {
73 error!(?ks_err);
74 IdpError::KeyStore
75 })?;
76
77 let loadable_hmac_key = if let Some(loadable_hmac_key) = loadable_hmac_key {
78 loadable_hmac_key
79 } else {
80 let loadable_hmac_key = tpm_ctx.hmac_s256_create(machine_key).map_err(|tpm_err| {
81 error!(?tpm_err);
82 IdpError::Tpm
83 })?;
84
85 keystore
86 .insert_tagged_hsm_key(KANIDM_HMAC_KEY, &loadable_hmac_key)
87 .map_err(|ks_err| {
88 error!(?ks_err);
89 IdpError::KeyStore
90 })?;
91
92 loadable_hmac_key
93 };
94
95 let hmac_key = tpm_ctx
96 .hmac_s256_load(machine_key, &loadable_hmac_key)
97 .map_err(|tpm_err| {
98 error!(?tpm_err);
99 IdpError::Tpm
100 })?;
101
102 let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250));
103
104 let pam_allow_groups = config.pam_allowed_login_groups.iter().cloned().collect();
105
106 let map_group = config
107 .map_group
108 .iter()
109 .cloned()
110 .map(|GroupMap { local, with }| (local, Id::Name(with)))
111 .collect();
112
113 if let Some(token) = config.service_account_token.clone() {
115 client.set_token(token).await;
116 };
117 let bearer_token_set = config.service_account_token.is_some();
118
119 Ok(KanidmProvider {
120 inner: Mutex::new(KanidmProviderInternal {
121 state: CacheState::OfflineNextCheck(now),
122 client,
123 hmac_key,
124 crypto_policy,
125 pam_allow_groups,
126 bearer_token_set,
127 }),
128 map_group,
129 })
130 }
131}
132
133impl From<UnixUserToken> for UserToken {
134 fn from(value: UnixUserToken) -> UserToken {
135 let UnixUserToken {
136 name,
137 spn,
138 displayname,
139 gidnumber,
140 uuid,
141 shell,
142 groups,
143 sshkeys,
144 valid,
145 } = value;
146
147 let sshkeys = sshkeys.iter().map(|s| s.to_string()).collect();
148
149 let groups = groups.into_iter().map(GroupToken::from).collect();
150
151 UserToken {
152 provider: ProviderOrigin::Kanidm,
153 name,
154 spn,
155 uuid,
156 gidnumber,
157 displayname,
158 shell,
159 groups,
160 sshkeys,
161 valid,
162 extra_keys: Default::default(),
163 }
164 }
165}
166
167impl From<UnixGroupToken> for GroupToken {
168 fn from(value: UnixGroupToken) -> GroupToken {
169 let UnixGroupToken {
170 name,
171 spn,
172 uuid,
173 gidnumber,
174 } = value;
175
176 GroupToken {
177 provider: ProviderOrigin::Kanidm,
178 name,
179 spn,
180 uuid,
181 gidnumber,
182 extra_keys: Default::default(),
183 }
184 }
185}
186
187impl UserToken {
188 pub fn kanidm_update_cached_password(
189 &mut self,
190 crypto_policy: &CryptoPolicy,
191 cred: &str,
192 tpm: &mut BoxedDynTpm,
193 hmac_key: &HmacS256Key,
194 ) {
195 let tpm_ctx: &mut dyn TpmHmacS256 = &mut **tpm;
196
197 let pw = match Password::new_argon2id_hsm(crypto_policy, cred, tpm_ctx, hmac_key) {
198 Ok(pw) => pw,
199 Err(reason) => {
200 self.extra_keys.remove(KANIDM_PWV1_KEY);
202 warn!(
203 ?reason,
204 "unable to apply kdf to password, clearing cached password."
205 );
206 return;
207 }
208 };
209
210 let pw_value = match serde_json::to_value(pw.to_dbpasswordv1()) {
211 Ok(pw) => pw,
212 Err(reason) => {
213 self.extra_keys.remove(KANIDM_PWV1_KEY);
215 warn!(
216 ?reason,
217 "unable to serialise credential, clearing cached password."
218 );
219 return;
220 }
221 };
222
223 self.extra_keys.insert(KANIDM_PWV1_KEY.into(), pw_value);
224 debug!(spn = %self.spn, "Updated cached pw");
225 }
226
227 pub fn kanidm_has_offline_credentials(&self) -> bool {
228 self.extra_keys.contains_key(KANIDM_PWV1_KEY)
229 }
230
231 pub fn kanidm_check_cached_password(
232 &self,
233 cred: &str,
234 tpm: &mut BoxedDynTpm,
235 hmac_key: &HmacS256Key,
236 ) -> bool {
237 let pw_value = match self.extra_keys.get(KANIDM_PWV1_KEY) {
238 Some(pw_value) => pw_value,
239 None => {
240 debug!(spn = %self.spn, "no cached pw available");
241 return false;
242 }
243 };
244
245 let dbpw = match serde_json::from_value::<DbPasswordV1>(pw_value.clone()) {
246 Ok(dbpw) => dbpw,
247 Err(reason) => {
248 warn!(spn = %self.spn, ?reason, "unable to deserialise credential");
249 return false;
250 }
251 };
252
253 let pw = match Password::try_from(dbpw) {
254 Ok(pw) => pw,
255 Err(reason) => {
256 warn!(spn = %self.spn, ?reason, "unable to process credential");
257 return false;
258 }
259 };
260
261 let tpm_ctx: &mut dyn TpmHmacS256 = &mut **tpm;
262
263 pw.verify_ctx(cred, Some((tpm_ctx, hmac_key)))
264 .unwrap_or_default()
265 }
266}
267
268impl KanidmProviderInternal {
269 #[instrument(level = "debug", skip_all)]
270 async fn check_online(&mut self, tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
271 match self.state {
272 CacheState::Online => true,
274 CacheState::OfflineNextCheck(at_time) if now >= at_time => {
275 self.attempt_online(tpm, now).await
276 }
277 CacheState::OfflineNextCheck(_) | CacheState::Offline => false,
278 }
279 }
280
281 #[instrument(level = "debug", skip_all)]
282 async fn check_online_right_meow(&mut self, tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
283 match self.state {
284 CacheState::Online => true,
285 CacheState::OfflineNextCheck(_) => self.attempt_online(tpm, now).await,
286 CacheState::Offline => false,
287 }
288 }
289
290 #[instrument(level = "debug", skip_all)]
291 async fn is_online(&mut self) -> bool {
292 matches!(self.state, CacheState::Online)
293 }
294
295 #[instrument(level = "debug", skip_all)]
296 async fn attempt_online(&mut self, _tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
297 let mut max_attempts = 3;
298 while max_attempts > 0 {
299 max_attempts -= 1;
300
301 let check_online_result = if self.bearer_token_set {
304 self.client.whoami().await.map(|_| ())
305 } else {
306 self.client.auth_anonymous().await
307 };
308
309 match check_online_result {
310 Ok(_uat) => {
311 debug!("provider is now online");
312 self.state = CacheState::Online;
313 return true;
314 }
315 Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
316 error!(?reason, ?opid, "Provider authentication returned unauthorized, {max_attempts} attempts remaining.");
317 self.state = CacheState::OfflineNextCheck(now);
321 continue;
323 }
324 Err(err) => {
325 error!(?err, "Provider online failed");
326 self.state = CacheState::OfflineNextCheck(next_offline_check(now));
327 return false;
328 }
329 }
330 }
331 warn!("Exceeded maximum number of attempts to bring provider online");
332 return false;
333 }
334}
335
336#[async_trait]
337impl IdProvider for KanidmProvider {
338 fn origin(&self) -> ProviderOrigin {
339 ProviderOrigin::Kanidm
340 }
341
342 async fn attempt_online(&self, tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
343 let mut inner = self.inner.lock().await;
344 inner.check_online_right_meow(tpm, now).await
345 }
346
347 async fn is_online(&self) -> bool {
348 let mut inner = self.inner.lock().await;
349 inner.is_online().await
350 }
351
352 async fn mark_next_check(&self, now: SystemTime) {
353 let mut inner = self.inner.lock().await;
354 inner.state = CacheState::OfflineNextCheck(now);
355 }
356
357 fn has_map_group(&self, local: &str) -> Option<&Id> {
358 self.map_group.get(local)
359 }
360
361 async fn mark_offline(&self) {
362 let mut inner = self.inner.lock().await;
363 inner.state = CacheState::Offline;
364 }
365
366 #[instrument(level = "debug", skip_all, fields(id = ?id))]
367 async fn unix_user_get(
368 &self,
369 id: &Id,
370 token: Option<&UserToken>,
371 tpm: &mut BoxedDynTpm,
372 now: SystemTime,
373 ) -> Result<UserTokenState, IdpError> {
374 let mut inner = self.inner.lock().await;
375
376 if !inner.check_online(tpm, now).await {
377 return Ok(UserTokenState::UseCached);
379 }
380
381 match inner
383 .client
384 .idm_account_unix_token_get(id.to_string().as_str())
385 .await
386 {
387 Ok(tok) => {
388 let mut ut = UserToken::from(tok);
389
390 if let Some(previous_token) = token {
391 ut.extra_keys = previous_token.extra_keys.clone();
392 }
393
394 Ok(UserTokenState::Update(ut))
395 }
396 Err(ClientError::Transport(err)) => {
398 error!(?err, "transport error");
399 inner.state = CacheState::OfflineNextCheck(next_offline_check(now));
400 Ok(UserTokenState::UseCached)
401 }
402 Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
404 match reason {
405 Some(OperationError::NotAuthenticated) => warn!(
406 "session not authenticated - attempting reauthentication - eventid {}",
407 opid
408 ),
409 Some(OperationError::SessionExpired) => warn!(
410 "session expired - attempting reauthentication - eventid {}",
411 opid
412 ),
413 e => error!(
414 "authentication error {:?}, moving to offline - eventid {}",
415 e, opid
416 ),
417 };
418 inner.state = CacheState::OfflineNextCheck(now);
420 Ok(UserTokenState::UseCached)
421 }
422 Err(ClientError::Http(
424 StatusCode::BAD_REQUEST,
425 Some(OperationError::NoMatchingEntries),
426 opid,
427 ))
428 | Err(ClientError::Http(
429 StatusCode::NOT_FOUND,
430 Some(OperationError::NoMatchingEntries),
431 opid,
432 ))
433 | Err(ClientError::Http(
434 StatusCode::NOT_FOUND,
435 Some(OperationError::MissingAttribute(_)),
436 opid,
437 ))
438 | Err(ClientError::Http(
439 StatusCode::NOT_FOUND,
440 Some(OperationError::MissingClass(_)),
441 opid,
442 ))
443 | Err(ClientError::Http(
444 StatusCode::BAD_REQUEST,
445 Some(OperationError::InvalidAccountState(_)),
446 opid,
447 )) => {
448 debug!(
449 ?opid,
450 "entry has been removed or is no longer a valid posix account"
451 );
452 Ok(UserTokenState::NotFound)
453 }
454 Err(err) => {
456 error!(?err, "client error");
457 Err(IdpError::BadRequest)
458 }
459 }
460 }
461
462 #[instrument(level = "debug", skip_all)]
463 async fn unix_user_online_auth_init(
464 &self,
465 _account_id: &str,
466 _token: &UserToken,
467 _tpm: &mut BoxedDynTpm,
468 _shutdown_rx: &broadcast::Receiver<()>,
469 ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
470 Ok((AuthRequest::Password, AuthCredHandler::Password))
472 }
473
474 async fn unix_unknown_user_online_auth_init(
475 &self,
476 _account_id: &str,
477 _tpm: &mut BoxedDynTpm,
478 _shutdown_rx: &broadcast::Receiver<()>,
479 ) -> Result<Option<(AuthRequest, AuthCredHandler)>, IdpError> {
480 Ok(None)
482 }
483
484 #[instrument(level = "debug", skip_all)]
485 async fn unix_user_online_auth_step(
486 &self,
487 account_id: &str,
488 current_token: Option<&UserToken>,
489 cred_handler: &mut AuthCredHandler,
490 pam_next_req: PamAuthRequest,
491 tpm: &mut BoxedDynTpm,
492 _shutdown_rx: &broadcast::Receiver<()>,
493 ) -> Result<AuthResult, IdpError> {
494 match (cred_handler, pam_next_req) {
495 (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
496 let inner = self.inner.lock().await;
497
498 let auth_result = inner
499 .client
500 .idm_account_unix_cred_verify(account_id, &cred)
501 .await;
502
503 trace!(?auth_result);
504
505 match auth_result {
506 Ok(Some(n_tok)) => {
507 let mut new_token = UserToken::from(n_tok);
508
509 if let Some(previous_token) = current_token {
512 new_token.extra_keys = previous_token.extra_keys.clone();
513 }
514
515 new_token.kanidm_update_cached_password(
517 &inner.crypto_policy,
518 cred.as_str(),
519 tpm,
520 &inner.hmac_key,
521 );
522
523 Ok(AuthResult::SuccessUpdate { new_token })
524 }
525 Ok(None) => {
526 Ok(AuthResult::Denied)
535 }
536 Err(ClientError::Transport(err)) => {
537 error!(?err, "A client transport error occurred.");
538 Err(IdpError::Transport)
539 }
540 Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
541 match reason {
542 Some(OperationError::NotAuthenticated) => warn!(
543 "session not authenticated - attempting reauthentication - eventid {}",
544 opid
545 ),
546 Some(OperationError::SessionExpired) => warn!(
547 "session expired - attempting reauthentication - eventid {}",
548 opid
549 ),
550 e => error!(
551 "authentication error {:?}, moving to offline - eventid {}",
552 e, opid
553 ),
554 };
555 Err(IdpError::ProviderUnauthorised)
556 }
557 Err(ClientError::Http(
558 StatusCode::BAD_REQUEST,
559 Some(OperationError::NoMatchingEntries),
560 opid,
561 ))
562 | Err(ClientError::Http(
563 StatusCode::NOT_FOUND,
564 Some(OperationError::NoMatchingEntries),
565 opid,
566 ))
567 | Err(ClientError::Http(
568 StatusCode::NOT_FOUND,
569 Some(OperationError::MissingAttribute(_)),
570 opid,
571 ))
572 | Err(ClientError::Http(
573 StatusCode::NOT_FOUND,
574 Some(OperationError::MissingClass(_)),
575 opid,
576 ))
577 | Err(ClientError::Http(
578 StatusCode::BAD_REQUEST,
579 Some(OperationError::InvalidAccountState(_)),
580 opid,
581 )) => {
582 error!(
583 "unknown account or is not a valid posix account - eventid {}",
584 opid
585 );
586 Err(IdpError::NotFound)
587 }
588 Err(err) => {
589 error!(?err, "client error");
590 Err(IdpError::BadRequest)
592 }
593 }
594 }
595 (
596 AuthCredHandler::DeviceAuthorizationGrant,
597 PamAuthRequest::DeviceAuthorizationGrant { .. },
598 ) => {
599 error!("DeviceAuthorizationGrant not implemented!");
600 Err(IdpError::BadRequest)
601 }
602 _ => {
603 error!("invalid authentication request state");
604 Err(IdpError::BadRequest)
605 }
606 }
607 }
608
609 async fn unix_user_can_offline_auth(&self, token: &UserToken) -> bool {
610 token.kanidm_has_offline_credentials()
611 }
612
613 async fn unix_user_offline_auth_init(
614 &self,
615 token: &UserToken,
616 ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
617 if token.kanidm_has_offline_credentials() {
618 Ok((AuthRequest::Password, AuthCredHandler::Password))
619 } else {
620 Err(IdpError::NoOfflineCredentials)
621 }
622 }
623
624 async fn unix_user_offline_auth_step(
625 &self,
626 current_token: Option<&UserToken>,
627 session_token: &UserToken,
628 cred_handler: &mut AuthCredHandler,
629 pam_next_req: PamAuthRequest,
630 tpm: &mut BoxedDynTpm,
631 ) -> Result<AuthResult, IdpError> {
632 match (cred_handler, pam_next_req) {
633 (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
634 let inner = self.inner.lock().await;
635
636 if session_token.kanidm_check_cached_password(cred.as_str(), tpm, &inner.hmac_key) {
637 let new_token = current_token.unwrap_or(session_token).clone();
639
640 Ok(AuthResult::SuccessUpdate { new_token })
643 } else {
644 Ok(AuthResult::Denied)
645 }
646 }
647 (
648 AuthCredHandler::DeviceAuthorizationGrant,
649 PamAuthRequest::DeviceAuthorizationGrant { .. },
650 ) => {
651 error!("DeviceAuthorizationGrant not implemented!");
652 Err(IdpError::BadRequest)
653 }
654 _ => {
655 error!("invalid authentication request state");
656 Err(IdpError::BadRequest)
657 }
658 }
659 }
660
661 #[instrument(level = "debug", skip_all)]
662 async fn unix_group_get(
663 &self,
664 id: &Id,
665 tpm: &mut BoxedDynTpm,
666 now: SystemTime,
667 ) -> Result<GroupTokenState, IdpError> {
668 let mut inner = self.inner.lock().await;
669
670 if !inner.check_online(tpm, now).await {
671 return Ok(GroupTokenState::UseCached);
673 }
674
675 match inner
676 .client
677 .idm_group_unix_token_get(id.to_string().as_str())
678 .await
679 {
680 Ok(tok) => {
681 let gt = GroupToken::from(tok);
682 Ok(GroupTokenState::Update(gt))
683 }
684 Err(ClientError::Transport(err)) => {
686 error!(?err, "transport error");
687 inner.state = CacheState::OfflineNextCheck(next_offline_check(now));
688 Ok(GroupTokenState::UseCached)
689 }
690 Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
692 match reason {
693 Some(OperationError::NotAuthenticated) => warn!(
694 "session not authenticated - attempting reauthentication - eventid {}",
695 opid
696 ),
697 Some(OperationError::SessionExpired) => warn!(
698 "session expired - attempting reauthentication - eventid {}",
699 opid
700 ),
701 e => error!(
702 "authentication error {:?}, moving to offline - eventid {}",
703 e, opid
704 ),
705 };
706 inner.state = CacheState::OfflineNextCheck(next_offline_check(now));
707 Ok(GroupTokenState::UseCached)
708 }
709 Err(ClientError::Http(
711 StatusCode::BAD_REQUEST,
712 Some(OperationError::NoMatchingEntries),
713 opid,
714 ))
715 | Err(ClientError::Http(
716 StatusCode::NOT_FOUND,
717 Some(OperationError::NoMatchingEntries),
718 opid,
719 ))
720 | Err(ClientError::Http(
721 StatusCode::NOT_FOUND,
722 Some(OperationError::MissingAttribute(_)),
723 opid,
724 ))
725 | Err(ClientError::Http(
726 StatusCode::NOT_FOUND,
727 Some(OperationError::MissingClass(_)),
728 opid,
729 ))
730 | Err(ClientError::Http(
731 StatusCode::BAD_REQUEST,
732 Some(OperationError::InvalidAccountState(_)),
733 opid,
734 )) => {
735 debug!(
736 ?opid,
737 "entry has been removed or is no longer a valid posix account"
738 );
739 Ok(GroupTokenState::NotFound)
740 }
741 Err(err) => {
743 error!(?err, "client error");
744 Err(IdpError::BadRequest)
745 }
746 }
747 }
748
749 async fn unix_user_authorise(&self, token: &UserToken) -> Result<Option<bool>, IdpError> {
750 let inner = self.inner.lock().await;
751
752 if inner.pam_allow_groups.is_empty() {
753 warn!("NO USERS CAN LOGIN TO THIS SYSTEM! There are no `pam_allowed_login_groups` in configuration!");
755 Ok(Some(false))
756 } else {
757 let user_set: BTreeSet<_> = token
758 .groups
759 .iter()
760 .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()])
761 .collect();
762
763 debug!(
764 "Checking if user is in allowed groups ({:?}) -> {:?}",
765 inner.pam_allow_groups, user_set,
766 );
767 let intersection_count = user_set.intersection(&inner.pam_allow_groups).count();
768 debug!("Number of intersecting groups: {}", intersection_count);
769 debug!("User token is valid: {}", token.valid);
770
771 if intersection_count == 0 && token.valid {
772 warn!("The user {} authenticated successfully but is NOT a member of a group defined in `pam_allowed_login_groups`. They have been denied access to this system.", token.spn);
773 }
774
775 Ok(Some(intersection_count > 0 && token.valid))
776 }
777 }
778}