kanidmd_lib/valueset/
session.rs

1use crate::be::dbvalue::{
2    DbCidV1, DbValueAccessScopeV1, DbValueApiToken, DbValueApiTokenScopeV1, DbValueAuthTypeV1,
3    DbValueIdentityId, DbValueOauth2Session, DbValueSession, DbValueSessionExtMetadataV1,
4    DbValueSessionStateV1,
5};
6use crate::prelude::*;
7use crate::repl::cid::Cid;
8use crate::schema::SchemaAttribute;
9use crate::value::{
10    ApiToken, ApiTokenScope, AuthType, Oauth2Session, Session, SessionExtMetadata, SessionScope,
11    SessionState,
12};
13use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ValueSet};
14use kanidm_proto::scim_v1::server::ScimApiToken;
15use kanidm_proto::scim_v1::server::ScimAuthSession;
16use kanidm_proto::scim_v1::server::ScimOAuth2Session;
17use std::collections::btree_map::Entry as BTreeEntry;
18use std::collections::BTreeMap;
19use time::OffsetDateTime;
20
21#[derive(Debug, Clone)]
22pub struct ValueSetSession {
23    map: BTreeMap<Uuid, Session>,
24}
25
26impl ValueSetSession {
27    pub fn new(u: Uuid, m: Session) -> Box<Self> {
28        let mut map = BTreeMap::new();
29        map.insert(u, m);
30        Box::new(ValueSetSession { map })
31    }
32
33    pub fn push(&mut self, u: Uuid, m: Session) -> bool {
34        self.map.insert(u, m).is_none()
35    }
36
37    fn to_vec_dbvs(&self) -> Vec<DbValueSession> {
38        self.map
39            .iter()
40            .map(|(u, m)| DbValueSession::V4 {
41                refer: *u,
42                label: m.label.clone(),
43
44                state: match &m.state {
45                    SessionState::ExpiresAt(odt) => {
46                        debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
47                        #[allow(clippy::expect_used)]
48                        odt.format(&Rfc3339)
49                            .map(DbValueSessionStateV1::ExpiresAt)
50                            .expect("Failed to format timestamp into RFC3339!")
51                    }
52                    SessionState::NeverExpires => DbValueSessionStateV1::Never,
53                    SessionState::RevokedAt(c) => DbValueSessionStateV1::RevokedAt(DbCidV1 {
54                        server_id: c.s_uuid,
55                        timestamp: c.ts,
56                    }),
57                },
58
59                issued_at: {
60                    debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
61                    #[allow(clippy::expect_used)]
62                    m.issued_at
63                        .format(&Rfc3339)
64                        .expect("Failed to format timestamp into RFC3339!")
65                },
66                issued_by: match m.issued_by {
67                    IdentityId::Internal => DbValueIdentityId::V1Internal,
68                    IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
69                    IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
70                },
71                cred_id: m.cred_id,
72                scope: match m.scope {
73                    SessionScope::ReadOnly => DbValueAccessScopeV1::ReadOnly,
74                    SessionScope::ReadWrite => DbValueAccessScopeV1::ReadWrite,
75                    SessionScope::PrivilegeCapable => DbValueAccessScopeV1::PrivilegeCapable,
76                    SessionScope::Synchronise => DbValueAccessScopeV1::Synchronise,
77                },
78                type_: match m.type_ {
79                    AuthType::Anonymous => DbValueAuthTypeV1::Anonymous,
80                    AuthType::Password => DbValueAuthTypeV1::Password,
81                    AuthType::GeneratedPassword => DbValueAuthTypeV1::GeneratedPassword,
82                    AuthType::PasswordTotp => DbValueAuthTypeV1::PasswordTotp,
83                    AuthType::PasswordBackupCode => DbValueAuthTypeV1::PasswordBackupCode,
84                    AuthType::PasswordSecurityKey => DbValueAuthTypeV1::PasswordSecurityKey,
85                    AuthType::Passkey => DbValueAuthTypeV1::Passkey,
86                    AuthType::AttestedPasskey => DbValueAuthTypeV1::AttestedPasskey,
87                    AuthType::OAuth2Trust => DbValueAuthTypeV1::OAuth2Trust,
88                },
89                ext_metadata: match &m.ext_metadata {
90                    SessionExtMetadata::None => DbValueSessionExtMetadataV1::None,
91                    SessionExtMetadata::OAuth2 {
92                        access_expires_at,
93                        access_token,
94                        refresh_token,
95                    } => DbValueSessionExtMetadataV1::OAuth2 {
96                        access_expires_at: *access_expires_at,
97                        access_token: access_token.clone(),
98                        refresh_token: refresh_token.clone(),
99                    },
100                },
101            })
102            .collect()
103    }
104
105    fn from_dbv_iter<'a>(
106        iter: impl Iterator<Item = &'a DbValueSession>,
107    ) -> Result<ValueSet, OperationError> {
108        let map = iter
109            .filter_map(|dbv| {
110                match dbv {
111                    // We need to ignore all older session records as they lack the AuthType
112                    // record which prevents re-auth working.
113                    DbValueSession::V1 { .. }
114                    | DbValueSession::V2 { .. }
115                    | DbValueSession::V3 { .. } => None,
116                    DbValueSession::V4 {
117                        refer,
118                        label,
119                        state,
120                        issued_at,
121                        issued_by,
122                        cred_id,
123                        scope,
124                        type_,
125                        ext_metadata,
126                    } => {
127                        // Convert things.
128                        let issued_at = OffsetDateTime::parse(issued_at, &Rfc3339)
129                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
130                            .map_err(|e| {
131                                admin_error!(
132                                    ?e,
133                                    "Invalidating session {} due to invalid issued_at timestamp",
134                                    refer
135                                )
136                            })
137                            .ok()?;
138
139                        let state = match state {
140                            DbValueSessionStateV1::ExpiresAt(e_inner) => {
141                                OffsetDateTime::parse(e_inner, &Rfc3339)
142                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
143                                    .map(SessionState::ExpiresAt)
144                                    .map_err(|e| {
145                                        admin_error!(
146                                        ?e,
147                                        "Invalidating session {} due to invalid expiry timestamp",
148                                        refer
149                                    )
150                                    })
151                                    .ok()?
152                            }
153                            DbValueSessionStateV1::Never => SessionState::NeverExpires,
154                            DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
155                                s_uuid: dc.server_id,
156                                ts: dc.timestamp,
157                            }),
158                        };
159
160                        let issued_by = match issued_by {
161                            DbValueIdentityId::V1Internal => IdentityId::Internal,
162                            DbValueIdentityId::V1Uuid(u) => IdentityId::User(*u),
163                            DbValueIdentityId::V1Sync(u) => IdentityId::Synch(*u),
164                        };
165
166                        let scope = match scope {
167                            DbValueAccessScopeV1::IdentityOnly | DbValueAccessScopeV1::ReadOnly => {
168                                SessionScope::ReadOnly
169                            }
170                            DbValueAccessScopeV1::ReadWrite => SessionScope::ReadWrite,
171                            DbValueAccessScopeV1::PrivilegeCapable => {
172                                SessionScope::PrivilegeCapable
173                            }
174                            DbValueAccessScopeV1::Synchronise => SessionScope::Synchronise,
175                        };
176
177                        let type_ = match type_ {
178                            DbValueAuthTypeV1::Anonymous => AuthType::Anonymous,
179                            DbValueAuthTypeV1::Password => AuthType::Password,
180                            DbValueAuthTypeV1::GeneratedPassword => AuthType::GeneratedPassword,
181                            DbValueAuthTypeV1::PasswordTotp => AuthType::PasswordTotp,
182                            DbValueAuthTypeV1::PasswordBackupCode => AuthType::PasswordBackupCode,
183                            DbValueAuthTypeV1::PasswordSecurityKey => AuthType::PasswordSecurityKey,
184                            DbValueAuthTypeV1::Passkey => AuthType::Passkey,
185                            DbValueAuthTypeV1::AttestedPasskey => AuthType::AttestedPasskey,
186                            DbValueAuthTypeV1::OAuth2Trust => AuthType::OAuth2Trust,
187                        };
188
189                        let ext_metadata = match ext_metadata {
190                            DbValueSessionExtMetadataV1::None => SessionExtMetadata::None,
191                            DbValueSessionExtMetadataV1::OAuth2 {
192                                access_expires_at,
193                                access_token,
194                                refresh_token,
195                            } => SessionExtMetadata::OAuth2 {
196                                access_expires_at: *access_expires_at,
197                                access_token: access_token.clone(),
198                                refresh_token: refresh_token.clone(),
199                            },
200                        };
201
202                        Some((
203                            *refer,
204                            Session {
205                                label: label.clone(),
206                                state,
207                                issued_at,
208                                issued_by,
209                                cred_id: *cred_id,
210                                scope,
211                                type_,
212                                ext_metadata,
213                            },
214                        ))
215                    }
216                }
217            })
218            .collect();
219        Ok(Box::new(ValueSetSession { map }))
220    }
221
222    pub fn from_dbvs2(data: &[DbValueSession]) -> Result<ValueSet, OperationError> {
223        Self::from_dbv_iter(data.iter())
224    }
225
226    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
227    // types, and tuples are always foreign.
228    #[allow(clippy::should_implement_trait)]
229    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
230    where
231        T: IntoIterator<Item = (Uuid, Session)>,
232    {
233        let map = iter.into_iter().collect();
234        Some(Box::new(ValueSetSession { map }))
235    }
236}
237
238impl ValueSetT for ValueSetSession {
239    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
240        match value {
241            Value::Session(u, m) => {
242                if let BTreeEntry::Vacant(e) = self.map.entry(u) {
243                    e.insert(m);
244                    Ok(true)
245                } else {
246                    Ok(false)
247                }
248            }
249            _ => Err(OperationError::InvalidValueState),
250        }
251    }
252
253    fn clear(&mut self) {
254        self.map.clear();
255    }
256
257    fn remove(&mut self, pv: &PartialValue, cid: &Cid) -> bool {
258        match pv {
259            PartialValue::Refer(u) => {
260                if let Some(session) = self.map.get_mut(u) {
261                    if !matches!(session.state, SessionState::RevokedAt(_)) {
262                        session.state = SessionState::RevokedAt(cid.clone());
263                        true
264                    } else {
265                        false
266                    }
267                } else {
268                    false
269                }
270            }
271            _ => false,
272        }
273    }
274
275    fn purge(&mut self, cid: &Cid) -> bool {
276        for (_uuid, session) in self.map.iter_mut() {
277            // Send them all to the shadow realm
278            if !matches!(session.state, SessionState::RevokedAt(_)) {
279                session.state = SessionState::RevokedAt(cid.clone())
280            }
281        }
282        // Can't be purged since we need the cid's of revoked to persist.
283        false
284    }
285
286    fn trim(&mut self, trim_cid: &Cid) {
287        // There might be a neater way to do this with less iterations. The problem
288        // is we can't just check on what was in b/older, because then we miss
289        // trimmable content from the local map. So once the merge is complete we
290        // do a pass for trim.
291        self.map.retain(|_, session| {
292            match &session.state {
293                SessionState::RevokedAt(cid) if cid < trim_cid => {
294                    // This value is past the replication trim window and can now safely
295                    // be removed
296                    false
297                }
298                // Retain all else
299                _ => true,
300            }
301        });
302
303        // Now, assert that there are fewer or equal sessions to the limit.
304        if self.map.len() > SESSION_MAXIMUM {
305            // At this point we will force a number of sessions to be removed. This
306            // is replication safe since other replicas will also be performing
307            // the same operation on merge, since we trim by session issuance order.
308
309            // This is a "slow path". This is because we optimise session storage
310            // based on fast session lookup, so now we need to actually create an
311            // index based on time. We need to also clone here since we need to mutate
312            // self.map which would violate mut/imut.
313
314            warn!(
315                "entry has exceeded session_maximum limit ({:?}), force trimming will occur",
316                SESSION_MAXIMUM
317            );
318
319            let time_idx: BTreeMap<OffsetDateTime, Uuid> = self
320                .map
321                .iter()
322                .map(|(session_id, session)| (session.issued_at, *session_id))
323                .collect();
324
325            let to_take = self.map.len() - SESSION_MAXIMUM;
326
327            time_idx.values().take(to_take).for_each(|session_id| {
328                warn!(?session_id, "force trimmed");
329                self.map.remove(session_id);
330            });
331        }
332        // And we're done.
333    }
334
335    fn contains(&self, pv: &PartialValue) -> bool {
336        match pv {
337            PartialValue::Refer(u) => self.map.contains_key(u),
338            _ => false,
339        }
340    }
341
342    fn substring(&self, _pv: &PartialValue) -> bool {
343        false
344    }
345
346    fn startswith(&self, _pv: &PartialValue) -> bool {
347        false
348    }
349
350    fn endswith(&self, _pv: &PartialValue) -> bool {
351        false
352    }
353
354    fn lessthan(&self, _pv: &PartialValue) -> bool {
355        false
356    }
357
358    fn len(&self) -> usize {
359        self.map.len()
360    }
361
362    fn generate_idx_eq_keys(&self) -> Vec<String> {
363        self.map
364            .keys()
365            .map(|u| u.as_hyphenated().to_string())
366            .collect()
367    }
368
369    fn syntax(&self) -> SyntaxType {
370        SyntaxType::Session
371    }
372
373    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
374        true
375    }
376
377    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
378        Box::new(
379            self.map
380                .iter()
381                .map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
382        )
383    }
384
385    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
386        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
387            self.map
388                .iter()
389                .map(|(session_id, session)| {
390                    let (expires, revoked) = match &session.state {
391                        SessionState::ExpiresAt(odt) => (Some(*odt), None),
392                        SessionState::NeverExpires => (None, None),
393                        SessionState::RevokedAt(cid) => {
394                            let odt: OffsetDateTime = cid.into();
395                            (None, Some(odt))
396                        }
397                    };
398
399                    ScimAuthSession {
400                        id: *session_id,
401                        expires,
402                        revoked,
403
404                        issued_at: session.issued_at,
405                        issued_by: Uuid::from(&session.issued_by),
406                        credential_id: session.cred_id,
407                        auth_type: session.type_.to_string(),
408                        session_scope: session.scope.to_string(),
409                    }
410                })
411                .collect::<Vec<_>>(),
412        )))
413    }
414
415    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
416        DbValueSetV2::Session(self.to_vec_dbvs())
417    }
418
419    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
420        Box::new(self.map.keys().cloned().map(PartialValue::Refer))
421    }
422
423    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
424        Box::new(self.map.iter().map(|(u, m)| Value::Session(*u, m.clone())))
425    }
426
427    fn equal(&self, other: &ValueSet) -> bool {
428        if let Some(other) = other.as_session_map() {
429            &self.map == other
430        } else {
431            debug_assert!(false);
432            false
433        }
434    }
435
436    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
437        if let Some(b) = other.as_session_map() {
438            // We can't just do merge maps here, we have to be aware of the
439            // session.state value and what it currently is set to.
440            for (k_other, v_other) in b.iter() {
441                if let Some(v_self) = self.map.get_mut(k_other) {
442                    // We only update if greater. This is where RevokedAt
443                    // always proceeds other states, and lower revoked
444                    // cids will always take effect.
445                    if v_other.state > v_self.state {
446                        *v_self = v_other.clone();
447                    }
448                } else {
449                    // Not present, just insert.
450                    self.map.insert(*k_other, v_other.clone());
451                }
452            }
453            Ok(())
454        } else {
455            debug_assert!(false);
456            Err(OperationError::InvalidValueState)
457        }
458    }
459
460    fn as_session_map(&self) -> Option<&BTreeMap<Uuid, Session>> {
461        Some(&self.map)
462    }
463
464    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
465        // This is what ties us as a type that can be refint checked.
466        Some(Box::new(self.map.keys().copied()))
467    }
468
469    fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
470        // If the older value has a different type - return nothing, we
471        // just take the newer value.
472        let b = older.as_session_map()?;
473        // We can't just do merge maps here, we have to be aware of the
474        // session.state value and what it currently is set to.
475        let mut map = self.map.clone();
476        for (k_other, v_other) in b.iter() {
477            if let Some(v_self) = map.get_mut(k_other) {
478                // We only update if lower. This is where RevokedAt
479                // always proceeds other states, and lower revoked
480                // cids will always take effect.
481                if v_other.state > v_self.state {
482                    *v_self = v_other.clone();
483                }
484            } else {
485                // Not present, just insert.
486                map.insert(*k_other, v_other.clone());
487            }
488        }
489
490        let mut vs = Box::new(ValueSetSession { map });
491
492        vs.trim(trim_cid);
493
494        Some(vs)
495    }
496}
497
498// == oauth2 session ==
499
500#[derive(Debug, Clone)]
501pub struct ValueSetOauth2Session {
502    map: BTreeMap<Uuid, Oauth2Session>,
503    // this is a "filter" to tell us if as rs_id is used anywhere
504    // in this set. The reason is so that we don't do O(n) searches
505    // on a refer if it's not in this set. The alternate approach is
506    // an index on these maps, but its more work to maintain for a rare
507    // situation where we actually want to query rs_uuid -> sessions.
508    rs_filter: u128,
509}
510
511impl ValueSetOauth2Session {
512    pub fn new(u: Uuid, m: Oauth2Session) -> Box<Self> {
513        let mut map = BTreeMap::new();
514        let rs_filter = m.rs_uuid.as_u128();
515        map.insert(u, m);
516        Box::new(ValueSetOauth2Session { map, rs_filter })
517    }
518
519    pub fn push(&mut self, u: Uuid, m: Oauth2Session) -> bool {
520        self.rs_filter |= m.rs_uuid.as_u128();
521        self.map.insert(u, m).is_none()
522    }
523
524    pub fn from_dbvs2(data: Vec<DbValueOauth2Session>) -> Result<ValueSet, OperationError> {
525        let mut rs_filter = u128::MIN;
526        let map = data
527            .into_iter()
528            .filter_map(|dbv| {
529                match dbv {
530                    DbValueOauth2Session::V1 {
531                        refer,
532                        parent,
533                        expiry,
534                        issued_at,
535                        rs_uuid,
536                    } => {
537                        // Convert things.
538                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
539                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
540                            .map_err(|e| {
541                                admin_error!(
542                                    ?e,
543                                    "Invalidating session {} due to invalid issued_at timestamp",
544                                    refer
545                                )
546                            })
547                            .ok()?;
548
549                        // This is a bit annoying. In the case we can't parse the optional
550                        // expiry, we need to NOT return the session so that it's immediately
551                        // invalidated. To do this we have to invert some of the options involved
552                        // here.
553                        let expiry = expiry
554                            .map(|e_inner| {
555                                OffsetDateTime::parse(&e_inner, &Rfc3339)
556                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
557                                // We now have an
558                                // Option<Result<ODT, _>>
559                            })
560                            .transpose()
561                            // Result<Option<ODT>, _>
562                            .map_err(|e| {
563                                admin_error!(
564                                    ?e,
565                                    "Invalidating session {} due to invalid expiry timestamp",
566                                    refer
567                                )
568                            })
569                            // Option<Option<ODT>>
570                            .ok()?;
571
572                        let state = expiry
573                            .map(SessionState::ExpiresAt)
574                            .unwrap_or(SessionState::NeverExpires);
575
576                        let parent = Some(parent);
577
578                        // Insert to the rs_filter.
579                        rs_filter |= rs_uuid.as_u128();
580                        Some((
581                            refer,
582                            Oauth2Session {
583                                parent,
584                                state,
585                                issued_at,
586                                rs_uuid,
587                            },
588                        ))
589                    }
590                    DbValueOauth2Session::V2 {
591                        refer,
592                        parent,
593                        state,
594                        issued_at,
595                        rs_uuid,
596                    } => {
597                        // Convert things.
598                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
599                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
600                            .map_err(|e| {
601                                admin_error!(
602                                    ?e,
603                                    "Invalidating session {} due to invalid issued_at timestamp",
604                                    refer
605                                )
606                            })
607                            .ok()?;
608
609                        let state = match state {
610                            DbValueSessionStateV1::ExpiresAt(e_inner) => {
611                                OffsetDateTime::parse(&e_inner, &Rfc3339)
612                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
613                                    .map(SessionState::ExpiresAt)
614                                    .map_err(|e| {
615                                        admin_error!(
616                                    ?e,
617                                    "Invalidating session {} due to invalid expiry timestamp",
618                                    refer
619                                )
620                                    })
621                                    .ok()?
622                            }
623                            DbValueSessionStateV1::Never => SessionState::NeverExpires,
624                            DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
625                                s_uuid: dc.server_id,
626                                ts: dc.timestamp,
627                            }),
628                        };
629
630                        rs_filter |= rs_uuid.as_u128();
631
632                        let parent = Some(parent);
633
634                        Some((
635                            refer,
636                            Oauth2Session {
637                                parent,
638                                state,
639                                issued_at,
640                                rs_uuid,
641                            },
642                        ))
643                    } // End V2
644                    DbValueOauth2Session::V3 {
645                        refer,
646                        parent,
647                        state,
648                        issued_at,
649                        rs_uuid,
650                    } => {
651                        // Convert things.
652                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
653                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
654                            .map_err(|e| {
655                                admin_error!(
656                                    ?e,
657                                    "Invalidating session {} due to invalid issued_at timestamp",
658                                    refer
659                                )
660                            })
661                            .ok()?;
662
663                        let state = match state {
664                            DbValueSessionStateV1::ExpiresAt(e_inner) => {
665                                OffsetDateTime::parse(&e_inner, &Rfc3339)
666                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
667                                    .map(SessionState::ExpiresAt)
668                                    .map_err(|e| {
669                                        admin_error!(
670                                    ?e,
671                                    "Invalidating session {} due to invalid expiry timestamp",
672                                    refer
673                                )
674                                    })
675                                    .ok()?
676                            }
677                            DbValueSessionStateV1::Never => SessionState::NeverExpires,
678                            DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
679                                s_uuid: dc.server_id,
680                                ts: dc.timestamp,
681                            }),
682                        };
683
684                        rs_filter |= rs_uuid.as_u128();
685
686                        Some((
687                            refer,
688                            Oauth2Session {
689                                parent,
690                                state,
691                                issued_at,
692                                rs_uuid,
693                            },
694                        ))
695                    } // End V3
696                }
697            })
698            .collect();
699        Ok(Box::new(ValueSetOauth2Session { map, rs_filter }))
700    }
701
702    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
703    // types, and tuples are always foreign.
704    #[allow(clippy::should_implement_trait)]
705    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
706    where
707        T: IntoIterator<Item = (Uuid, Oauth2Session)>,
708    {
709        let mut rs_filter = u128::MIN;
710        let map = iter
711            .into_iter()
712            .map(|(u, m)| {
713                rs_filter |= m.rs_uuid.as_u128();
714                (u, m)
715            })
716            .collect();
717        Some(Box::new(ValueSetOauth2Session { map, rs_filter }))
718    }
719}
720
721impl ValueSetT for ValueSetOauth2Session {
722    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
723        match value {
724            Value::Oauth2Session(u, m) => {
725                // Unlike other types, this allows overwriting as oauth2 sessions
726                // can be *extended* in time length.
727                match self.map.entry(u) {
728                    BTreeEntry::Vacant(e) => {
729                        self.rs_filter |= m.rs_uuid.as_u128();
730                        e.insert(m);
731                        Ok(true)
732                    }
733                    BTreeEntry::Occupied(mut e) => {
734                        let e_v = e.get_mut();
735                        if m.state > e_v.state {
736                            // Replace if the state has higher priority.
737                            *e_v = m;
738                            Ok(true)
739                        } else {
740                            // Else take no action.
741                            Ok(false)
742                        }
743                    }
744                }
745            }
746            _ => Err(OperationError::InvalidValueState),
747        }
748    }
749
750    fn clear(&mut self) {
751        self.rs_filter = u128::MIN;
752        self.map.clear();
753    }
754
755    fn remove(&mut self, pv: &PartialValue, cid: &Cid) -> bool {
756        match pv {
757            PartialValue::Refer(u) => {
758                if let Some(session) = self.map.get_mut(u) {
759                    if !matches!(session.state, SessionState::RevokedAt(_)) {
760                        session.state = SessionState::RevokedAt(cid.clone());
761                        true
762                    } else {
763                        false
764                    }
765                } else {
766                    // What if it's an rs_uuid?
767                    let u_int = u.as_u128();
768                    if self.rs_filter & u_int == u_int {
769                        // It's there, so we need to do a more costly revoke over the values
770                        // that are present.
771                        let mut removed = false;
772                        self.map.values_mut().for_each(|session| {
773                            if session.rs_uuid == *u {
774                                session.state = SessionState::RevokedAt(cid.clone());
775                                removed = true;
776                            }
777                        });
778                        removed
779                    } else {
780                        // It's not in the rs_filter or the map, false.
781                        false
782                    }
783                }
784            }
785            _ => false,
786        }
787    }
788
789    fn purge(&mut self, cid: &Cid) -> bool {
790        for (_uuid, session) in self.map.iter_mut() {
791            // Send them all to the shadow realm
792            if !matches!(session.state, SessionState::RevokedAt(_)) {
793                session.state = SessionState::RevokedAt(cid.clone())
794            }
795        }
796        // Can't be purged since we need the cid's of revoked to persist.
797        false
798    }
799
800    fn trim(&mut self, trim_cid: &Cid) {
801        // There might be a neater way to do this with less iterations. The problem
802        // is we can't just check on what was in b/older, because then we miss
803        // trimmable content from the local map. So once the merge is complete we
804        // do a pass for trim.
805        self.map.retain(|_, session| {
806            match &session.state {
807                SessionState::RevokedAt(cid) if cid < trim_cid => {
808                    // This value is past the replication trim window and can now safely
809                    // be removed
810                    false
811                }
812                // Retain all else
813                _ => true,
814            }
815        })
816    }
817
818    fn contains(&self, pv: &PartialValue) -> bool {
819        match pv {
820            PartialValue::Refer(u) => {
821                self.map.contains_key(u) || {
822                    let u_int = u.as_u128();
823                    if self.rs_filter & u_int == u_int {
824                        self.map.values().any(|session| {
825                            session.rs_uuid == *u
826                                && !matches!(session.state, SessionState::RevokedAt(_))
827                        })
828                    } else {
829                        false
830                    }
831                }
832            }
833            _ => false,
834        }
835    }
836
837    fn substring(&self, _pv: &PartialValue) -> bool {
838        false
839    }
840
841    fn startswith(&self, _pv: &PartialValue) -> bool {
842        false
843    }
844
845    fn endswith(&self, _pv: &PartialValue) -> bool {
846        false
847    }
848
849    fn lessthan(&self, _pv: &PartialValue) -> bool {
850        false
851    }
852
853    fn len(&self) -> usize {
854        self.map.len()
855    }
856
857    fn generate_idx_eq_keys(&self) -> Vec<String> {
858        // Allocate twice as much for worst-case when every session is
859        // a unique rs-uuid to prevent re-allocs.
860        let mut idx_keys = Vec::with_capacity(self.map.len() * 2);
861        for (k, v) in self.map.iter() {
862            idx_keys.push(k.as_hyphenated().to_string());
863            idx_keys.push(v.rs_uuid.as_hyphenated().to_string());
864        }
865        idx_keys.sort_unstable();
866        idx_keys.dedup();
867        idx_keys
868    }
869
870    fn syntax(&self) -> SyntaxType {
871        SyntaxType::Oauth2Session
872    }
873
874    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
875        true
876    }
877
878    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
879        Box::new(
880            self.map
881                .iter()
882                .map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
883        )
884    }
885
886    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
887        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
888            self.map
889                .iter()
890                .map(|(session_id, session)| {
891                    let (expires, revoked) = match &session.state {
892                        SessionState::ExpiresAt(odt) => (Some(*odt), None),
893                        SessionState::NeverExpires => (None, None),
894                        SessionState::RevokedAt(cid) => {
895                            let odt: OffsetDateTime = cid.into();
896                            (None, Some(odt))
897                        }
898                    };
899
900                    ScimOAuth2Session {
901                        id: *session_id,
902                        parent_id: session.parent,
903                        client_id: session.rs_uuid,
904                        issued_at: session.issued_at,
905                        expires,
906                        revoked,
907                    }
908                })
909                .collect::<Vec<_>>(),
910        )))
911    }
912
913    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
914        DbValueSetV2::Oauth2Session(
915            self.map
916                .iter()
917                .map(|(u, m)| DbValueOauth2Session::V3 {
918                    refer: *u,
919                    parent: m.parent,
920                    state: match &m.state {
921                        SessionState::ExpiresAt(odt) => {
922                            debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
923                            #[allow(clippy::expect_used)]
924                            odt.format(&Rfc3339)
925                                .map(DbValueSessionStateV1::ExpiresAt)
926                                .expect("Failed to format timestamp into RFC3339!")
927                        }
928                        SessionState::NeverExpires => DbValueSessionStateV1::Never,
929                        SessionState::RevokedAt(c) => DbValueSessionStateV1::RevokedAt(DbCidV1 {
930                            server_id: c.s_uuid,
931                            timestamp: c.ts,
932                        }),
933                    },
934                    issued_at: {
935                        debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
936                        #[allow(clippy::expect_used)]
937                        m.issued_at
938                            .format(&Rfc3339)
939                            .expect("Failed to format timestamp as RFC3339")
940                    },
941                    rs_uuid: m.rs_uuid,
942                })
943                .collect(),
944        )
945    }
946
947    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
948        Box::new(self.map.keys().cloned().map(PartialValue::Refer))
949    }
950
951    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
952        Box::new(
953            self.map
954                .iter()
955                .map(|(u, m)| Value::Oauth2Session(*u, m.clone())),
956        )
957    }
958
959    fn equal(&self, other: &ValueSet) -> bool {
960        if let Some(other) = other.as_oauth2session_map() {
961            &self.map == other
962        } else {
963            debug_assert!(false);
964            false
965        }
966    }
967
968    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
969        if let Some(b) = other.as_oauth2session_map() {
970            // We can't just do merge maps here, we have to be aware of the
971            // session.state value and what it currently is set to. We also
972            // need to make sure the rs_filter is updated too!
973            for (k_other, v_other) in b.iter() {
974                if let Some(v_self) = self.map.get_mut(k_other) {
975                    // We only update if lower. This is where RevokedAt
976                    // always proceeds other states, and lower revoked
977                    // cids will always take effect.
978                    if v_other.state > v_self.state {
979                        *v_self = v_other.clone();
980                    }
981                } else {
982                    // Update the rs_filter!
983                    self.rs_filter |= v_other.rs_uuid.as_u128();
984                    // Not present, just insert.
985                    self.map.insert(*k_other, v_other.clone());
986                }
987            }
988            Ok(())
989        } else {
990            debug_assert!(false);
991            Err(OperationError::InvalidValueState)
992        }
993    }
994
995    fn as_oauth2session_map(&self) -> Option<&BTreeMap<Uuid, Oauth2Session>> {
996        Some(&self.map)
997    }
998
999    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
1000        // This is what ties us as a type that can be refint checked. We need to
1001        // bind to our resource servers, not our ids!
1002        Some(Box::new(self.map.values().map(|m| &m.rs_uuid).copied()))
1003    }
1004
1005    fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
1006        if let Some(b) = older.as_oauth2session_map() {
1007            // We can't just do merge maps here, we have to be aware of the
1008            // session.state value and what it currently is set to.
1009            let mut map = self.map.clone();
1010            let mut rs_filter = self.rs_filter;
1011            for (k_other, v_other) in b.iter() {
1012                if let Some(v_self) = map.get_mut(k_other) {
1013                    // We only update if greater. This is where RevokedAt
1014                    // always proceeds other states, and lower revoked
1015                    // cids will always take effect.
1016                    if v_other.state > v_self.state {
1017                        *v_self = v_other.clone();
1018                    }
1019                } else {
1020                    // Not present, just insert.
1021                    rs_filter |= v_other.rs_uuid.as_u128();
1022                    map.insert(*k_other, v_other.clone());
1023                }
1024            }
1025
1026            let mut vs = Box::new(ValueSetOauth2Session { map, rs_filter });
1027
1028            vs.trim(trim_cid);
1029
1030            Some(vs)
1031        } else {
1032            // The older value has a different type - return nothing, we
1033            // just take the newer value.
1034            None
1035        }
1036    }
1037}
1038
1039#[derive(Debug, Clone)]
1040pub struct ValueSetApiToken {
1041    map: BTreeMap<Uuid, ApiToken>,
1042}
1043
1044impl ValueSetApiToken {
1045    pub fn new(u: Uuid, m: ApiToken) -> Box<Self> {
1046        let mut map = BTreeMap::new();
1047        map.insert(u, m);
1048        Box::new(ValueSetApiToken { map })
1049    }
1050
1051    pub fn push(&mut self, u: Uuid, m: ApiToken) -> bool {
1052        self.map.insert(u, m).is_none()
1053    }
1054
1055    pub fn from_dbvs2(data: Vec<DbValueApiToken>) -> Result<ValueSet, OperationError> {
1056        let map = data
1057            .into_iter()
1058            .filter_map(|dbv| {
1059                match dbv {
1060                    DbValueApiToken::V1 {
1061                        refer,
1062                        label,
1063                        expiry,
1064                        issued_at,
1065                        issued_by,
1066                        scope,
1067                    } => {
1068                        // Convert things.
1069                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
1070                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
1071                            .map_err(|e| {
1072                                admin_error!(
1073                                    ?e,
1074                                    "Invalidating api token {} due to invalid issued_at timestamp",
1075                                    refer
1076                                )
1077                            })
1078                            .ok()?;
1079
1080                        // This is a bit annoying. In the case we can't parse the optional
1081                        // expiry, we need to NOT return the session so that it's immediately
1082                        // invalidated. To do this we have to invert some of the options involved
1083                        // here.
1084                        let expiry = expiry
1085                            .map(|e_inner| {
1086                                OffsetDateTime::parse(&e_inner, &Rfc3339)
1087                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
1088                                // We now have an
1089                                // Option<Result<ODT, _>>
1090                            })
1091                            .transpose()
1092                            // Result<Option<ODT>, _>
1093                            .map_err(|e| {
1094                                admin_error!(
1095                                    ?e,
1096                                    "Invalidating api token {} due to invalid expiry timestamp",
1097                                    refer
1098                                )
1099                            })
1100                            // Option<Option<ODT>>
1101                            .ok()?;
1102
1103                        let issued_by = match issued_by {
1104                            DbValueIdentityId::V1Internal => IdentityId::Internal,
1105                            DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
1106                            DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
1107                        };
1108
1109                        let scope = match scope {
1110                            DbValueApiTokenScopeV1::ReadOnly => ApiTokenScope::ReadOnly,
1111                            DbValueApiTokenScopeV1::ReadWrite => ApiTokenScope::ReadWrite,
1112                            DbValueApiTokenScopeV1::Synchronise => ApiTokenScope::Synchronise,
1113                        };
1114
1115                        Some((
1116                            refer,
1117                            ApiToken {
1118                                label,
1119                                expiry,
1120                                issued_at,
1121                                issued_by,
1122                                scope,
1123                            },
1124                        ))
1125                    }
1126                }
1127            })
1128            .collect();
1129        Ok(Box::new(ValueSetApiToken { map }))
1130    }
1131
1132    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
1133    // types, and tuples are always foreign.
1134    #[allow(clippy::should_implement_trait)]
1135    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
1136    where
1137        T: IntoIterator<Item = (Uuid, ApiToken)>,
1138    {
1139        let map = iter.into_iter().collect();
1140        Some(Box::new(ValueSetApiToken { map }))
1141    }
1142}
1143
1144impl ValueSetT for ValueSetApiToken {
1145    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
1146        match value {
1147            Value::ApiToken(u, m) => {
1148                if let BTreeEntry::Vacant(e) = self.map.entry(u) {
1149                    e.insert(m);
1150                    Ok(true)
1151                } else {
1152                    Ok(false)
1153                }
1154            }
1155            _ => Err(OperationError::InvalidValueState),
1156        }
1157    }
1158
1159    fn clear(&mut self) {
1160        self.map.clear();
1161    }
1162
1163    fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
1164        match pv {
1165            PartialValue::Refer(u) => self.map.remove(u).is_some(),
1166            _ => false,
1167        }
1168    }
1169
1170    fn purge(&mut self, _cid: &Cid) -> bool {
1171        // Could consider making this a TS capable entry.
1172        true
1173    }
1174
1175    fn contains(&self, pv: &PartialValue) -> bool {
1176        match pv {
1177            PartialValue::Refer(u) => self.map.contains_key(u),
1178            _ => false,
1179        }
1180    }
1181
1182    fn substring(&self, _pv: &PartialValue) -> bool {
1183        false
1184    }
1185
1186    fn startswith(&self, _pv: &PartialValue) -> bool {
1187        false
1188    }
1189
1190    fn endswith(&self, _pv: &PartialValue) -> bool {
1191        false
1192    }
1193
1194    fn lessthan(&self, _pv: &PartialValue) -> bool {
1195        false
1196    }
1197
1198    fn len(&self) -> usize {
1199        self.map.len()
1200    }
1201
1202    fn generate_idx_eq_keys(&self) -> Vec<String> {
1203        self.map
1204            .keys()
1205            .map(|u| u.as_hyphenated().to_string())
1206            .collect()
1207    }
1208
1209    fn syntax(&self) -> SyntaxType {
1210        SyntaxType::ApiToken
1211    }
1212
1213    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
1214        self.map.iter().all(|(_, at)| {
1215            Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label)
1216        })
1217    }
1218
1219    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
1220        Box::new(
1221            self.map
1222                .iter()
1223                .map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
1224        )
1225    }
1226
1227    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
1228        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
1229            self.map
1230                .iter()
1231                .map(|(token_id, token)| ScimApiToken {
1232                    id: *token_id,
1233                    label: token.label.clone(),
1234                    issued_by: Uuid::from(&token.issued_by),
1235                    issued_at: token.issued_at,
1236                    expires: token.expiry,
1237                    scope: token.scope.to_string(),
1238                })
1239                .collect::<Vec<_>>(),
1240        )))
1241    }
1242
1243    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
1244        DbValueSetV2::ApiToken(
1245            self.map
1246                .iter()
1247                .map(|(u, m)| DbValueApiToken::V1 {
1248                    refer: *u,
1249                    label: m.label.clone(),
1250                    expiry: m.expiry.map(|odt| {
1251                        debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
1252                        #[allow(clippy::expect_used)]
1253                        odt.format(&Rfc3339)
1254                            .expect("Failed to format timestamp into RFC3339")
1255                    }),
1256                    issued_at: {
1257                        debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
1258                        #[allow(clippy::expect_used)]
1259                        m.issued_at
1260                            .format(&Rfc3339)
1261                            .expect("Failed to format timestamp into RFC3339")
1262                    },
1263                    issued_by: match m.issued_by {
1264                        IdentityId::Internal => DbValueIdentityId::V1Internal,
1265                        IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
1266                        IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
1267                    },
1268                    scope: match m.scope {
1269                        ApiTokenScope::ReadOnly => DbValueApiTokenScopeV1::ReadOnly,
1270                        ApiTokenScope::ReadWrite => DbValueApiTokenScopeV1::ReadWrite,
1271                        ApiTokenScope::Synchronise => DbValueApiTokenScopeV1::Synchronise,
1272                    },
1273                })
1274                .collect(),
1275        )
1276    }
1277
1278    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
1279        Box::new(self.map.keys().cloned().map(PartialValue::Refer))
1280    }
1281
1282    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
1283        Box::new(self.map.iter().map(|(u, m)| Value::ApiToken(*u, m.clone())))
1284    }
1285
1286    fn equal(&self, other: &ValueSet) -> bool {
1287        if let Some(other) = other.as_apitoken_map() {
1288            &self.map == other
1289        } else {
1290            debug_assert!(false);
1291            false
1292        }
1293    }
1294
1295    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
1296        if let Some(b) = other.as_apitoken_map() {
1297            mergemaps!(self.map, b)
1298        } else {
1299            debug_assert!(false);
1300            Err(OperationError::InvalidValueState)
1301        }
1302    }
1303
1304    fn as_apitoken_map(&self) -> Option<&BTreeMap<Uuid, ApiToken>> {
1305        Some(&self.map)
1306    }
1307
1308    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
1309        // This is what ties us as a type that can be refint checked.
1310        Some(Box::new(self.map.keys().copied()))
1311    }
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316    use super::{ValueSetOauth2Session, ValueSetSession, SESSION_MAXIMUM};
1317    use crate::prelude::ValueSet;
1318    use crate::prelude::{IdentityId, SessionScope, Uuid};
1319    use crate::repl::cid::Cid;
1320    use crate::value::{AuthType, Oauth2Session, Session, SessionState};
1321    use time::OffsetDateTime;
1322
1323    #[test]
1324    fn test_valueset_session_purge() {
1325        let s_uuid = Uuid::new_v4();
1326
1327        let mut vs: ValueSet = ValueSetSession::new(
1328            s_uuid,
1329            Session {
1330                label: "hacks".to_string(),
1331                state: SessionState::NeverExpires,
1332                issued_at: OffsetDateTime::now_utc(),
1333                issued_by: IdentityId::Internal,
1334                cred_id: Uuid::new_v4(),
1335                scope: SessionScope::ReadOnly,
1336                type_: AuthType::Passkey,
1337                ext_metadata: Default::default(),
1338            },
1339        );
1340
1341        let zero_cid = Cid::new_zero();
1342
1343        // Simulate session revocation.
1344        vs.purge(&zero_cid);
1345
1346        assert_eq!(vs.len(), 1);
1347
1348        let session = vs
1349            .as_session_map()
1350            .and_then(|map| map.get(&s_uuid))
1351            .expect("Unable to locate session");
1352
1353        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1354    }
1355
1356    #[test]
1357    fn test_valueset_session_merge_left() {
1358        let s_uuid = Uuid::new_v4();
1359        let zero_cid = Cid::new_zero();
1360
1361        let mut vs_a: ValueSet = ValueSetSession::new(
1362            s_uuid,
1363            Session {
1364                label: "hacks".to_string(),
1365                state: SessionState::NeverExpires,
1366                issued_at: OffsetDateTime::now_utc(),
1367                issued_by: IdentityId::Internal,
1368                cred_id: Uuid::new_v4(),
1369                scope: SessionScope::ReadOnly,
1370                type_: AuthType::Passkey,
1371                ext_metadata: Default::default(),
1372            },
1373        );
1374
1375        let vs_b: ValueSet = ValueSetSession::new(
1376            s_uuid,
1377            Session {
1378                label: "hacks".to_string(),
1379                state: SessionState::RevokedAt(zero_cid.clone()),
1380                issued_at: OffsetDateTime::now_utc(),
1381                issued_by: IdentityId::Internal,
1382                cred_id: Uuid::new_v4(),
1383                scope: SessionScope::ReadOnly,
1384                type_: AuthType::Passkey,
1385                ext_metadata: Default::default(),
1386            },
1387        );
1388
1389        vs_a.merge(&vs_b).expect("failed to merge");
1390
1391        let session = vs_a
1392            .as_session_map()
1393            .and_then(|map| map.get(&s_uuid))
1394            .expect("Unable to locate session");
1395
1396        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1397    }
1398
1399    #[test]
1400    fn test_valueset_session_merge_right() {
1401        let s_uuid = Uuid::new_v4();
1402        let zero_cid = Cid::new_zero();
1403
1404        let vs_a: ValueSet = ValueSetSession::new(
1405            s_uuid,
1406            Session {
1407                label: "hacks".to_string(),
1408                state: SessionState::NeverExpires,
1409                issued_at: OffsetDateTime::now_utc(),
1410                issued_by: IdentityId::Internal,
1411                cred_id: Uuid::new_v4(),
1412                scope: SessionScope::ReadOnly,
1413                type_: AuthType::Passkey,
1414                ext_metadata: Default::default(),
1415            },
1416        );
1417
1418        let mut vs_b: ValueSet = ValueSetSession::new(
1419            s_uuid,
1420            Session {
1421                label: "hacks".to_string(),
1422                state: SessionState::RevokedAt(zero_cid.clone()),
1423                issued_at: OffsetDateTime::now_utc(),
1424                issued_by: IdentityId::Internal,
1425                cred_id: Uuid::new_v4(),
1426                scope: SessionScope::ReadOnly,
1427                type_: AuthType::Passkey,
1428                ext_metadata: Default::default(),
1429            },
1430        );
1431
1432        // Note - inverse order!
1433        vs_b.merge(&vs_a).expect("failed to merge");
1434
1435        let session = vs_b
1436            .as_session_map()
1437            .and_then(|map| map.get(&s_uuid))
1438            .expect("Unable to locate session");
1439
1440        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1441    }
1442
1443    #[test]
1444    fn test_valueset_session_repl_merge_left() {
1445        let s_uuid = Uuid::new_v4();
1446        let r_uuid = Uuid::new_v4();
1447        let zero_cid = Cid::new_zero();
1448        let one_cid = Cid::new_count(1);
1449
1450        let vs_a: ValueSet = ValueSetSession::new(
1451            s_uuid,
1452            Session {
1453                label: "hacks".to_string(),
1454                state: SessionState::NeverExpires,
1455                issued_at: OffsetDateTime::now_utc(),
1456                issued_by: IdentityId::Internal,
1457                cred_id: Uuid::new_v4(),
1458                scope: SessionScope::ReadOnly,
1459                type_: AuthType::Passkey,
1460                ext_metadata: Default::default(),
1461            },
1462        );
1463
1464        let vs_b: ValueSet = ValueSetSession::from_iter([
1465            (
1466                s_uuid,
1467                Session {
1468                    label: "hacks".to_string(),
1469                    state: SessionState::RevokedAt(one_cid.clone()),
1470                    issued_at: OffsetDateTime::now_utc(),
1471                    issued_by: IdentityId::Internal,
1472                    cred_id: Uuid::new_v4(),
1473                    scope: SessionScope::ReadOnly,
1474                    type_: AuthType::Passkey,
1475                    ext_metadata: Default::default(),
1476                },
1477            ),
1478            (
1479                r_uuid,
1480                Session {
1481                    label: "hacks".to_string(),
1482                    state: SessionState::RevokedAt(zero_cid.clone()),
1483                    issued_at: OffsetDateTime::now_utc(),
1484                    issued_by: IdentityId::Internal,
1485                    cred_id: Uuid::new_v4(),
1486                    scope: SessionScope::ReadOnly,
1487                    type_: AuthType::Passkey,
1488                    ext_metadata: Default::default(),
1489                },
1490            ),
1491        ])
1492        .expect("Unable to build valueset session");
1493
1494        let r_vs = vs_a
1495            .repl_merge_valueset(&vs_b, &one_cid)
1496            .expect("failed to merge");
1497
1498        let sessions = r_vs.as_session_map().expect("Unable to locate sessions");
1499
1500        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1501
1502        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1503
1504        assert!(!sessions.contains_key(&r_uuid));
1505    }
1506
1507    #[test]
1508    fn test_valueset_session_repl_merge_right() {
1509        let s_uuid = Uuid::new_v4();
1510        let r_uuid = Uuid::new_v4();
1511        let zero_cid = Cid::new_zero();
1512        let one_cid = Cid::new_count(1);
1513
1514        let vs_a: ValueSet = ValueSetSession::new(
1515            s_uuid,
1516            Session {
1517                label: "hacks".to_string(),
1518                state: SessionState::NeverExpires,
1519                issued_at: OffsetDateTime::now_utc(),
1520                issued_by: IdentityId::Internal,
1521                cred_id: Uuid::new_v4(),
1522                scope: SessionScope::ReadOnly,
1523                type_: AuthType::Passkey,
1524                ext_metadata: Default::default(),
1525            },
1526        );
1527
1528        let vs_b: ValueSet = ValueSetSession::from_iter([
1529            (
1530                s_uuid,
1531                Session {
1532                    label: "hacks".to_string(),
1533                    state: SessionState::RevokedAt(one_cid.clone()),
1534                    issued_at: OffsetDateTime::now_utc(),
1535                    issued_by: IdentityId::Internal,
1536                    cred_id: Uuid::new_v4(),
1537                    scope: SessionScope::ReadOnly,
1538                    type_: AuthType::Passkey,
1539                    ext_metadata: Default::default(),
1540                },
1541            ),
1542            (
1543                r_uuid,
1544                Session {
1545                    label: "hacks".to_string(),
1546                    state: SessionState::RevokedAt(zero_cid.clone()),
1547                    issued_at: OffsetDateTime::now_utc(),
1548                    issued_by: IdentityId::Internal,
1549                    cred_id: Uuid::new_v4(),
1550                    scope: SessionScope::ReadOnly,
1551                    type_: AuthType::Passkey,
1552                    ext_metadata: Default::default(),
1553                },
1554            ),
1555        ])
1556        .expect("Unable to build valueset session");
1557
1558        // Note - inverse order!
1559        let r_vs = vs_b
1560            .repl_merge_valueset(&vs_a, &one_cid)
1561            .expect("failed to merge");
1562
1563        let sessions = r_vs.as_session_map().expect("Unable to locate sessions");
1564
1565        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1566
1567        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1568
1569        assert!(!sessions.contains_key(&r_uuid));
1570    }
1571
1572    #[test]
1573    fn test_valueset_session_repl_trim() {
1574        let zero_uuid = Uuid::new_v4();
1575        let zero_cid = Cid::new_zero();
1576        let one_uuid = Uuid::new_v4();
1577        let one_cid = Cid::new_count(1);
1578        let two_uuid = Uuid::new_v4();
1579        let two_cid = Cid::new_count(2);
1580
1581        let mut vs_a: ValueSet = ValueSetSession::from_iter([
1582            (
1583                zero_uuid,
1584                Session {
1585                    state: SessionState::RevokedAt(zero_cid),
1586                    label: "hacks".to_string(),
1587                    issued_at: OffsetDateTime::now_utc(),
1588                    issued_by: IdentityId::Internal,
1589                    cred_id: Uuid::new_v4(),
1590                    scope: SessionScope::ReadOnly,
1591                    type_: AuthType::Passkey,
1592                    ext_metadata: Default::default(),
1593                },
1594            ),
1595            (
1596                one_uuid,
1597                Session {
1598                    state: SessionState::RevokedAt(one_cid),
1599                    label: "hacks".to_string(),
1600                    issued_at: OffsetDateTime::now_utc(),
1601                    issued_by: IdentityId::Internal,
1602                    cred_id: Uuid::new_v4(),
1603                    scope: SessionScope::ReadOnly,
1604                    type_: AuthType::Passkey,
1605                    ext_metadata: Default::default(),
1606                },
1607            ),
1608            (
1609                two_uuid,
1610                Session {
1611                    state: SessionState::RevokedAt(two_cid.clone()),
1612                    label: "hacks".to_string(),
1613                    issued_at: OffsetDateTime::now_utc(),
1614                    issued_by: IdentityId::Internal,
1615                    cred_id: Uuid::new_v4(),
1616                    scope: SessionScope::ReadOnly,
1617                    type_: AuthType::Passkey,
1618                    ext_metadata: Default::default(),
1619                },
1620            ),
1621        ])
1622        .unwrap();
1623
1624        vs_a.trim(&two_cid);
1625
1626        let sessions = vs_a.as_session_map().expect("Unable to locate session");
1627
1628        assert!(!sessions.contains_key(&zero_uuid));
1629        assert!(!sessions.contains_key(&one_uuid));
1630        assert!(sessions.contains_key(&two_uuid));
1631    }
1632
1633    #[test]
1634    fn test_valueset_session_limit_trim() {
1635        // Create a session that will be trimmed.
1636        let zero_uuid = Uuid::new_v4();
1637        let zero_cid = Cid::new_zero();
1638        let issued_at = OffsetDateTime::UNIX_EPOCH;
1639
1640        let session_iter = std::iter::once((
1641            zero_uuid,
1642            Session {
1643                state: SessionState::NeverExpires,
1644                label: "hacks".to_string(),
1645                issued_at,
1646                issued_by: IdentityId::Internal,
1647                cred_id: Uuid::new_v4(),
1648                scope: SessionScope::ReadOnly,
1649                type_: AuthType::Passkey,
1650                ext_metadata: Default::default(),
1651            },
1652        ))
1653        .chain((0..SESSION_MAXIMUM).map(|_| {
1654            (
1655                Uuid::new_v4(),
1656                Session {
1657                    state: SessionState::NeverExpires,
1658                    label: "hacks".to_string(),
1659                    issued_at: OffsetDateTime::now_utc(),
1660                    issued_by: IdentityId::Internal,
1661                    cred_id: Uuid::new_v4(),
1662                    scope: SessionScope::ReadOnly,
1663                    type_: AuthType::Passkey,
1664                    ext_metadata: Default::default(),
1665                },
1666            )
1667        }));
1668
1669        let mut vs_a: ValueSet = ValueSetSession::from_iter(session_iter).unwrap();
1670
1671        assert!(vs_a.len() > SESSION_MAXIMUM);
1672
1673        vs_a.trim(&zero_cid);
1674
1675        assert_eq!(vs_a.len(), SESSION_MAXIMUM);
1676
1677        let sessions = vs_a.as_session_map().expect("Unable to access sessions");
1678
1679        assert!(!sessions.contains_key(&zero_uuid));
1680    }
1681
1682    #[test]
1683    fn test_valueset_oauth2_session_purge() {
1684        let s_uuid = Uuid::new_v4();
1685        let mut vs: ValueSet = ValueSetOauth2Session::new(
1686            s_uuid,
1687            Oauth2Session {
1688                state: SessionState::NeverExpires,
1689                issued_at: OffsetDateTime::now_utc(),
1690                parent: Some(Uuid::new_v4()),
1691                rs_uuid: Uuid::new_v4(),
1692            },
1693        );
1694
1695        let zero_cid = Cid::new_zero();
1696
1697        // Simulate session revocation.
1698        vs.purge(&zero_cid);
1699
1700        assert_eq!(vs.len(), 1);
1701
1702        let session = vs
1703            .as_oauth2session_map()
1704            .and_then(|map| map.get(&s_uuid))
1705            .expect("Unable to locate session");
1706
1707        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1708    }
1709
1710    #[test]
1711    fn test_valueset_oauth2_session_merge_left() {
1712        let s_uuid = Uuid::new_v4();
1713        let zero_cid = Cid::new_zero();
1714
1715        let mut vs_a: ValueSet = ValueSetOauth2Session::new(
1716            s_uuid,
1717            Oauth2Session {
1718                state: SessionState::NeverExpires,
1719                issued_at: OffsetDateTime::now_utc(),
1720                parent: Some(Uuid::new_v4()),
1721                rs_uuid: Uuid::new_v4(),
1722            },
1723        );
1724
1725        let vs_b: ValueSet = ValueSetOauth2Session::new(
1726            s_uuid,
1727            Oauth2Session {
1728                state: SessionState::RevokedAt(zero_cid.clone()),
1729                issued_at: OffsetDateTime::now_utc(),
1730                parent: Some(Uuid::new_v4()),
1731                rs_uuid: Uuid::new_v4(),
1732            },
1733        );
1734
1735        vs_a.merge(&vs_b).expect("failed to merge");
1736
1737        let session = vs_a
1738            .as_oauth2session_map()
1739            .and_then(|map| map.get(&s_uuid))
1740            .expect("Unable to locate session");
1741
1742        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1743    }
1744
1745    #[test]
1746    fn test_valueset_oauth2_session_merge_right() {
1747        let s_uuid = Uuid::new_v4();
1748        let zero_cid = Cid::new_zero();
1749
1750        let vs_a: ValueSet = ValueSetOauth2Session::new(
1751            s_uuid,
1752            Oauth2Session {
1753                state: SessionState::NeverExpires,
1754                issued_at: OffsetDateTime::now_utc(),
1755                parent: Some(Uuid::new_v4()),
1756                rs_uuid: Uuid::new_v4(),
1757            },
1758        );
1759
1760        let mut vs_b: ValueSet = ValueSetOauth2Session::new(
1761            s_uuid,
1762            Oauth2Session {
1763                state: SessionState::RevokedAt(zero_cid.clone()),
1764                issued_at: OffsetDateTime::now_utc(),
1765                parent: Some(Uuid::new_v4()),
1766                rs_uuid: Uuid::new_v4(),
1767            },
1768        );
1769
1770        // Note inverse order
1771        vs_b.merge(&vs_a).expect("failed to merge");
1772
1773        let session = vs_b
1774            .as_oauth2session_map()
1775            .and_then(|map| map.get(&s_uuid))
1776            .expect("Unable to locate session");
1777
1778        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1779    }
1780
1781    #[test]
1782    fn test_valueset_oauth2_session_repl_merge_left() {
1783        let s_uuid = Uuid::new_v4();
1784        let r_uuid = Uuid::new_v4();
1785        let zero_cid = Cid::new_zero();
1786        let one_cid = Cid::new_count(1);
1787
1788        let vs_a: ValueSet = ValueSetOauth2Session::new(
1789            s_uuid,
1790            Oauth2Session {
1791                state: SessionState::NeverExpires,
1792                issued_at: OffsetDateTime::now_utc(),
1793                parent: Some(Uuid::new_v4()),
1794                rs_uuid: Uuid::new_v4(),
1795            },
1796        );
1797
1798        let vs_b: ValueSet = ValueSetOauth2Session::from_iter([
1799            (
1800                s_uuid,
1801                Oauth2Session {
1802                    state: SessionState::RevokedAt(one_cid.clone()),
1803                    issued_at: OffsetDateTime::now_utc(),
1804                    parent: Some(Uuid::new_v4()),
1805                    rs_uuid: Uuid::new_v4(),
1806                },
1807            ),
1808            (
1809                r_uuid,
1810                Oauth2Session {
1811                    state: SessionState::RevokedAt(zero_cid.clone()),
1812                    issued_at: OffsetDateTime::now_utc(),
1813                    parent: Some(Uuid::new_v4()),
1814                    rs_uuid: Uuid::new_v4(),
1815                },
1816            ),
1817        ])
1818        .expect("Unable to build valueset oauth2 session");
1819
1820        let r_vs = vs_a
1821            .repl_merge_valueset(&vs_b, &one_cid)
1822            .expect("failed to merge");
1823
1824        let sessions = r_vs
1825            .as_oauth2session_map()
1826            .expect("Unable to locate sessions");
1827
1828        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1829
1830        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1831
1832        assert!(!sessions.contains_key(&r_uuid));
1833    }
1834
1835    #[test]
1836    fn test_valueset_oauth2_session_repl_merge_right() {
1837        let s_uuid = Uuid::new_v4();
1838        let r_uuid = Uuid::new_v4();
1839        let zero_cid = Cid::new_zero();
1840        let one_cid = Cid::new_count(1);
1841
1842        let vs_a: ValueSet = ValueSetOauth2Session::new(
1843            s_uuid,
1844            Oauth2Session {
1845                state: SessionState::NeverExpires,
1846                issued_at: OffsetDateTime::now_utc(),
1847                parent: Some(Uuid::new_v4()),
1848                rs_uuid: Uuid::new_v4(),
1849            },
1850        );
1851
1852        let vs_b: ValueSet = ValueSetOauth2Session::from_iter([
1853            (
1854                s_uuid,
1855                Oauth2Session {
1856                    state: SessionState::RevokedAt(one_cid.clone()),
1857                    issued_at: OffsetDateTime::now_utc(),
1858                    parent: Some(Uuid::new_v4()),
1859                    rs_uuid: Uuid::new_v4(),
1860                },
1861            ),
1862            (
1863                r_uuid,
1864                Oauth2Session {
1865                    state: SessionState::RevokedAt(zero_cid.clone()),
1866                    issued_at: OffsetDateTime::now_utc(),
1867                    parent: Some(Uuid::new_v4()),
1868                    rs_uuid: Uuid::new_v4(),
1869                },
1870            ),
1871        ])
1872        .expect("Unable to build valueset oauth2 session");
1873
1874        // Note inverse order
1875        let r_vs = vs_b
1876            .repl_merge_valueset(&vs_a, &one_cid)
1877            .expect("failed to merge");
1878
1879        let sessions = r_vs
1880            .as_oauth2session_map()
1881            .expect("Unable to locate sessions");
1882
1883        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1884
1885        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1886
1887        assert!(!sessions.contains_key(&r_uuid));
1888    }
1889
1890    #[test]
1891    fn test_valueset_oauth2_session_repl_trim() {
1892        let zero_uuid = Uuid::new_v4();
1893        let zero_cid = Cid::new_zero();
1894        let one_uuid = Uuid::new_v4();
1895        let one_cid = Cid::new_count(1);
1896        let two_uuid = Uuid::new_v4();
1897        let two_cid = Cid::new_count(2);
1898
1899        let mut vs_a: ValueSet = ValueSetOauth2Session::from_iter([
1900            (
1901                zero_uuid,
1902                Oauth2Session {
1903                    state: SessionState::RevokedAt(zero_cid),
1904                    issued_at: OffsetDateTime::now_utc(),
1905                    parent: Some(Uuid::new_v4()),
1906                    rs_uuid: Uuid::new_v4(),
1907                },
1908            ),
1909            (
1910                one_uuid,
1911                Oauth2Session {
1912                    state: SessionState::RevokedAt(one_cid),
1913                    issued_at: OffsetDateTime::now_utc(),
1914                    parent: Some(Uuid::new_v4()),
1915                    rs_uuid: Uuid::new_v4(),
1916                },
1917            ),
1918            (
1919                two_uuid,
1920                Oauth2Session {
1921                    state: SessionState::RevokedAt(two_cid.clone()),
1922                    issued_at: OffsetDateTime::now_utc(),
1923                    parent: Some(Uuid::new_v4()),
1924                    rs_uuid: Uuid::new_v4(),
1925                },
1926            ),
1927        ])
1928        .unwrap();
1929
1930        vs_a.trim(&two_cid);
1931
1932        let sessions = vs_a
1933            .as_oauth2session_map()
1934            .expect("Unable to locate session");
1935
1936        assert!(!sessions.contains_key(&zero_uuid));
1937        assert!(!sessions.contains_key(&one_uuid));
1938        assert!(sessions.contains_key(&two_uuid));
1939    }
1940
1941    #[test]
1942    fn test_scim_session() {
1943        let s_uuid = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
1944
1945        let vs: ValueSet = ValueSetSession::new(
1946            s_uuid,
1947            Session {
1948                label: "hacks".to_string(),
1949                state: SessionState::NeverExpires,
1950                issued_at: OffsetDateTime::UNIX_EPOCH,
1951                issued_by: IdentityId::Internal,
1952                cred_id: s_uuid,
1953                scope: SessionScope::ReadOnly,
1954                type_: AuthType::Passkey,
1955                ext_metadata: Default::default(),
1956            },
1957        );
1958
1959        let data = r#"
1960[
1961  {
1962    "authType": "passkey",
1963    "credentialId": "3a163ca0-4762-4620-a188-06b750c84c86",
1964    "issuedAt": "1970-01-01T00:00:00Z",
1965    "issuedBy": "00000000-0000-0000-0000-ffffff000000",
1966    "id": "3a163ca0-4762-4620-a188-06b750c84c86",
1967    "sessionScope": "read_only"
1968  }
1969]
1970        "#;
1971        crate::valueset::scim_json_reflexive(&vs, data);
1972    }
1973
1974    #[test]
1975    fn test_scim_oauth2_session() {
1976        let s_uuid = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
1977
1978        let vs: ValueSet = ValueSetOauth2Session::new(
1979            s_uuid,
1980            Oauth2Session {
1981                state: SessionState::NeverExpires,
1982                issued_at: OffsetDateTime::UNIX_EPOCH,
1983                parent: Some(s_uuid),
1984                rs_uuid: s_uuid,
1985            },
1986        );
1987
1988        let data = r#"
1989[
1990  {
1991    "clientId": "3a163ca0-4762-4620-a188-06b750c84c86",
1992    "issuedAt": "1970-01-01T00:00:00Z",
1993    "parentId": "3a163ca0-4762-4620-a188-06b750c84c86",
1994    "id": "3a163ca0-4762-4620-a188-06b750c84c86"
1995  }
1996]
1997        "#;
1998
1999        crate::valueset::scim_json_reflexive(&vs, data);
2000    }
2001}