kanidmd_lib/
value.rs

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