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