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 async fn unix_user_get(
348 &self,
349 id: &Id,
350 token: Option<&UserToken>,
351 tpm: &mut BoxedDynTpm,
352 now: SystemTime,
353 ) -> Result<UserTokenState, IdpError> {
354 let mut inner = self.inner.lock().await;
355
356 if !inner.check_online(tpm, now).await {
357 return Ok(UserTokenState::UseCached);
359 }
360
361 match inner
363 .client
364 .idm_account_unix_token_get(id.to_string().as_str())
365 .await
366 {
367 Ok(tok) => {
368 let mut ut = UserToken::from(tok);
369
370 if let Some(previous_token) = token {
371 ut.extra_keys = previous_token.extra_keys.clone();
372 }
373
374 Ok(UserTokenState::Update(ut))
375 }
376 Err(ClientError::Transport(err)) => {
378 error!(?err, "transport error");
379 inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
380 Ok(UserTokenState::UseCached)
381 }
382 Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
384 match reason {
385 Some(OperationError::NotAuthenticated) => warn!(
386 "session not authenticated - attempting reauthentication - eventid {}",
387 opid
388 ),
389 Some(OperationError::SessionExpired) => warn!(
390 "session expired - attempting reauthentication - eventid {}",
391 opid
392 ),
393 e => error!(
394 "authentication error {:?}, moving to offline - eventid {}",
395 e, opid
396 ),
397 };
398 inner.state = CacheState::OfflineNextCheck(now);
400 Ok(UserTokenState::UseCached)
401 }
402 Err(ClientError::Http(
404 StatusCode::BAD_REQUEST,
405 Some(OperationError::NoMatchingEntries),
406 opid,
407 ))
408 | Err(ClientError::Http(
409 StatusCode::NOT_FOUND,
410 Some(OperationError::NoMatchingEntries),
411 opid,
412 ))
413 | Err(ClientError::Http(
414 StatusCode::NOT_FOUND,
415 Some(OperationError::MissingAttribute(_)),
416 opid,
417 ))
418 | Err(ClientError::Http(
419 StatusCode::NOT_FOUND,
420 Some(OperationError::MissingClass(_)),
421 opid,
422 ))
423 | Err(ClientError::Http(
424 StatusCode::BAD_REQUEST,
425 Some(OperationError::InvalidAccountState(_)),
426 opid,
427 )) => {
428 debug!(
429 ?opid,
430 "entry has been removed or is no longer a valid posix account"
431 );
432 Ok(UserTokenState::NotFound)
433 }
434 Err(err) => {
436 error!(?err, "client error");
437 Err(IdpError::BadRequest)
438 }
439 }
440 }
441
442 async fn unix_user_online_auth_init(
443 &self,
444 _account_id: &str,
445 _token: &UserToken,
446 _tpm: &mut BoxedDynTpm,
447 _shutdown_rx: &broadcast::Receiver<()>,
448 ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
449 Ok((AuthRequest::Password, AuthCredHandler::Password))
451 }
452
453 async fn unix_unknown_user_online_auth_init(
454 &self,
455 _account_id: &str,
456 _tpm: &mut BoxedDynTpm,
457 _shutdown_rx: &broadcast::Receiver<()>,
458 ) -> Result<Option<(AuthRequest, AuthCredHandler)>, IdpError> {
459 Ok(None)
461 }
462
463 async fn unix_user_online_auth_step(
464 &self,
465 account_id: &str,
466 current_token: Option<&UserToken>,
467 cred_handler: &mut AuthCredHandler,
468 pam_next_req: PamAuthRequest,
469 tpm: &mut BoxedDynTpm,
470 _shutdown_rx: &broadcast::Receiver<()>,
471 ) -> Result<AuthResult, IdpError> {
472 match (cred_handler, pam_next_req) {
473 (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
474 let inner = self.inner.lock().await;
475
476 let auth_result = inner
477 .client
478 .idm_account_unix_cred_verify(account_id, &cred)
479 .await;
480
481 trace!(?auth_result);
482
483 match auth_result {
484 Ok(Some(n_tok)) => {
485 let mut new_token = UserToken::from(n_tok);
486
487 if let Some(previous_token) = current_token {
490 new_token.extra_keys = previous_token.extra_keys.clone();
491 }
492
493 new_token.kanidm_update_cached_password(
495 &inner.crypto_policy,
496 cred.as_str(),
497 tpm,
498 &inner.hmac_key,
499 );
500
501 Ok(AuthResult::SuccessUpdate { new_token })
502 }
503 Ok(None) => {
504 Ok(AuthResult::Denied)
513 }
514 Err(ClientError::Transport(err)) => {
515 error!(?err, "A client transport error occured.");
516 Err(IdpError::Transport)
517 }
518 Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
519 match reason {
520 Some(OperationError::NotAuthenticated) => warn!(
521 "session not authenticated - attempting reauthentication - eventid {}",
522 opid
523 ),
524 Some(OperationError::SessionExpired) => warn!(
525 "session expired - attempting reauthentication - eventid {}",
526 opid
527 ),
528 e => error!(
529 "authentication error {:?}, moving to offline - eventid {}",
530 e, opid
531 ),
532 };
533 Err(IdpError::ProviderUnauthorised)
534 }
535 Err(ClientError::Http(
536 StatusCode::BAD_REQUEST,
537 Some(OperationError::NoMatchingEntries),
538 opid,
539 ))
540 | Err(ClientError::Http(
541 StatusCode::NOT_FOUND,
542 Some(OperationError::NoMatchingEntries),
543 opid,
544 ))
545 | Err(ClientError::Http(
546 StatusCode::NOT_FOUND,
547 Some(OperationError::MissingAttribute(_)),
548 opid,
549 ))
550 | Err(ClientError::Http(
551 StatusCode::NOT_FOUND,
552 Some(OperationError::MissingClass(_)),
553 opid,
554 ))
555 | Err(ClientError::Http(
556 StatusCode::BAD_REQUEST,
557 Some(OperationError::InvalidAccountState(_)),
558 opid,
559 )) => {
560 error!(
561 "unknown account or is not a valid posix account - eventid {}",
562 opid
563 );
564 Err(IdpError::NotFound)
565 }
566 Err(err) => {
567 error!(?err, "client error");
568 Err(IdpError::BadRequest)
570 }
571 }
572 }
573 (
574 AuthCredHandler::DeviceAuthorizationGrant,
575 PamAuthRequest::DeviceAuthorizationGrant { .. },
576 ) => {
577 error!("DeviceAuthorizationGrant not implemented!");
578 Err(IdpError::BadRequest)
579 }
580 _ => {
581 error!("invalid authentication request state");
582 Err(IdpError::BadRequest)
583 }
584 }
585 }
586
587 async fn unix_user_offline_auth_init(
588 &self,
589 _token: &UserToken,
590 ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
591 Ok((AuthRequest::Password, AuthCredHandler::Password))
592 }
593
594 async fn unix_user_offline_auth_step(
595 &self,
596 current_token: Option<&UserToken>,
597 session_token: &UserToken,
598 cred_handler: &mut AuthCredHandler,
599 pam_next_req: PamAuthRequest,
600 tpm: &mut BoxedDynTpm,
601 ) -> Result<AuthResult, IdpError> {
602 match (cred_handler, pam_next_req) {
603 (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
604 let inner = self.inner.lock().await;
605
606 if session_token.kanidm_check_cached_password(cred.as_str(), tpm, &inner.hmac_key) {
607 let new_token = current_token.unwrap_or(session_token).clone();
609
610 Ok(AuthResult::SuccessUpdate { new_token })
613 } else {
614 Ok(AuthResult::Denied)
615 }
616 }
617 (
618 AuthCredHandler::DeviceAuthorizationGrant,
619 PamAuthRequest::DeviceAuthorizationGrant { .. },
620 ) => {
621 error!("DeviceAuthorizationGrant not implemented!");
622 Err(IdpError::BadRequest)
623 }
624 _ => {
625 error!("invalid authentication request state");
626 Err(IdpError::BadRequest)
627 }
628 }
629 }
630
631 async fn unix_group_get(
632 &self,
633 id: &Id,
634 tpm: &mut BoxedDynTpm,
635 now: SystemTime,
636 ) -> Result<GroupTokenState, IdpError> {
637 let mut inner = self.inner.lock().await;
638
639 if !inner.check_online(tpm, now).await {
640 return Ok(GroupTokenState::UseCached);
642 }
643
644 match inner
645 .client
646 .idm_group_unix_token_get(id.to_string().as_str())
647 .await
648 {
649 Ok(tok) => {
650 let gt = GroupToken::from(tok);
651 Ok(GroupTokenState::Update(gt))
652 }
653 Err(ClientError::Transport(err)) => {
655 error!(?err, "transport error");
656 inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
657 Ok(GroupTokenState::UseCached)
658 }
659 Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
661 match reason {
662 Some(OperationError::NotAuthenticated) => warn!(
663 "session not authenticated - attempting reauthentication - eventid {}",
664 opid
665 ),
666 Some(OperationError::SessionExpired) => warn!(
667 "session expired - attempting reauthentication - eventid {}",
668 opid
669 ),
670 e => error!(
671 "authentication error {:?}, moving to offline - eventid {}",
672 e, opid
673 ),
674 };
675 inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
676 Ok(GroupTokenState::UseCached)
677 }
678 Err(ClientError::Http(
680 StatusCode::BAD_REQUEST,
681 Some(OperationError::NoMatchingEntries),
682 opid,
683 ))
684 | Err(ClientError::Http(
685 StatusCode::NOT_FOUND,
686 Some(OperationError::NoMatchingEntries),
687 opid,
688 ))
689 | Err(ClientError::Http(
690 StatusCode::NOT_FOUND,
691 Some(OperationError::MissingAttribute(_)),
692 opid,
693 ))
694 | Err(ClientError::Http(
695 StatusCode::NOT_FOUND,
696 Some(OperationError::MissingClass(_)),
697 opid,
698 ))
699 | Err(ClientError::Http(
700 StatusCode::BAD_REQUEST,
701 Some(OperationError::InvalidAccountState(_)),
702 opid,
703 )) => {
704 debug!(
705 ?opid,
706 "entry has been removed or is no longer a valid posix account"
707 );
708 Ok(GroupTokenState::NotFound)
709 }
710 Err(err) => {
712 error!(?err, "client error");
713 Err(IdpError::BadRequest)
714 }
715 }
716 }
717
718 async fn unix_user_authorise(&self, token: &UserToken) -> Result<Option<bool>, IdpError> {
719 let inner = self.inner.lock().await;
720
721 if inner.pam_allow_groups.is_empty() {
722 warn!("NO USERS CAN LOGIN TO THIS SYSTEM! There are no `pam_allowed_login_groups` in configuration!");
724 Ok(Some(false))
725 } else {
726 let user_set: BTreeSet<_> = token
727 .groups
728 .iter()
729 .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()])
730 .collect();
731
732 debug!(
733 "Checking if user is in allowed groups ({:?}) -> {:?}",
734 inner.pam_allow_groups, user_set,
735 );
736 let intersection_count = user_set.intersection(&inner.pam_allow_groups).count();
737 debug!("Number of intersecting groups: {}", intersection_count);
738 debug!("User token is valid: {}", token.valid);
739
740 if intersection_count == 0 && token.valid {
741 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);
742 }
743
744 Ok(Some(intersection_count > 0 && token.valid))
745 }
746 }
747}