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