kanidmd_lib/
value.rs

1//! Inside an entry, the key-value pairs are stored in these [`Value`] types. The components of
2//! the [`Value`] module allow storage and transformation of various types of input into strongly
3//! typed values, allows their comparison, filtering and more. It also has the code for serialising
4//! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend).
5
6#![allow(non_upper_case_globals)]
7
8use crate::be::dbentry::DbIdentSpn;
9use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
10use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
11use crate::prelude::*;
12use crate::repl::cid::Cid;
13use crate::server::identity::IdentityId;
14use crate::server::keys::KeyId;
15use crate::valueset::image::ImageValueThings;
16use crate::valueset::uuid_to_proto_string;
17use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
18use crypto_glue::traits::Zeroizing;
19use hashbrown::HashSet;
20use kanidm_lib_crypto::x509_cert::{der::DecodePem, Certificate};
21use kanidm_proto::internal::ImageValue;
22use kanidm_proto::internal::{ApiTokenPurpose, Filter as ProtoFilter, UiHint};
23use kanidm_proto::scim_v1::ScimOauth2ClaimMapJoinChar;
24use kanidm_proto::v1::UatPurposeStatus;
25use num_enum::TryFromPrimitive;
26use openssl::ec::EcKey;
27use openssl::pkey::Private;
28use regex::Regex;
29use serde::{Deserialize, Serialize};
30use sshkey_attest::proto::PublicKey as SshPublicKey;
31use std::cmp::Ordering;
32use std::collections::BTreeSet;
33use std::convert::TryFrom;
34use std::fmt;
35use std::fmt::Formatter;
36use std::hash::Hash;
37use std::str::FromStr;
38use std::time::Duration;
39use time::OffsetDateTime;
40use url::Url;
41use uuid::Uuid;
42use webauthn_rs::prelude::{
43    AttestationCaList, AttestedPasskey as AttestedPasskeyV4, Passkey as PasskeyV4,
44};
45
46#[cfg(test)]
47use base64::{engine::general_purpose, Engine as _};
48
49lazy_static! {
50    pub static ref SPN_RE: Regex = {
51        #[allow(clippy::expect_used)]
52        Regex::new("(?P<name>[^@]+)@(?P<realm>[^@]+)").expect("Invalid SPN regex found")
53    };
54
55    pub static ref DISALLOWED_NAMES: HashSet<&'static str> = {
56        // Most of these were removed in favour of the unixd daemon filtering out
57        // local users instead.
58        let mut m = HashSet::with_capacity(2);
59        m.insert("root");
60        m.insert("dn=token");
61        m
62    };
63
64    /// Only lowercase+numbers, with limited chars.
65    pub static ref INAME_RE: Regex = {
66        #[allow(clippy::expect_used)]
67        Regex::new("^[a-z][a-z0-9-_\\.]{0,63}$").expect("Invalid Iname regex found")
68    };
69
70    /// Only alpha-numeric with limited special chars and space
71    pub static ref LABEL_RE: Regex = {
72        #[allow(clippy::expect_used)]
73        Regex::new("^[a-zA-Z0-9][ a-zA-Z0-9-_\\.@]{0,63}$").expect("Invalid Iname regex found")
74    };
75
76    /// Only lowercase+numbers, with limited chars.
77    pub static ref HEXSTR_RE: Regex = {
78        #[allow(clippy::expect_used)]
79        Regex::new("^[a-f0-9]+$").expect("Invalid hexstring regex found")
80    };
81
82    pub static ref EXTRACT_VAL_DN: Regex = {
83        #[allow(clippy::expect_used)]
84        Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)").expect("extract val from dn regex")
85        // Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)(,.*)?$").expect("Invalid Iname regex found")
86    };
87
88    pub static ref NSUNIQUEID_RE: Regex = {
89        #[allow(clippy::expect_used)]
90        Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
91    };
92
93    /// Must not contain whitespace.
94    pub static ref OAUTHSCOPE_RE: Regex = {
95        #[allow(clippy::expect_used)]
96        Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
97    };
98
99    pub static ref SINGLELINE_RE: Regex = {
100        #[allow(clippy::expect_used)]
101        Regex::new("[\n\r\t]").expect("Invalid singleline regex found")
102    };
103
104    /// Per https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
105    /// this regex validates for valid emails.
106    pub static ref VALIDATE_EMAIL_RE: Regex = {
107        #[allow(clippy::expect_used)]
108        Regex::new(r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
109            .expect("Invalid singleline regex found")
110    };
111
112    // Formerly checked with
113    /*
114    pub static ref ESCAPES_RE: Regex = {
115        #[allow(clippy::expect_used)]
116        Regex::new(r"\x1b\[([\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])")
117            .expect("Invalid escapes regex found")
118    };
119    */
120
121    pub static ref UNICODE_CONTROL_RE: Regex = {
122        #[allow(clippy::expect_used)]
123        Regex::new(r"[[:cntrl:]]")
124            .expect("Invalid unicode control regex found")
125    };
126}
127
128#[derive(Debug, Clone, PartialOrd, Ord, Eq, PartialEq, Hash)]
129// https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim
130pub struct Address {
131    pub formatted: String,
132    pub street_address: String,
133    pub locality: String,
134    pub region: String,
135    pub postal_code: String,
136    // Must be validated.
137    pub country: String,
138}
139#[derive(Debug, Copy, Clone, PartialEq, Eq)]
140pub struct CredUpdateSessionPerms {
141    pub ext_cred_portal_can_view: bool,
142    pub primary_can_edit: bool,
143    pub passkeys_can_edit: bool,
144    pub attested_passkeys_can_edit: bool,
145    pub unixcred_can_edit: bool,
146    pub sshpubkey_can_edit: bool,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum IntentTokenState {
151    Valid {
152        max_ttl: Duration,
153        perms: CredUpdateSessionPerms,
154    },
155    InProgress {
156        max_ttl: Duration,
157        perms: CredUpdateSessionPerms,
158        session_id: Uuid,
159        session_ttl: Duration,
160    },
161    Consumed {
162        max_ttl: Duration,
163    },
164}
165
166#[allow(non_camel_case_types)]
167#[derive(
168    Debug,
169    Clone,
170    Copy,
171    PartialEq,
172    Eq,
173    PartialOrd,
174    Ord,
175    Deserialize,
176    Serialize,
177    Hash,
178    TryFromPrimitive,
179)]
180#[repr(u16)]
181pub enum IndexType {
182    Equality,
183    Presence,
184    SubString,
185    Ordering,
186}
187
188impl TryFrom<&str> for IndexType {
189    type Error = ();
190
191    fn try_from(value: &str) -> Result<Self, Self::Error> {
192        let n_value = value.to_uppercase();
193        match n_value.as_str() {
194            "EQUALITY" => Ok(IndexType::Equality),
195            "PRESENCE" => Ok(IndexType::Presence),
196            "SUBSTRING" => Ok(IndexType::SubString),
197            "ORDERING" => Ok(IndexType::Ordering),
198            // UUID map?
199            // UUID rev map?
200            _ => Err(()),
201        }
202    }
203}
204
205impl IndexType {
206    pub fn as_idx_str(&self) -> &str {
207        match self {
208            IndexType::Equality => "eq",
209            IndexType::Presence => "pres",
210            IndexType::SubString => "sub",
211            IndexType::Ordering => "ord",
212        }
213    }
214}
215
216impl fmt::Display for IndexType {
217    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218        write!(
219            f,
220            "{}",
221            match self {
222                IndexType::Equality => "EQUALITY",
223                IndexType::Presence => "PRESENCE",
224                IndexType::SubString => "SUBSTRING",
225                IndexType::Ordering => "ORDERING",
226            }
227        )
228    }
229}
230
231#[allow(non_camel_case_types)]
232#[derive(
233    Hash,
234    Debug,
235    Clone,
236    Copy,
237    PartialEq,
238    Eq,
239    PartialOrd,
240    Ord,
241    Deserialize,
242    Serialize,
243    TryFromPrimitive,
244    Default,
245)]
246#[repr(u16)]
247pub enum SyntaxType {
248    #[default]
249    Utf8String = 0,
250    Utf8StringInsensitive = 1,
251    Uuid = 2,
252    Boolean = 3,
253    SyntaxId = 4,
254    IndexId = 5,
255    ReferenceUuid = 6,
256    JsonFilter = 7,
257    Credential = 8,
258    SecretUtf8String = 9,
259    SshKey = 10,
260    SecurityPrincipalName = 11,
261    Uint32 = 12,
262    Cid = 13,
263    Utf8StringIname = 14,
264    NsUniqueId = 15,
265    DateTime = 16,
266    EmailAddress = 17,
267    Url = 18,
268    OauthScope = 19,
269    OauthScopeMap = 20,
270    PrivateBinary = 21,
271    IntentToken = 22,
272    Passkey = 23,
273    AttestedPasskey = 24,
274    Session = 25,
275    JwsKeyEs256 = 26,
276    JwsKeyRs256 = 27,
277    Oauth2Session = 28,
278    UiHint = 29,
279    TotpSecret = 30,
280    ApiToken = 31,
281    AuditLogString = 32,
282    EcKeyPrivate = 33,
283    Image = 34,
284    CredentialType = 35,
285    WebauthnAttestationCaList = 36,
286    OauthClaimMap = 37,
287    KeyInternal = 38,
288    HexString = 39,
289    Certificate = 40,
290    ApplicationPassword = 41,
291    Json = 42,
292    Message = 43,
293}
294
295impl TryFrom<&str> for SyntaxType {
296    type Error = ();
297
298    fn try_from(value: &str) -> Result<SyntaxType, Self::Error> {
299        let n_value = value.to_uppercase();
300        match n_value.as_str() {
301            "UTF8STRING" => Ok(SyntaxType::Utf8String),
302            "UTF8STRING_INSENSITIVE" => Ok(SyntaxType::Utf8StringInsensitive),
303            "UTF8STRING_INAME" => Ok(SyntaxType::Utf8StringIname),
304            "UUID" => Ok(SyntaxType::Uuid),
305            "BOOLEAN" => Ok(SyntaxType::Boolean),
306            "SYNTAX_ID" => Ok(SyntaxType::SyntaxId),
307            "INDEX_ID" => Ok(SyntaxType::IndexId),
308            "REFERENCE_UUID" => Ok(SyntaxType::ReferenceUuid),
309            "JSON_FILTER" => Ok(SyntaxType::JsonFilter),
310            "CREDENTIAL" => Ok(SyntaxType::Credential),
311            // Compatibility for older syntax name.
312            "RADIUS_UTF8STRING" | "SECRET_UTF8STRING" => Ok(SyntaxType::SecretUtf8String),
313            "SSHKEY" => Ok(SyntaxType::SshKey),
314            "SECURITY_PRINCIPAL_NAME" => Ok(SyntaxType::SecurityPrincipalName),
315            "UINT32" => Ok(SyntaxType::Uint32),
316            "CID" => Ok(SyntaxType::Cid),
317            "NSUNIQUEID" => Ok(SyntaxType::NsUniqueId),
318            "DATETIME" => Ok(SyntaxType::DateTime),
319            "EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress),
320            "URL" => Ok(SyntaxType::Url),
321            "OAUTH_SCOPE" => Ok(SyntaxType::OauthScope),
322            "OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap),
323            "PRIVATE_BINARY" => Ok(SyntaxType::PrivateBinary),
324            "INTENT_TOKEN" => Ok(SyntaxType::IntentToken),
325            "PASSKEY" => Ok(SyntaxType::Passkey),
326            "ATTESTED_PASSKEY" => Ok(SyntaxType::AttestedPasskey),
327            "SESSION" => Ok(SyntaxType::Session),
328            "JWS_KEY_ES256" => Ok(SyntaxType::JwsKeyEs256),
329            "JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256),
330            "OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session),
331            "UIHINT" => Ok(SyntaxType::UiHint),
332            "TOTPSECRET" => Ok(SyntaxType::TotpSecret),
333            "APITOKEN" => Ok(SyntaxType::ApiToken),
334            "AUDIT_LOG_STRING" => Ok(SyntaxType::AuditLogString),
335            "EC_KEY_PRIVATE" => Ok(SyntaxType::EcKeyPrivate),
336            "CREDENTIAL_TYPE" => Ok(SyntaxType::CredentialType),
337            "WEBAUTHN_ATTESTATION_CA_LIST" => Ok(SyntaxType::WebauthnAttestationCaList),
338            "OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
339            "KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
340            "HEX_STRING" => Ok(SyntaxType::HexString),
341            "CERTIFICATE" => Ok(SyntaxType::Certificate),
342            "APPLICATION_PASSWORD" => Ok(SyntaxType::ApplicationPassword),
343            "JSON" => Ok(SyntaxType::Json),
344            "MESSAGE" => Ok(SyntaxType::Message),
345            _ => Err(()),
346        }
347    }
348}
349
350impl fmt::Display for SyntaxType {
351    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
352        f.write_str(match self {
353            SyntaxType::Utf8String => "UTF8STRING",
354            SyntaxType::Utf8StringInsensitive => "UTF8STRING_INSENSITIVE",
355            SyntaxType::Utf8StringIname => "UTF8STRING_INAME",
356            SyntaxType::Uuid => "UUID",
357            SyntaxType::Boolean => "BOOLEAN",
358            SyntaxType::SyntaxId => "SYNTAX_ID",
359            SyntaxType::IndexId => "INDEX_ID",
360            SyntaxType::ReferenceUuid => "REFERENCE_UUID",
361            SyntaxType::JsonFilter => "JSON_FILTER",
362            SyntaxType::Credential => "CREDENTIAL",
363            SyntaxType::SecretUtf8String => "SECRET_UTF8STRING",
364            SyntaxType::SshKey => "SSHKEY",
365            SyntaxType::SecurityPrincipalName => "SECURITY_PRINCIPAL_NAME",
366            SyntaxType::Uint32 => "UINT32",
367            SyntaxType::Cid => "CID",
368            SyntaxType::NsUniqueId => "NSUNIQUEID",
369            SyntaxType::DateTime => "DATETIME",
370            SyntaxType::EmailAddress => "EMAIL_ADDRESS",
371            SyntaxType::Url => "URL",
372            SyntaxType::OauthScope => "OAUTH_SCOPE",
373            SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP",
374            SyntaxType::PrivateBinary => "PRIVATE_BINARY",
375            SyntaxType::IntentToken => "INTENT_TOKEN",
376            SyntaxType::Passkey => "PASSKEY",
377            SyntaxType::AttestedPasskey => "ATTESTED_PASSKEY",
378            SyntaxType::Session => "SESSION",
379            SyntaxType::JwsKeyEs256 => "JWS_KEY_ES256",
380            SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256",
381            SyntaxType::Oauth2Session => "OAUTH2SESSION",
382            SyntaxType::UiHint => "UIHINT",
383            SyntaxType::TotpSecret => "TOTPSECRET",
384            SyntaxType::ApiToken => "APITOKEN",
385            SyntaxType::AuditLogString => "AUDIT_LOG_STRING",
386            SyntaxType::EcKeyPrivate => "EC_KEY_PRIVATE",
387            SyntaxType::Image => "IMAGE",
388            SyntaxType::CredentialType => "CREDENTIAL_TYPE",
389            SyntaxType::WebauthnAttestationCaList => "WEBAUTHN_ATTESTATION_CA_LIST",
390            SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
391            SyntaxType::KeyInternal => "KEY_INTERNAL",
392            SyntaxType::HexString => "HEX_STRING",
393            SyntaxType::Certificate => "CERTIFICATE",
394            SyntaxType::ApplicationPassword => "APPLICATION_PASSWORD",
395            SyntaxType::Json => "JSON",
396            SyntaxType::Message => "MESSAGE",
397        })
398    }
399}
400
401impl SyntaxType {
402    pub fn index_types(&self) -> &[IndexType] {
403        match self {
404            SyntaxType::Utf8String => &[IndexType::Equality, IndexType::Presence],
405            // Used by classes, needs to change ...
406            // Probably need an attrname syntax too
407            SyntaxType::Utf8StringInsensitive => &[IndexType::Equality, IndexType::Presence],
408            SyntaxType::Utf8StringIname => &[
409                IndexType::Equality,
410                IndexType::Presence,
411                IndexType::SubString,
412            ],
413            SyntaxType::Uuid => &[IndexType::Equality, IndexType::Presence],
414            SyntaxType::Boolean => &[IndexType::Equality],
415            SyntaxType::ReferenceUuid => &[IndexType::Equality, IndexType::Presence],
416            SyntaxType::Credential => &[IndexType::Equality],
417            SyntaxType::SshKey => &[IndexType::Equality, IndexType::Presence],
418            SyntaxType::SecurityPrincipalName => &[
419                IndexType::Equality,
420                IndexType::Presence,
421                IndexType::SubString,
422            ],
423            SyntaxType::Uint32 => &[IndexType::Equality, IndexType::Presence],
424            SyntaxType::Cid => &[
425                IndexType::Equality,
426                IndexType::Presence,
427                IndexType::Ordering,
428            ],
429            SyntaxType::NsUniqueId => &[IndexType::Equality, IndexType::Presence],
430            SyntaxType::DateTime => &[
431                IndexType::Equality,
432                IndexType::Presence,
433                IndexType::Ordering,
434            ],
435            SyntaxType::EmailAddress => &[IndexType::Equality, IndexType::SubString],
436            SyntaxType::OauthScopeMap => &[IndexType::Equality],
437            SyntaxType::IntentToken => &[IndexType::Equality],
438            SyntaxType::Passkey => &[IndexType::Equality],
439            SyntaxType::AttestedPasskey => &[IndexType::Equality],
440            SyntaxType::Session => &[IndexType::Equality],
441            SyntaxType::Oauth2Session => &[IndexType::Equality],
442            SyntaxType::ApiToken => &[IndexType::Equality],
443            SyntaxType::OauthClaimMap => &[IndexType::Equality],
444            SyntaxType::ApplicationPassword => &[IndexType::Equality],
445            SyntaxType::SecretUtf8String => &[],
446            SyntaxType::Url => &[],
447            SyntaxType::OauthScope => &[],
448            SyntaxType::PrivateBinary => &[],
449            SyntaxType::JwsKeyEs256 => &[],
450            SyntaxType::JwsKeyRs256 => &[],
451            SyntaxType::UiHint => &[],
452            SyntaxType::TotpSecret => &[],
453            SyntaxType::AuditLogString => &[],
454            SyntaxType::EcKeyPrivate => &[],
455            SyntaxType::Image => &[],
456            SyntaxType::CredentialType => &[],
457            SyntaxType::WebauthnAttestationCaList => &[],
458            SyntaxType::KeyInternal => &[],
459            SyntaxType::HexString => &[],
460            SyntaxType::Certificate => &[],
461            SyntaxType::SyntaxId => &[],
462            SyntaxType::IndexId => &[],
463            SyntaxType::JsonFilter => &[],
464            SyntaxType::Json => &[],
465            SyntaxType::Message => &[],
466        }
467    }
468}
469
470#[derive(
471    Hash,
472    Debug,
473    Clone,
474    Copy,
475    PartialEq,
476    Eq,
477    PartialOrd,
478    Ord,
479    Deserialize,
480    Serialize,
481    TryFromPrimitive,
482    Default,
483)]
484#[repr(u16)]
485pub enum CredentialType {
486    Any = 0,
487    #[default]
488    Mfa = 10,
489    Passkey = 20,
490    AttestedPasskey = 30,
491    AttestedResidentkey = 40,
492    Invalid = u16::MAX,
493}
494
495impl TryFrom<&str> for CredentialType {
496    type Error = ();
497
498    fn try_from(value: &str) -> Result<CredentialType, Self::Error> {
499        match value {
500            "any" => Ok(CredentialType::Any),
501            "mfa" => Ok(CredentialType::Mfa),
502            "passkey" => Ok(CredentialType::Passkey),
503            "attested_passkey" => Ok(CredentialType::AttestedPasskey),
504            "attested_residentkey" => Ok(CredentialType::AttestedResidentkey),
505            "invalid" => Ok(CredentialType::Invalid),
506            _ => Err(()),
507        }
508    }
509}
510
511impl fmt::Display for CredentialType {
512    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
513        f.write_str(match self {
514            CredentialType::Any => "any",
515            CredentialType::Mfa => "mfa",
516            CredentialType::Passkey => "passkey",
517            CredentialType::AttestedPasskey => "attested_passkey",
518            CredentialType::AttestedResidentkey => "attested_residentkey",
519            CredentialType::Invalid => "invalid",
520        })
521    }
522}
523
524impl From<CredentialType> for Value {
525    fn from(ct: CredentialType) -> Value {
526        Value::CredentialType(ct)
527    }
528}
529
530impl From<CredentialType> for PartialValue {
531    fn from(ct: CredentialType) -> PartialValue {
532        PartialValue::CredentialType(ct)
533    }
534}
535
536impl From<Attribute> for Value {
537    fn from(attr: Attribute) -> Value {
538        let s: &str = attr.as_str();
539        Value::new_iutf8(s)
540    }
541}
542
543impl From<Attribute> for PartialValue {
544    fn from(attr: Attribute) -> PartialValue {
545        let s: &str = attr.as_str();
546        PartialValue::new_iutf8(s)
547    }
548}
549
550/// A partial value is a key or key subset that can be used to match for equality or substring
551/// against a complete Value within a set in an Entry.
552///
553/// A partialValue is typically used when you need to match against a value, but without
554/// requiring all of its data or expression. This is common in Filters or other direct
555/// lookups and requests.
556#[derive(Hash, Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
557pub enum PartialValue {
558    Utf8(String),
559    Iutf8(String),
560    Iname(String),
561    Uuid(Uuid),
562    Bool(bool),
563    Syntax(SyntaxType),
564    Index(IndexType),
565    Refer(Uuid),
566    // Does this make sense?
567    // TODO: We'll probably add tagging to this type for the partial matching
568    JsonFilt(ProtoFilter),
569    // Tag, matches to a DataValue.
570    Cred(String),
571    SshKey(String),
572    SecretValue,
573    Spn(String, String),
574    Uint32(u32),
575    Cid(Cid),
576    Nsuniqueid(String),
577    DateTime(OffsetDateTime),
578    EmailAddress(String),
579    PhoneNumber(String),
580    Address(String),
581    // Can add other selectors later.
582    Url(Url),
583    OauthScope(String),
584    // OauthScopeMap(Uuid),
585    PrivateBinary,
586    PublicBinary(String),
587    // Enumeration(String),
588    // Float64(f64),
589    RestrictedString(String),
590    IntentToken(String),
591    UiHint(UiHint),
592    Passkey(Uuid),
593    AttestedPasskey(Uuid),
594    /// We compare on the value hash
595    Image(String),
596    CredentialType(CredentialType),
597
598    OauthClaim(String, Uuid),
599    OauthClaimValue(String, Uuid, String),
600
601    HexString(String),
602    Json,
603}
604
605impl From<SyntaxType> for PartialValue {
606    fn from(s: SyntaxType) -> Self {
607        PartialValue::Syntax(s)
608    }
609}
610
611impl From<IndexType> for PartialValue {
612    fn from(i: IndexType) -> Self {
613        PartialValue::Index(i)
614    }
615}
616
617impl From<bool> for PartialValue {
618    fn from(b: bool) -> Self {
619        PartialValue::Bool(b)
620    }
621}
622
623impl From<&bool> for PartialValue {
624    fn from(b: &bool) -> Self {
625        PartialValue::Bool(*b)
626    }
627}
628
629impl From<ProtoFilter> for PartialValue {
630    fn from(i: ProtoFilter) -> Self {
631        PartialValue::JsonFilt(i)
632    }
633}
634
635impl From<u32> for PartialValue {
636    fn from(i: u32) -> Self {
637        PartialValue::Uint32(i)
638    }
639}
640
641impl From<OffsetDateTime> for PartialValue {
642    fn from(i: OffsetDateTime) -> Self {
643        PartialValue::DateTime(i)
644    }
645}
646
647impl From<Url> for PartialValue {
648    fn from(i: Url) -> Self {
649        PartialValue::Url(i)
650    }
651}
652
653impl PartialValue {
654    pub fn new_utf8(s: String) -> Self {
655        PartialValue::Utf8(s)
656    }
657
658    pub fn new_utf8s(s: &str) -> Self {
659        PartialValue::Utf8(s.to_string())
660    }
661
662    pub fn is_utf8(&self) -> bool {
663        matches!(self, PartialValue::Utf8(_))
664    }
665
666    pub fn new_iutf8(s: &str) -> Self {
667        PartialValue::Iutf8(s.to_lowercase())
668    }
669
670    pub fn new_iname(s: &str) -> Self {
671        PartialValue::Iname(s.to_lowercase())
672    }
673
674    pub fn is_iutf8(&self) -> bool {
675        matches!(self, PartialValue::Iutf8(_))
676    }
677
678    pub fn is_iname(&self) -> bool {
679        matches!(self, PartialValue::Iname(_))
680    }
681
682    pub fn new_bool(b: bool) -> Self {
683        PartialValue::Bool(b)
684    }
685
686    pub fn new_bools(s: &str) -> Option<Self> {
687        bool::from_str(s).map(PartialValue::Bool).ok()
688    }
689
690    pub fn is_bool(&self) -> bool {
691        matches!(self, PartialValue::Bool(_))
692    }
693
694    pub fn new_uuid_s(us: &str) -> Option<Self> {
695        Uuid::parse_str(us).map(PartialValue::Uuid).ok()
696    }
697
698    pub fn is_uuid(&self) -> bool {
699        matches!(self, PartialValue::Uuid(_))
700    }
701
702    pub fn new_refer_s(us: &str) -> Option<Self> {
703        match Uuid::parse_str(us) {
704            Ok(u) => Some(PartialValue::Refer(u)),
705            Err(_) => None,
706        }
707    }
708
709    pub fn is_refer(&self) -> bool {
710        matches!(self, PartialValue::Refer(_))
711    }
712
713    pub fn new_indexes(s: &str) -> Option<Self> {
714        IndexType::try_from(s).map(PartialValue::Index).ok()
715    }
716
717    pub fn is_index(&self) -> bool {
718        matches!(self, PartialValue::Index(_))
719    }
720
721    pub fn new_syntaxs(s: &str) -> Option<Self> {
722        SyntaxType::try_from(s).map(PartialValue::Syntax).ok()
723    }
724
725    pub fn is_syntax(&self) -> bool {
726        matches!(self, PartialValue::Syntax(_))
727    }
728
729    pub fn new_json_filter_s(s: &str) -> Option<Self> {
730        serde_json::from_str(s)
731            .map(PartialValue::JsonFilt)
732            .map_err(|e| {
733                trace!(?e, ?s);
734            })
735            .ok()
736    }
737
738    pub fn is_json_filter(&self) -> bool {
739        matches!(self, PartialValue::JsonFilt(_))
740    }
741
742    pub fn new_credential_tag(s: &str) -> Self {
743        PartialValue::Cred(s.to_lowercase())
744    }
745
746    pub fn is_credential(&self) -> bool {
747        matches!(self, PartialValue::Cred(_))
748    }
749
750    pub fn new_secret_str() -> Self {
751        PartialValue::SecretValue
752    }
753
754    pub fn is_secret_string(&self) -> bool {
755        matches!(self, PartialValue::SecretValue)
756    }
757
758    pub fn new_sshkey_tag(s: String) -> Self {
759        PartialValue::SshKey(s)
760    }
761
762    pub fn new_sshkey_tag_s(s: &str) -> Self {
763        PartialValue::SshKey(s.to_string())
764    }
765
766    pub fn is_sshkey(&self) -> bool {
767        matches!(self, PartialValue::SshKey(_))
768    }
769
770    pub fn new_spn_s(s: &str) -> Option<Self> {
771        SPN_RE.captures(s).and_then(|caps| {
772            let name = match caps.name(Attribute::Name.as_ref()) {
773                Some(v) => v.as_str().to_string(),
774                None => return None,
775            };
776            let realm = match caps.name("realm") {
777                Some(v) => v.as_str().to_string(),
778                None => return None,
779            };
780            Some(PartialValue::Spn(name, realm))
781        })
782    }
783
784    pub fn new_spn_nrs(n: &str, r: &str) -> Self {
785        PartialValue::Spn(n.to_string(), r.to_string())
786    }
787
788    pub fn is_spn(&self) -> bool {
789        matches!(self, PartialValue::Spn(_, _))
790    }
791
792    pub fn new_uint32(u: u32) -> Self {
793        PartialValue::Uint32(u)
794    }
795
796    pub fn new_uint32_str(u: &str) -> Option<Self> {
797        u.parse::<u32>().ok().map(PartialValue::Uint32)
798    }
799
800    pub fn is_uint32(&self) -> bool {
801        matches!(self, PartialValue::Uint32(_))
802    }
803
804    pub fn new_cid(c: Cid) -> Self {
805        PartialValue::Cid(c)
806    }
807
808    pub fn new_cid_s(_c: &str) -> Option<Self> {
809        None
810    }
811
812    pub fn is_cid(&self) -> bool {
813        matches!(self, PartialValue::Cid(_))
814    }
815
816    pub fn new_nsuniqueid_s(s: &str) -> Self {
817        PartialValue::Nsuniqueid(s.to_lowercase())
818    }
819
820    pub fn is_nsuniqueid(&self) -> bool {
821        matches!(self, PartialValue::Nsuniqueid(_))
822    }
823
824    pub fn new_datetime_epoch(ts: Duration) -> Self {
825        PartialValue::DateTime(OffsetDateTime::UNIX_EPOCH + ts)
826    }
827
828    pub fn new_datetime_s(s: &str) -> Option<Self> {
829        OffsetDateTime::parse(s, &Rfc3339)
830            .ok()
831            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
832            .map(PartialValue::DateTime)
833    }
834
835    pub fn is_datetime(&self) -> bool {
836        matches!(self, PartialValue::DateTime(_))
837    }
838
839    pub fn new_email_address_s(s: &str) -> Self {
840        PartialValue::EmailAddress(s.to_string())
841    }
842
843    pub fn is_email_address(&self) -> bool {
844        matches!(self, PartialValue::EmailAddress(_))
845    }
846
847    pub fn new_phonenumber_s(s: &str) -> Self {
848        PartialValue::PhoneNumber(s.to_string())
849    }
850
851    pub fn new_address(s: &str) -> Self {
852        PartialValue::Address(s.to_string())
853    }
854
855    pub fn new_url_s(s: &str) -> Option<Self> {
856        Url::parse(s).ok().map(PartialValue::Url)
857    }
858
859    pub fn is_url(&self) -> bool {
860        matches!(self, PartialValue::Url(_))
861    }
862
863    pub fn new_oauthscope(s: &str) -> Self {
864        PartialValue::OauthScope(s.to_string())
865    }
866
867    pub fn is_oauthscope(&self) -> bool {
868        matches!(self, PartialValue::OauthScope(_))
869    }
870
871    /*
872    pub fn new_oauthscopemap(u: Uuid) -> Self {
873        PartialValue::OauthScopeMap(u)
874    }
875
876    pub fn new_oauthscopemap_s(us: &str) -> Option<Self> {
877        match Uuid::parse_str(us) {
878            Ok(u) => Some(PartialValue::OauthScopeMap(u)),
879            Err(_) => None,
880        }
881    }
882
883    pub fn is_oauthscopemap(&self) -> bool {
884        matches!(self, PartialValue::OauthScopeMap(_))
885    }
886    */
887
888    pub fn is_privatebinary(&self) -> bool {
889        matches!(self, PartialValue::PrivateBinary)
890    }
891
892    pub fn new_publicbinary_tag_s(s: &str) -> Self {
893        PartialValue::PublicBinary(s.to_string())
894    }
895
896    pub fn new_restrictedstring_s(s: &str) -> Self {
897        PartialValue::RestrictedString(s.to_string())
898    }
899
900    pub fn new_intenttoken_s(s: String) -> Option<Self> {
901        Some(PartialValue::IntentToken(s))
902    }
903
904    pub fn new_passkey_s(us: &str) -> Option<Self> {
905        Uuid::parse_str(us).map(PartialValue::Passkey).ok()
906    }
907
908    pub fn new_attested_passkey_s(us: &str) -> Option<Self> {
909        Uuid::parse_str(us).map(PartialValue::AttestedPasskey).ok()
910    }
911
912    pub fn new_hex_string_s(hexstr: &str) -> Option<Self> {
913        let hexstr_lower = hexstr.to_lowercase();
914        if HEXSTR_RE.is_match(&hexstr_lower) {
915            Some(PartialValue::HexString(hexstr_lower))
916        } else {
917            None
918        }
919    }
920
921    pub fn new_image(input: &str) -> Self {
922        PartialValue::Image(input.to_string())
923    }
924
925    pub fn to_str(&self) -> Option<&str> {
926        match self {
927            PartialValue::Utf8(s) => Some(s.as_str()),
928            PartialValue::Iutf8(s) => Some(s.as_str()),
929            PartialValue::Iname(s) => Some(s.as_str()),
930            _ => None,
931        }
932    }
933
934    pub fn to_url(&self) -> Option<&Url> {
935        match self {
936            PartialValue::Url(u) => Some(u),
937            _ => None,
938        }
939    }
940
941    pub fn get_idx_eq_key(&self) -> String {
942        match self {
943            PartialValue::Utf8(s)
944            | PartialValue::Iutf8(s)
945            | PartialValue::Iname(s)
946            | PartialValue::Nsuniqueid(s)
947            | PartialValue::EmailAddress(s)
948            | PartialValue::RestrictedString(s) => s.clone(),
949            PartialValue::Passkey(u)
950            | PartialValue::AttestedPasskey(u)
951            | PartialValue::Refer(u)
952            | PartialValue::Uuid(u) => u.as_hyphenated().to_string(),
953            PartialValue::Bool(b) => b.to_string(),
954            PartialValue::Syntax(syn) => syn.to_string(),
955            PartialValue::Index(it) => it.to_string(),
956            PartialValue::JsonFilt(s) =>
957            {
958                #[allow(clippy::expect_used)]
959                serde_json::to_string(s).expect("A json filter value was corrupted during run-time")
960            }
961            PartialValue::Cred(tag)
962            | PartialValue::PublicBinary(tag)
963            | PartialValue::SshKey(tag) => tag.to_string(),
964            // This will never match as we never index radius creds! See generate_idx_eq_keys
965            PartialValue::SecretValue | PartialValue::PrivateBinary => "_".to_string(),
966            PartialValue::Spn(name, realm) => format!("{name}@{realm}"),
967            PartialValue::Uint32(u) => u.to_string(),
968            PartialValue::DateTime(odt) => {
969                debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
970                #[allow(clippy::expect_used)]
971                odt.format(&Rfc3339)
972                    .expect("Failed to format timestamp into RFC3339")
973            }
974            PartialValue::Url(u) => u.to_string(),
975            PartialValue::OauthScope(u) => u.to_string(),
976            PartialValue::Address(a) => a.to_string(),
977            PartialValue::PhoneNumber(a) => a.to_string(),
978            PartialValue::IntentToken(u) => u.clone(),
979            PartialValue::UiHint(u) => (*u as u16).to_string(),
980            PartialValue::Image(imagehash) => imagehash.to_owned(),
981            PartialValue::CredentialType(ct) => ct.to_string(),
982            // This will never work, we don't allow equality searching on Cid's
983            PartialValue::Cid(_) => "_".to_string(),
984            // We don't allow searching on claim/uuid pairs.
985            PartialValue::OauthClaim(_, _) => "_".to_string(),
986            PartialValue::OauthClaimValue(_, _, _) => "_".to_string(),
987            PartialValue::HexString(hexstr) => hexstr.to_string(),
988            PartialValue::Json => "_".to_string(),
989        }
990    }
991
992    pub fn get_idx_sub_key(&self) -> Option<String> {
993        match self {
994            PartialValue::Utf8(s)
995            | PartialValue::Iutf8(s)
996            | PartialValue::Iname(s)
997            // | PartialValue::Nsuniqueid(s)
998            | PartialValue::EmailAddress(s)
999            | PartialValue::RestrictedString(s) => Some(s.to_lowercase()),
1000
1001            PartialValue::Cred(tag)
1002            | PartialValue::PublicBinary(tag)
1003            | PartialValue::SshKey(tag) => Some(tag.to_lowercase()),
1004
1005            // PartialValue::Spn(name, realm) => format!("{name}@{realm}"),
1006            _ => None,
1007        }
1008    }
1009}
1010
1011#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1012pub enum ApiTokenScope {
1013    ReadOnly,
1014    ReadWrite,
1015    Synchronise,
1016}
1017
1018impl fmt::Display for ApiTokenScope {
1019    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1020        match self {
1021            ApiTokenScope::ReadOnly => write!(f, "read_only"),
1022            ApiTokenScope::ReadWrite => write!(f, "read_write"),
1023            ApiTokenScope::Synchronise => write!(f, "synchronise"),
1024        }
1025    }
1026}
1027
1028impl TryInto<ApiTokenPurpose> for ApiTokenScope {
1029    type Error = OperationError;
1030
1031    fn try_into(self: ApiTokenScope) -> Result<ApiTokenPurpose, OperationError> {
1032        match self {
1033            ApiTokenScope::ReadOnly => Ok(ApiTokenPurpose::ReadOnly),
1034            ApiTokenScope::ReadWrite => Ok(ApiTokenPurpose::ReadWrite),
1035            ApiTokenScope::Synchronise => Ok(ApiTokenPurpose::Synchronise),
1036        }
1037    }
1038}
1039
1040#[derive(Clone, Debug, PartialEq, Eq)]
1041pub struct ApiToken {
1042    pub label: String,
1043    pub expiry: Option<OffsetDateTime>,
1044    pub issued_at: OffsetDateTime,
1045    pub issued_by: IdentityId,
1046    pub scope: ApiTokenScope,
1047}
1048
1049#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1050pub enum SessionScope {
1051    ReadOnly,
1052    ReadWrite,
1053    PrivilegeCapable,
1054    // For migration! To be removed in future!
1055    Synchronise,
1056}
1057
1058impl fmt::Display for SessionScope {
1059    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1060        match self {
1061            SessionScope::ReadOnly => write!(f, "read_only"),
1062            SessionScope::ReadWrite => write!(f, "read_write"),
1063            SessionScope::PrivilegeCapable => write!(f, "privilege_capable"),
1064            SessionScope::Synchronise => write!(f, "synchronise"),
1065        }
1066    }
1067}
1068
1069impl TryInto<UatPurposeStatus> for SessionScope {
1070    type Error = OperationError;
1071
1072    fn try_into(self: SessionScope) -> Result<UatPurposeStatus, OperationError> {
1073        match self {
1074            SessionScope::ReadOnly => Ok(UatPurposeStatus::ReadOnly),
1075            SessionScope::ReadWrite => Ok(UatPurposeStatus::ReadWrite),
1076            SessionScope::PrivilegeCapable => Ok(UatPurposeStatus::PrivilegeCapable),
1077            SessionScope::Synchronise => Err(OperationError::InvalidEntryState),
1078        }
1079    }
1080}
1081
1082#[derive(Clone, Debug, PartialEq, Eq)]
1083pub enum SessionState {
1084    // IMPORTANT - this order allows sorting by
1085    // lowest to highest, we always want to take
1086    // the lowest value!
1087    RevokedAt(Cid),
1088    ExpiresAt(OffsetDateTime),
1089    NeverExpires,
1090}
1091
1092impl PartialOrd for SessionState {
1093    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1094        Some(self.cmp(other))
1095    }
1096}
1097
1098impl Ord for SessionState {
1099    fn cmp(&self, other: &Self) -> Ordering {
1100        // We need this to order by highest -> least which represents
1101        // priority amongst these elements.
1102        match (self, other) {
1103            // RevokedAt with the earliest time = highest
1104            (SessionState::RevokedAt(c_self), SessionState::RevokedAt(c_other)) => {
1105                // We need to reverse this - we need the "lower value" to take priority.
1106                // This is similar to tombstones where the earliest CID must be persisted
1107                c_other.cmp(c_self)
1108            }
1109            (SessionState::RevokedAt(_), _) => Ordering::Greater,
1110            (_, SessionState::RevokedAt(_)) => Ordering::Less,
1111            // ExpiresAt with a greater time = higher
1112            (SessionState::ExpiresAt(e_self), SessionState::ExpiresAt(e_other)) => {
1113                // Keep the "newer" expiry. This can be because a session was extended
1114                // by some mechanism, generally in oauth2.
1115                e_self.cmp(e_other)
1116            }
1117            (SessionState::ExpiresAt(_), _) => Ordering::Greater,
1118            (_, SessionState::ExpiresAt(_)) => Ordering::Less,
1119            // NeverExpires = least.
1120            (SessionState::NeverExpires, SessionState::NeverExpires) => Ordering::Equal,
1121        }
1122    }
1123}
1124
1125#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
1126pub enum AuthType {
1127    Anonymous,
1128    Password,
1129    GeneratedPassword,
1130    PasswordTotp,
1131    PasswordBackupCode,
1132    PasswordSecurityKey,
1133    Passkey,
1134    AttestedPasskey,
1135}
1136
1137impl fmt::Display for AuthType {
1138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1139        match self {
1140            AuthType::Anonymous => write!(f, "anonymous"),
1141            AuthType::Password => write!(f, "password"),
1142            AuthType::GeneratedPassword => write!(f, "generatedpassword"),
1143            AuthType::PasswordTotp => write!(f, "passwordtotp"),
1144            AuthType::PasswordBackupCode => write!(f, "passwordbackupcode"),
1145            AuthType::PasswordSecurityKey => write!(f, "passwordsecuritykey"),
1146            AuthType::Passkey => write!(f, "passkey"),
1147            AuthType::AttestedPasskey => write!(f, "attested_passkey"),
1148        }
1149    }
1150}
1151
1152#[derive(Clone, PartialEq, Eq)]
1153pub struct Session {
1154    pub label: String,
1155    // pub expiry: Option<OffsetDateTime>,
1156    pub state: SessionState,
1157    pub issued_at: OffsetDateTime,
1158    pub issued_by: IdentityId,
1159    pub cred_id: Uuid,
1160    pub scope: SessionScope,
1161    pub type_: AuthType,
1162}
1163
1164impl fmt::Debug for Session {
1165    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1166        let issuer = match self.issued_by {
1167            IdentityId::User(u) => format!("User - {}", uuid_to_proto_string(u)),
1168            IdentityId::Synch(u) => format!("Synch - {}", uuid_to_proto_string(u)),
1169            IdentityId::Internal => "Internal".to_string(),
1170        };
1171        let expiry = match self.state {
1172            SessionState::ExpiresAt(e) => e.to_string(),
1173            SessionState::NeverExpires => "never".to_string(),
1174            SessionState::RevokedAt(_) => "revoked".to_string(),
1175        };
1176        write!(
1177            f,
1178            "state: {}, issued at: {}, issued by: {}, credential id: {}, scope: {:?}",
1179            expiry, self.issued_at, issuer, self.cred_id, self.scope
1180        )
1181    }
1182}
1183
1184#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
1185pub enum OauthClaimMapJoin {
1186    CommaSeparatedValue,
1187    SpaceSeparatedValue,
1188    #[default]
1189    JsonArray,
1190}
1191
1192impl From<OauthClaimMapJoin> for ScimOauth2ClaimMapJoinChar {
1193    fn from(value: OauthClaimMapJoin) -> Self {
1194        match value {
1195            OauthClaimMapJoin::CommaSeparatedValue => {
1196                ScimOauth2ClaimMapJoinChar::CommaSeparatedValue
1197            }
1198            OauthClaimMapJoin::SpaceSeparatedValue => {
1199                ScimOauth2ClaimMapJoinChar::SpaceSeparatedValue
1200            }
1201            OauthClaimMapJoin::JsonArray => ScimOauth2ClaimMapJoinChar::JsonArray,
1202        }
1203    }
1204}
1205
1206impl From<ScimOauth2ClaimMapJoinChar> for OauthClaimMapJoin {
1207    fn from(value: ScimOauth2ClaimMapJoinChar) -> Self {
1208        match value {
1209            ScimOauth2ClaimMapJoinChar::CommaSeparatedValue => {
1210                OauthClaimMapJoin::CommaSeparatedValue
1211            }
1212            ScimOauth2ClaimMapJoinChar::SpaceSeparatedValue => {
1213                OauthClaimMapJoin::SpaceSeparatedValue
1214            }
1215            ScimOauth2ClaimMapJoinChar::JsonArray => OauthClaimMapJoin::JsonArray,
1216        }
1217    }
1218}
1219
1220impl OauthClaimMapJoin {
1221    pub(crate) fn to_str(self) -> &'static str {
1222        match self {
1223            OauthClaimMapJoin::CommaSeparatedValue => ",",
1224            OauthClaimMapJoin::SpaceSeparatedValue => " ",
1225            // Should this be something else?
1226            OauthClaimMapJoin::JsonArray => ";",
1227        }
1228    }
1229}
1230
1231impl From<DbValueOauthClaimMapJoinV1> for OauthClaimMapJoin {
1232    fn from(value: DbValueOauthClaimMapJoinV1) -> OauthClaimMapJoin {
1233        match value {
1234            DbValueOauthClaimMapJoinV1::CommaSeparatedValue => {
1235                OauthClaimMapJoin::CommaSeparatedValue
1236            }
1237            DbValueOauthClaimMapJoinV1::SpaceSeparatedValue => {
1238                OauthClaimMapJoin::SpaceSeparatedValue
1239            }
1240            DbValueOauthClaimMapJoinV1::JsonArray => OauthClaimMapJoin::JsonArray,
1241        }
1242    }
1243}
1244
1245impl From<OauthClaimMapJoin> for DbValueOauthClaimMapJoinV1 {
1246    fn from(value: OauthClaimMapJoin) -> DbValueOauthClaimMapJoinV1 {
1247        match value {
1248            OauthClaimMapJoin::CommaSeparatedValue => {
1249                DbValueOauthClaimMapJoinV1::CommaSeparatedValue
1250            }
1251            OauthClaimMapJoin::SpaceSeparatedValue => {
1252                DbValueOauthClaimMapJoinV1::SpaceSeparatedValue
1253            }
1254            OauthClaimMapJoin::JsonArray => DbValueOauthClaimMapJoinV1::JsonArray,
1255        }
1256    }
1257}
1258
1259#[derive(Clone, Debug, PartialEq, Eq)]
1260pub struct Oauth2Session {
1261    pub parent: Option<Uuid>,
1262    pub state: SessionState,
1263    pub issued_at: OffsetDateTime,
1264    pub rs_uuid: Uuid,
1265}
1266
1267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1268pub enum KeyUsage {
1269    JwsEs256,
1270    JwsHs256,
1271    JwsRs256,
1272    JweA128GCM,
1273    HkdfS256,
1274}
1275
1276impl fmt::Display for KeyUsage {
1277    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1278        write!(
1279            f,
1280            "{}",
1281            match self {
1282                KeyUsage::JwsEs256 => "jws_es256",
1283                KeyUsage::JwsHs256 => "jws_hs256",
1284                KeyUsage::JwsRs256 => "jws_rs256",
1285                KeyUsage::JweA128GCM => "jwe_a128gcm",
1286                KeyUsage::HkdfS256 => "hkdf_s256",
1287            }
1288        )
1289    }
1290}
1291
1292#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1293pub enum KeyStatus {
1294    Valid,
1295    Retained,
1296    Revoked,
1297}
1298
1299impl fmt::Display for KeyStatus {
1300    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1301        write!(
1302            f,
1303            "{}",
1304            match self {
1305                KeyStatus::Valid => "valid",
1306                KeyStatus::Retained => "retained",
1307                KeyStatus::Revoked => "revoked",
1308            }
1309        )
1310    }
1311}
1312
1313/// A value is a complete unit of data for an attribute. It is made up of a PartialValue, which is
1314/// used for selection, filtering, searching, matching etc. It also contains supplemental data
1315/// which may be stored inside of the Value, such as credential secrets, blobs etc.
1316///
1317/// This type is used when you need the "full data" of an attribute. Typically this is in a create
1318/// or modification operation where you are applying a set of complete values into an entry.
1319#[derive(Clone, Debug)]
1320pub enum Value {
1321    Utf8(String),
1322    /// Case insensitive string
1323    Iutf8(String),
1324    /// Case insensitive Name for a thing
1325    Iname(String),
1326    Uuid(Uuid),
1327    Bool(bool),
1328    Syntax(SyntaxType),
1329    Index(IndexType),
1330    Refer(Uuid),
1331    JsonFilt(ProtoFilter),
1332    Cred(String, Credential),
1333    SshKey(String, SshPublicKey),
1334    SecretValue(String),
1335    Spn(String, String),
1336    Uint32(u32),
1337    Cid(Cid),
1338    Nsuniqueid(String),
1339    DateTime(OffsetDateTime),
1340    EmailAddress(String, bool),
1341    PhoneNumber(String, bool),
1342    Address(Address),
1343    Url(Url),
1344    OauthScope(String),
1345    OauthScopeMap(Uuid, BTreeSet<String>),
1346    PrivateBinary(Vec<u8>),
1347    PublicBinary(String, Vec<u8>),
1348    RestrictedString(String),
1349    IntentToken(String, IntentTokenState),
1350    Passkey(Uuid, String, PasskeyV4),
1351    AttestedPasskey(Uuid, String, AttestedPasskeyV4),
1352
1353    Session(Uuid, Session),
1354    ApiToken(Uuid, ApiToken),
1355    Oauth2Session(Uuid, Oauth2Session),
1356
1357    JwsKeyEs256(JwsEs256Signer),
1358    JwsKeyRs256(JwsRs256Signer),
1359    UiHint(UiHint),
1360
1361    TotpSecret(String, Totp),
1362    AuditLogString(Cid, String),
1363    EcKeyPrivate(EcKey<Private>),
1364
1365    Image(ImageValue),
1366    CredentialType(CredentialType),
1367    WebauthnAttestationCaList(AttestationCaList),
1368
1369    OauthClaimValue(String, Uuid, BTreeSet<String>),
1370    OauthClaimMap(String, OauthClaimMapJoin),
1371
1372    KeyInternal {
1373        id: KeyId,
1374        usage: KeyUsage,
1375        valid_from: u64,
1376        status: KeyStatus,
1377        status_cid: Cid,
1378        der: Zeroizing<Vec<u8>>,
1379    },
1380
1381    HexString(String),
1382
1383    Certificate(Box<Certificate>),
1384    ApplicationPassword(ApplicationPassword),
1385    Json(JsonValue),
1386}
1387
1388impl PartialEq for Value {
1389    fn eq(&self, other: &Self) -> bool {
1390        match (self, other) {
1391            (Value::Utf8(a), Value::Utf8(b))
1392            | (Value::Iutf8(a), Value::Iutf8(b))
1393            | (Value::Iname(a), Value::Iname(b))
1394            | (Value::Cred(a, _), Value::Cred(b, _))
1395            | (Value::SshKey(a, _), Value::SshKey(b, _))
1396            | (Value::Nsuniqueid(a), Value::Nsuniqueid(b))
1397            | (Value::EmailAddress(a, _), Value::EmailAddress(b, _))
1398            | (Value::PhoneNumber(a, _), Value::PhoneNumber(b, _))
1399            | (Value::OauthScope(a), Value::OauthScope(b))
1400            | (Value::PublicBinary(a, _), Value::PublicBinary(b, _))
1401            | (Value::RestrictedString(a), Value::RestrictedString(b)) => a.eq(b),
1402            // Spn - need to check both name and domain.
1403            (Value::Spn(a, c), Value::Spn(b, d)) => a.eq(b) && c.eq(d),
1404            // Uuid, Refer
1405            (Value::Uuid(a), Value::Uuid(b)) | (Value::Refer(a), Value::Refer(b)) => a.eq(b),
1406            // Bool
1407            (Value::Bool(a), Value::Bool(b)) => a.eq(b),
1408            // Syntax
1409            (Value::Syntax(a), Value::Syntax(b)) => a.eq(b),
1410            // Index
1411            (Value::Index(a), Value::Index(b)) => a.eq(b),
1412            // JsonFilt
1413            (Value::JsonFilt(a), Value::JsonFilt(b)) => a.eq(b),
1414            // Uint32
1415            (Value::Uint32(a), Value::Uint32(b)) => a.eq(b),
1416            // Cid
1417            (Value::Cid(a), Value::Cid(b)) => a.eq(b),
1418            // DateTime
1419            (Value::DateTime(a), Value::DateTime(b)) => a.eq(b),
1420            // Url
1421            (Value::Url(a), Value::Url(b)) => a.eq(b),
1422            // OauthScopeMap
1423            (Value::OauthScopeMap(a, c), Value::OauthScopeMap(b, d)) => a.eq(b) && c.eq(d),
1424
1425            (Value::Image(image1), Value::Image(image2)) => {
1426                image1.hash_imagevalue().eq(&image2.hash_imagevalue())
1427            }
1428            (Value::Address(_), Value::Address(_))
1429            | (Value::PrivateBinary(_), Value::PrivateBinary(_))
1430            | (Value::SecretValue(_), Value::SecretValue(_)) => false,
1431            // Specifically related to migrations, we allow the invalid comparison.
1432            (Value::Iutf8(_), Value::Iname(_)) | (Value::Iname(_), Value::Iutf8(_)) => false,
1433            // When upgrading between uuid -> name -> spn we have to allow some invalid types.
1434            (Value::Uuid(_), Value::Iname(_))
1435            | (Value::Iname(_), Value::Spn(_, _))
1436            | (Value::Uuid(_), Value::Spn(_, _)) => false,
1437            (l, r) => {
1438                error!(?l, ?r, "mismatched value types");
1439                debug_assert!(false);
1440                false
1441            }
1442        }
1443    }
1444}
1445
1446impl Eq for Value {}
1447
1448impl From<bool> for Value {
1449    fn from(b: bool) -> Self {
1450        Value::Bool(b)
1451    }
1452}
1453
1454impl From<&bool> for Value {
1455    fn from(b: &bool) -> Self {
1456        Value::Bool(*b)
1457    }
1458}
1459
1460impl From<SyntaxType> for Value {
1461    fn from(s: SyntaxType) -> Self {
1462        Value::Syntax(s)
1463    }
1464}
1465
1466impl From<IndexType> for Value {
1467    fn from(i: IndexType) -> Self {
1468        Value::Index(i)
1469    }
1470}
1471
1472impl From<ProtoFilter> for Value {
1473    fn from(i: ProtoFilter) -> Self {
1474        Value::JsonFilt(i)
1475    }
1476}
1477
1478impl From<OffsetDateTime> for Value {
1479    fn from(i: OffsetDateTime) -> Self {
1480        Value::DateTime(i)
1481    }
1482}
1483
1484impl From<u32> for Value {
1485    fn from(i: u32) -> Self {
1486        Value::Uint32(i)
1487    }
1488}
1489
1490impl From<Url> for Value {
1491    fn from(i: Url) -> Self {
1492        Value::Url(i)
1493    }
1494}
1495
1496// Because these are potentially ambiguous, we limit them to tests where we can contain
1497// any....mistakes.
1498#[cfg(test)]
1499impl From<&str> for Value {
1500    fn from(s: &str) -> Self {
1501        // Fuzzy match for uuid's
1502        match Uuid::parse_str(s) {
1503            Ok(u) => Value::Uuid(u),
1504            Err(_) => Value::Utf8(s.to_string()),
1505        }
1506    }
1507}
1508
1509#[cfg(test)]
1510impl From<&Uuid> for Value {
1511    fn from(u: &Uuid) -> Self {
1512        Value::Uuid(*u)
1513    }
1514}
1515
1516#[cfg(test)]
1517impl From<Uuid> for Value {
1518    fn from(u: Uuid) -> Self {
1519        Value::Uuid(u)
1520    }
1521}
1522
1523impl From<DbIdentSpn> for Value {
1524    fn from(dis: DbIdentSpn) -> Self {
1525        match dis {
1526            DbIdentSpn::Spn(n, r) => Value::Spn(n, r),
1527            DbIdentSpn::Iname(n) => Value::Iname(n),
1528            DbIdentSpn::Uuid(u) => Value::Uuid(u),
1529        }
1530    }
1531}
1532
1533impl Value {
1534    // I get the feeling this will have a lot of matching ... sigh.
1535    pub fn new_utf8(s: String) -> Self {
1536        Value::Utf8(s)
1537    }
1538
1539    pub fn new_utf8s(s: &str) -> Self {
1540        Value::Utf8(s.to_string())
1541    }
1542
1543    pub fn is_utf8(&self) -> bool {
1544        matches!(self, Value::Utf8(_))
1545    }
1546
1547    pub fn new_iutf8(s: &str) -> Self {
1548        Value::Iutf8(s.to_lowercase())
1549    }
1550
1551    pub fn is_iutf8(&self) -> bool {
1552        matches!(self, Value::Iutf8(_))
1553    }
1554
1555    pub fn new_attr(s: &str) -> Self {
1556        Value::Iutf8(s.to_lowercase())
1557    }
1558
1559    pub fn is_insensitive_utf8(&self) -> bool {
1560        matches!(self, Value::Iutf8(_))
1561    }
1562
1563    pub fn new_iname(s: &str) -> Self {
1564        Value::Iname(s.to_lowercase())
1565    }
1566
1567    pub fn is_iname(&self) -> bool {
1568        matches!(self, Value::Iname(_))
1569    }
1570
1571    pub fn new_uuid_s(s: &str) -> Option<Self> {
1572        Uuid::parse_str(s).map(Value::Uuid).ok()
1573    }
1574
1575    // Is this correct? Should ref be separate?
1576    pub fn is_uuid(&self) -> bool {
1577        matches!(self, Value::Uuid(_))
1578    }
1579
1580    pub fn new_bool(b: bool) -> Self {
1581        Value::Bool(b)
1582    }
1583
1584    pub fn new_bools(s: &str) -> Option<Self> {
1585        bool::from_str(s).map(Value::Bool).ok()
1586    }
1587
1588    pub fn new_audit_log_string(e: (Cid, String)) -> Option<Self> {
1589        Some(Value::AuditLogString(e.0, e.1))
1590    }
1591
1592    #[inline]
1593    pub fn is_bool(&self) -> bool {
1594        matches!(self, Value::Bool(_))
1595    }
1596
1597    pub fn new_syntaxs(s: &str) -> Option<Self> {
1598        SyntaxType::try_from(s).map(Value::Syntax).ok()
1599    }
1600
1601    pub fn new_syntax(s: SyntaxType) -> Self {
1602        Value::Syntax(s)
1603    }
1604
1605    pub fn is_syntax(&self) -> bool {
1606        matches!(self, Value::Syntax(_))
1607    }
1608
1609    pub fn new_indexes(s: &str) -> Option<Self> {
1610        IndexType::try_from(s).map(Value::Index).ok()
1611    }
1612
1613    pub fn new_index(i: IndexType) -> Self {
1614        Value::Index(i)
1615    }
1616
1617    pub fn is_index(&self) -> bool {
1618        matches!(self, Value::Index(_))
1619    }
1620
1621    pub fn new_refer_s(us: &str) -> Option<Self> {
1622        Uuid::parse_str(us).map(Value::Refer).ok()
1623    }
1624
1625    pub fn is_refer(&self) -> bool {
1626        matches!(self, Value::Refer(_))
1627    }
1628
1629    pub fn new_json_filter_s(s: &str) -> Option<Self> {
1630        serde_json::from_str(s).map(Value::JsonFilt).ok()
1631    }
1632
1633    pub fn new_json_filter(f: ProtoFilter) -> Self {
1634        Value::JsonFilt(f)
1635    }
1636
1637    pub fn is_json_filter(&self) -> bool {
1638        matches!(self, Value::JsonFilt(_))
1639    }
1640
1641    pub fn as_json_filter(&self) -> Option<&ProtoFilter> {
1642        match &self {
1643            Value::JsonFilt(f) => Some(f),
1644            _ => None,
1645        }
1646    }
1647
1648    pub fn new_credential(tag: &str, cred: Credential) -> Self {
1649        Value::Cred(tag.to_string(), cred)
1650    }
1651
1652    pub fn is_credential(&self) -> bool {
1653        matches!(&self, Value::Cred(_, _))
1654    }
1655
1656    pub fn to_credential(&self) -> Option<&Credential> {
1657        match &self {
1658            Value::Cred(_, cred) => Some(cred),
1659            _ => None,
1660        }
1661    }
1662
1663    pub fn new_hex_string_s(hexstr: &str) -> Option<Self> {
1664        let hexstr_lower = hexstr.to_lowercase();
1665        if HEXSTR_RE.is_match(&hexstr_lower) {
1666            Some(Value::HexString(hexstr_lower))
1667        } else {
1668            None
1669        }
1670    }
1671
1672    pub fn new_certificate_s(cert_str: &str) -> Option<Self> {
1673        Certificate::from_pem(cert_str)
1674            .map(Box::new)
1675            .map(Value::Certificate)
1676            .ok()
1677    }
1678
1679    /// Want a `Value::Image`? use this!
1680    pub fn new_image(input: &str) -> Result<Self, OperationError> {
1681        serde_json::from_str::<ImageValue>(input)
1682            .map(Value::Image)
1683            .map_err(|_e| OperationError::InvalidValueState)
1684    }
1685
1686    pub fn new_secret_str(cleartext: &str) -> Self {
1687        Value::SecretValue(cleartext.to_string())
1688    }
1689
1690    pub fn is_secret_string(&self) -> bool {
1691        matches!(&self, Value::SecretValue(_))
1692    }
1693
1694    pub fn get_secret_str(&self) -> Option<&str> {
1695        match &self {
1696            Value::SecretValue(c) => Some(c.as_str()),
1697            _ => None,
1698        }
1699    }
1700
1701    pub fn new_sshkey_str(tag: &str, key: &str) -> Result<Self, OperationError> {
1702        SshPublicKey::from_string(key)
1703            .map(|pk| Value::SshKey(tag.to_string(), pk))
1704            .map_err(|err| {
1705                error!(?err, "value sshkey failed to parse string");
1706                OperationError::VL0001ValueSshPublicKeyString
1707            })
1708    }
1709
1710    pub fn is_sshkey(&self) -> bool {
1711        matches!(&self, Value::SshKey(_, _))
1712    }
1713
1714    pub fn get_sshkey(&self) -> Option<String> {
1715        match &self {
1716            Value::SshKey(_, key) => Some(key.to_string()),
1717            _ => None,
1718        }
1719    }
1720
1721    pub fn new_spn_parse(s: &str) -> Option<Self> {
1722        SPN_RE.captures(s).and_then(|caps| {
1723            let name = match caps.name(Attribute::Name.as_ref()) {
1724                Some(v) => v.as_str().to_string(),
1725                None => return None,
1726            };
1727            let realm = match caps.name("realm") {
1728                Some(v) => v.as_str().to_string(),
1729                None => return None,
1730            };
1731            Some(Value::Spn(name, realm))
1732        })
1733    }
1734
1735    pub fn new_spn_str(n: &str, r: &str) -> Self {
1736        Value::Spn(n.to_string(), r.to_string())
1737    }
1738
1739    pub fn is_spn(&self) -> bool {
1740        matches!(&self, Value::Spn(_, _))
1741    }
1742
1743    pub fn new_uint32(u: u32) -> Self {
1744        Value::Uint32(u)
1745    }
1746
1747    pub fn new_uint32_str(u: &str) -> Option<Self> {
1748        u.parse::<u32>().ok().map(Value::Uint32)
1749    }
1750
1751    pub fn is_uint32(&self) -> bool {
1752        matches!(&self, Value::Uint32(_))
1753    }
1754
1755    pub fn new_cid(c: Cid) -> Self {
1756        Value::Cid(c)
1757    }
1758
1759    pub fn is_cid(&self) -> bool {
1760        matches!(&self, Value::Cid(_))
1761    }
1762
1763    pub fn new_nsuniqueid_s(s: &str) -> Option<Self> {
1764        if NSUNIQUEID_RE.is_match(s) {
1765            Some(Value::Nsuniqueid(s.to_lowercase()))
1766        } else {
1767            None
1768        }
1769    }
1770
1771    pub fn is_nsuniqueid(&self) -> bool {
1772        matches!(&self, Value::Nsuniqueid(_))
1773    }
1774
1775    pub fn new_datetime_epoch(ts: Duration) -> Self {
1776        Value::DateTime(OffsetDateTime::UNIX_EPOCH + ts)
1777    }
1778
1779    pub fn new_datetime_s(s: &str) -> Option<Self> {
1780        OffsetDateTime::parse(s, &Rfc3339)
1781            .ok()
1782            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
1783            .map(Value::DateTime)
1784    }
1785
1786    pub fn new_datetime(dt: OffsetDateTime) -> Self {
1787        Value::DateTime(dt)
1788    }
1789
1790    pub fn to_datetime(&self) -> Option<OffsetDateTime> {
1791        match &self {
1792            Value::DateTime(odt) => {
1793                debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
1794                Some(*odt)
1795            }
1796            _ => None,
1797        }
1798    }
1799
1800    pub fn is_datetime(&self) -> bool {
1801        matches!(&self, Value::DateTime(_))
1802    }
1803
1804    pub fn new_email_address_s(s: &str) -> Option<Self> {
1805        if VALIDATE_EMAIL_RE.is_match(s) {
1806            Some(Value::EmailAddress(s.to_string(), false))
1807        } else {
1808            None
1809        }
1810    }
1811
1812    pub fn new_email_address_primary_s(s: &str) -> Option<Self> {
1813        if VALIDATE_EMAIL_RE.is_match(s) {
1814            Some(Value::EmailAddress(s.to_string(), true))
1815        } else {
1816            None
1817        }
1818    }
1819
1820    pub fn is_email_address(&self) -> bool {
1821        matches!(&self, Value::EmailAddress(_, _))
1822    }
1823
1824    pub fn new_phonenumber_s(s: &str) -> Self {
1825        Value::PhoneNumber(s.to_string(), false)
1826    }
1827
1828    pub fn new_address(a: Address) -> Self {
1829        Value::Address(a)
1830    }
1831
1832    pub fn new_url_s(s: &str) -> Option<Self> {
1833        Url::parse(s).ok().map(Value::Url)
1834    }
1835
1836    pub fn new_url(u: Url) -> Self {
1837        Value::Url(u)
1838    }
1839
1840    pub fn is_url(&self) -> bool {
1841        matches!(&self, Value::Url(_))
1842    }
1843
1844    pub fn new_oauthscope(s: &str) -> Option<Self> {
1845        if OAUTHSCOPE_RE.is_match(s) {
1846            Some(Value::OauthScope(s.to_string()))
1847        } else {
1848            None
1849        }
1850    }
1851
1852    pub fn is_oauthscope(&self) -> bool {
1853        matches!(&self, Value::OauthScope(_))
1854    }
1855
1856    pub fn new_oauthscopemap(u: Uuid, m: BTreeSet<String>) -> Option<Self> {
1857        if m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)) {
1858            Some(Value::OauthScopeMap(u, m))
1859        } else {
1860            None
1861        }
1862    }
1863
1864    pub fn new_oauthclaimmap(n: String, u: Uuid, c: BTreeSet<String>) -> Option<Self> {
1865        if OAUTHSCOPE_RE.is_match(&n) && c.iter().all(|s| OAUTHSCOPE_RE.is_match(s)) {
1866            Some(Value::OauthClaimValue(n, u, c))
1867        } else {
1868            None
1869        }
1870    }
1871
1872    pub fn is_oauthscopemap(&self) -> bool {
1873        matches!(&self, Value::OauthScopeMap(_, _))
1874    }
1875
1876    #[cfg(test)]
1877    pub fn new_privatebinary_base64(der: &str) -> Self {
1878        let der = general_purpose::STANDARD
1879            .decode(der)
1880            .expect("Failed to decode base64 der value");
1881        Value::PrivateBinary(der)
1882    }
1883
1884    pub fn new_privatebinary(der: &[u8]) -> Self {
1885        Value::PrivateBinary(der.to_owned())
1886    }
1887
1888    pub fn to_privatebinary(&self) -> Option<&Vec<u8>> {
1889        match &self {
1890            Value::PrivateBinary(c) => Some(c),
1891            _ => None,
1892        }
1893    }
1894
1895    pub fn is_privatebinary(&self) -> bool {
1896        matches!(&self, Value::PrivateBinary(_))
1897    }
1898
1899    pub fn new_publicbinary(tag: String, der: Vec<u8>) -> Self {
1900        Value::PublicBinary(tag, der)
1901    }
1902
1903    pub fn new_restrictedstring(s: String) -> Self {
1904        Value::RestrictedString(s)
1905    }
1906
1907    pub fn new_webauthn_attestation_ca_list(s: &str) -> Option<Self> {
1908        serde_json::from_str(s)
1909            .map(Value::WebauthnAttestationCaList)
1910            .map_err(|err| {
1911                debug!(?err, ?s);
1912            })
1913            .ok()
1914    }
1915
1916    #[allow(clippy::unreachable)]
1917    pub(crate) fn to_db_ident_spn(&self) -> DbIdentSpn {
1918        // This has to clone due to how the backend works.
1919        match &self {
1920            Value::Spn(n, r) => DbIdentSpn::Spn(n.clone(), r.clone()),
1921            Value::Iname(s) => DbIdentSpn::Iname(s.clone()),
1922            Value::Uuid(u) => DbIdentSpn::Uuid(*u),
1923            // Value::Iutf8(s) => DbValueV1::Iutf8(s.clone()),
1924            // Value::Utf8(s) => DbValueV1::Utf8(s.clone()),
1925            // Value::Nsuniqueid(s) => DbValueV1::NsUniqueId(s.clone()),
1926            v => unreachable!("-> {:?}", v),
1927        }
1928    }
1929
1930    pub fn to_str(&self) -> Option<&str> {
1931        match &self {
1932            Value::Utf8(s) => Some(s.as_str()),
1933            Value::Iutf8(s) => Some(s.as_str()),
1934            Value::Iname(s) => Some(s.as_str()),
1935            _ => None,
1936        }
1937    }
1938
1939    pub fn to_url(&self) -> Option<&Url> {
1940        match &self {
1941            Value::Url(u) => Some(u),
1942            _ => None,
1943        }
1944    }
1945
1946    pub fn as_string(&self) -> Option<&String> {
1947        match &self {
1948            Value::Utf8(s) => Some(s),
1949            Value::Iutf8(s) => Some(s),
1950            Value::Iname(s) => Some(s),
1951            _ => None,
1952        }
1953    }
1954
1955    // We need a separate to-ref_uuid to distinguish from normal uuids
1956    // in refint plugin.
1957    pub fn to_ref_uuid(&self) -> Option<Uuid> {
1958        match &self {
1959            Value::Refer(u) => Some(*u),
1960            Value::OauthScopeMap(u, _) => Some(*u),
1961            // We need to assert that our reference to our rs exists.
1962            Value::Oauth2Session(_, m) => Some(m.rs_uuid),
1963            _ => None,
1964        }
1965    }
1966
1967    pub fn to_uuid(&self) -> Option<&Uuid> {
1968        match &self {
1969            Value::Uuid(u) => Some(u),
1970            _ => None,
1971        }
1972    }
1973
1974    pub fn to_indextype(&self) -> Option<&IndexType> {
1975        match &self {
1976            Value::Index(i) => Some(i),
1977            _ => None,
1978        }
1979    }
1980
1981    pub fn to_syntaxtype(&self) -> Option<&SyntaxType> {
1982        match &self {
1983            Value::Syntax(s) => Some(s),
1984            _ => None,
1985        }
1986    }
1987
1988    pub fn to_bool(&self) -> Option<bool> {
1989        match self {
1990            // *v is to invoke a copy, but this is cheap af
1991            Value::Bool(v) => Some(*v),
1992            _ => None,
1993        }
1994    }
1995
1996    pub fn to_uint32(&self) -> Option<u32> {
1997        match &self {
1998            Value::Uint32(v) => Some(*v),
1999            _ => None,
2000        }
2001    }
2002
2003    pub fn to_utf8(self) -> Option<String> {
2004        match self {
2005            Value::Utf8(s) => Some(s),
2006            _ => None,
2007        }
2008    }
2009
2010    pub fn to_iutf8(self) -> Option<String> {
2011        match self {
2012            Value::Iutf8(s) => Some(s),
2013            _ => None,
2014        }
2015    }
2016
2017    pub fn to_iname(self) -> Option<String> {
2018        match self {
2019            Value::Iname(s) => Some(s),
2020            _ => None,
2021        }
2022    }
2023
2024    pub fn to_jsonfilt(self) -> Option<ProtoFilter> {
2025        match self {
2026            Value::JsonFilt(f) => Some(f),
2027            _ => None,
2028        }
2029    }
2030
2031    pub fn to_cred(self) -> Option<(String, Credential)> {
2032        match self {
2033            Value::Cred(tag, c) => Some((tag, c)),
2034            _ => None,
2035        }
2036    }
2037
2038    /*
2039    pub(crate) fn to_sshkey(self) -> Option<(String, SshPublicKey)> {
2040        match self {
2041            Value::SshKey(tag, k) => Some((tag, k)),
2042            _ => None,
2043        }
2044    }
2045    */
2046
2047    pub fn to_spn(self) -> Option<(String, String)> {
2048        match self {
2049            Value::Spn(n, d) => Some((n, d)),
2050            _ => None,
2051        }
2052    }
2053
2054    pub fn to_cid(self) -> Option<Cid> {
2055        match self {
2056            Value::Cid(s) => Some(s),
2057            _ => None,
2058        }
2059    }
2060
2061    pub fn to_nsuniqueid(self) -> Option<String> {
2062        match self {
2063            Value::Nsuniqueid(s) => Some(s),
2064            _ => None,
2065        }
2066    }
2067
2068    pub fn to_emailaddress(self) -> Option<String> {
2069        match self {
2070            Value::EmailAddress(s, _) => Some(s),
2071            _ => None,
2072        }
2073    }
2074
2075    pub fn to_oauthscope(self) -> Option<String> {
2076        match self {
2077            Value::OauthScope(s) => Some(s),
2078            _ => None,
2079        }
2080    }
2081
2082    pub fn to_oauthscopemap(self) -> Option<(Uuid, BTreeSet<String>)> {
2083        match self {
2084            Value::OauthScopeMap(u, m) => Some((u, m)),
2085            _ => None,
2086        }
2087    }
2088
2089    pub fn to_restrictedstring(self) -> Option<String> {
2090        match self {
2091            Value::RestrictedString(s) => Some(s),
2092            _ => None,
2093        }
2094    }
2095
2096    pub fn to_phonenumber(self) -> Option<String> {
2097        match self {
2098            Value::PhoneNumber(p, _b) => Some(p),
2099            _ => None,
2100        }
2101    }
2102
2103    pub fn to_publicbinary(self) -> Option<(String, Vec<u8>)> {
2104        match self {
2105            Value::PublicBinary(t, d) => Some((t, d)),
2106            _ => None,
2107        }
2108    }
2109
2110    pub fn to_address(self) -> Option<Address> {
2111        match self {
2112            Value::Address(a) => Some(a),
2113            _ => None,
2114        }
2115    }
2116
2117    pub fn to_intenttoken(self) -> Option<(String, IntentTokenState)> {
2118        match self {
2119            Value::IntentToken(u, s) => Some((u, s)),
2120            _ => None,
2121        }
2122    }
2123
2124    pub fn to_session(self) -> Option<(Uuid, Session)> {
2125        match self {
2126            Value::Session(u, s) => Some((u, s)),
2127            _ => None,
2128        }
2129    }
2130
2131    pub fn migrate_iutf8_iname(self) -> Option<Self> {
2132        match self {
2133            Value::Iutf8(v) => Some(Value::Iname(v)),
2134            _ => None,
2135        }
2136    }
2137
2138    // !!!! This function is being phased out !!!
2139    #[allow(clippy::unreachable)]
2140    pub(crate) fn to_proto_string_clone(&self) -> String {
2141        match &self {
2142            Value::Iname(s) => s.clone(),
2143            Value::Uuid(u) => u.as_hyphenated().to_string(),
2144            // We display the tag and fingerprint.
2145            Value::SshKey(tag, key) => format!("{tag}: {key}"),
2146            Value::Spn(n, r) => format!("{n}@{r}"),
2147            _ => unreachable!(
2148                "You've specified the wrong type for the attribute, got: {:?}",
2149                self
2150            ),
2151        }
2152    }
2153
2154    pub(crate) fn validate(&self) -> bool {
2155        // Validate that extra-data constraints on the type exist and are
2156        // valid. IE json filter is really a filter, or cred types have supplemental
2157        // data.
2158        match &self {
2159            // String security is required here
2160            Value::Utf8(s)
2161            | Value::Iutf8(s)
2162            | Value::Cred(s, _)
2163            | Value::PublicBinary(s, _)
2164            | Value::IntentToken(s, _)
2165            | Value::Passkey(_, s, _)
2166            | Value::AttestedPasskey(_, s, _)
2167            | Value::TotpSecret(s, _) => {
2168                Value::validate_str_escapes(s) && Value::validate_singleline(s)
2169            }
2170
2171            Value::Spn(a, b) => {
2172                Value::validate_str_escapes(a)
2173                    && Value::validate_str_escapes(b)
2174                    && Value::validate_singleline(a)
2175                    && Value::validate_singleline(b)
2176            }
2177            Value::Image(image) => image.validate_image().is_ok(),
2178            Value::Iname(s) => {
2179                Value::validate_str_escapes(s)
2180                    && Value::validate_iname(s)
2181                    && Value::validate_singleline(s)
2182            }
2183
2184            Value::SshKey(s, _key) => {
2185                Value::validate_str_escapes(s)
2186                    // && Value::validate_iname(s)
2187                    && Value::validate_singleline(s)
2188            }
2189
2190            Value::ApiToken(_, at) => {
2191                Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label)
2192            }
2193            Value::AuditLogString(_, s) => {
2194                Value::validate_str_escapes(s) && Value::validate_singleline(s)
2195            }
2196            Value::ApplicationPassword(ap) => {
2197                Value::validate_str_escapes(&ap.label) && Value::validate_singleline(&ap.label)
2198            }
2199
2200            // These have stricter validators so not needed.
2201            Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
2202            Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
2203            Value::EmailAddress(mail, _) => VALIDATE_EMAIL_RE.is_match(mail.as_str()),
2204            Value::OauthScope(s) => OAUTHSCOPE_RE.is_match(s),
2205            Value::OauthScopeMap(_, m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)),
2206
2207            Value::OauthClaimMap(name, _) => OAUTHSCOPE_RE.is_match(name),
2208            Value::OauthClaimValue(name, _, value) => {
2209                OAUTHSCOPE_RE.is_match(name) && value.iter().all(|s| OAUTHSCOPE_RE.is_match(s))
2210            }
2211
2212            Value::HexString(id) | Value::KeyInternal { id, .. } => {
2213                Value::validate_str_escapes(id.as_str())
2214                    && Value::validate_singleline(id.as_str())
2215                    && Value::validate_hexstr(id.as_str())
2216            }
2217
2218            Value::PhoneNumber(_, _) => true,
2219            Value::Address(_) => true,
2220            Value::Certificate(_) => true,
2221
2222            Value::Uuid(_)
2223            | Value::Bool(_)
2224            | Value::Syntax(_)
2225            | Value::Index(_)
2226            | Value::Refer(_)
2227            | Value::JsonFilt(_)
2228            | Value::SecretValue(_)
2229            | Value::Uint32(_)
2230            | Value::Url(_)
2231            | Value::Cid(_)
2232            | Value::PrivateBinary(_)
2233            | Value::RestrictedString(_)
2234            | Value::JwsKeyEs256(_)
2235            | Value::Session(_, _)
2236            | Value::Oauth2Session(_, _)
2237            | Value::JwsKeyRs256(_)
2238            | Value::EcKeyPrivate(_)
2239            | Value::UiHint(_)
2240            | Value::CredentialType(_)
2241            | Value::Json(_)
2242            | Value::WebauthnAttestationCaList(_) => true,
2243        }
2244    }
2245
2246    pub(crate) fn validate_iname(s: &str) -> bool {
2247        match Uuid::parse_str(s) {
2248            // It is a uuid, disallow.
2249            Ok(_) => {
2250                error!("iname values may not contain uuids");
2251                false
2252            }
2253            // Not a uuid, check it against the re.
2254            Err(_) => {
2255                if !INAME_RE.is_match(s) {
2256                    error!("iname values may only contain limited characters - \"{}\" does not pass regex pattern \"{}\"", s, *INAME_RE);
2257                    false
2258                } else if DISALLOWED_NAMES.contains(s) {
2259                    error!("iname value \"{}\" is in denied list", s);
2260                    false
2261                } else {
2262                    true
2263                }
2264            }
2265        }
2266    }
2267
2268    pub(crate) fn validate_hexstr(s: &str) -> bool {
2269        if !HEXSTR_RE.is_match(s) {
2270            error!("hexstrings may only contain limited characters. - \"{}\" does not pass regex pattern \"{}\"", s, *HEXSTR_RE);
2271            false
2272        } else {
2273            true
2274        }
2275    }
2276
2277    pub(crate) fn validate_singleline(s: &str) -> bool {
2278        if !SINGLELINE_RE.is_match(s) {
2279            true
2280        } else {
2281            error!(
2282                "value contains invalid whitespace chars forbidden by \"{}\"",
2283                *SINGLELINE_RE
2284            );
2285            // Trace only, could be an injection attack of some kind.
2286            trace!(?s, "Invalid whitespace");
2287            false
2288        }
2289    }
2290
2291    pub(crate) fn validate_str_escapes(s: &str) -> bool {
2292        // Look for and prevent certain types of string escapes and injections.
2293        if UNICODE_CONTROL_RE.is_match(s) {
2294            error!("value contains invalid unicode control character",);
2295            // Trace only, could be an injection attack of some kind.
2296            trace!(?s, "Invalid Unicode Control");
2297            false
2298        } else {
2299            true
2300        }
2301    }
2302}
2303
2304#[cfg(test)]
2305mod tests {
2306    use crate::value::*;
2307
2308    #[test]
2309    fn test_value_index_tryfrom() {
2310        let r1 = IndexType::try_from("EQualiTY");
2311        assert_eq!(r1, Ok(IndexType::Equality));
2312
2313        let r2 = IndexType::try_from("PResenCE");
2314        assert_eq!(r2, Ok(IndexType::Presence));
2315
2316        let r3 = IndexType::try_from("SUbstrING");
2317        assert_eq!(r3, Ok(IndexType::SubString));
2318
2319        let r4 = IndexType::try_from("thaoeusaneuh");
2320        assert_eq!(r4, Err(()));
2321    }
2322
2323    #[test]
2324    fn test_value_syntax_tryfrom() {
2325        let r1 = SyntaxType::try_from("UTF8strinG");
2326        assert_eq!(r1, Ok(SyntaxType::Utf8String));
2327
2328        let r2 = SyntaxType::try_from("UTF8STRING_INSensitIVE");
2329        assert_eq!(r2, Ok(SyntaxType::Utf8StringInsensitive));
2330
2331        let r3 = SyntaxType::try_from("BOOLEAN");
2332        assert_eq!(r3, Ok(SyntaxType::Boolean));
2333
2334        let r4 = SyntaxType::try_from("SYNTAX_ID");
2335        assert_eq!(r4, Ok(SyntaxType::SyntaxId));
2336
2337        let r5 = SyntaxType::try_from("INDEX_ID");
2338        assert_eq!(r5, Ok(SyntaxType::IndexId));
2339
2340        let r6 = SyntaxType::try_from("zzzzantheou");
2341        assert_eq!(r6, Err(()));
2342    }
2343
2344    #[test]
2345    fn test_value_sshkey_validation_display() {
2346        let ecdsa = concat!("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3B",
2347        "tOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81l",
2348        "zPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@a",
2349        "methyst");
2350        let ed25519 = concat!(
2351            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP",
2352            " william@amethyst"
2353        );
2354        let rsa = concat!("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTcXpclurQpyOHZBM/cDY9EvInSYkYSGe51by/wJP0Njgi",
2355        "GZUJ3HTaPqoGWux0PKd7KJki+onLYt4IwDV1RhV/GtMML2U9v94+pA8RIK4khCxvpUxlM7Kt/svjOzzzqiZfKdV37/",
2356        "OUXmM7bwVGOvm3EerDOwmO/QdzNGfkca12aWLoz97YrleXnCoAzr3IN7j3rwmfJGDyuUtGTdmyS/QWhK9FPr8Ic3eM",
2357        "QK1JSAQqVfGhA8lLbJHmnQ/b/KMl2lzzp7SXej0wPUfvI/IP3NGb8irLzq8+JssAzXGJ+HMql+mNHiSuPaktbFzZ6y",
2358        "ikMR6Rx/psU07nAkxKZDEYpNVv william@amethyst");
2359
2360        let sk1 = Value::new_sshkey_str("tag", ecdsa).expect("Invalid ssh key");
2361        assert!(sk1.validate());
2362        // to proto them
2363        let psk1 = sk1.to_proto_string_clone();
2364        assert_eq!(psk1, format!("tag: {ecdsa}"));
2365
2366        let sk2 = Value::new_sshkey_str("tag", ed25519).expect("Invalid ssh key");
2367        assert!(sk2.validate());
2368        let psk2 = sk2.to_proto_string_clone();
2369        assert_eq!(psk2, format!("tag: {ed25519}"));
2370
2371        let sk3 = Value::new_sshkey_str("tag", rsa).expect("Invalid ssh key");
2372        assert!(sk3.validate());
2373        let psk3 = sk3.to_proto_string_clone();
2374        assert_eq!(psk3, format!("tag: {rsa}"));
2375
2376        let sk4 = Value::new_sshkey_str("tag", "ntaouhtnhtnuehtnuhotnuhtneouhtneouh");
2377        assert!(sk4.is_err());
2378
2379        let sk5 = Value::new_sshkey_str(
2380            "tag",
2381            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo",
2382        );
2383        assert!(sk5.is_err());
2384    }
2385
2386    /*
2387    #[test]
2388    fn test_value_spn() {
2389        // Create an spn vale
2390        let spnv = Value::new_spn_str("claire", "example.net.au");
2391        // create an spn pv
2392        let spnp = PartialValue::new_spn_nrs("claire", "example.net.au");
2393        // check it's indexing output
2394        let vidx_key = spnv.generate_idx_eq_keys().pop().unwrap();
2395        let idx_key = spnp.get_idx_eq_key();
2396        assert_eq!(idx_key,vidx_key);
2397        // check it can parse from name@realm
2398        let spn_parse = PartialValue::new_spn_s("claire@example.net.au").unwrap();
2399        assert_eq!(spn_parse,spnp);
2400        // check it can produce name@realm as str from the pv.
2401        assert_eq!("claire@example.net.au",spnv.to_proto_string_clone());
2402    }
2403    */
2404
2405    /*
2406    #[test]
2407    fn test_value_uint32() {
2408        assert!(Value::new_uint32_str("test").is_none());
2409        assert!(Value::new_uint32_str("18446744073709551615").is_none());
2410
2411        let u32v = Value::new_uint32_str("4000").unwrap();
2412        let u32pv = PartialValue::new_uint32_str("4000").unwrap();
2413
2414        let idx_key = u32pv.get_idx_eq_key();
2415        let vidx_key = u32v.generate_idx_eq_keys().pop().unwrap();
2416
2417        assert_eq!(idx_key,vidx_key);
2418    }
2419    */
2420
2421    #[test]
2422    fn test_value_cid() {
2423        assert!(PartialValue::new_cid_s("_").is_none());
2424    }
2425
2426    #[test]
2427    fn test_value_iname() {
2428        /*
2429         * name MUST NOT:
2430         * - be a pure int (confusion to gid/uid/linux)
2431         * - a uuid (confuses our name mapper)
2432         * - contain an @ (confuses SPN)
2433         * - can not start with _ (... api end points have _ as a magic char)
2434         * - can not have spaces (confuses too many systems :()
2435         * - can not have = or , (confuses ldap)
2436         * - can not have ., /, \ (path injection attacks)
2437         */
2438        let inv1 = Value::new_iname("1234");
2439        let inv2 = Value::new_iname("bc23f637-4439-4c07-b95d-eaed0d9e4b8b");
2440        let inv3 = Value::new_iname("hello@test.com");
2441        let inv4 = Value::new_iname("_bad");
2442        let inv5 = Value::new_iname("no spaces I'm sorry :(");
2443        let inv6 = Value::new_iname("bad=equals");
2444        let inv7 = Value::new_iname("bad,comma");
2445        let inv8 = Value::new_iname("123_456");
2446        let inv9 = Value::new_iname("🍿");
2447
2448        let val1 = Value::new_iname("William");
2449        let val2 = Value::new_iname("this_is_okay");
2450        let val3 = Value::new_iname("a123_456");
2451
2452        assert!(!inv1.validate());
2453        assert!(!inv2.validate());
2454        assert!(!inv3.validate());
2455        assert!(!inv4.validate());
2456        assert!(!inv5.validate());
2457        assert!(!inv6.validate());
2458        assert!(!inv7.validate());
2459        assert!(!inv8.validate());
2460        assert!(!inv9.validate());
2461
2462        assert!(val1.validate());
2463        assert!(val2.validate());
2464        assert!(val3.validate());
2465    }
2466
2467    #[test]
2468    fn test_value_nsuniqueid() {
2469        // nsunique
2470        // d765e707-48e111e6-8c9ebed8-f7926cc3
2471        // uuid
2472        // d765e707-48e1-11e6-8c9e-bed8f7926cc3
2473        let val1 = Value::new_nsuniqueid_s("d765e707-48e111e6-8c9ebed8-f7926cc3");
2474        let val2 = Value::new_nsuniqueid_s("D765E707-48E111E6-8C9EBED8-F7926CC3");
2475        let inv1 = Value::new_nsuniqueid_s("d765e707-48e1-11e6-8c9e-bed8f7926cc3");
2476        let inv2 = Value::new_nsuniqueid_s("xxxx");
2477
2478        assert!(inv1.is_none());
2479        assert!(inv2.is_none());
2480        assert!(val1.unwrap().validate());
2481        assert!(val2.unwrap().validate());
2482    }
2483
2484    #[test]
2485    fn test_value_datetime() {
2486        // Datetimes must always convert to UTC, and must always be rfc3339
2487        let val1 = Value::new_datetime_s("2020-09-25T11:22:02+10:00").expect("Must be valid");
2488        assert!(val1.validate());
2489        let val2 = Value::new_datetime_s("2020-09-25T01:22:02+00:00").expect("Must be valid");
2490        assert!(val2.validate());
2491        // Spaces are now valid in rfc3339 for parsing.
2492        let val3 = Value::new_datetime_s("2020-09-25 01:22:02+00:00").expect("Must be valid");
2493        assert!(val3.validate());
2494
2495        assert!(Value::new_datetime_s("2020-09-25T01:22:02").is_none());
2496        assert!(Value::new_datetime_s("2020-09-25").is_none());
2497        assert!(Value::new_datetime_s("2020-09-25T01:22:02+10").is_none());
2498
2499        // Manually craft
2500        let inv1 = Value::DateTime(
2501            OffsetDateTime::now_utc()
2502                .to_offset(time::UtcOffset::from_whole_seconds(36000).unwrap()),
2503        );
2504        assert!(!inv1.validate());
2505
2506        let val3 = Value::DateTime(OffsetDateTime::now_utc());
2507        assert!(val3.validate());
2508    }
2509
2510    #[test]
2511    fn test_value_email_address() {
2512        // https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
2513        let val1 = Value::new_email_address_s("william@blackhats.net.au");
2514        let val2 = Value::new_email_address_s("alice@idm.example.com");
2515        let val3 = Value::new_email_address_s("test+mailbox@foo.com");
2516        let inv1 = Value::new_email_address_s("william");
2517        let inv2 = Value::new_email_address_s("test~uuid");
2518
2519        assert!(inv1.is_none());
2520        assert!(inv2.is_none());
2521        assert!(val1.unwrap().validate());
2522        assert!(val2.unwrap().validate());
2523        assert!(val3.unwrap().validate());
2524    }
2525
2526    #[test]
2527    fn test_value_url() {
2528        // https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
2529        let val1 = Value::new_url_s("https://localhost:8000/search?q=text#hello");
2530        let val2 = Value::new_url_s("https://github.com/kanidm/kanidm");
2531        let val3 = Value::new_url_s("ldap://foo.com");
2532        let inv1 = Value::new_url_s("127.0.");
2533        let inv2 = Value::new_url_s("🤔");
2534
2535        assert!(inv1.is_none());
2536        assert!(inv2.is_none());
2537        assert!(val1.is_some());
2538        assert!(val2.is_some());
2539        assert!(val3.is_some());
2540    }
2541
2542    #[test]
2543    fn test_singleline() {
2544        assert!(Value::validate_singleline("no new lines"));
2545
2546        assert!(!Value::validate_singleline("contains a \n new line"));
2547        assert!(!Value::validate_singleline("contains a \r return feed"));
2548        assert!(!Value::validate_singleline("contains a \t tab char"));
2549    }
2550
2551    #[test]
2552    fn test_str_escapes() {
2553        assert!(Value::validate_str_escapes("safe str"));
2554        assert!(Value::validate_str_escapes("🙃 emoji are 👍"));
2555
2556        assert!(!Value::validate_str_escapes("naughty \x1b[31mred"));
2557    }
2558
2559    #[test]
2560    fn test_value_key_internal_status_order() {
2561        assert!(KeyStatus::Valid < KeyStatus::Retained);
2562        assert!(KeyStatus::Retained < KeyStatus::Revoked);
2563    }
2564
2565    #[test]
2566    fn test_value_session_state_order() {
2567        assert!(
2568            SessionState::RevokedAt(Cid::new_zero()) > SessionState::RevokedAt(Cid::new_count(1))
2569        );
2570        assert!(
2571            SessionState::RevokedAt(Cid::new_zero())
2572                > SessionState::ExpiresAt(OffsetDateTime::UNIX_EPOCH)
2573        );
2574        assert!(
2575            SessionState::ExpiresAt(OffsetDateTime::UNIX_EPOCH + Duration::from_secs(1))
2576                > SessionState::ExpiresAt(OffsetDateTime::UNIX_EPOCH)
2577        );
2578        assert!(SessionState::ExpiresAt(OffsetDateTime::UNIX_EPOCH) > SessionState::NeverExpires);
2579    }
2580}