1use crate::event::ModifyEvent;
11use crate::plugins::Plugin;
12use crate::prelude::*;
13use crate::value::SessionState;
14use std::collections::BTreeSet;
15use std::sync::Arc;
16use time::OffsetDateTime;
17
18pub struct SessionConsistency {}
19
20impl Plugin for SessionConsistency {
21 fn id() -> &'static str {
22 "plugin_session_consistency"
23 }
24
25 #[instrument(level = "debug", name = "session_consistency", skip_all)]
26 fn pre_modify(
27 qs: &mut QueryServerWriteTransaction,
28 _pre_cand: &[Arc<EntrySealedCommitted>],
29 cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
30 _me: &ModifyEvent,
31 ) -> Result<(), OperationError> {
32 Self::modify_inner(qs, cand)
33 }
34
35 #[instrument(level = "debug", name = "session_consistency", skip_all)]
36 fn pre_batch_modify(
37 qs: &mut QueryServerWriteTransaction,
38 _pre_cand: &[Arc<EntrySealedCommitted>],
39 cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
40 _me: &BatchModifyEvent,
41 ) -> Result<(), OperationError> {
42 Self::modify_inner(qs, cand)
43 }
44}
45
46impl SessionConsistency {
47 fn modify_inner<T: Clone + std::fmt::Debug>(
48 qs: &mut QueryServerWriteTransaction,
49 cand: &mut [Entry<EntryInvalid, T>],
50 ) -> Result<(), OperationError> {
51 let curtime = qs.get_curtime();
52 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
53 trace!(%curtime_odt);
54
55 cand.iter_mut().try_for_each(|entry| {
57 let cred_ids: BTreeSet<Uuid> =
59 entry
60 .get_ava_single_credential(Attribute::PrimaryCredential)
61 .iter()
62 .map(|c| c.uuid)
63
64 .chain(
65 entry.get_ava_passkeys(Attribute::PassKeys)
66 .iter()
67 .flat_map(|pks| pks.keys().copied())
68 )
69 .chain(
70 entry.get_ava_attestedpasskeys(Attribute::AttestedPasskeys)
71 .iter()
72 .flat_map(|pks| pks.keys().copied())
73 )
74 .collect();
75
76 let invalidate: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
77 .map(|sessions| {
78 sessions.iter().filter_map(|(session_id, session)| {
79 match &session.state {
80 SessionState::RevokedAt(_) => {
81 None
83 }
84 SessionState::ExpiresAt(_) |
85 SessionState::NeverExpires =>
86 if !cred_ids.contains(&session.cred_id) {
87 info!(%session_id, "Revoking auth session whose issuing credential no longer exists");
88 Some(PartialValue::Refer(*session_id))
89 } else {
90 None
91 },
92 }
93 })
94 .collect()
95 });
96
97 if let Some(invalidate) = invalidate.as_ref() {
98 entry.remove_avas(Attribute::UserAuthTokenSession, invalidate);
99 }
100
101 let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
103 .map(|sessions| {
104 sessions.iter().filter_map(|(session_id, session)| {
105 trace!(?session_id, ?session);
106 match &session.state {
107 SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
108 info!(%session_id, "Removing expired auth session");
109 Some(PartialValue::Refer(*session_id))
110 }
111 _ => None,
112 }
113 })
114 .collect()
115 });
116
117 if let Some(expired) = expired.as_ref() {
118 entry.remove_avas(Attribute::UserAuthTokenSession, expired);
119 }
120
121 let oauth2_remove: Option<BTreeSet<_>> = entry.get_ava_as_oauth2session_map(Attribute::OAuth2Session).map(|oauth2_sessions| {
124 let sessions = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession);
126
127 oauth2_sessions.iter().filter_map(|(o2_session_id, session)| {
128 trace!(?o2_session_id, ?session);
129 match &session.state {
130 SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
131 info!(%o2_session_id, "Removing expired oauth2 session");
132 Some(PartialValue::Refer(*o2_session_id))
133 }
134 SessionState::RevokedAt(_) => {
135 trace!("Skip already revoked session");
137 None
138 }
139 _ => {
140 if sessions.map(|session_map| {
142 if let Some(parent_session_id) = session.parent.as_ref() {
143 if let Some(parent_session) = session_map.get(parent_session_id) {
145 !matches!(parent_session.state, SessionState::RevokedAt(_))
147 } else {
148 false
150 }
151 } else {
152 true
155 }
156 }).unwrap_or(false) {
157 debug!("Parent session remains valid.");
159 None
160 } else {
161 if session.issued_at + AUTH_TOKEN_GRACE_WINDOW <= curtime_odt {
163 info!(%o2_session_id, parent_id = ?session.parent, "Removing orphaned oauth2 session");
164 Some(PartialValue::Refer(*o2_session_id))
165 } else {
166 debug!("Not enforcing parent session consistency on session within grace window");
168 None
169 }
170
171 }
172 }
173 }
174
175 })
176 .collect()
177 });
178
179 if let Some(oauth2_remove) = oauth2_remove.as_ref() {
180 entry.remove_avas(Attribute::OAuth2Session, oauth2_remove);
181 }
182
183 Ok(())
184 })
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use crate::prelude::*;
191
192 use crate::event::CreateEvent;
193 use crate::value::{AuthType, Oauth2Session, Session, SessionState};
194 use kanidm_proto::constants::OAUTH2_SCOPE_OPENID;
195 use std::time::Duration;
196 use time::OffsetDateTime;
197 use uuid::uuid;
198
199 use crate::credential::Credential;
200 use kanidm_lib_crypto::CryptoPolicy;
201
202 #[qs_test]
205 async fn test_session_consistency_expire_old_sessions(server: &QueryServer) {
206 let curtime = duration_from_epoch_now();
207 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
208
209 let p = CryptoPolicy::minimum();
210 let cred = Credential::new_password_only(&p, "test_password").unwrap();
211 let cred_id = cred.uuid;
212
213 let exp_curtime = curtime + Duration::from_secs(60);
214 let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
215
216 let mut server_txn = server.write(curtime).await.unwrap();
218
219 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
220
221 let e1 = entry_init!(
222 (Attribute::Class, EntryClass::Object.to_value()),
223 (Attribute::Class, EntryClass::Person.to_value()),
224 (Attribute::Class, EntryClass::Account.to_value()),
225 (Attribute::Name, Value::new_iname("testperson1")),
226 (Attribute::Uuid, Value::Uuid(tuuid)),
227 (Attribute::Description, Value::new_utf8s("testperson1")),
228 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
229 (
230 Attribute::PrimaryCredential,
231 Value::Cred("primary".to_string(), cred.clone())
232 )
233 );
234
235 let ce = CreateEvent::new_internal(vec![e1]);
236 assert!(server_txn.create(&ce).is_ok());
237
238 let session_id = Uuid::new_v4();
240 let state = SessionState::ExpiresAt(exp_curtime_odt);
241 let issued_at = curtime_odt;
242 let issued_by = IdentityId::User(tuuid);
243 let scope = SessionScope::ReadOnly;
244
245 let session = Value::Session(
246 session_id,
247 Session {
248 label: "label".to_string(),
249 state,
250 issued_at,
253 issued_by,
255 cred_id,
256 scope,
259 type_: AuthType::Passkey,
260 },
261 );
262
263 let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
265
266 server_txn
267 .internal_modify(
268 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
269 &modlist,
270 )
271 .expect("Failed to modify user");
272
273 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
276
277 let session = entry
278 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
279 .and_then(|sessions| sessions.get(&session_id))
280 .expect("No session map found");
281 assert!(matches!(session.state, SessionState::ExpiresAt(_)));
282
283 assert!(server_txn.commit().is_ok());
284 let mut server_txn = server.write(exp_curtime).await.unwrap();
285
286 let modlist = ModifyList::new_purge_and_set(
288 Attribute::Description,
289 Value::new_utf8s("test person 1 change"),
290 );
291
292 server_txn
293 .internal_modify(
294 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
295 &modlist,
296 )
297 .expect("Failed to modify user");
298
299 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
301
302 let session = entry
304 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
305 .and_then(|sessions| sessions.get(&session_id))
306 .expect("No session map found");
307 assert!(matches!(session.state, SessionState::RevokedAt(_)));
308
309 assert!(server_txn.commit().is_ok());
310 }
311
312 #[qs_test]
314 async fn test_session_consistency_oauth2_expiry_cleanup(server: &QueryServer) {
315 let curtime = duration_from_epoch_now();
316 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
317
318 let p = CryptoPolicy::minimum();
319 let cred = Credential::new_password_only(&p, "test_password").unwrap();
320 let cred_id = cred.uuid;
321
322 let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
324 let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
325
326 let mut server_txn = server.write(curtime).await.unwrap();
328
329 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
330 let rs_uuid = Uuid::new_v4();
331
332 let e1 = entry_init!(
333 (Attribute::Class, EntryClass::Object.to_value()),
334 (Attribute::Class, EntryClass::Person.to_value()),
335 (Attribute::Class, EntryClass::Account.to_value()),
336 (Attribute::Name, Value::new_iname("testperson1")),
337 (Attribute::Uuid, Value::Uuid(tuuid)),
338 (Attribute::Description, Value::new_utf8s("testperson1")),
339 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
340 (
341 Attribute::PrimaryCredential,
342 Value::Cred("primary".to_string(), cred.clone())
343 )
344 );
345
346 let e2 = entry_init!(
347 (Attribute::Class, EntryClass::Object.to_value()),
348 (Attribute::Class, EntryClass::Account.to_value()),
349 (
350 Attribute::Class,
351 EntryClass::OAuth2ResourceServer.to_value()
352 ),
353 (
354 Attribute::Class,
355 EntryClass::OAuth2ResourceServerBasic.to_value()
356 ),
357 (Attribute::Uuid, Value::Uuid(rs_uuid)),
358 (Attribute::Name, Value::new_iname("test_resource_server")),
359 (
360 Attribute::DisplayName,
361 Value::new_utf8s("test_resource_server")
362 ),
363 (
364 Attribute::OAuth2RsOriginLanding,
365 Value::new_url_s("https://demo.example.com").unwrap()
366 ),
367 (
369 Attribute::OAuth2RsScopeMap,
370 Value::new_oauthscopemap(
371 UUID_IDM_ALL_ACCOUNTS,
372 btreeset![OAUTH2_SCOPE_OPENID.to_string()]
373 )
374 .expect("invalid oauthscope")
375 )
376 );
377
378 let ce = CreateEvent::new_internal(vec![e1, e2]);
379 assert!(server_txn.create(&ce).is_ok());
380
381 let session_id = Uuid::new_v4();
384 let parent_id = Uuid::new_v4();
385 let state = SessionState::ExpiresAt(exp_curtime_odt);
386 let issued_at = curtime_odt;
387 let issued_by = IdentityId::User(tuuid);
388 let scope = SessionScope::ReadOnly;
389
390 let modlist = modlist!([
392 Modify::Present(
393 "oauth2_session".into(),
394 Value::Oauth2Session(
395 session_id,
396 Oauth2Session {
397 parent: Some(parent_id),
398 state,
400 issued_at,
401 rs_uuid,
402 },
403 )
404 ),
405 Modify::Present(
406 Attribute::UserAuthTokenSession,
407 Value::Session(
408 parent_id,
409 Session {
410 label: "label".to_string(),
411 state: SessionState::NeverExpires,
413 issued_at,
416 issued_by,
418 cred_id,
419 scope,
422 type_: AuthType::Passkey,
423 },
424 )
425 ),
426 ]);
427
428 server_txn
429 .internal_modify(
430 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
431 &modlist,
432 )
433 .expect("Failed to modify user");
434
435 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
438
439 let session = entry
440 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
441 .and_then(|sessions| sessions.get(&parent_id))
442 .expect("No session map found");
443 assert!(matches!(session.state, SessionState::NeverExpires));
444
445 let session = entry
446 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
447 .and_then(|sessions| sessions.get(&session_id))
448 .expect("No session map found");
449 assert!(matches!(session.state, SessionState::ExpiresAt(_)));
450
451 assert!(server_txn.commit().is_ok());
452
453 let mut server_txn = server.write(exp_curtime).await.unwrap();
456
457 let modlist = ModifyList::new_purge_and_set(
459 Attribute::Description,
460 Value::new_utf8s("test person 1 change"),
461 );
462
463 server_txn
464 .internal_modify(
465 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
466 &modlist,
467 )
468 .expect("Failed to modify user");
469
470 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
472
473 let session = entry
475 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
476 .and_then(|sessions| sessions.get(&parent_id))
477 .expect("No session map found");
478 assert!(matches!(session.state, SessionState::NeverExpires));
479
480 let session = entry
481 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
482 .and_then(|sessions| sessions.get(&session_id))
483 .expect("No session map found");
484 assert!(matches!(session.state, SessionState::RevokedAt(_)));
485
486 assert!(server_txn.commit().is_ok());
487 }
488
489 #[qs_test]
491 async fn test_session_consistency_oauth2_removed_by_parent(server: &QueryServer) {
492 let curtime = duration_from_epoch_now();
493 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
494 let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
495
496 let p = CryptoPolicy::minimum();
497 let cred = Credential::new_password_only(&p, "test_password").unwrap();
498 let cred_id = cred.uuid;
499
500 let mut server_txn = server.write(curtime).await.unwrap();
502
503 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
504 let rs_uuid = Uuid::new_v4();
505
506 let e1 = entry_init!(
507 (Attribute::Class, EntryClass::Object.to_value()),
508 (Attribute::Class, EntryClass::Person.to_value()),
509 (Attribute::Class, EntryClass::Account.to_value()),
510 (Attribute::Name, Value::new_iname("testperson1")),
511 (Attribute::Uuid, Value::Uuid(tuuid)),
512 (Attribute::Description, Value::new_utf8s("testperson1")),
513 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
514 (
515 Attribute::PrimaryCredential,
516 Value::Cred("primary".to_string(), cred.clone())
517 )
518 );
519
520 let e2 = entry_init!(
521 (Attribute::Class, EntryClass::Object.to_value()),
522 (Attribute::Class, EntryClass::Account.to_value()),
523 (
524 Attribute::Class,
525 EntryClass::OAuth2ResourceServer.to_value()
526 ),
527 (
528 Attribute::Class,
529 EntryClass::OAuth2ResourceServerBasic.to_value()
530 ),
531 (Attribute::Uuid, Value::Uuid(rs_uuid)),
532 (Attribute::Name, Value::new_iname("test_resource_server")),
533 (
534 Attribute::DisplayName,
535 Value::new_utf8s("test_resource_server")
536 ),
537 (
538 Attribute::OAuth2RsOriginLanding,
539 Value::new_url_s("https://demo.example.com").unwrap()
540 ),
541 (
543 Attribute::OAuth2RsScopeMap,
544 Value::new_oauthscopemap(
545 UUID_IDM_ALL_ACCOUNTS,
546 btreeset![OAUTH2_SCOPE_OPENID.to_string()]
547 )
548 .expect("invalid oauthscope")
549 )
550 );
551
552 let ce = CreateEvent::new_internal(vec![e1, e2]);
553 assert!(server_txn.create(&ce).is_ok());
554
555 let session_id = Uuid::new_v4();
558 let parent_id = Uuid::new_v4();
559 let issued_at = curtime_odt;
560 let issued_by = IdentityId::User(tuuid);
561 let scope = SessionScope::ReadOnly;
562
563 let modlist = modlist!([
565 Modify::Present(
566 "oauth2_session".into(),
567 Value::Oauth2Session(
568 session_id,
569 Oauth2Session {
570 parent: Some(parent_id),
571 state: SessionState::NeverExpires,
573 issued_at,
574 rs_uuid,
575 },
576 )
577 ),
578 Modify::Present(
579 Attribute::UserAuthTokenSession,
580 Value::Session(
581 parent_id,
582 Session {
583 label: "label".to_string(),
584 state: SessionState::NeverExpires,
586 issued_at,
589 issued_by,
591 cred_id,
592 scope,
595 type_: AuthType::Passkey,
596 },
597 )
598 ),
599 ]);
600
601 server_txn
602 .internal_modify(
603 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
604 &modlist,
605 )
606 .expect("Failed to modify user");
607
608 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
611
612 let session = entry
613 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
614 .and_then(|sessions| sessions.get(&parent_id))
615 .expect("No session map found");
616 assert!(matches!(session.state, SessionState::NeverExpires));
617
618 let session = entry
619 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
620 .and_then(|sessions| sessions.get(&session_id))
621 .expect("No session map found");
622 assert!(matches!(session.state, SessionState::NeverExpires));
623
624 assert!(server_txn.commit().is_ok());
626 let mut server_txn = server.write(exp_curtime).await.unwrap();
627
628 let modlist = ModifyList::new_remove(
630 Attribute::UserAuthTokenSession,
631 PartialValue::Refer(parent_id),
632 );
633
634 server_txn
635 .internal_modify(
636 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
637 &modlist,
638 )
639 .expect("Failed to modify user");
640
641 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
643
644 let session = entry
646 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
647 .and_then(|sessions| sessions.get(&parent_id))
648 .expect("No session map found");
649 assert!(matches!(session.state, SessionState::RevokedAt(_)));
650
651 let session = entry
653 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
654 .and_then(|sessions| sessions.get(&session_id))
655 .expect("No session map found");
656 assert!(matches!(session.state, SessionState::RevokedAt(_)));
657
658 assert!(server_txn.commit().is_ok());
659 }
660
661 #[qs_test]
663 async fn test_session_consistency_oauth2_grace_window_past(server: &QueryServer) {
664 let curtime = duration_from_epoch_now();
665 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
666
667 let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
669 let mut server_txn = server.write(curtime).await.unwrap();
673
674 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
675 let rs_uuid = Uuid::new_v4();
676
677 let e1 = entry_init!(
678 (Attribute::Class, EntryClass::Object.to_value()),
679 (Attribute::Class, EntryClass::Person.to_value()),
680 (Attribute::Class, EntryClass::Account.to_value()),
681 (Attribute::Name, Value::new_iname("testperson1")),
682 (Attribute::Uuid, Value::Uuid(tuuid)),
683 (Attribute::Description, Value::new_utf8s("testperson1")),
684 (Attribute::DisplayName, Value::new_utf8s("testperson1"))
685 );
686
687 let e2 = entry_init!(
688 (Attribute::Class, EntryClass::Object.to_value()),
689 (Attribute::Class, EntryClass::Account.to_value()),
690 (
691 Attribute::Class,
692 EntryClass::OAuth2ResourceServer.to_value()
693 ),
694 (
695 Attribute::Class,
696 EntryClass::OAuth2ResourceServerBasic.to_value()
697 ),
698 (Attribute::Uuid, Value::Uuid(rs_uuid)),
699 (Attribute::Name, Value::new_iname("test_resource_server")),
700 (
701 Attribute::DisplayName,
702 Value::new_utf8s("test_resource_server")
703 ),
704 (
705 Attribute::OAuth2RsOriginLanding,
706 Value::new_url_s("https://demo.example.com").unwrap()
707 ),
708 (
710 Attribute::OAuth2RsScopeMap,
711 Value::new_oauthscopemap(
712 UUID_IDM_ALL_ACCOUNTS,
713 btreeset![OAUTH2_SCOPE_OPENID.to_string()]
714 )
715 .expect("invalid oauthscope")
716 )
717 );
718
719 let ce = CreateEvent::new_internal(vec![e1, e2]);
720 assert!(server_txn.create(&ce).is_ok());
721
722 let session_id = Uuid::new_v4();
724 let parent = Uuid::new_v4();
725 let issued_at = curtime_odt;
726
727 let session = Value::Oauth2Session(
728 session_id,
729 Oauth2Session {
730 parent: Some(parent),
731 state: SessionState::NeverExpires,
734 issued_at,
735 rs_uuid,
736 },
737 );
738
739 let modlist = ModifyList::new_append(Attribute::OAuth2Session, session);
741
742 server_txn
743 .internal_modify(
744 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
745 &modlist,
746 )
747 .expect("Failed to modify user");
748
749 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
752
753 let session = entry
754 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
755 .and_then(|sessions| sessions.get(&session_id))
756 .expect("No session map found");
757 assert!(matches!(session.state, SessionState::NeverExpires));
758
759 assert!(server_txn.commit().is_ok());
760
761 let mut server_txn = server.write(exp_curtime).await.unwrap();
764
765 let modlist = ModifyList::new_purge_and_set(
767 Attribute::Description,
768 Value::new_utf8s("test person 1 change"),
769 );
770
771 server_txn
772 .internal_modify(
773 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
774 &modlist,
775 )
776 .expect("Failed to modify user");
777
778 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
780
781 let session = entry
783 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
784 .and_then(|sessions| sessions.get(&session_id))
785 .expect("No session map found");
786 assert!(matches!(session.state, SessionState::RevokedAt(_)));
787
788 assert!(server_txn.commit().is_ok());
789 }
790
791 #[qs_test]
792 async fn test_session_consistency_expire_when_cred_removed(server: &QueryServer) {
793 let curtime = duration_from_epoch_now();
794 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
795
796 let p = CryptoPolicy::minimum();
797 let cred = Credential::new_password_only(&p, "test_password").unwrap();
798 let cred_id = cred.uuid;
799
800 let mut server_txn = server.write(curtime).await.unwrap();
802
803 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
804
805 let e1 = entry_init!(
806 (Attribute::Class, EntryClass::Object.to_value()),
807 (Attribute::Class, EntryClass::Person.to_value()),
808 (Attribute::Class, EntryClass::Account.to_value()),
809 (Attribute::Name, Value::new_iname("testperson1")),
810 (Attribute::Uuid, Value::Uuid(tuuid)),
811 (Attribute::Description, Value::new_utf8s("testperson1")),
812 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
813 (
814 Attribute::PrimaryCredential,
815 Value::Cred("primary".to_string(), cred.clone())
816 )
817 );
818
819 let ce = CreateEvent::new_internal(vec![e1]);
820 assert!(server_txn.create(&ce).is_ok());
821
822 let session_id = Uuid::new_v4();
824 let issued_at = curtime_odt;
826 let issued_by = IdentityId::User(tuuid);
827 let scope = SessionScope::ReadOnly;
828
829 let session = Value::Session(
830 session_id,
831 Session {
832 label: "label".to_string(),
833 state: SessionState::NeverExpires,
834 issued_at,
837 issued_by,
839 cred_id,
840 scope,
843 type_: AuthType::Passkey,
844 },
845 );
846
847 let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
849
850 server_txn
851 .internal_modify(
852 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
853 &modlist,
854 )
855 .expect("Failed to modify user");
856
857 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
860
861 let session = entry
862 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
863 .and_then(|sessions| sessions.get(&session_id))
864 .expect("No session map found");
865 assert!(matches!(session.state, SessionState::NeverExpires));
866
867 assert!(server_txn.commit().is_ok());
868
869 let mut server_txn = server.write(curtime).await.unwrap();
871
872 let modlist = ModifyList::new_purge(Attribute::PrimaryCredential);
874
875 server_txn
876 .internal_modify(
877 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
878 &modlist,
879 )
880 .expect("Failed to modify user");
881
882 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
884
885 let session = entry
887 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
888 .and_then(|sessions| sessions.get(&session_id))
889 .expect("No session map found");
890 assert!(matches!(session.state, SessionState::RevokedAt(_)));
891
892 assert!(server_txn.commit().is_ok());
893 }
894}