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