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