kanidmd_lib/plugins/
session.rs

1//! This plugin maintains consistency of authenticated sessions on accounts.
2//!
3//! An example of this is that oauth2 sessions are child of user auth sessions,
4//! such than when the user auth session is terminated, then the corresponding
5//! oauth2 session should also be terminated.
6//!
7//! This plugin is also responsible for invaliding old sessions that are past
8//! their expiry.
9
10use 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        // We need to assert a number of properties. We must do these *in order*.
56        cand.iter_mut().try_for_each(|entry| {
57            // * If the session's credential is no longer on the account, we remove the session.
58            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                .chain(
75                    entry.get_ava_single_uuid(Attribute::OAuth2AccountCredentialUuid)
76                )
77                .collect();
78
79            let invalidate: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
80                .map(|sessions| {
81                    sessions.iter().filter_map(|(session_id, session)| {
82                        match &session.state {
83                            SessionState::RevokedAt(_) => {
84                                // Ignore, it's already revoked.
85                                None
86                            }
87                            SessionState::ExpiresAt(_) |
88                            SessionState::NeverExpires =>
89                                if !cred_ids.contains(&session.cred_id) {
90                                    info!(%session_id, "Revoking auth session whose issuing credential no longer exists");
91                                    Some(PartialValue::Refer(*session_id))
92                                } else {
93                                    None
94                                },
95                        }
96                    })
97                    .collect()
98                });
99
100            if let Some(invalidate) = invalidate.as_ref() {
101                entry.remove_avas(Attribute::UserAuthTokenSession, invalidate);
102            }
103
104            // * If a UAT is past its expiry, remove it.
105            let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
106                .map(|sessions| {
107                    sessions.iter().filter_map(|(session_id, session)| {
108                        trace!(?session_id, ?session);
109                        match &session.state {
110                            SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
111                                info!(%session_id, "Removing expired auth session");
112                                Some(PartialValue::Refer(*session_id))
113                            }
114                            _ => None,
115                        }
116                    })
117                    .collect()
118                });
119
120            if let Some(expired) = expired.as_ref() {
121                entry.remove_avas(Attribute::UserAuthTokenSession, expired);
122            }
123
124            // * If an oauth2 session is past it's expiry, remove it.
125            // * If an oauth2 session is past the grace window, and no parent session exists, remove it.
126            let oauth2_remove: Option<BTreeSet<_>> = entry.get_ava_as_oauth2session_map(Attribute::OAuth2Session).map(|oauth2_sessions| {
127                // If we have oauth2 sessions, we need to be able to lookup if sessions exist in the uat.
128                let sessions = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession);
129
130                oauth2_sessions.iter().filter_map(|(o2_session_id, session)| {
131                    trace!(?o2_session_id, ?session);
132                    match &session.state {
133                        SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
134                            info!(%o2_session_id, "Removing expired oauth2 session");
135                            Some(PartialValue::Refer(*o2_session_id))
136                        }
137                        SessionState::RevokedAt(_) => {
138                            // no-op, it's already revoked.
139                            trace!("Skip already revoked session");
140                            None
141                        }
142                        _ => {
143                            // Okay, now check the issued / grace time for parent enforcement.
144                                if sessions.map(|session_map| {
145                                    if let Some(parent_session_id) = session.parent.as_ref() {
146                                        // A parent session id exists - validate it exists in the account.
147                                        if let Some(parent_session) = session_map.get(parent_session_id) {
148                                            // Only match non-revoked sessions
149                                            !matches!(parent_session.state, SessionState::RevokedAt(_))
150                                        } else {
151                                            // not found
152                                            false
153                                        }
154                                    } else {
155                                        // The session specifically has no parent session and so is
156                                        // not bounded by it's presence.
157                                        true
158                                    }
159                                }).unwrap_or(false) {
160                                    // The parent exists and is still valid, go ahead
161                                    debug!("Parent session remains valid.");
162                                    None
163                                } else {
164                                    // Can't find the parent. Are we within grace window
165                                    if session.issued_at + AUTH_TOKEN_GRACE_WINDOW <= curtime_odt {
166                                        info!(%o2_session_id, parent_id = ?session.parent, "Removing orphaned oauth2 session");
167                                        Some(PartialValue::Refer(*o2_session_id))
168                                    } else {
169                                        // Grace window is still in effect
170                                        debug!("Not enforcing parent session consistency on session within grace window");
171                                        None
172                                    }
173
174                                }
175                        }
176                    }
177
178                })
179                .collect()
180            });
181
182            if let Some(oauth2_remove) = oauth2_remove.as_ref() {
183                entry.remove_avas(Attribute::OAuth2Session, oauth2_remove);
184            }
185
186            Ok(())
187        })
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use crate::prelude::*;
194
195    use crate::event::CreateEvent;
196    use crate::value::{AuthType, Oauth2Session, Session, SessionState};
197    use kanidm_proto::constants::OAUTH2_SCOPE_OPENID;
198    use std::time::Duration;
199    use time::OffsetDateTime;
200    use uuid::uuid;
201
202    use crate::credential::Credential;
203    use kanidm_lib_crypto::CryptoPolicy;
204
205    // Test expiry of old sessions
206
207    #[qs_test]
208    async fn test_session_consistency_expire_old_sessions(server: &QueryServer) {
209        let curtime = duration_from_epoch_now();
210        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
211
212        let p = CryptoPolicy::minimum();
213        let cred = Credential::new_password_only(&p, "test_password").unwrap();
214        let cred_id = cred.uuid;
215
216        let exp_curtime = curtime + Duration::from_secs(60);
217        let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
218
219        // Create a user
220        let mut server_txn = server.write(curtime).await.unwrap();
221
222        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
223
224        let e1 = entry_init!(
225            (Attribute::Class, EntryClass::Object.to_value()),
226            (Attribute::Class, EntryClass::Person.to_value()),
227            (Attribute::Class, EntryClass::Account.to_value()),
228            (Attribute::Name, Value::new_iname("testperson1")),
229            (Attribute::Uuid, Value::Uuid(tuuid)),
230            (Attribute::Description, Value::new_utf8s("testperson1")),
231            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
232            (
233                Attribute::PrimaryCredential,
234                Value::Cred("primary".to_string(), cred.clone())
235            )
236        );
237
238        let ce = CreateEvent::new_internal(vec![e1]);
239        assert!(server_txn.create(&ce).is_ok());
240
241        // Create a fake session.
242        let session_id = Uuid::new_v4();
243        let state = SessionState::ExpiresAt(exp_curtime_odt);
244        let issued_at = curtime_odt;
245        let issued_by = IdentityId::User(tuuid);
246        let scope = SessionScope::ReadOnly;
247
248        let session = Value::Session(
249            session_id,
250            Session {
251                label: "label".to_string(),
252                state,
253                // Need the other inner bits?
254                // for the gracewindow.
255                issued_at,
256                // Who actually created this?
257                issued_by,
258                cred_id,
259                // What is the access scope of this session? This is
260                // for auditing purposes.
261                scope,
262                type_: AuthType::Passkey,
263                ext_metadata: Default::default(),
264            },
265        );
266
267        // Mod the user
268        let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
269
270        server_txn
271            .internal_modify(
272                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
273                &modlist,
274            )
275            .expect("Failed to modify user");
276
277        // Still there
278
279        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
280
281        let session = entry
282            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
283            .and_then(|sessions| sessions.get(&session_id))
284            .expect("No session map found");
285        assert!(matches!(session.state, SessionState::ExpiresAt(_)));
286
287        assert!(server_txn.commit().is_ok());
288        let mut server_txn = server.write(exp_curtime).await.unwrap();
289
290        // Mod again - anything will do.
291        let modlist = ModifyList::new_purge_and_set(
292            Attribute::Description,
293            Value::new_utf8s("test person 1 change"),
294        );
295
296        server_txn
297            .internal_modify(
298                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
299                &modlist,
300            )
301            .expect("Failed to modify user");
302
303        // Session gone.
304        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
305
306        // We get the attribute and have to check it's now in a revoked state.
307        let session = entry
308            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
309            .and_then(|sessions| sessions.get(&session_id))
310            .expect("No session map found");
311        assert!(matches!(session.state, SessionState::RevokedAt(_)));
312
313        assert!(server_txn.commit().is_ok());
314    }
315
316    // Test expiry of old oauth2 sessions
317    #[qs_test]
318    async fn test_session_consistency_oauth2_expiry_cleanup(server: &QueryServer) {
319        let curtime = duration_from_epoch_now();
320        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
321
322        let p = CryptoPolicy::minimum();
323        let cred = Credential::new_password_only(&p, "test_password").unwrap();
324        let cred_id = cred.uuid;
325
326        // Set exp to gracewindow.
327        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
328        let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
329
330        // Create a user
331        let mut server_txn = server.write(curtime).await.unwrap();
332
333        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
334        let rs_uuid = Uuid::new_v4();
335
336        let e1 = entry_init!(
337            (Attribute::Class, EntryClass::Object.to_value()),
338            (Attribute::Class, EntryClass::Person.to_value()),
339            (Attribute::Class, EntryClass::Account.to_value()),
340            (Attribute::Name, Value::new_iname("testperson1")),
341            (Attribute::Uuid, Value::Uuid(tuuid)),
342            (Attribute::Description, Value::new_utf8s("testperson1")),
343            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
344            (
345                Attribute::PrimaryCredential,
346                Value::Cred("primary".to_string(), cred.clone())
347            )
348        );
349
350        let e2 = entry_init!(
351            (Attribute::Class, EntryClass::Object.to_value()),
352            (Attribute::Class, EntryClass::Account.to_value()),
353            (
354                Attribute::Class,
355                EntryClass::OAuth2ResourceServer.to_value()
356            ),
357            (
358                Attribute::Class,
359                EntryClass::OAuth2ResourceServerBasic.to_value()
360            ),
361            (Attribute::Uuid, Value::Uuid(rs_uuid)),
362            (Attribute::Name, Value::new_iname("test_resource_server")),
363            (
364                Attribute::DisplayName,
365                Value::new_utf8s("test_resource_server")
366            ),
367            (
368                Attribute::OAuth2RsOriginLanding,
369                Value::new_url_s("https://demo.example.com").unwrap()
370            ),
371            // System admins
372            (
373                Attribute::OAuth2RsScopeMap,
374                Value::new_oauthscopemap(
375                    UUID_IDM_ALL_ACCOUNTS,
376                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
377                )
378                .expect("invalid oauthscope")
379            )
380        );
381
382        let ce = CreateEvent::new_internal(vec![e1, e2]);
383        assert!(server_txn.create(&ce).is_ok());
384
385        // Create a fake session and oauth2 session.
386
387        let session_id = Uuid::new_v4();
388        let parent_id = Uuid::new_v4();
389        let state = SessionState::ExpiresAt(exp_curtime_odt);
390        let issued_at = curtime_odt;
391        let issued_by = IdentityId::User(tuuid);
392        let scope = SessionScope::ReadOnly;
393
394        // Mod the user
395        let modlist = modlist!([
396            Modify::Present(
397                "oauth2_session".into(),
398                Value::Oauth2Session(
399                    session_id,
400                    Oauth2Session {
401                        parent: Some(parent_id),
402                        // Set to the exp window.
403                        state,
404                        issued_at,
405                        rs_uuid,
406                    },
407                )
408            ),
409            Modify::Present(
410                Attribute::UserAuthTokenSession,
411                Value::Session(
412                    parent_id,
413                    Session {
414                        label: "label".to_string(),
415                        // Note we set the exp to None so we are not removing based on removal of the parent.
416                        state: SessionState::NeverExpires,
417                        // Need the other inner bits?
418                        // for the gracewindow.
419                        issued_at,
420                        // Who actually created this?
421                        issued_by,
422                        cred_id,
423                        // What is the access scope of this session? This is
424                        // for auditing purposes.
425                        scope,
426                        type_: AuthType::Passkey,
427                        ext_metadata: Default::default(),
428                    },
429                )
430            ),
431        ]);
432
433        server_txn
434            .internal_modify(
435                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
436                &modlist,
437            )
438            .expect("Failed to modify user");
439
440        // Still there
441
442        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
443
444        let session = entry
445            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
446            .and_then(|sessions| sessions.get(&parent_id))
447            .expect("No session map found");
448        assert!(matches!(session.state, SessionState::NeverExpires));
449
450        let session = entry
451            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
452            .and_then(|sessions| sessions.get(&session_id))
453            .expect("No session map found");
454        assert!(matches!(session.state, SessionState::ExpiresAt(_)));
455
456        assert!(server_txn.commit().is_ok());
457
458        // Note as we are now past exp time, the oauth2 session will be removed, but the uat session
459        // will remain.
460        let mut server_txn = server.write(exp_curtime).await.unwrap();
461
462        // Mod again - anything will do.
463        let modlist = ModifyList::new_purge_and_set(
464            Attribute::Description,
465            Value::new_utf8s("test person 1 change"),
466        );
467
468        server_txn
469            .internal_modify(
470                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
471                &modlist,
472            )
473            .expect("Failed to modify user");
474
475        // Session gone.
476        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
477
478        // Note the uat is still present
479        let session = entry
480            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
481            .and_then(|sessions| sessions.get(&parent_id))
482            .expect("No session map found");
483        assert!(matches!(session.state, SessionState::NeverExpires));
484
485        let session = entry
486            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
487            .and_then(|sessions| sessions.get(&session_id))
488            .expect("No session map found");
489        assert!(matches!(session.state, SessionState::RevokedAt(_)));
490
491        assert!(server_txn.commit().is_ok());
492    }
493
494    // test removal of a session removes related oauth2 sessions.
495    #[qs_test]
496    async fn test_session_consistency_oauth2_removed_by_parent(server: &QueryServer) {
497        let curtime = duration_from_epoch_now();
498        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
499        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
500
501        let p = CryptoPolicy::minimum();
502        let cred = Credential::new_password_only(&p, "test_password").unwrap();
503        let cred_id = cred.uuid;
504
505        // Create a user
506        let mut server_txn = server.write(curtime).await.unwrap();
507
508        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
509        let rs_uuid = Uuid::new_v4();
510
511        let e1 = entry_init!(
512            (Attribute::Class, EntryClass::Object.to_value()),
513            (Attribute::Class, EntryClass::Person.to_value()),
514            (Attribute::Class, EntryClass::Account.to_value()),
515            (Attribute::Name, Value::new_iname("testperson1")),
516            (Attribute::Uuid, Value::Uuid(tuuid)),
517            (Attribute::Description, Value::new_utf8s("testperson1")),
518            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
519            (
520                Attribute::PrimaryCredential,
521                Value::Cred("primary".to_string(), cred.clone())
522            )
523        );
524
525        let e2 = entry_init!(
526            (Attribute::Class, EntryClass::Object.to_value()),
527            (Attribute::Class, EntryClass::Account.to_value()),
528            (
529                Attribute::Class,
530                EntryClass::OAuth2ResourceServer.to_value()
531            ),
532            (
533                Attribute::Class,
534                EntryClass::OAuth2ResourceServerBasic.to_value()
535            ),
536            (Attribute::Uuid, Value::Uuid(rs_uuid)),
537            (Attribute::Name, Value::new_iname("test_resource_server")),
538            (
539                Attribute::DisplayName,
540                Value::new_utf8s("test_resource_server")
541            ),
542            (
543                Attribute::OAuth2RsOriginLanding,
544                Value::new_url_s("https://demo.example.com").unwrap()
545            ),
546            // System admins
547            (
548                Attribute::OAuth2RsScopeMap,
549                Value::new_oauthscopemap(
550                    UUID_IDM_ALL_ACCOUNTS,
551                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
552                )
553                .expect("invalid oauthscope")
554            )
555        );
556
557        let ce = CreateEvent::new_internal(vec![e1, e2]);
558        assert!(server_txn.create(&ce).is_ok());
559
560        // Create a fake session and oauth2 session.
561
562        let session_id = Uuid::new_v4();
563        let parent_id = Uuid::new_v4();
564        let issued_at = curtime_odt;
565        let issued_by = IdentityId::User(tuuid);
566        let scope = SessionScope::ReadOnly;
567
568        // Mod the user
569        let modlist = modlist!([
570            Modify::Present(
571                "oauth2_session".into(),
572                Value::Oauth2Session(
573                    session_id,
574                    Oauth2Session {
575                        parent: Some(parent_id),
576                        // Note we set the exp to None so we are not removing based on exp
577                        state: SessionState::NeverExpires,
578                        issued_at,
579                        rs_uuid,
580                    },
581                )
582            ),
583            Modify::Present(
584                Attribute::UserAuthTokenSession,
585                Value::Session(
586                    parent_id,
587                    Session {
588                        label: "label".to_string(),
589                        // Note we set the exp to None so we are not removing based on removal of the parent.
590                        state: SessionState::NeverExpires,
591                        // Need the other inner bits?
592                        // for the gracewindow.
593                        issued_at,
594                        // Who actually created this?
595                        issued_by,
596                        cred_id,
597                        // What is the access scope of this session? This is
598                        // for auditing purposes.
599                        scope,
600                        type_: AuthType::Passkey,
601                        ext_metadata: Default::default(),
602                    },
603                )
604            ),
605        ]);
606
607        server_txn
608            .internal_modify(
609                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
610                &modlist,
611            )
612            .expect("Failed to modify user");
613
614        // Still there
615
616        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
617
618        let session = entry
619            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
620            .and_then(|sessions| sessions.get(&parent_id))
621            .expect("No session map found");
622        assert!(matches!(session.state, SessionState::NeverExpires));
623
624        let session = entry
625            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
626            .and_then(|sessions| sessions.get(&session_id))
627            .expect("No session map found");
628        assert!(matches!(session.state, SessionState::NeverExpires));
629
630        // We need the time to be past grace_window.
631        assert!(server_txn.commit().is_ok());
632        let mut server_txn = server.write(exp_curtime).await.unwrap();
633
634        // Mod again - remove the parent session.
635        let modlist = ModifyList::new_remove(
636            Attribute::UserAuthTokenSession,
637            PartialValue::Refer(parent_id),
638        );
639
640        server_txn
641            .internal_modify(
642                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
643                &modlist,
644            )
645            .expect("Failed to modify user");
646
647        // Session gone.
648        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
649
650        // Note the uat is removed
651        let session = entry
652            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
653            .and_then(|sessions| sessions.get(&parent_id))
654            .expect("No session map found");
655        assert!(matches!(session.state, SessionState::RevokedAt(_)));
656
657        // The oauth2 session is also removed.
658        let session = entry
659            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
660            .and_then(|sessions| sessions.get(&session_id))
661            .expect("No session map found");
662        assert!(matches!(session.state, SessionState::RevokedAt(_)));
663
664        assert!(server_txn.commit().is_ok());
665    }
666
667    // Test if an oauth2 session exists, the grace window passes and it's UAT doesn't exist.
668    #[qs_test]
669    async fn test_session_consistency_oauth2_grace_window_past(server: &QueryServer) {
670        let curtime = duration_from_epoch_now();
671        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
672
673        // Set exp to gracewindow.
674        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
675        // let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
676
677        // Create a user
678        let mut server_txn = server.write(curtime).await.unwrap();
679
680        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
681        let rs_uuid = Uuid::new_v4();
682
683        let e1 = entry_init!(
684            (Attribute::Class, EntryClass::Object.to_value()),
685            (Attribute::Class, EntryClass::Person.to_value()),
686            (Attribute::Class, EntryClass::Account.to_value()),
687            (Attribute::Name, Value::new_iname("testperson1")),
688            (Attribute::Uuid, Value::Uuid(tuuid)),
689            (Attribute::Description, Value::new_utf8s("testperson1")),
690            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
691        );
692
693        let e2 = entry_init!(
694            (Attribute::Class, EntryClass::Object.to_value()),
695            (Attribute::Class, EntryClass::Account.to_value()),
696            (
697                Attribute::Class,
698                EntryClass::OAuth2ResourceServer.to_value()
699            ),
700            (
701                Attribute::Class,
702                EntryClass::OAuth2ResourceServerBasic.to_value()
703            ),
704            (Attribute::Uuid, Value::Uuid(rs_uuid)),
705            (Attribute::Name, Value::new_iname("test_resource_server")),
706            (
707                Attribute::DisplayName,
708                Value::new_utf8s("test_resource_server")
709            ),
710            (
711                Attribute::OAuth2RsOriginLanding,
712                Value::new_url_s("https://demo.example.com").unwrap()
713            ),
714            // System admins
715            (
716                Attribute::OAuth2RsScopeMap,
717                Value::new_oauthscopemap(
718                    UUID_IDM_ALL_ACCOUNTS,
719                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
720                )
721                .expect("invalid oauthscope")
722            )
723        );
724
725        let ce = CreateEvent::new_internal(vec![e1, e2]);
726        assert!(server_txn.create(&ce).is_ok());
727
728        // Create a fake session.
729        let session_id = Uuid::new_v4();
730        let parent = Uuid::new_v4();
731        let issued_at = curtime_odt;
732
733        let session = Value::Oauth2Session(
734            session_id,
735            Oauth2Session {
736                parent: Some(parent),
737                // Note we set the exp to None so we are asserting the removal is due to the lack
738                // of the parent session.
739                state: SessionState::NeverExpires,
740                issued_at,
741                rs_uuid,
742            },
743        );
744
745        // Mod the user
746        let modlist = ModifyList::new_append(Attribute::OAuth2Session, session);
747
748        server_txn
749            .internal_modify(
750                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
751                &modlist,
752            )
753            .expect("Failed to modify user");
754
755        // Still there
756
757        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
758
759        let session = entry
760            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
761            .and_then(|sessions| sessions.get(&session_id))
762            .expect("No session map found");
763        assert!(matches!(session.state, SessionState::NeverExpires));
764
765        assert!(server_txn.commit().is_ok());
766
767        // Note the exp_curtime now is past the gracewindow. This will trigger
768        // consistency to purge the un-matched session.
769        let mut server_txn = server.write(exp_curtime).await.unwrap();
770
771        // Mod again - anything will do.
772        let modlist = ModifyList::new_purge_and_set(
773            Attribute::Description,
774            Value::new_utf8s("test person 1 change"),
775        );
776
777        server_txn
778            .internal_modify(
779                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
780                &modlist,
781            )
782            .expect("Failed to modify user");
783
784        // Session gone.
785        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
786
787        // Note it's a not condition now.
788        let session = entry
789            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
790            .and_then(|sessions| sessions.get(&session_id))
791            .expect("No session map found");
792        assert!(matches!(session.state, SessionState::RevokedAt(_)));
793
794        assert!(server_txn.commit().is_ok());
795    }
796
797    #[qs_test]
798    async fn test_session_consistency_expire_when_cred_removed(server: &QueryServer) {
799        let curtime = duration_from_epoch_now();
800        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
801
802        let p = CryptoPolicy::minimum();
803        let cred = Credential::new_password_only(&p, "test_password").unwrap();
804        let cred_id = cred.uuid;
805
806        // Create a user
807        let mut server_txn = server.write(curtime).await.unwrap();
808
809        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
810
811        let e1 = entry_init!(
812            (Attribute::Class, EntryClass::Object.to_value()),
813            (Attribute::Class, EntryClass::Person.to_value()),
814            (Attribute::Class, EntryClass::Account.to_value()),
815            (Attribute::Name, Value::new_iname("testperson1")),
816            (Attribute::Uuid, Value::Uuid(tuuid)),
817            (Attribute::Description, Value::new_utf8s("testperson1")),
818            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
819            (
820                Attribute::PrimaryCredential,
821                Value::Cred("primary".to_string(), cred.clone())
822            )
823        );
824
825        let ce = CreateEvent::new_internal(vec![e1]);
826        assert!(server_txn.create(&ce).is_ok());
827
828        // Create a fake session.
829        let session_id = Uuid::new_v4();
830        // No expiry!
831        let issued_at = curtime_odt;
832        let issued_by = IdentityId::User(tuuid);
833        let scope = SessionScope::ReadOnly;
834
835        let session = Value::Session(
836            session_id,
837            Session {
838                label: "label".to_string(),
839                state: SessionState::NeverExpires,
840                // Need the other inner bits?
841                // for the gracewindow.
842                issued_at,
843                // Who actually created this?
844                issued_by,
845                cred_id,
846                // What is the access scope of this session? This is
847                // for auditing purposes.
848                scope,
849                type_: AuthType::Passkey,
850                ext_metadata: Default::default(),
851            },
852        );
853
854        // Mod the user
855        let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
856
857        server_txn
858            .internal_modify(
859                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
860                &modlist,
861            )
862            .expect("Failed to modify user");
863
864        // Still there
865
866        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
867
868        let session = entry
869            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
870            .and_then(|sessions| sessions.get(&session_id))
871            .expect("No session map found");
872        assert!(matches!(session.state, SessionState::NeverExpires));
873
874        assert!(server_txn.commit().is_ok());
875
876        // Notice we keep the time the same for the txn.
877        let mut server_txn = server.write(curtime).await.unwrap();
878
879        // Remove the primary credential
880        let modlist = ModifyList::new_purge(Attribute::PrimaryCredential);
881
882        server_txn
883            .internal_modify(
884                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
885                &modlist,
886            )
887            .expect("Failed to modify user");
888
889        // Session gone.
890        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
891
892        // Note it's a not condition now.
893        let session = entry
894            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
895            .and_then(|sessions| sessions.get(&session_id))
896            .expect("No session map found");
897        assert!(matches!(session.state, SessionState::RevokedAt(_)));
898
899        assert!(server_txn.commit().is_ok());
900    }
901}