Skip to main content

kanidmd_lib/
value.rs

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