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::{
17    s256::Sha256Output,
18    traits::{DecodePem, Zeroizing},
19    x509::Certificate,
20};
21use hashbrown::HashSet;
22use kanidm_proto::internal::ImageValue;
23use kanidm_proto::internal::{ApiTokenPurpose, Filter as ProtoFilter, UiHint};
24use kanidm_proto::scim_v1::ScimOauth2ClaimMapJoinChar;
25use kanidm_proto::v1::UatPurposeStatus;
26use num_enum::TryFromPrimitive;
27use regex::Regex;
28use serde::{Deserialize, Serialize};
29use sshkey_attest::proto::PublicKey as SshPublicKey;
30use std::cmp::Ordering;
31use std::collections::BTreeSet;
32use std::convert::TryFrom;
33use std::fmt;
34use std::fmt::Formatter;
35use std::hash::Hash;
36use std::str::FromStr;
37use std::time::Duration;
38use time::OffsetDateTime;
39use url::Url;
40use uuid::Uuid;
41use webauthn_rs::prelude::{
42    AttestationCaList, AttestedPasskey as AttestedPasskeyV4, Passkey as PasskeyV4,
43};
44
45#[cfg(test)]
46use base64::{engine::general_purpose, Engine as _};
47
48pub static SPN_RE: LazyLock<Regex> = LazyLock::new(|| {
49    #[allow(clippy::expect_used)]
50    Regex::new("(?P<name>[^@]+)@(?P<realm>[^@]+)").expect("Invalid SPN regex found")
51});
52
53pub static DISALLOWED_NAMES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
54    // Most of these were removed in favour of the unixd daemon filtering out
55    // local users instead.
56    let mut m = HashSet::with_capacity(2);
57    m.insert("root");
58    m.insert("dn=token");
59    m
60});
61
62/// Only lowercase+numbers, with limited chars.
63pub static INAME_RE: LazyLock<Regex> = LazyLock::new(|| {
64    #[allow(clippy::expect_used)]
65    Regex::new("^[a-z][a-z0-9-_\\.]{0,63}$").expect("Invalid Iname regex found")
66});
67
68/// Only alpha-numeric with limited special chars and space
69pub static LABEL_RE: LazyLock<Regex> = LazyLock::new(|| {
70    #[allow(clippy::expect_used)]
71    Regex::new("^[a-zA-Z0-9][ a-zA-Z0-9-_\\.@]{0,63}$").expect("Invalid Iname regex found")
72});
73
74/// Only lowercase+numbers, with limited chars.
75pub static HEXSTR_RE: LazyLock<Regex> = LazyLock::new(|| {
76    #[allow(clippy::expect_used)]
77    Regex::new("^[a-f0-9]+$").expect("Invalid hexstring regex found")
78});
79
80pub static EXTRACT_VAL_DN: LazyLock<Regex> = LazyLock::new(|| {
81    #[allow(clippy::expect_used)]
82    Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)").expect("extract val from dn regex")
83});
84
85pub static NSUNIQUEID_RE: LazyLock<Regex> = LazyLock::new(|| {
86    #[allow(clippy::expect_used)]
87    Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$")
88        .expect("Invalid Nsunique regex found")
89});
90
91/// Must not contain whitespace.
92pub static OAUTHSCOPE_RE: LazyLock<Regex> = LazyLock::new(|| {
93    #[allow(clippy::expect_used)]
94    Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
95});
96
97/// Must not contain whitespace. Allows "abcd", "abcd:efgh", but ":" and "-" need to be separating
98/// groups of characters, and are not able to be leading or trailing.
99pub static OAUTH_CLAIMNAME_RE: LazyLock<Regex> = LazyLock::new(|| {
100    #[allow(clippy::expect_used)]
101    Regex::new("^[0-9a-zA-Z_]+([:\\-][0-9a-zA-Z_]+)*$").expect("Invalid oauth claim regex found")
102});
103
104pub static SINGLELINE_RE: LazyLock<Regex> = LazyLock::new(|| {
105    #[allow(clippy::expect_used)]
106    Regex::new("[\n\r\t]").expect("Invalid singleline regex found")
107});
108
109/// Per <https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address>
110/// this regex validates for valid emails.
111pub static VALIDATE_EMAIL_RE: LazyLock<Regex> = LazyLock::new(|| {
112    #[allow(clippy::expect_used)]
113        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])?)*$")
114            .expect("Invalid singleline regex found")
115});
116
117pub static UNICODE_CONTROL_RE: LazyLock<Regex> = LazyLock::new(|| {
118    #[allow(clippy::expect_used)]
119    Regex::new(r"[[:cntrl:]]").expect("Invalid unicode control regex found")
120});
121
122#[derive(Debug, Clone, PartialOrd, Ord, Eq, PartialEq, Hash)]
123/// Per <https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim>
124pub struct Address {
125    pub formatted: String,
126    pub street_address: String,
127    pub locality: String,
128    pub region: String,
129    pub postal_code: String,
130    // Must be validated.
131    pub country: String,
132}
133#[derive(Debug, Copy, Clone, PartialEq, Eq)]
134pub struct CredUpdateSessionPerms {
135    pub ext_cred_portal_can_view: bool,
136    pub primary_can_edit: bool,
137    pub passkeys_can_edit: bool,
138    pub attested_passkeys_can_edit: bool,
139    pub unixcred_can_edit: bool,
140    pub sshpubkey_can_edit: bool,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum IntentTokenState {
145    Valid {
146        max_ttl: Duration,
147        perms: CredUpdateSessionPerms,
148    },
149    InProgress {
150        max_ttl: Duration,
151        perms: CredUpdateSessionPerms,
152        session_id: Uuid,
153        session_ttl: Duration,
154    },
155    Consumed {
156        max_ttl: Duration,
157    },
158}
159
160#[allow(non_camel_case_types)]
161#[derive(
162    Debug,
163    Clone,
164    Copy,
165    PartialEq,
166    Eq,
167    PartialOrd,
168    Ord,
169    Deserialize,
170    Serialize,
171    Hash,
172    TryFromPrimitive,
173)]
174#[repr(u16)]
175pub enum IndexType {
176    Equality,
177    Presence,
178    SubString,
179    Ordering,
180}
181
182impl TryFrom<&str> for IndexType {
183    type Error = ();
184
185    fn try_from(value: &str) -> Result<Self, Self::Error> {
186        let n_value = value.to_uppercase();
187        match n_value.as_str() {
188            "EQUALITY" => Ok(IndexType::Equality),
189            "PRESENCE" => Ok(IndexType::Presence),
190            "SUBSTRING" => Ok(IndexType::SubString),
191            "ORDERING" => Ok(IndexType::Ordering),
192            // UUID map?
193            // UUID rev map?
194            _ => Err(()),
195        }
196    }
197}
198
199impl IndexType {
200    pub fn as_idx_str(&self) -> &str {
201        match self {
202            IndexType::Equality => "eq",
203            IndexType::Presence => "pres",
204            IndexType::SubString => "sub",
205            IndexType::Ordering => "ord",
206        }
207    }
208}
209
210impl fmt::Display for IndexType {
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        write!(
213            f,
214            "{}",
215            match self {
216                IndexType::Equality => "EQUALITY",
217                IndexType::Presence => "PRESENCE",
218                IndexType::SubString => "SUBSTRING",
219                IndexType::Ordering => "ORDERING",
220            }
221        )
222    }
223}
224
225#[allow(non_camel_case_types)]
226#[derive(
227    Hash,
228    Debug,
229    Clone,
230    Copy,
231    PartialEq,
232    Eq,
233    PartialOrd,
234    Ord,
235    Deserialize,
236    Serialize,
237    TryFromPrimitive,
238    Default,
239)]
240#[repr(u16)]
241pub enum SyntaxType {
242    #[default]
243    Utf8String = 0,
244    Utf8StringInsensitive = 1,
245    Uuid = 2,
246    Boolean = 3,
247    SyntaxId = 4,
248    IndexId = 5,
249    ReferenceUuid = 6,
250    JsonFilter = 7,
251    Credential = 8,
252    SecretUtf8String = 9,
253    SshKey = 10,
254    SecurityPrincipalName = 11,
255    Uint32 = 12,
256    Cid = 13,
257    Utf8StringIname = 14,
258    NsUniqueId = 15,
259    DateTime = 16,
260    EmailAddress = 17,
261    Url = 18,
262    OauthScope = 19,
263    OauthScopeMap = 20,
264    PrivateBinary = 21,
265    IntentToken = 22,
266    Passkey = 23,
267    AttestedPasskey = 24,
268    Session = 25,
269    JwsKeyEs256 = 26,
270    JwsKeyRs256 = 27,
271    Oauth2Session = 28,
272    UiHint = 29,
273    TotpSecret = 30,
274    ApiToken = 31,
275    AuditLogString = 32,
276    EcKeyPrivate = 33,
277    Image = 34,
278    CredentialType = 35,
279    WebauthnAttestationCaList = 36,
280    OauthClaimMap = 37,
281    KeyInternal = 38,
282    HexString = 39,
283    Certificate = 40,
284    ApplicationPassword = 41,
285    Json = 42,
286    Message = 43,
287    Sha256 = 44,
288    Int64 = 45,
289    Uint64 = 46,
290}
291
292impl TryFrom<&str> for SyntaxType {
293    type Error = ();
294
295    fn try_from(value: &str) -> Result<SyntaxType, Self::Error> {
296        let n_value = value.to_uppercase();
297        match n_value.as_str() {
298            "UTF8STRING" => Ok(SyntaxType::Utf8String),
299            "UTF8STRING_INSENSITIVE" => Ok(SyntaxType::Utf8StringInsensitive),
300            "UTF8STRING_INAME" => Ok(SyntaxType::Utf8StringIname),
301            "UUID" => Ok(SyntaxType::Uuid),
302            "BOOLEAN" => Ok(SyntaxType::Boolean),
303            "SYNTAX_ID" => Ok(SyntaxType::SyntaxId),
304            "INDEX_ID" => Ok(SyntaxType::IndexId),
305            "REFERENCE_UUID" => Ok(SyntaxType::ReferenceUuid),
306            "JSON_FILTER" => Ok(SyntaxType::JsonFilter),
307            "CREDENTIAL" => Ok(SyntaxType::Credential),
308            // Compatibility for older syntax name.
309            "RADIUS_UTF8STRING" | "SECRET_UTF8STRING" => Ok(SyntaxType::SecretUtf8String),
310            "SSHKEY" => Ok(SyntaxType::SshKey),
311            "SECURITY_PRINCIPAL_NAME" => Ok(SyntaxType::SecurityPrincipalName),
312            "UINT32" => Ok(SyntaxType::Uint32),
313            "CID" => Ok(SyntaxType::Cid),
314            "NSUNIQUEID" => Ok(SyntaxType::NsUniqueId),
315            "DATETIME" => Ok(SyntaxType::DateTime),
316            "EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress),
317            "URL" => Ok(SyntaxType::Url),
318            "OAUTH_SCOPE" => Ok(SyntaxType::OauthScope),
319            "OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap),
320            "PRIVATE_BINARY" => Ok(SyntaxType::PrivateBinary),
321            "INTENT_TOKEN" => Ok(SyntaxType::IntentToken),
322            "PASSKEY" => Ok(SyntaxType::Passkey),
323            "ATTESTED_PASSKEY" => Ok(SyntaxType::AttestedPasskey),
324            "SESSION" => Ok(SyntaxType::Session),
325            "JWS_KEY_ES256" => Ok(SyntaxType::JwsKeyEs256),
326            "JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256),
327            "OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session),
328            "UIHINT" => Ok(SyntaxType::UiHint),
329            "TOTPSECRET" => Ok(SyntaxType::TotpSecret),
330            "APITOKEN" => Ok(SyntaxType::ApiToken),
331            "AUDIT_LOG_STRING" => Ok(SyntaxType::AuditLogString),
332            "EC_KEY_PRIVATE" => Ok(SyntaxType::EcKeyPrivate),
333            "CREDENTIAL_TYPE" => Ok(SyntaxType::CredentialType),
334            "WEBAUTHN_ATTESTATION_CA_LIST" => Ok(SyntaxType::WebauthnAttestationCaList),
335            "OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
336            "KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
337            "HEX_STRING" => Ok(SyntaxType::HexString),
338            "CERTIFICATE" => Ok(SyntaxType::Certificate),
339            "APPLICATION_PASSWORD" => Ok(SyntaxType::ApplicationPassword),
340            "JSON" => Ok(SyntaxType::Json),
341            "MESSAGE" => Ok(SyntaxType::Message),
342            "SHA256" => Ok(SyntaxType::Sha256),
343            "INT64" => Ok(SyntaxType::Int64),
344            "UINT64" => Ok(SyntaxType::Uint64),
345            _ => Err(()),
346        }
347    }
348}
349
350impl fmt::Display for SyntaxType {
351    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
352        f.write_str(match self {
353            SyntaxType::Utf8String => "UTF8STRING",
354            SyntaxType::Utf8StringInsensitive => "UTF8STRING_INSENSITIVE",
355            SyntaxType::Utf8StringIname => "UTF8STRING_INAME",
356            SyntaxType::Uuid => "UUID",
357            SyntaxType::Boolean => "BOOLEAN",
358            SyntaxType::SyntaxId => "SYNTAX_ID",
359            SyntaxType::IndexId => "INDEX_ID",
360            SyntaxType::ReferenceUuid => "REFERENCE_UUID",
361            SyntaxType::JsonFilter => "JSON_FILTER",
362            SyntaxType::Credential => "CREDENTIAL",
363            SyntaxType::SecretUtf8String => "SECRET_UTF8STRING",
364            SyntaxType::SshKey => "SSHKEY",
365            SyntaxType::SecurityPrincipalName => "SECURITY_PRINCIPAL_NAME",
366            SyntaxType::Uint32 => "UINT32",
367            SyntaxType::Cid => "CID",
368            SyntaxType::NsUniqueId => "NSUNIQUEID",
369            SyntaxType::DateTime => "DATETIME",
370            SyntaxType::EmailAddress => "EMAIL_ADDRESS",
371            SyntaxType::Url => "URL",
372            SyntaxType::OauthScope => "OAUTH_SCOPE",
373            SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP",
374            SyntaxType::PrivateBinary => "PRIVATE_BINARY",
375            SyntaxType::IntentToken => "INTENT_TOKEN",
376            SyntaxType::Passkey => "PASSKEY",
377            SyntaxType::AttestedPasskey => "ATTESTED_PASSKEY",
378            SyntaxType::Session => "SESSION",
379            SyntaxType::JwsKeyEs256 => "JWS_KEY_ES256",
380            SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256",
381            SyntaxType::Oauth2Session => "OAUTH2SESSION",
382            SyntaxType::UiHint => "UIHINT",
383            SyntaxType::TotpSecret => "TOTPSECRET",
384            SyntaxType::ApiToken => "APITOKEN",
385            SyntaxType::AuditLogString => "AUDIT_LOG_STRING",
386            SyntaxType::EcKeyPrivate => "EC_KEY_PRIVATE",
387            SyntaxType::Image => "IMAGE",
388            SyntaxType::CredentialType => "CREDENTIAL_TYPE",
389            SyntaxType::WebauthnAttestationCaList => "WEBAUTHN_ATTESTATION_CA_LIST",
390            SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
391            SyntaxType::KeyInternal => "KEY_INTERNAL",
392            SyntaxType::HexString => "HEX_STRING",
393            SyntaxType::Certificate => "CERTIFICATE",
394            SyntaxType::ApplicationPassword => "APPLICATION_PASSWORD",
395            SyntaxType::Json => "JSON",
396            SyntaxType::Message => "MESSAGE",
397            SyntaxType::Sha256 => "SHA256",
398            SyntaxType::Int64 => "INT64",
399            SyntaxType::Uint64 => "UINT64",
400        })
401    }
402}
403
404impl SyntaxType {
405    pub fn index_types(&self) -> &[IndexType] {
406        match self {
407            SyntaxType::Utf8String => &[IndexType::Equality, IndexType::Presence],
408            // Used by classes, needs to change ...
409            // Probably need an attrname syntax too
410            SyntaxType::Utf8StringInsensitive => &[IndexType::Equality, IndexType::Presence],
411            SyntaxType::Utf8StringIname => &[
412                IndexType::Equality,
413                IndexType::Presence,
414                IndexType::SubString,
415            ],
416            SyntaxType::Uuid => &[IndexType::Equality, IndexType::Presence],
417            SyntaxType::Boolean => &[IndexType::Equality],
418            SyntaxType::ReferenceUuid => &[IndexType::Equality, IndexType::Presence],
419            SyntaxType::Credential => &[IndexType::Equality],
420            SyntaxType::SshKey => &[IndexType::Equality, IndexType::Presence],
421            SyntaxType::SecurityPrincipalName => &[
422                IndexType::Equality,
423                IndexType::Presence,
424                IndexType::SubString,
425            ],
426            SyntaxType::Uint32 | SyntaxType::Int64 | SyntaxType::Uint64 => &[
427                IndexType::Equality,
428                IndexType::Presence,
429                IndexType::Ordering,
430            ],
431            SyntaxType::Cid => &[
432                IndexType::Equality,
433                IndexType::Presence,
434                IndexType::Ordering,
435            ],
436            SyntaxType::NsUniqueId => &[IndexType::Equality, IndexType::Presence],
437            SyntaxType::DateTime => &[
438                IndexType::Equality,
439                IndexType::Presence,
440                IndexType::Ordering,
441            ],
442            SyntaxType::EmailAddress => &[IndexType::Equality, IndexType::SubString],
443            SyntaxType::OauthScopeMap => &[IndexType::Equality],
444            SyntaxType::IntentToken => &[IndexType::Equality],
445            SyntaxType::Passkey => &[IndexType::Equality],
446            SyntaxType::AttestedPasskey => &[IndexType::Equality],
447            SyntaxType::Session => &[IndexType::Equality],
448            SyntaxType::Oauth2Session => &[IndexType::Equality],
449            SyntaxType::ApiToken => &[IndexType::Equality],
450            SyntaxType::OauthClaimMap => &[IndexType::Equality],
451            SyntaxType::ApplicationPassword => &[IndexType::Equality],
452            SyntaxType::SecretUtf8String => &[],
453            SyntaxType::Url => &[],
454            SyntaxType::OauthScope => &[],
455            SyntaxType::PrivateBinary => &[],
456            SyntaxType::JwsKeyEs256 => &[],
457            SyntaxType::JwsKeyRs256 => &[],
458            SyntaxType::UiHint => &[],
459            SyntaxType::TotpSecret => &[],
460            SyntaxType::AuditLogString => &[],
461            SyntaxType::EcKeyPrivate => &[],
462            SyntaxType::Image => &[],
463            SyntaxType::CredentialType => &[],
464            SyntaxType::WebauthnAttestationCaList => &[],
465            SyntaxType::KeyInternal => &[],
466            SyntaxType::HexString => &[],
467            SyntaxType::Certificate => &[],
468            SyntaxType::SyntaxId => &[],
469            SyntaxType::IndexId => &[],
470            SyntaxType::JsonFilter => &[],
471            SyntaxType::Json => &[],
472            SyntaxType::Message => &[],
473            SyntaxType::Sha256 => &[IndexType::Equality],
474        }
475    }
476}
477
478#[derive(
479    Hash,
480    Debug,
481    Clone,
482    Copy,
483    PartialEq,
484    Eq,
485    PartialOrd,
486    Ord,
487    Deserialize,
488    Serialize,
489    TryFromPrimitive,
490    Default,
491)]
492#[repr(u16)]
493pub enum CredentialType {
494    Any = 0,
495    /// Since we have no control over external authentication providers, we need
496    /// to rank these below our own authentication methods.
497    External = 5,
498    #[default]
499    Mfa = 10,
500    Passkey = 20,
501    AttestedPasskey = 30,
502    AttestedResidentkey = 40,
503    Invalid = u16::MAX,
504}
505
506impl TryFrom<&str> for CredentialType {
507    type Error = ();
508
509    fn try_from(value: &str) -> Result<CredentialType, Self::Error> {
510        match value {
511            "any" => Ok(CredentialType::Any),
512            "external" => Ok(CredentialType::External),
513            "mfa" => Ok(CredentialType::Mfa),
514            "passkey" => Ok(CredentialType::Passkey),
515            "attested_passkey" => Ok(CredentialType::AttestedPasskey),
516            "attested_residentkey" => Ok(CredentialType::AttestedResidentkey),
517            "invalid" => Ok(CredentialType::Invalid),
518            _ => Err(()),
519        }
520    }
521}
522
523impl fmt::Display for CredentialType {
524    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
525        f.write_str(match self {
526            CredentialType::Any => "any",
527            CredentialType::External => "external",
528            CredentialType::Mfa => "mfa",
529            CredentialType::Passkey => "passkey",
530            CredentialType::AttestedPasskey => "attested_passkey",
531            CredentialType::AttestedResidentkey => "attested_residentkey",
532            CredentialType::Invalid => "invalid",
533        })
534    }
535}
536
537impl From<CredentialType> for Value {
538    fn from(ct: CredentialType) -> Value {
539        Value::CredentialType(ct)
540    }
541}
542
543impl From<CredentialType> for PartialValue {
544    fn from(ct: CredentialType) -> PartialValue {
545        PartialValue::CredentialType(ct)
546    }
547}
548
549impl From<Attribute> for Value {
550    fn from(attr: Attribute) -> Value {
551        let s: &str = attr.as_str();
552        Value::new_iutf8(s)
553    }
554}
555
556impl From<Attribute> for PartialValue {
557    fn from(attr: Attribute) -> PartialValue {
558        let s: &str = attr.as_str();
559        PartialValue::new_iutf8(s)
560    }
561}
562
563/// A partial value is a key or key subset that can be used to match for equality or substring
564/// against a complete Value within a set in an Entry.
565///
566/// A partialValue is typically used when you need to match against a value, but without
567/// requiring all of its data or expression. This is common in Filters or other direct
568/// lookups and requests.
569#[derive(Hash, Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
570pub enum PartialValue {
571    Utf8(String),
572    Iutf8(String),
573    Iname(String),
574    Uuid(Uuid),
575    Bool(bool),
576    Syntax(SyntaxType),
577    Index(IndexType),
578    Refer(Uuid),
579    // Does this make sense?
580    // TODO: We'll probably add tagging to this type for the partial matching
581    JsonFilt(ProtoFilter),
582    // Tag, matches to a DataValue.
583    Cred(String),
584    SshKey(String),
585    SecretValue,
586    Spn(String, String),
587    Uint32(u32),
588    Cid(Cid),
589    Nsuniqueid(String),
590    DateTime(OffsetDateTime),
591    EmailAddress(String),
592    PhoneNumber(String),
593    Address(String),
594    // Can add other selectors later.
595    Url(Url),
596    OauthScope(String),
597    // OauthScopeMap(Uuid),
598    PrivateBinary,
599    PublicBinary(String),
600    // Enumeration(String),
601    // Float64(f64),
602    RestrictedString(String),
603    IntentToken(String),
604    UiHint(UiHint),
605    Passkey(Uuid),
606    AttestedPasskey(Uuid),
607    /// We compare on the value hash
608    Image(String),
609    CredentialType(CredentialType),
610
611    OauthClaim(String, Uuid),
612    OauthClaimValue(String, Uuid, String),
613
614    HexString(String),
615    Json,
616    Sha256(Sha256Output),
617    Int64(i64),
618    Uint64(u64),
619}
620
621impl From<SyntaxType> for PartialValue {
622    fn from(s: SyntaxType) -> Self {
623        PartialValue::Syntax(s)
624    }
625}
626
627impl From<IndexType> for PartialValue {
628    fn from(i: IndexType) -> Self {
629        PartialValue::Index(i)
630    }
631}
632
633impl From<bool> for PartialValue {
634    fn from(b: bool) -> Self {
635        PartialValue::Bool(b)
636    }
637}
638
639impl From<&bool> for PartialValue {
640    fn from(b: &bool) -> Self {
641        PartialValue::Bool(*b)
642    }
643}
644
645impl From<ProtoFilter> for PartialValue {
646    fn from(i: ProtoFilter) -> Self {
647        PartialValue::JsonFilt(i)
648    }
649}
650
651impl From<u32> for PartialValue {
652    fn from(i: u32) -> Self {
653        PartialValue::Uint32(i)
654    }
655}
656
657impl From<OffsetDateTime> for PartialValue {
658    fn from(i: OffsetDateTime) -> Self {
659        PartialValue::DateTime(i)
660    }
661}
662
663impl From<Url> for PartialValue {
664    fn from(i: Url) -> Self {
665        PartialValue::Url(i)
666    }
667}
668
669impl From<Sha256Output> for PartialValue {
670    fn from(i: Sha256Output) -> Self {
671        PartialValue::Sha256(i)
672    }
673}
674
675impl PartialValue {
676    pub fn new_utf8(s: String) -> Self {
677        PartialValue::Utf8(s)
678    }
679
680    pub fn new_utf8s(s: &str) -> Self {
681        PartialValue::Utf8(s.to_string())
682    }
683
684    pub fn is_utf8(&self) -> bool {
685        matches!(self, PartialValue::Utf8(_))
686    }
687
688    pub fn new_iutf8(s: &str) -> Self {
689        PartialValue::Iutf8(s.to_lowercase())
690    }
691
692    pub fn new_iname(s: &str) -> Self {
693        PartialValue::Iname(s.to_lowercase())
694    }
695
696    pub fn is_iutf8(&self) -> bool {
697        matches!(self, PartialValue::Iutf8(_))
698    }
699
700    pub fn is_iname(&self) -> bool {
701        matches!(self, PartialValue::Iname(_))
702    }
703
704    pub const fn new_bool(b: bool) -> Self {
705        PartialValue::Bool(b)
706    }
707
708    pub fn new_bools(s: &str) -> Option<Self> {
709        bool::from_str(s).map(PartialValue::Bool).ok()
710    }
711
712    pub fn is_bool(&self) -> bool {
713        matches!(self, PartialValue::Bool(_))
714    }
715
716    pub fn new_uuid_s(us: &str) -> Option<Self> {
717        Uuid::parse_str(us).map(PartialValue::Uuid).ok()
718    }
719
720    pub fn is_uuid(&self) -> bool {
721        matches!(self, PartialValue::Uuid(_))
722    }
723
724    pub fn new_refer_s(us: &str) -> Option<Self> {
725        match Uuid::parse_str(us) {
726            Ok(u) => Some(PartialValue::Refer(u)),
727            Err(_) => None,
728        }
729    }
730
731    pub fn is_refer(&self) -> bool {
732        matches!(self, PartialValue::Refer(_))
733    }
734
735    pub fn new_indexes(s: &str) -> Option<Self> {
736        IndexType::try_from(s).map(PartialValue::Index).ok()
737    }
738
739    pub fn is_index(&self) -> bool {
740        matches!(self, PartialValue::Index(_))
741    }
742
743    pub fn new_syntaxs(s: &str) -> Option<Self> {
744        SyntaxType::try_from(s).map(PartialValue::Syntax).ok()
745    }
746
747    pub fn is_syntax(&self) -> bool {
748        matches!(self, PartialValue::Syntax(_))
749    }
750
751    pub fn new_json_filter_s(s: &str) -> Option<Self> {
752        serde_json::from_str(s)
753            .map(PartialValue::JsonFilt)
754            .map_err(|e| {
755                trace!(?e, ?s);
756            })
757            .ok()
758    }
759
760    pub fn is_json_filter(&self) -> bool {
761        matches!(self, PartialValue::JsonFilt(_))
762    }
763
764    pub fn new_credential_tag(s: &str) -> Self {
765        PartialValue::Cred(s.to_lowercase())
766    }
767
768    pub fn is_credential(&self) -> bool {
769        matches!(self, PartialValue::Cred(_))
770    }
771
772    pub fn new_secret_str() -> Self {
773        PartialValue::SecretValue
774    }
775
776    pub fn is_secret_string(&self) -> bool {
777        matches!(self, PartialValue::SecretValue)
778    }
779
780    pub fn new_sshkey_tag(s: String) -> Self {
781        PartialValue::SshKey(s)
782    }
783
784    pub fn new_sshkey_tag_s(s: &str) -> Self {
785        PartialValue::SshKey(s.to_string())
786    }
787
788    pub fn is_sshkey(&self) -> bool {
789        matches!(self, PartialValue::SshKey(_))
790    }
791
792    pub fn new_spn_s(s: &str) -> Option<Self> {
793        SPN_RE.captures(s).and_then(|caps| {
794            let name = match caps.name(Attribute::Name.as_ref()) {
795                Some(v) => v.as_str().to_string(),
796                None => return None,
797            };
798            let realm = match caps.name("realm") {
799                Some(v) => v.as_str().to_string(),
800                None => return None,
801            };
802            Some(PartialValue::Spn(name, realm))
803        })
804    }
805
806    pub fn new_spn_nrs(n: &str, r: &str) -> Self {
807        PartialValue::Spn(n.to_string(), r.to_string())
808    }
809
810    pub fn is_spn(&self) -> bool {
811        matches!(self, PartialValue::Spn(_, _))
812    }
813
814    pub fn new_uint32(u: u32) -> Self {
815        PartialValue::Uint32(u)
816    }
817
818    pub fn new_uint32_str(u: &str) -> Option<Self> {
819        u.parse::<u32>().ok().map(PartialValue::Uint32)
820    }
821
822    pub fn new_int64_str(u: &str) -> Option<Self> {
823        u.parse::<i64>().ok().map(PartialValue::Int64)
824    }
825
826    pub fn new_uint64_str(u: &str) -> Option<Self> {
827        u.parse::<u64>().ok().map(PartialValue::Uint64)
828    }
829
830    pub fn is_uint32(&self) -> bool {
831        matches!(self, PartialValue::Uint32(_))
832    }
833
834    pub fn new_cid(c: Cid) -> Self {
835        PartialValue::Cid(c)
836    }
837
838    pub fn new_cid_s(_c: &str) -> Option<Self> {
839        None
840    }
841
842    pub fn is_cid(&self) -> bool {
843        matches!(self, PartialValue::Cid(_))
844    }
845
846    pub fn new_nsuniqueid_s(s: &str) -> Self {
847        PartialValue::Nsuniqueid(s.to_lowercase())
848    }
849
850    pub fn is_nsuniqueid(&self) -> bool {
851        matches!(self, PartialValue::Nsuniqueid(_))
852    }
853
854    pub fn new_datetime_epoch(ts: Duration) -> Self {
855        PartialValue::DateTime(OffsetDateTime::UNIX_EPOCH + ts)
856    }
857
858    pub fn new_datetime_s(s: &str) -> Option<Self> {
859        OffsetDateTime::parse(s, &Rfc3339)
860            .ok()
861            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
862            .map(PartialValue::DateTime)
863    }
864
865    pub fn is_datetime(&self) -> bool {
866        matches!(self, PartialValue::DateTime(_))
867    }
868
869    pub fn new_email_address_s(s: &str) -> Self {
870        PartialValue::EmailAddress(s.to_string())
871    }
872
873    pub fn is_email_address(&self) -> bool {
874        matches!(self, PartialValue::EmailAddress(_))
875    }
876
877    pub fn new_phonenumber_s(s: &str) -> Self {
878        PartialValue::PhoneNumber(s.to_string())
879    }
880
881    pub fn new_address(s: &str) -> Self {
882        PartialValue::Address(s.to_string())
883    }
884
885    pub fn new_url_s(s: &str) -> Option<Self> {
886        Url::parse(s).ok().map(PartialValue::Url)
887    }
888
889    pub fn is_url(&self) -> bool {
890        matches!(self, PartialValue::Url(_))
891    }
892
893    pub fn new_oauthscope(s: &str) -> Self {
894        PartialValue::OauthScope(s.to_string())
895    }
896
897    pub fn is_oauthscope(&self) -> bool {
898        matches!(self, PartialValue::OauthScope(_))
899    }
900
901    /*
902    pub fn new_oauthscopemap(u: Uuid) -> Self {
903        PartialValue::OauthScopeMap(u)
904    }
905
906    pub fn new_oauthscopemap_s(us: &str) -> Option<Self> {
907        match Uuid::parse_str(us) {
908            Ok(u) => Some(PartialValue::OauthScopeMap(u)),
909            Err(_) => None,
910        }
911    }
912
913    pub fn is_oauthscopemap(&self) -> bool {
914        matches!(self, PartialValue::OauthScopeMap(_))
915    }
916    */
917
918    pub fn is_privatebinary(&self) -> bool {
919        matches!(self, PartialValue::PrivateBinary)
920    }
921
922    pub fn new_publicbinary_tag_s(s: &str) -> Self {
923        PartialValue::PublicBinary(s.to_string())
924    }
925
926    pub fn new_restrictedstring_s(s: &str) -> Self {
927        PartialValue::RestrictedString(s.to_string())
928    }
929
930    pub fn new_intenttoken_s(s: String) -> Option<Self> {
931        Some(PartialValue::IntentToken(s))
932    }
933
934    pub fn new_passkey_s(us: &str) -> Option<Self> {
935        Uuid::parse_str(us).map(PartialValue::Passkey).ok()
936    }
937
938    pub fn new_attested_passkey_s(us: &str) -> Option<Self> {
939        Uuid::parse_str(us).map(PartialValue::AttestedPasskey).ok()
940    }
941
942    pub fn new_hex_string_s(hexstr: &str) -> Option<Self> {
943        let hexstr_lower = hexstr.to_lowercase();
944        if HEXSTR_RE.is_match(&hexstr_lower) {
945            Some(PartialValue::HexString(hexstr_lower))
946        } else {
947            None
948        }
949    }
950
951    pub fn new_image(input: &str) -> Self {
952        PartialValue::Image(input.to_string())
953    }
954
955    pub fn to_str(&self) -> Option<&str> {
956        match self {
957            PartialValue::Utf8(s) => Some(s.as_str()),
958            PartialValue::Iutf8(s) => Some(s.as_str()),
959            PartialValue::Iname(s) => Some(s.as_str()),
960            _ => None,
961        }
962    }
963
964    pub fn to_url(&self) -> Option<&Url> {
965        match self {
966            PartialValue::Url(u) => Some(u),
967            _ => None,
968        }
969    }
970
971    pub fn get_idx_eq_key(&self) -> String {
972        match self {
973            PartialValue::Utf8(s)
974            | PartialValue::Iutf8(s)
975            | PartialValue::Iname(s)
976            | PartialValue::Nsuniqueid(s)
977            | PartialValue::EmailAddress(s)
978            | PartialValue::RestrictedString(s) => s.clone(),
979            PartialValue::Passkey(u)
980            | PartialValue::AttestedPasskey(u)
981            | PartialValue::Refer(u)
982            | PartialValue::Uuid(u) => u.as_hyphenated().to_string(),
983            PartialValue::Bool(b) => b.to_string(),
984            PartialValue::Syntax(syn) => syn.to_string(),
985            PartialValue::Index(it) => it.to_string(),
986            PartialValue::JsonFilt(s) =>
987            {
988                #[allow(clippy::expect_used)]
989                serde_json::to_string(s).expect("A json filter value was corrupted during run-time")
990            }
991            PartialValue::Cred(tag)
992            | PartialValue::PublicBinary(tag)
993            | PartialValue::SshKey(tag) => tag.to_string(),
994            // This will never match as we never index radius creds! See generate_idx_eq_keys
995            PartialValue::SecretValue | PartialValue::PrivateBinary => "_".to_string(),
996            PartialValue::Spn(name, realm) => format!("{name}@{realm}"),
997            PartialValue::Uint32(u) => u.to_string(),
998            PartialValue::Int64(u) => u.to_string(),
999            PartialValue::Uint64(u) => u.to_string(),
1000            PartialValue::DateTime(odt) => {
1001                debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
1002                #[allow(clippy::expect_used)]
1003                odt.format(&Rfc3339)
1004                    .expect("Failed to format timestamp into RFC3339")
1005            }
1006            PartialValue::Url(u) => u.to_string(),
1007            PartialValue::OauthScope(u) => u.to_string(),
1008            PartialValue::Address(a) => a.to_string(),
1009            PartialValue::PhoneNumber(a) => a.to_string(),
1010            PartialValue::IntentToken(u) => u.clone(),
1011            PartialValue::UiHint(u) => (*u as u16).to_string(),
1012            PartialValue::Image(imagehash) => imagehash.to_owned(),
1013            PartialValue::CredentialType(ct) => ct.to_string(),
1014            // This will never work, we don't allow equality searching on Cid's
1015            PartialValue::Cid(_) => "_".to_string(),
1016            // We don't allow searching on claim/uuid pairs.
1017            PartialValue::OauthClaim(_, _) => "_".to_string(),
1018            PartialValue::OauthClaimValue(_, _, _) => "_".to_string(),
1019            PartialValue::HexString(hexstr) => hexstr.to_string(),
1020            PartialValue::Json => "_".to_string(),
1021            PartialValue::Sha256(bytes) => hex::encode(bytes),
1022        }
1023    }
1024
1025    pub fn get_idx_sub_key(&self) -> Option<String> {
1026        match self {
1027            PartialValue::Utf8(s)
1028            | PartialValue::Iutf8(s)
1029            | PartialValue::Iname(s)
1030            // | PartialValue::Nsuniqueid(s)
1031            | PartialValue::EmailAddress(s)
1032            | PartialValue::RestrictedString(s) => Some(s.to_lowercase()),
1033
1034            PartialValue::Cred(tag)
1035            | PartialValue::PublicBinary(tag)
1036            | PartialValue::SshKey(tag) => Some(tag.to_lowercase()),
1037
1038            // PartialValue::Spn(name, realm) => format!("{name}@{realm}"),
1039            _ => None,
1040        }
1041    }
1042}
1043
1044#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1045pub enum ApiTokenScope {
1046    ReadOnly,
1047    ReadWrite,
1048    Synchronise,
1049}
1050
1051impl fmt::Display for ApiTokenScope {
1052    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1053        match self {
1054            ApiTokenScope::ReadOnly => write!(f, "read_only"),
1055            ApiTokenScope::ReadWrite => write!(f, "read_write"),
1056            ApiTokenScope::Synchronise => write!(f, "synchronise"),
1057        }
1058    }
1059}
1060
1061impl TryInto<ApiTokenPurpose> for ApiTokenScope {
1062    type Error = OperationError;
1063
1064    fn try_into(self: ApiTokenScope) -> Result<ApiTokenPurpose, OperationError> {
1065        match self {
1066            ApiTokenScope::ReadOnly => Ok(ApiTokenPurpose::ReadOnly),
1067            ApiTokenScope::ReadWrite => Ok(ApiTokenPurpose::ReadWrite),
1068            ApiTokenScope::Synchronise => Ok(ApiTokenPurpose::Synchronise),
1069        }
1070    }
1071}
1072
1073#[derive(Clone, Debug, PartialEq, Eq)]
1074pub struct ApiToken {
1075    pub label: String,
1076    pub expiry: Option<OffsetDateTime>,
1077    pub issued_at: OffsetDateTime,
1078    pub issued_by: IdentityId,
1079    pub scope: ApiTokenScope,
1080}
1081
1082#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1083pub enum SessionScope {
1084    ReadOnly,
1085    ReadWrite,
1086    PrivilegeCapable,
1087    // For migration! To be removed in future!
1088    Synchronise,
1089}
1090
1091impl fmt::Display for SessionScope {
1092    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1093        match self {
1094            SessionScope::ReadOnly => write!(f, "read_only"),
1095            SessionScope::ReadWrite => write!(f, "read_write"),
1096            SessionScope::PrivilegeCapable => write!(f, "privilege_capable"),
1097            SessionScope::Synchronise => write!(f, "synchronise"),
1098        }
1099    }
1100}
1101
1102impl TryInto<UatPurposeStatus> for SessionScope {
1103    type Error = OperationError;
1104
1105    fn try_into(self: SessionScope) -> Result<UatPurposeStatus, OperationError> {
1106        match self {
1107            SessionScope::ReadOnly => Ok(UatPurposeStatus::ReadOnly),
1108            SessionScope::ReadWrite => Ok(UatPurposeStatus::ReadWrite),
1109            SessionScope::PrivilegeCapable => Ok(UatPurposeStatus::PrivilegeCapable),
1110            SessionScope::Synchronise => Err(OperationError::InvalidEntryState),
1111        }
1112    }
1113}
1114
1115#[derive(Clone, Debug, PartialEq, Eq)]
1116pub enum SessionState {
1117    // IMPORTANT - this order allows sorting by
1118    // lowest to highest, we always want to take
1119    // the lowest value!
1120    RevokedAt(Cid),
1121    ExpiresAt(OffsetDateTime),
1122    NeverExpires,
1123}
1124
1125impl PartialOrd for SessionState {
1126    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1127        Some(self.cmp(other))
1128    }
1129}
1130
1131impl Ord for SessionState {
1132    fn cmp(&self, other: &Self) -> Ordering {
1133        // We need this to order by highest -> least which represents
1134        // priority amongst these elements.
1135        match (self, other) {
1136            // RevokedAt with the earliest time = highest
1137            (SessionState::RevokedAt(c_self), SessionState::RevokedAt(c_other)) => {
1138                // We need to reverse this - we need the "lower value" to take priority.
1139                // This is similar to tombstones where the earliest CID must be persisted
1140                c_other.cmp(c_self)
1141            }
1142            (SessionState::RevokedAt(_), _) => Ordering::Greater,
1143            (_, SessionState::RevokedAt(_)) => Ordering::Less,
1144            // ExpiresAt with a greater time = higher
1145            (SessionState::ExpiresAt(e_self), SessionState::ExpiresAt(e_other)) => {
1146                // Keep the "newer" expiry. This can be because a session was extended
1147                // by some mechanism, generally in oauth2.
1148                e_self.cmp(e_other)
1149            }
1150            (SessionState::ExpiresAt(_), _) => Ordering::Greater,
1151            (_, SessionState::ExpiresAt(_)) => Ordering::Less,
1152            // NeverExpires = least.
1153            (SessionState::NeverExpires, SessionState::NeverExpires) => Ordering::Equal,
1154        }
1155    }
1156}
1157
1158#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
1159pub enum AuthType {
1160    Anonymous,
1161    Password,
1162    GeneratedPassword,
1163    PasswordTotp,
1164    PasswordBackupCode,
1165    PasswordSecurityKey,
1166    Passkey,
1167    AttestedPasskey,
1168    OAuth2Trust,
1169}
1170
1171impl fmt::Display for AuthType {
1172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1173        match self {
1174            AuthType::Anonymous => write!(f, "anonymous"),
1175            AuthType::Password => write!(f, "password"),
1176            AuthType::GeneratedPassword => write!(f, "generatedpassword"),
1177            AuthType::PasswordTotp => write!(f, "passwordtotp"),
1178            AuthType::PasswordBackupCode => write!(f, "passwordbackupcode"),
1179            AuthType::PasswordSecurityKey => write!(f, "passwordsecuritykey"),
1180            AuthType::Passkey => write!(f, "passkey"),
1181            AuthType::AttestedPasskey => write!(f, "attested_passkey"),
1182            AuthType::OAuth2Trust => write!(f, "oauth2_trust"),
1183        }
1184    }
1185}
1186
1187#[derive(Clone, PartialEq, Eq, Default)]
1188pub enum SessionExtMetadata {
1189    #[default]
1190    None,
1191    OAuth2 {
1192        access_expires_at: Duration,
1193        access_token: String,
1194        refresh_token: Option<String>,
1195    },
1196}
1197
1198impl fmt::Debug for SessionExtMetadata {
1199    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1200        match self {
1201            Self::None => f.debug_struct("SessionExtMetadata::None").finish(),
1202            Self::OAuth2 {
1203                access_expires_at, ..
1204            } => f
1205                .debug_struct("SessionExtMetadata::OAuth2")
1206                .field("access_expires_at", access_expires_at)
1207                .finish(),
1208        }
1209    }
1210}
1211
1212#[derive(Clone, PartialEq, Eq)]
1213pub struct Session {
1214    pub label: String,
1215    // pub expiry: Option<OffsetDateTime>,
1216    pub state: SessionState,
1217    pub issued_at: OffsetDateTime,
1218    pub issued_by: IdentityId,
1219    pub cred_id: Uuid,
1220    pub scope: SessionScope,
1221    pub type_: AuthType,
1222    pub ext_metadata: SessionExtMetadata,
1223}
1224
1225impl fmt::Debug for Session {
1226    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1227        let issuer = match self.issued_by {
1228            IdentityId::User(u) => format!("User - {}", uuid_to_proto_string(u)),
1229            IdentityId::Synch(u) => format!("Synch - {}", uuid_to_proto_string(u)),
1230            IdentityId::Internal(u) => format!("Internal - {}", uuid_to_proto_string(u)),
1231        };
1232        let expiry = match self.state {
1233            SessionState::ExpiresAt(e) => e.to_string(),
1234            SessionState::NeverExpires => "never".to_string(),
1235            SessionState::RevokedAt(_) => "revoked".to_string(),
1236        };
1237        write!(
1238            f,
1239            "state: {}, issued at: {}, issued by: {}, credential id: {}, scope: {:?}",
1240            expiry, self.issued_at, issuer, self.cred_id, self.scope
1241        )
1242    }
1243}
1244
1245#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
1246pub enum OauthClaimMapJoin {
1247    CommaSeparatedValue,
1248    SpaceSeparatedValue,
1249    #[default]
1250    JsonArray,
1251}
1252
1253impl From<OauthClaimMapJoin> for ScimOauth2ClaimMapJoinChar {
1254    fn from(value: OauthClaimMapJoin) -> Self {
1255        match value {
1256            OauthClaimMapJoin::CommaSeparatedValue => {
1257                ScimOauth2ClaimMapJoinChar::CommaSeparatedValue
1258            }
1259            OauthClaimMapJoin::SpaceSeparatedValue => {
1260                ScimOauth2ClaimMapJoinChar::SpaceSeparatedValue
1261            }
1262            OauthClaimMapJoin::JsonArray => ScimOauth2ClaimMapJoinChar::JsonArray,
1263        }
1264    }
1265}
1266
1267impl From<ScimOauth2ClaimMapJoinChar> for OauthClaimMapJoin {
1268    fn from(value: ScimOauth2ClaimMapJoinChar) -> Self {
1269        match value {
1270            ScimOauth2ClaimMapJoinChar::CommaSeparatedValue => {
1271                OauthClaimMapJoin::CommaSeparatedValue
1272            }
1273            ScimOauth2ClaimMapJoinChar::SpaceSeparatedValue => {
1274                OauthClaimMapJoin::SpaceSeparatedValue
1275            }
1276            ScimOauth2ClaimMapJoinChar::JsonArray => OauthClaimMapJoin::JsonArray,
1277        }
1278    }
1279}
1280
1281impl OauthClaimMapJoin {
1282    pub(crate) fn to_str(self) -> &'static str {
1283        match self {
1284            OauthClaimMapJoin::CommaSeparatedValue => ",",
1285            OauthClaimMapJoin::SpaceSeparatedValue => " ",
1286            // Should this be something else?
1287            OauthClaimMapJoin::JsonArray => ";",
1288        }
1289    }
1290}
1291
1292impl From<DbValueOauthClaimMapJoinV1> for OauthClaimMapJoin {
1293    fn from(value: DbValueOauthClaimMapJoinV1) -> OauthClaimMapJoin {
1294        match value {
1295            DbValueOauthClaimMapJoinV1::CommaSeparatedValue => {
1296                OauthClaimMapJoin::CommaSeparatedValue
1297            }
1298            DbValueOauthClaimMapJoinV1::SpaceSeparatedValue => {
1299                OauthClaimMapJoin::SpaceSeparatedValue
1300            }
1301            DbValueOauthClaimMapJoinV1::JsonArray => OauthClaimMapJoin::JsonArray,
1302        }
1303    }
1304}
1305
1306impl From<OauthClaimMapJoin> for DbValueOauthClaimMapJoinV1 {
1307    fn from(value: OauthClaimMapJoin) -> DbValueOauthClaimMapJoinV1 {
1308        match value {
1309            OauthClaimMapJoin::CommaSeparatedValue => {
1310                DbValueOauthClaimMapJoinV1::CommaSeparatedValue
1311            }
1312            OauthClaimMapJoin::SpaceSeparatedValue => {
1313                DbValueOauthClaimMapJoinV1::SpaceSeparatedValue
1314            }
1315            OauthClaimMapJoin::JsonArray => DbValueOauthClaimMapJoinV1::JsonArray,
1316        }
1317    }
1318}
1319
1320#[derive(Clone, Debug, PartialEq, Eq)]
1321pub struct Oauth2Session {
1322    pub parent: Option<Uuid>,
1323    pub state: SessionState,
1324    pub issued_at: OffsetDateTime,
1325    pub rs_uuid: Uuid,
1326}
1327
1328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1329pub enum KeyUsage {
1330    JwsEs256,
1331    JwsHs256,
1332    JwsRs256,
1333    JweA128GCM,
1334    HkdfS256,
1335}
1336
1337impl fmt::Display for KeyUsage {
1338    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1339        write!(
1340            f,
1341            "{}",
1342            match self {
1343                KeyUsage::JwsEs256 => "jws_es256",
1344                KeyUsage::JwsHs256 => "jws_hs256",
1345                KeyUsage::JwsRs256 => "jws_rs256",
1346                KeyUsage::JweA128GCM => "jwe_a128gcm",
1347                KeyUsage::HkdfS256 => "hkdf_s256",
1348            }
1349        )
1350    }
1351}
1352
1353#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1354pub enum KeyStatus {
1355    Valid,
1356    Retained,
1357    Revoked,
1358}
1359
1360impl fmt::Display for KeyStatus {
1361    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1362        write!(
1363            f,
1364            "{}",
1365            match self {
1366                KeyStatus::Valid => "valid",
1367                KeyStatus::Retained => "retained",
1368                KeyStatus::Revoked => "revoked",
1369            }
1370        )
1371    }
1372}
1373
1374/// A value is a complete unit of data for an attribute. It is made up of a PartialValue, which is
1375/// used for selection, filtering, searching, matching etc. It also contains supplemental data
1376/// which may be stored inside of the Value, such as credential secrets, blobs etc.
1377///
1378/// This type is used when you need the "full data" of an attribute. Typically this is in a create
1379/// or modification operation where you are applying a set of complete values into an entry.
1380#[derive(Clone, Debug)]
1381pub enum Value {
1382    Utf8(String),
1383    /// Case insensitive string
1384    Iutf8(String),
1385    /// Case insensitive Name for a thing
1386    Iname(String),
1387    Uuid(Uuid),
1388    Bool(bool),
1389    Syntax(SyntaxType),
1390    Index(IndexType),
1391    Refer(Uuid),
1392    JsonFilt(ProtoFilter),
1393    Cred(String, Credential),
1394    SshKey(String, SshPublicKey),
1395    SecretValue(String),
1396    Spn(String, String),
1397    Uint32(u32),
1398    Int64(i64),
1399    Uint64(u64),
1400    Cid(Cid),
1401    Nsuniqueid(String),
1402    DateTime(OffsetDateTime),
1403    EmailAddress(String, bool),
1404    PhoneNumber(String, bool),
1405    Address(Address),
1406    Url(Url),
1407    OauthScope(String),
1408    OauthScopeMap(Uuid, BTreeSet<String>),
1409    PrivateBinary(Vec<u8>),
1410    PublicBinary(String, Vec<u8>),
1411    RestrictedString(String),
1412    IntentToken(String, IntentTokenState),
1413    Passkey(Uuid, String, PasskeyV4),
1414    AttestedPasskey(Uuid, String, AttestedPasskeyV4),
1415
1416    Session(Uuid, Session),
1417    ApiToken(Uuid, ApiToken),
1418    Oauth2Session(Uuid, Oauth2Session),
1419
1420    JwsKeyEs256(JwsEs256Signer),
1421    JwsKeyRs256(JwsRs256Signer),
1422    UiHint(UiHint),
1423
1424    TotpSecret(String, Totp),
1425    AuditLogString(Cid, String),
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::UiHint(_)
2323            | Value::CredentialType(_)
2324            | Value::Json(_)
2325            | Value::Sha256(_)
2326            | Value::WebauthnAttestationCaList(_) => true,
2327        }
2328    }
2329
2330    pub(crate) fn validate_iname(s: &str) -> bool {
2331        match Uuid::parse_str(s) {
2332            // It is a uuid, disallow.
2333            Ok(_) => {
2334                error!("iname values may not contain uuids");
2335                false
2336            }
2337            // Not a uuid, check it against the re.
2338            Err(_) => {
2339                if !INAME_RE.is_match(s) {
2340                    error!("iname values may only contain limited characters - \"{}\" does not pass regex pattern \"{}\"", s, *INAME_RE);
2341                    false
2342                } else if DISALLOWED_NAMES.contains(s) {
2343                    error!("iname value \"{}\" is in denied list", s);
2344                    false
2345                } else {
2346                    true
2347                }
2348            }
2349        }
2350    }
2351
2352    pub(crate) fn validate_hexstr(s: &str) -> bool {
2353        if !HEXSTR_RE.is_match(s) {
2354            error!("hexstrings may only contain limited characters. - \"{}\" does not pass regex pattern \"{}\"", s, *HEXSTR_RE);
2355            false
2356        } else {
2357            true
2358        }
2359    }
2360
2361    pub(crate) fn validate_singleline(s: &str) -> bool {
2362        if !SINGLELINE_RE.is_match(s) {
2363            true
2364        } else {
2365            error!(
2366                "value contains invalid whitespace chars forbidden by \"{}\"",
2367                *SINGLELINE_RE
2368            );
2369            // Trace only, could be an injection attack of some kind.
2370            trace!(?s, "Invalid whitespace");
2371            false
2372        }
2373    }
2374
2375    pub(crate) fn validate_str_escapes(s: &str) -> bool {
2376        // Look for and prevent certain types of string escapes and injections.
2377        if UNICODE_CONTROL_RE.is_match(s) {
2378            error!("value contains invalid unicode control character",);
2379            // Trace only, could be an injection attack of some kind.
2380            trace!(?s, "Invalid Unicode Control");
2381            false
2382        } else {
2383            true
2384        }
2385    }
2386}
2387
2388#[cfg(test)]
2389mod tests {
2390    use crate::value::*;
2391
2392    #[test]
2393    fn test_value_index_tryfrom() {
2394        let r1 = IndexType::try_from("EQualiTY");
2395        assert_eq!(r1, Ok(IndexType::Equality));
2396
2397        let r2 = IndexType::try_from("PResenCE");
2398        assert_eq!(r2, Ok(IndexType::Presence));
2399
2400        let r3 = IndexType::try_from("SUbstrING");
2401        assert_eq!(r3, Ok(IndexType::SubString));
2402
2403        let r4 = IndexType::try_from("thaoeusaneuh");
2404        assert_eq!(r4, Err(()));
2405    }
2406
2407    #[test]
2408    fn test_value_syntax_tryfrom() {
2409        let r1 = SyntaxType::try_from("UTF8strinG");
2410        assert_eq!(r1, Ok(SyntaxType::Utf8String));
2411
2412        let r2 = SyntaxType::try_from("UTF8STRING_INSensitIVE");
2413        assert_eq!(r2, Ok(SyntaxType::Utf8StringInsensitive));
2414
2415        let r3 = SyntaxType::try_from("BOOLEAN");
2416        assert_eq!(r3, Ok(SyntaxType::Boolean));
2417
2418        let r4 = SyntaxType::try_from("SYNTAX_ID");
2419        assert_eq!(r4, Ok(SyntaxType::SyntaxId));
2420
2421        let r5 = SyntaxType::try_from("INDEX_ID");
2422        assert_eq!(r5, Ok(SyntaxType::IndexId));
2423
2424        let r6 = SyntaxType::try_from("zzzzantheou");
2425        assert_eq!(r6, Err(()));
2426    }
2427
2428    #[test]
2429    fn test_value_sshkey_validation_display() {
2430        let ecdsa = concat!("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3B",
2431        "tOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81l",
2432        "zPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@a",
2433        "methyst");
2434        let ed25519 = concat!(
2435            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP",
2436            " william@amethyst"
2437        );
2438        let rsa = concat!("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTcXpclurQpyOHZBM/cDY9EvInSYkYSGe51by/wJP0Njgi",
2439        "GZUJ3HTaPqoGWux0PKd7KJki+onLYt4IwDV1RhV/GtMML2U9v94+pA8RIK4khCxvpUxlM7Kt/svjOzzzqiZfKdV37/",
2440        "OUXmM7bwVGOvm3EerDOwmO/QdzNGfkca12aWLoz97YrleXnCoAzr3IN7j3rwmfJGDyuUtGTdmyS/QWhK9FPr8Ic3eM",
2441        "QK1JSAQqVfGhA8lLbJHmnQ/b/KMl2lzzp7SXej0wPUfvI/IP3NGb8irLzq8+JssAzXGJ+HMql+mNHiSuPaktbFzZ6y",
2442        "ikMR6Rx/psU07nAkxKZDEYpNVv william@amethyst");
2443
2444        let sk1 = Value::new_sshkey_str("tag", ecdsa).expect("Invalid ssh key");
2445        assert!(sk1.validate());
2446        // to proto them
2447        let psk1 = sk1.to_proto_string_clone();
2448        assert_eq!(psk1, format!("tag: {ecdsa}"));
2449
2450        let sk2 = Value::new_sshkey_str("tag", ed25519).expect("Invalid ssh key");
2451        assert!(sk2.validate());
2452        let psk2 = sk2.to_proto_string_clone();
2453        assert_eq!(psk2, format!("tag: {ed25519}"));
2454
2455        let sk3 = Value::new_sshkey_str("tag", rsa).expect("Invalid ssh key");
2456        assert!(sk3.validate());
2457        let psk3 = sk3.to_proto_string_clone();
2458        assert_eq!(psk3, format!("tag: {rsa}"));
2459
2460        let sk4 = Value::new_sshkey_str("tag", "ntaouhtnhtnuehtnuhotnuhtneouhtneouh");
2461        assert!(sk4.is_err());
2462
2463        let sk5 = Value::new_sshkey_str(
2464            "tag",
2465            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo",
2466        );
2467        assert!(sk5.is_err());
2468    }
2469
2470    /*
2471    #[test]
2472    fn test_value_spn() {
2473        // Create an spn vale
2474        let spnv = Value::new_spn_str("claire", "example.net.au");
2475        // create an spn pv
2476        let spnp = PartialValue::new_spn_nrs("claire", "example.net.au");
2477        // check it's indexing output
2478        let vidx_key = spnv.generate_idx_eq_keys().pop().unwrap();
2479        let idx_key = spnp.get_idx_eq_key();
2480        assert_eq!(idx_key,vidx_key);
2481        // check it can parse from name@realm
2482        let spn_parse = PartialValue::new_spn_s("claire@example.net.au").unwrap();
2483        assert_eq!(spn_parse,spnp);
2484        // check it can produce name@realm as str from the pv.
2485        assert_eq!("claire@example.net.au",spnv.to_proto_string_clone());
2486    }
2487    */
2488
2489    /*
2490    #[test]
2491    fn test_value_uint32() {
2492        assert!(Value::new_uint32_str("test").is_none());
2493        assert!(Value::new_uint32_str("18446744073709551615").is_none());
2494
2495        let u32v = Value::new_uint32_str("4000").unwrap();
2496        let u32pv = PartialValue::new_uint32_str("4000").unwrap();
2497
2498        let idx_key = u32pv.get_idx_eq_key();
2499        let vidx_key = u32v.generate_idx_eq_keys().pop().unwrap();
2500
2501        assert_eq!(idx_key,vidx_key);
2502    }
2503    */
2504
2505    #[test]
2506    fn test_value_cid() {
2507        assert!(PartialValue::new_cid_s("_").is_none());
2508    }
2509
2510    #[test]
2511    fn test_value_iname() {
2512        /*
2513         * name MUST NOT:
2514         * - be a pure int (confusion to gid/uid/linux)
2515         * - a uuid (confuses our name mapper)
2516         * - contain an @ (confuses SPN)
2517         * - can not start with _ (... api end points have _ as a magic char)
2518         * - can not have spaces (confuses too many systems :()
2519         * - can not have = or , (confuses ldap)
2520         * - can not have ., /, \ (path injection attacks)
2521         */
2522        let inv1 = Value::new_iname("1234");
2523        let inv2 = Value::new_iname("bc23f637-4439-4c07-b95d-eaed0d9e4b8b");
2524        let inv3 = Value::new_iname("hello@test.com");
2525        let inv4 = Value::new_iname("_bad");
2526        let inv5 = Value::new_iname("no spaces I'm sorry :(");
2527        let inv6 = Value::new_iname("bad=equals");
2528        let inv7 = Value::new_iname("bad,comma");
2529        let inv8 = Value::new_iname("123_456");
2530        let inv9 = Value::new_iname("🍿");
2531
2532        let val1 = Value::new_iname("William");
2533        let val2 = Value::new_iname("this_is_okay");
2534        let val3 = Value::new_iname("a123_456");
2535
2536        assert!(!inv1.validate());
2537        assert!(!inv2.validate());
2538        assert!(!inv3.validate());
2539        assert!(!inv4.validate());
2540        assert!(!inv5.validate());
2541        assert!(!inv6.validate());
2542        assert!(!inv7.validate());
2543        assert!(!inv8.validate());
2544        assert!(!inv9.validate());
2545
2546        assert!(val1.validate());
2547        assert!(val2.validate());
2548        assert!(val3.validate());
2549    }
2550
2551    #[test]
2552    fn test_value_nsuniqueid() {
2553        // nsunique
2554        // d765e707-48e111e6-8c9ebed8-f7926cc3
2555        // uuid
2556        // d765e707-48e1-11e6-8c9e-bed8f7926cc3
2557        let val1 = Value::new_nsuniqueid_s("d765e707-48e111e6-8c9ebed8-f7926cc3");
2558        let val2 = Value::new_nsuniqueid_s("D765E707-48E111E6-8C9EBED8-F7926CC3");
2559        let inv1 = Value::new_nsuniqueid_s("d765e707-48e1-11e6-8c9e-bed8f7926cc3");
2560        let inv2 = Value::new_nsuniqueid_s("xxxx");
2561
2562        assert!(inv1.is_none());
2563        assert!(inv2.is_none());
2564        assert!(val1.unwrap().validate());
2565        assert!(val2.unwrap().validate());
2566    }
2567
2568    #[test]
2569    fn test_value_datetime() {
2570        // Datetimes must always convert to UTC, and must always be rfc3339
2571        let val1 = Value::new_datetime_s("2020-09-25T11:22:02+10:00").expect("Must be valid");
2572        assert!(val1.validate());
2573        let val2 = Value::new_datetime_s("2020-09-25T01:22:02+00:00").expect("Must be valid");
2574        assert!(val2.validate());
2575        // Spaces are now valid in rfc3339 for parsing.
2576        let val3 = Value::new_datetime_s("2020-09-25 01:22:02+00:00").expect("Must be valid");
2577        assert!(val3.validate());
2578
2579        assert!(Value::new_datetime_s("2020-09-25T01:22:02").is_none());
2580        assert!(Value::new_datetime_s("2020-09-25").is_none());
2581        assert!(Value::new_datetime_s("2020-09-25T01:22:02+10").is_none());
2582
2583        // Manually craft
2584        let inv1 = Value::DateTime(
2585            #[allow(clippy::disallowed_methods)]
2586            OffsetDateTime::now_utc()
2587                .to_offset(time::UtcOffset::from_whole_seconds(36000).unwrap()),
2588        );
2589        assert!(!inv1.validate());
2590        #[allow(clippy::disallowed_methods)]
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}