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