//! Inside an entry, the key-value pairs are stored in these [`Value`] types. The components of //! the [`Value`] module allow storage and transformation of various types of input into strongly //! typed values, allows their comparison, filtering and more. It also has the code for serialising //! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend). use crate::prelude::*; use std::collections::BTreeSet; use std::convert::TryFrom; use std::fmt; use std::fmt::Formatter; use std::str::FromStr; use std::time::Duration; use crate::valueset::uuid_to_proto_string; #[cfg(test)] use base64::{engine::general_purpose, Engine as _}; use compact_jwt::JwsSigner; use hashbrown::HashSet; use kanidm_proto::v1::ApiTokenPurpose; use kanidm_proto::v1::Filter as ProtoFilter; use kanidm_proto::v1::UatPurposeStatus; use kanidm_proto::v1::UiHint; use num_enum::TryFromPrimitive; use regex::Regex; use serde::{Deserialize, Serialize}; use sshkeys::PublicKey as SshPublicKey; use time::OffsetDateTime; use url::Url; use uuid::Uuid; use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4}; use crate::be::dbentry::DbIdentSpn; use crate::credential::{totp::Totp, Credential}; use crate::repl::cid::Cid; use crate::server::identity::IdentityId; lazy_static! { pub static ref SPN_RE: Regex = { #[allow(clippy::expect_used)] Regex::new("(?P[^@]+)@(?P[^@]+)").expect("Invalid SPN regex found") }; pub static ref DISALLOWED_NAMES: HashSet<&'static str> = { let mut m = HashSet::with_capacity(16); m.insert("root"); m.insert("nobody"); m.insert("nogroup"); m.insert("wheel"); m.insert("sshd"); m.insert("shadow"); m.insert("systemd"); m.insert("mail"); m.insert("man"); m.insert("administrator"); m.insert("dn=token"); m }; /// Only lowercase+numbers, with limited chars. pub static ref INAME_RE: Regex = { #[allow(clippy::expect_used)] Regex::new("^[a-z][a-z0-9-_\\.]+$").expect("Invalid Iname regex found") }; pub static ref NSUNIQUEID_RE: Regex = { #[allow(clippy::expect_used)] Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found") }; /// Must not contain whitespace. pub static ref OAUTHSCOPE_RE: Regex = { #[allow(clippy::expect_used)] Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found") }; pub static ref SINGLELINE_RE: Regex = { #[allow(clippy::expect_used)] Regex::new("[\n\r\t]").expect("Invalid singleline regex found") }; pub static ref ESCAPES_RE: Regex = { #[allow(clippy::expect_used)] Regex::new(r"\x1b\[([\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])") .expect("Invalid escapes regex found") }; } #[derive(Debug, Clone, PartialOrd, Ord, Eq, PartialEq, Hash)] // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim pub struct Address { pub formatted: String, pub street_address: String, pub locality: String, pub region: String, pub postal_code: String, // Must be validated. pub country: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum IntentTokenState { Valid { max_ttl: Duration, }, InProgress { max_ttl: Duration, session_id: Uuid, session_ttl: Duration, }, Consumed { max_ttl: Duration, }, } #[allow(non_camel_case_types)] #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Hash, TryFromPrimitive, )] #[repr(u16)] pub enum IndexType { Equality, Presence, SubString, } impl TryFrom<&str> for IndexType { type Error = (); fn try_from(value: &str) -> Result { let n_value = value.to_uppercase(); match n_value.as_str() { "EQUALITY" => Ok(IndexType::Equality), "PRESENCE" => Ok(IndexType::Presence), "SUBSTRING" => Ok(IndexType::SubString), // UUID map? // UUID rev map? _ => Err(()), } } } impl IndexType { pub fn as_idx_str(&self) -> &str { match self { IndexType::Equality => "eq", IndexType::Presence => "pres", IndexType::SubString => "sub", } } } impl fmt::Display for IndexType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match self { IndexType::Equality => "EQUALITY", IndexType::Presence => "PRESENCE", IndexType::SubString => "SUBSTRING", } ) } } #[allow(non_camel_case_types)] #[derive( Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TryFromPrimitive, Default, )] #[repr(u16)] pub enum SyntaxType { #[default] Utf8String = 0, Utf8StringInsensitive = 1, Uuid = 2, Boolean = 3, SyntaxId = 4, IndexId = 5, ReferenceUuid = 6, JsonFilter = 7, Credential = 8, SecretUtf8String = 9, SshKey = 10, SecurityPrincipalName = 11, Uint32 = 12, Cid = 13, Utf8StringIname = 14, NsUniqueId = 15, DateTime = 16, EmailAddress = 17, Url = 18, OauthScope = 19, OauthScopeMap = 20, PrivateBinary = 21, IntentToken = 22, Passkey = 23, DeviceKey = 24, Session = 25, JwsKeyEs256 = 26, JwsKeyRs256 = 27, Oauth2Session = 28, UiHint = 29, TotpSecret = 30, ApiToken = 31, } impl TryFrom<&str> for SyntaxType { type Error = (); fn try_from(value: &str) -> Result { let n_value = value.to_uppercase(); match n_value.as_str() { "UTF8STRING" => Ok(SyntaxType::Utf8String), "UTF8STRING_INSENSITIVE" => Ok(SyntaxType::Utf8StringInsensitive), "UTF8STRING_INAME" => Ok(SyntaxType::Utf8StringIname), "UUID" => Ok(SyntaxType::Uuid), "BOOLEAN" => Ok(SyntaxType::Boolean), "SYNTAX_ID" => Ok(SyntaxType::SyntaxId), "INDEX_ID" => Ok(SyntaxType::IndexId), "REFERENCE_UUID" => Ok(SyntaxType::ReferenceUuid), "JSON_FILTER" => Ok(SyntaxType::JsonFilter), "CREDENTIAL" => Ok(SyntaxType::Credential), // Compatibility for older syntax name. "RADIUS_UTF8STRING" | "SECRET_UTF8STRING" => Ok(SyntaxType::SecretUtf8String), "SSHKEY" => Ok(SyntaxType::SshKey), "SECURITY_PRINCIPAL_NAME" => Ok(SyntaxType::SecurityPrincipalName), "UINT32" => Ok(SyntaxType::Uint32), "CID" => Ok(SyntaxType::Cid), "NSUNIQUEID" => Ok(SyntaxType::NsUniqueId), "DATETIME" => Ok(SyntaxType::DateTime), "EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress), "URL" => Ok(SyntaxType::Url), "OAUTH_SCOPE" => Ok(SyntaxType::OauthScope), "OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap), "PRIVATE_BINARY" => Ok(SyntaxType::PrivateBinary), "INTENT_TOKEN" => Ok(SyntaxType::IntentToken), "PASSKEY" => Ok(SyntaxType::Passkey), "DEVICEKEY" => Ok(SyntaxType::DeviceKey), "SESSION" => Ok(SyntaxType::Session), "JWS_KEY_ES256" => Ok(SyntaxType::JwsKeyEs256), "JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256), "OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session), "UIHINT" => Ok(SyntaxType::UiHint), "TOTPSECRET" => Ok(SyntaxType::TotpSecret), "APITOKEN" => Ok(SyntaxType::ApiToken), _ => Err(()), } } } impl fmt::Display for SyntaxType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match self { SyntaxType::Utf8String => "UTF8STRING", SyntaxType::Utf8StringInsensitive => "UTF8STRING_INSENSITIVE", SyntaxType::Utf8StringIname => "UTF8STRING_INAME", SyntaxType::Uuid => "UUID", SyntaxType::Boolean => "BOOLEAN", SyntaxType::SyntaxId => "SYNTAX_ID", SyntaxType::IndexId => "INDEX_ID", SyntaxType::ReferenceUuid => "REFERENCE_UUID", SyntaxType::JsonFilter => "JSON_FILTER", SyntaxType::Credential => "CREDENTIAL", SyntaxType::SecretUtf8String => "SECRET_UTF8STRING", SyntaxType::SshKey => "SSHKEY", SyntaxType::SecurityPrincipalName => "SECURITY_PRINCIPAL_NAME", SyntaxType::Uint32 => "UINT32", SyntaxType::Cid => "CID", SyntaxType::NsUniqueId => "NSUNIQUEID", SyntaxType::DateTime => "DATETIME", SyntaxType::EmailAddress => "EMAIL_ADDRESS", SyntaxType::Url => "URL", SyntaxType::OauthScope => "OAUTH_SCOPE", SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP", SyntaxType::PrivateBinary => "PRIVATE_BINARY", SyntaxType::IntentToken => "INTENT_TOKEN", SyntaxType::Passkey => "PASSKEY", SyntaxType::DeviceKey => "DEVICEKEY", SyntaxType::Session => "SESSION", SyntaxType::JwsKeyEs256 => "JWS_KEY_ES256", SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256", SyntaxType::Oauth2Session => "OAUTH2SESSION", SyntaxType::UiHint => "UIHINT", SyntaxType::TotpSecret => "TOTPSECRET", SyntaxType::ApiToken => "APITOKEN", }) } } /// A partial value is a key or key subset that can be used to match for equality or substring /// against a complete Value within a set in an Entry. /// /// A partialValue is typically used when you need to match against a value, but without /// requiring all of it's data or expression. This is common in Filters or other direct /// lookups and requests. #[derive(Hash, Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)] pub enum PartialValue { Utf8(String), Iutf8(String), Iname(String), Uuid(Uuid), Bool(bool), Syntax(SyntaxType), Index(IndexType), Refer(Uuid), // Does this make sense? // TODO: We'll probably add tagging to this type for the partial matching JsonFilt(ProtoFilter), // Tag, matches to a DataValue. Cred(String), SshKey(String), SecretValue, Spn(String, String), Uint32(u32), Cid(Cid), Nsuniqueid(String), DateTime(OffsetDateTime), EmailAddress(String), PhoneNumber(String), Address(String), // Can add other selectors later. Url(Url), OauthScope(String), // OauthScopeMap(Uuid), PrivateBinary, PublicBinary(String), // Enumeration(String), // Float64(f64), RestrictedString(String), IntentToken(String), UiHint(UiHint), Passkey(Uuid), DeviceKey(Uuid), // The label, if any. } impl From for PartialValue { fn from(s: SyntaxType) -> Self { PartialValue::Syntax(s) } } impl From for PartialValue { fn from(i: IndexType) -> Self { PartialValue::Index(i) } } impl From for PartialValue { fn from(b: bool) -> Self { PartialValue::Bool(b) } } impl From<&bool> for PartialValue { fn from(b: &bool) -> Self { PartialValue::Bool(*b) } } impl From for PartialValue { fn from(i: ProtoFilter) -> Self { PartialValue::JsonFilt(i) } } impl From for PartialValue { fn from(i: u32) -> Self { PartialValue::Uint32(i) } } impl From for PartialValue { fn from(i: OffsetDateTime) -> Self { PartialValue::DateTime(i) } } impl From for PartialValue { fn from(i: Url) -> Self { PartialValue::Url(i) } } impl PartialValue { pub fn new_utf8(s: String) -> Self { PartialValue::Utf8(s) } pub fn new_utf8s(s: &str) -> Self { PartialValue::Utf8(s.to_string()) } pub fn is_utf8(&self) -> bool { matches!(self, PartialValue::Utf8(_)) } pub fn new_iutf8(s: &str) -> Self { PartialValue::Iutf8(s.to_lowercase()) } pub fn new_iname(s: &str) -> Self { PartialValue::Iname(s.to_lowercase()) } #[inline] pub fn new_class(s: &str) -> Self { PartialValue::new_iutf8(s) } pub fn is_iutf8(&self) -> bool { matches!(self, PartialValue::Iutf8(_)) } pub fn is_iname(&self) -> bool { matches!(self, PartialValue::Iname(_)) } pub fn new_bool(b: bool) -> Self { PartialValue::Bool(b) } pub fn new_bools(s: &str) -> Option { bool::from_str(s).map(PartialValue::Bool).ok() } pub fn is_bool(&self) -> bool { matches!(self, PartialValue::Bool(_)) } pub fn new_uuid_s(us: &str) -> Option { Uuid::parse_str(us).map(PartialValue::Uuid).ok() } pub fn is_uuid(&self) -> bool { matches!(self, PartialValue::Uuid(_)) } pub fn new_refer_s(us: &str) -> Option { match Uuid::parse_str(us) { Ok(u) => Some(PartialValue::Refer(u)), Err(_) => None, } } pub fn is_refer(&self) -> bool { matches!(self, PartialValue::Refer(_)) } pub fn new_indexes(s: &str) -> Option { IndexType::try_from(s).map(PartialValue::Index).ok() } pub fn is_index(&self) -> bool { matches!(self, PartialValue::Index(_)) } pub fn new_syntaxs(s: &str) -> Option { SyntaxType::try_from(s).map(PartialValue::Syntax).ok() } pub fn is_syntax(&self) -> bool { matches!(self, PartialValue::Syntax(_)) } pub fn new_json_filter_s(s: &str) -> Option { serde_json::from_str(s) .map(PartialValue::JsonFilt) .map_err(|e| { trace!(?e, ?s); }) .ok() } pub fn is_json_filter(&self) -> bool { matches!(self, PartialValue::JsonFilt(_)) } pub fn new_credential_tag(s: &str) -> Self { PartialValue::Cred(s.to_lowercase()) } pub fn is_credential(&self) -> bool { matches!(self, PartialValue::Cred(_)) } pub fn new_secret_str() -> Self { PartialValue::SecretValue } pub fn is_secret_string(&self) -> bool { matches!(self, PartialValue::SecretValue) } pub fn new_sshkey_tag(s: String) -> Self { PartialValue::SshKey(s) } pub fn new_sshkey_tag_s(s: &str) -> Self { PartialValue::SshKey(s.to_string()) } pub fn is_sshkey(&self) -> bool { matches!(self, PartialValue::SshKey(_)) } pub fn new_spn_s(s: &str) -> Option { SPN_RE.captures(s).and_then(|caps| { let name = match caps.name("name") { Some(v) => v.as_str().to_string(), None => return None, }; let realm = match caps.name("realm") { Some(v) => v.as_str().to_string(), None => return None, }; Some(PartialValue::Spn(name, realm)) }) } pub fn new_spn_nrs(n: &str, r: &str) -> Self { PartialValue::Spn(n.to_string(), r.to_string()) } pub fn is_spn(&self) -> bool { matches!(self, PartialValue::Spn(_, _)) } pub fn new_uint32(u: u32) -> Self { PartialValue::Uint32(u) } pub fn new_uint32_str(u: &str) -> Option { u.parse::().ok().map(PartialValue::Uint32) } pub fn is_uint32(&self) -> bool { matches!(self, PartialValue::Uint32(_)) } pub fn new_cid(c: Cid) -> Self { PartialValue::Cid(c) } pub fn new_cid_s(_c: &str) -> Option { None } pub fn is_cid(&self) -> bool { matches!(self, PartialValue::Cid(_)) } pub fn new_nsuniqueid_s(s: &str) -> Self { PartialValue::Nsuniqueid(s.to_lowercase()) } pub fn is_nsuniqueid(&self) -> bool { matches!(self, PartialValue::Nsuniqueid(_)) } pub fn new_datetime_epoch(ts: Duration) -> Self { PartialValue::DateTime(OffsetDateTime::unix_epoch() + ts) } pub fn new_datetime_s(s: &str) -> Option { OffsetDateTime::parse(s, time::Format::Rfc3339) .ok() .map(|odt| odt.to_offset(time::UtcOffset::UTC)) .map(PartialValue::DateTime) } pub fn is_datetime(&self) -> bool { matches!(self, PartialValue::DateTime(_)) } pub fn new_email_address_s(s: &str) -> Self { PartialValue::EmailAddress(s.to_string()) } pub fn is_email_address(&self) -> bool { matches!(self, PartialValue::EmailAddress(_)) } pub fn new_phonenumber_s(s: &str) -> Self { PartialValue::PhoneNumber(s.to_string()) } pub fn new_address(s: &str) -> Self { PartialValue::Address(s.to_string()) } pub fn new_url_s(s: &str) -> Option { Url::parse(s).ok().map(PartialValue::Url) } pub fn is_url(&self) -> bool { matches!(self, PartialValue::Url(_)) } pub fn new_oauthscope(s: &str) -> Self { PartialValue::OauthScope(s.to_string()) } pub fn is_oauthscope(&self) -> bool { matches!(self, PartialValue::OauthScope(_)) } /* pub fn new_oauthscopemap(u: Uuid) -> Self { PartialValue::OauthScopeMap(u) } pub fn new_oauthscopemap_s(us: &str) -> Option { match Uuid::parse_str(us) { Ok(u) => Some(PartialValue::OauthScopeMap(u)), Err(_) => None, } } pub fn is_oauthscopemap(&self) -> bool { matches!(self, PartialValue::OauthScopeMap(_)) } */ pub fn is_privatebinary(&self) -> bool { matches!(self, PartialValue::PrivateBinary) } pub fn new_publicbinary_tag_s(s: &str) -> Self { PartialValue::PublicBinary(s.to_string()) } pub fn new_restrictedstring_s(s: &str) -> Self { PartialValue::RestrictedString(s.to_string()) } pub fn new_intenttoken_s(s: String) -> Option { Some(PartialValue::IntentToken(s)) } pub fn new_passkey_s(us: &str) -> Option { Uuid::parse_str(us).map(PartialValue::Passkey).ok() } pub fn new_devicekey_s(us: &str) -> Option { Uuid::parse_str(us).map(PartialValue::DeviceKey).ok() } pub fn to_str(&self) -> Option<&str> { match self { PartialValue::Utf8(s) => Some(s.as_str()), PartialValue::Iutf8(s) => Some(s.as_str()), PartialValue::Iname(s) => Some(s.as_str()), _ => None, } } pub fn to_url(&self) -> Option<&Url> { match self { PartialValue::Url(u) => Some(u), _ => None, } } pub fn get_idx_eq_key(&self) -> String { match self { PartialValue::Utf8(s) | PartialValue::Iutf8(s) | PartialValue::Iname(s) | PartialValue::Nsuniqueid(s) | PartialValue::EmailAddress(s) | PartialValue::RestrictedString(s) => s.clone(), PartialValue::Passkey(u) | PartialValue::DeviceKey(u) | PartialValue::Refer(u) | PartialValue::Uuid(u) => u.as_hyphenated().to_string(), PartialValue::Bool(b) => b.to_string(), PartialValue::Syntax(syn) => syn.to_string(), PartialValue::Index(it) => it.to_string(), PartialValue::JsonFilt(s) => { #[allow(clippy::expect_used)] serde_json::to_string(s).expect("A json filter value was corrupted during run-time") } PartialValue::Cred(tag) | PartialValue::PublicBinary(tag) | PartialValue::SshKey(tag) => tag.to_string(), // This will never match as we never index radius creds! See generate_idx_eq_keys PartialValue::SecretValue | PartialValue::PrivateBinary => "_".to_string(), PartialValue::Spn(name, realm) => format!("{name}@{realm}"), PartialValue::Uint32(u) => u.to_string(), // This will never work, we don't allow equality searching on Cid's PartialValue::Cid(_) => "_".to_string(), PartialValue::DateTime(odt) => { debug_assert!(odt.offset() == time::UtcOffset::UTC); odt.format(time::Format::Rfc3339) } PartialValue::Url(u) => u.to_string(), PartialValue::OauthScope(u) => u.to_string(), PartialValue::Address(a) => a.to_string(), PartialValue::PhoneNumber(a) => a.to_string(), PartialValue::IntentToken(u) => u.clone(), PartialValue::UiHint(u) => (*u as u16).to_string(), } } #[allow(clippy::unimplemented)] pub fn get_idx_sub_key(&self) -> String { unimplemented!(); } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ApiTokenScope { ReadOnly, ReadWrite, Synchronise, } impl TryInto for ApiTokenScope { type Error = OperationError; fn try_into(self: ApiTokenScope) -> Result { match self { ApiTokenScope::ReadOnly => Ok(ApiTokenPurpose::ReadOnly), ApiTokenScope::ReadWrite => Ok(ApiTokenPurpose::ReadWrite), ApiTokenScope::Synchronise => Ok(ApiTokenPurpose::Synchronise), } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ApiToken { pub label: String, pub expiry: Option, pub issued_at: OffsetDateTime, pub issued_by: IdentityId, pub scope: ApiTokenScope, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SessionScope { ReadOnly, ReadWrite, PrivilegeCapable, // For migration! To be removed in future! Synchronise, } impl TryInto for SessionScope { type Error = OperationError; fn try_into(self: SessionScope) -> Result { match self { SessionScope::ReadOnly => Ok(UatPurposeStatus::ReadOnly), SessionScope::ReadWrite => Ok(UatPurposeStatus::ReadWrite), SessionScope::PrivilegeCapable => Ok(UatPurposeStatus::PrivilegeCapable), SessionScope::Synchronise => Err(OperationError::InvalidEntryState), } } } #[derive(Clone, PartialEq, Eq)] pub struct Session { pub label: String, pub expiry: Option, pub issued_at: OffsetDateTime, pub issued_by: IdentityId, pub cred_id: Uuid, pub scope: SessionScope, } impl fmt::Debug for Session { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let issuer = match self.issued_by { IdentityId::User(u) => format!("User - {}", uuid_to_proto_string(u)), IdentityId::Synch(u) => format!("Synch - {}", uuid_to_proto_string(u)), IdentityId::Internal => "Internal".to_string(), }; let expiry = match self.expiry { Some(e) => e.to_string(), None => "never".to_string(), }; write!( f, "expiry: {}, issued at: {}, issued by: {}, credential id: {}, scope: {:?}", expiry, self.issued_at, issuer, self.cred_id, self.scope ) } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Oauth2Session { pub parent: Uuid, pub expiry: Option, pub issued_at: OffsetDateTime, pub rs_uuid: Uuid, } /// A value is a complete unit of data for an attribute. It is made up of a PartialValue, which is /// used for selection, filtering, searching, matching etc. It also contains supplemental data /// which may be stored inside of the Value, such as credential secrets, blobs etc. /// /// This type is used when you need the "full data" of an attribute. Typically this is in a create /// or modification operation where you are applying a set of complete values into an entry. #[derive(Clone, Debug)] pub enum Value { Utf8(String), // Case insensitive string Iutf8(String), /// Case insensitive Name for a thing? Iname(String), Uuid(Uuid), Bool(bool), Syntax(SyntaxType), Index(IndexType), Refer(Uuid), JsonFilt(ProtoFilter), Cred(String, Credential), SshKey(String, String), SecretValue(String), Spn(String, String), Uint32(u32), Cid(Cid), Nsuniqueid(String), DateTime(OffsetDateTime), EmailAddress(String, bool), PhoneNumber(String, bool), Address(Address), Url(Url), OauthScope(String), OauthScopeMap(Uuid, BTreeSet), PrivateBinary(Vec), PublicBinary(String, Vec), // Enumeration(String), // Float64(f64), RestrictedString(String), IntentToken(String, IntentTokenState), Passkey(Uuid, String, PasskeyV4), DeviceKey(Uuid, String, DeviceKeyV4), Session(Uuid, Session), ApiToken(Uuid, ApiToken), Oauth2Session(Uuid, Oauth2Session), JwsKeyEs256(JwsSigner), JwsKeyRs256(JwsSigner), UiHint(UiHint), TotpSecret(String, Totp), } impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match (self, other) { (Value::Utf8(a), Value::Utf8(b)) | (Value::Iutf8(a), Value::Iutf8(b)) | (Value::Iname(a), Value::Iname(b)) | (Value::Cred(a, _), Value::Cred(b, _)) | (Value::SshKey(a, _), Value::SshKey(b, _)) | (Value::Spn(a, _), Value::Spn(b, _)) | (Value::Nsuniqueid(a), Value::Nsuniqueid(b)) | (Value::EmailAddress(a, _), Value::EmailAddress(b, _)) | (Value::PhoneNumber(a, _), Value::PhoneNumber(b, _)) | (Value::OauthScope(a), Value::OauthScope(b)) | (Value::PublicBinary(a, _), Value::PublicBinary(b, _)) | (Value::RestrictedString(a), Value::RestrictedString(b)) => a.eq(b), // Uuid, Refer (Value::Uuid(a), Value::Uuid(b)) | (Value::Refer(a), Value::Refer(b)) => a.eq(b), // Bool (Value::Bool(a), Value::Bool(b)) => a.eq(b), // Syntax (Value::Syntax(a), Value::Syntax(b)) => a.eq(b), // Index (Value::Index(a), Value::Index(b)) => a.eq(b), // JsonFilt (Value::JsonFilt(a), Value::JsonFilt(b)) => a.eq(b), // Uint32 (Value::Uint32(a), Value::Uint32(b)) => a.eq(b), // Cid (Value::Cid(a), Value::Cid(b)) => a.eq(b), // DateTime (Value::DateTime(a), Value::DateTime(b)) => a.eq(b), // Url (Value::Url(a), Value::Url(b)) => a.eq(b), // OauthScopeMap (Value::OauthScopeMap(a, c), Value::OauthScopeMap(b, d)) => a.eq(b) && c.eq(d), (Value::Address(_), Value::Address(_)) | (Value::PrivateBinary(_), Value::PrivateBinary(_)) | (Value::SecretValue(_), Value::SecretValue(_)) => false, // Specifically related to migrations, we allow the invalid comparison. (Value::Iutf8(_), Value::Iname(_)) | (Value::Iname(_), Value::Iutf8(_)) => false, // When upgrading between uuid -> name -> spn we have to allow some invalid types. (Value::Uuid(_), Value::Iname(_)) | (Value::Iname(_), Value::Spn(_, _)) | (Value::Uuid(_), Value::Spn(_, _)) => false, (l, r) => { error!(?l, ?r, "mismatched value types"); debug_assert!(false); false } } } } impl Eq for Value {} impl From for Value { fn from(b: bool) -> Self { Value::Bool(b) } } impl From<&bool> for Value { fn from(b: &bool) -> Self { Value::Bool(*b) } } impl From for Value { fn from(s: SyntaxType) -> Self { Value::Syntax(s) } } impl From for Value { fn from(i: IndexType) -> Self { Value::Index(i) } } impl From for Value { fn from(i: ProtoFilter) -> Self { Value::JsonFilt(i) } } impl From for Value { fn from(i: OffsetDateTime) -> Self { Value::DateTime(i) } } impl From for Value { fn from(i: u32) -> Self { Value::Uint32(i) } } impl From for Value { fn from(i: Url) -> Self { Value::Url(i) } } // Because these are potentially ambiguous, we limit them to tests where we can contain // any....mistakes. #[cfg(test)] impl From<&str> for Value { fn from(s: &str) -> Self { // Fuzzy match for uuid's match Uuid::parse_str(s) { Ok(u) => Value::Uuid(u), Err(_) => Value::Utf8(s.to_string()), } } } #[cfg(test)] impl From<&Uuid> for Value { fn from(u: &Uuid) -> Self { Value::Uuid(*u) } } #[cfg(test)] impl From for Value { fn from(u: Uuid) -> Self { Value::Uuid(u) } } impl From for Value { fn from(dis: DbIdentSpn) -> Self { match dis { DbIdentSpn::Spn(n, r) => Value::Spn(n, r), DbIdentSpn::Iname(n) => Value::Iname(n), DbIdentSpn::Uuid(u) => Value::Uuid(u), } } } impl Value { // I get the feeling this will have a lot of matching ... sigh. pub fn new_utf8(s: String) -> Self { Value::Utf8(s) } pub fn new_utf8s(s: &str) -> Self { Value::Utf8(s.to_string()) } pub fn is_utf8(&self) -> bool { matches!(self, Value::Utf8(_)) } pub fn new_iutf8(s: &str) -> Self { Value::Iutf8(s.to_lowercase()) } pub fn is_iutf8(&self) -> bool { matches!(self, Value::Iutf8(_)) } pub fn new_class(s: &str) -> Self { Value::Iutf8(s.to_lowercase()) } pub fn new_attr(s: &str) -> Self { Value::Iutf8(s.to_lowercase()) } pub fn is_insensitive_utf8(&self) -> bool { matches!(self, Value::Iutf8(_)) } pub fn new_iname(s: &str) -> Self { Value::Iname(s.to_lowercase()) } pub fn is_iname(&self) -> bool { matches!(self, Value::Iname(_)) } pub fn new_uuid_s(s: &str) -> Option { Uuid::parse_str(s).map(Value::Uuid).ok() } // Is this correct? Should ref be separate? pub fn is_uuid(&self) -> bool { matches!(self, Value::Uuid(_)) } pub fn new_bool(b: bool) -> Self { Value::Bool(b) } pub fn new_bools(s: &str) -> Option { bool::from_str(s).map(Value::Bool).ok() } #[inline] pub fn is_bool(&self) -> bool { matches!(self, Value::Bool(_)) } pub fn new_syntaxs(s: &str) -> Option { SyntaxType::try_from(s).map(Value::Syntax).ok() } pub fn new_syntax(s: SyntaxType) -> Self { Value::Syntax(s) } pub fn is_syntax(&self) -> bool { matches!(self, Value::Syntax(_)) } pub fn new_indexes(s: &str) -> Option { IndexType::try_from(s).map(Value::Index).ok() } pub fn new_index(i: IndexType) -> Self { Value::Index(i) } pub fn is_index(&self) -> bool { matches!(self, Value::Index(_)) } pub fn new_refer_s(us: &str) -> Option { Uuid::parse_str(us).map(Value::Refer).ok() } pub fn is_refer(&self) -> bool { matches!(self, Value::Refer(_)) } pub fn new_json_filter_s(s: &str) -> Option { serde_json::from_str(s).map(Value::JsonFilt).ok() } pub fn new_json_filter(f: ProtoFilter) -> Self { Value::JsonFilt(f) } pub fn is_json_filter(&self) -> bool { matches!(self, Value::JsonFilt(_)) } pub fn as_json_filter(&self) -> Option<&ProtoFilter> { match &self { Value::JsonFilt(f) => Some(f), _ => None, } } pub fn new_credential(tag: &str, cred: Credential) -> Self { Value::Cred(tag.to_string(), cred) } pub fn is_credential(&self) -> bool { matches!(&self, Value::Cred(_, _)) } pub fn to_credential(&self) -> Option<&Credential> { match &self { Value::Cred(_, cred) => Some(cred), _ => None, } } pub fn new_secret_str(cleartext: &str) -> Self { Value::SecretValue(cleartext.to_string()) } pub fn is_secret_string(&self) -> bool { matches!(&self, Value::SecretValue(_)) } pub fn get_secret_str(&self) -> Option<&str> { match &self { Value::SecretValue(c) => Some(c.as_str()), _ => None, } } pub fn new_sshkey_str(tag: &str, key: &str) -> Self { Value::SshKey(tag.to_string(), key.to_string()) } pub fn new_sshkey(tag: String, key: String) -> Self { Value::SshKey(tag, key) } pub fn is_sshkey(&self) -> bool { matches!(&self, Value::SshKey(_, _)) } pub fn get_sshkey(&self) -> Option<&str> { match &self { Value::SshKey(_, key) => Some(key.as_str()), _ => None, } } pub fn new_spn_parse(s: &str) -> Option { SPN_RE.captures(s).and_then(|caps| { let name = match caps.name("name") { Some(v) => v.as_str().to_string(), None => return None, }; let realm = match caps.name("realm") { Some(v) => v.as_str().to_string(), None => return None, }; Some(Value::Spn(name, realm)) }) } pub fn new_spn_str(n: &str, r: &str) -> Self { Value::Spn(n.to_string(), r.to_string()) } pub fn is_spn(&self) -> bool { matches!(&self, Value::Spn(_, _)) } pub fn new_uint32(u: u32) -> Self { Value::Uint32(u) } pub fn new_uint32_str(u: &str) -> Option { u.parse::().ok().map(Value::Uint32) } pub fn is_uint32(&self) -> bool { matches!(&self, Value::Uint32(_)) } pub fn new_cid(c: Cid) -> Self { Value::Cid(c) } pub fn is_cid(&self) -> bool { matches!(&self, Value::Cid(_)) } pub fn new_nsuniqueid_s(s: &str) -> Option { if NSUNIQUEID_RE.is_match(s) { Some(Value::Nsuniqueid(s.to_lowercase())) } else { None } } pub fn is_nsuniqueid(&self) -> bool { matches!(&self, Value::Nsuniqueid(_)) } pub fn new_datetime_epoch(ts: Duration) -> Self { Value::DateTime(OffsetDateTime::unix_epoch() + ts) } pub fn new_datetime_s(s: &str) -> Option { OffsetDateTime::parse(s, time::Format::Rfc3339) .ok() .map(|odt| odt.to_offset(time::UtcOffset::UTC)) .map(Value::DateTime) } pub fn new_datetime(dt: OffsetDateTime) -> Self { Value::DateTime(dt) } pub fn to_datetime(&self) -> Option { match &self { Value::DateTime(odt) => { debug_assert!(odt.offset() == time::UtcOffset::UTC); Some(*odt) } _ => None, } } pub fn is_datetime(&self) -> bool { matches!(&self, Value::DateTime(_)) } pub fn new_email_address_s(s: &str) -> Option { if validator::validate_email(s) { Some(Value::EmailAddress(s.to_string(), false)) } else { None } } pub fn new_email_address_primary_s(s: &str) -> Option { if validator::validate_email(s) { Some(Value::EmailAddress(s.to_string(), true)) } else { None } } pub fn is_email_address(&self) -> bool { matches!(&self, Value::EmailAddress(_, _)) } pub fn new_phonenumber_s(s: &str) -> Self { Value::PhoneNumber(s.to_string(), false) } pub fn new_address(a: Address) -> Self { Value::Address(a) } pub fn new_url_s(s: &str) -> Option { Url::parse(s).ok().map(Value::Url) } pub fn new_url(u: Url) -> Self { Value::Url(u) } pub fn is_url(&self) -> bool { matches!(&self, Value::Url(_)) } pub fn new_oauthscope(s: &str) -> Option { if OAUTHSCOPE_RE.is_match(s) { Some(Value::OauthScope(s.to_string())) } else { None } } pub fn is_oauthscope(&self) -> bool { matches!(&self, Value::OauthScope(_)) } pub fn new_oauthscopemap(u: Uuid, m: BTreeSet) -> Option { if m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)) { Some(Value::OauthScopeMap(u, m)) } else { None } } pub fn is_oauthscopemap(&self) -> bool { matches!(&self, Value::OauthScopeMap(_, _)) } #[cfg(test)] pub fn new_privatebinary_base64(der: &str) -> Self { let der = general_purpose::STANDARD.decode(der).unwrap(); Value::PrivateBinary(der) } pub fn new_privatebinary(der: &[u8]) -> Self { Value::PrivateBinary(der.to_owned()) } pub fn to_privatebinary(&self) -> Option<&Vec> { match &self { Value::PrivateBinary(c) => Some(c), _ => None, } } pub fn is_privatebinary(&self) -> bool { matches!(&self, Value::PrivateBinary(_)) } pub fn new_publicbinary(tag: String, der: Vec) -> Self { Value::PublicBinary(tag, der) } pub fn new_restrictedstring(s: String) -> Self { Value::RestrictedString(s) } #[allow(clippy::unreachable)] pub(crate) fn to_db_ident_spn(&self) -> DbIdentSpn { // This has to clone due to how the backend works. match &self { Value::Spn(n, r) => DbIdentSpn::Spn(n.clone(), r.clone()), Value::Iname(s) => DbIdentSpn::Iname(s.clone()), Value::Uuid(u) => DbIdentSpn::Uuid(*u), // Value::Iutf8(s) => DbValueV1::Iutf8(s.clone()), // Value::Utf8(s) => DbValueV1::Utf8(s.clone()), // Value::Nsuniqueid(s) => DbValueV1::NsUniqueId(s.clone()), v => unreachable!("-> {:?}", v), } } pub fn to_str(&self) -> Option<&str> { match &self { Value::Utf8(s) => Some(s.as_str()), Value::Iutf8(s) => Some(s.as_str()), Value::Iname(s) => Some(s.as_str()), _ => None, } } pub fn to_url(&self) -> Option<&Url> { match &self { Value::Url(u) => Some(u), _ => None, } } pub fn as_string(&self) -> Option<&String> { match &self { Value::Utf8(s) => Some(s), Value::Iutf8(s) => Some(s), Value::Iname(s) => Some(s), _ => None, } } // We need a separate to-ref_uuid to distinguish from normal uuids // in refint plugin. pub fn to_ref_uuid(&self) -> Option { match &self { Value::Refer(u) => Some(*u), Value::OauthScopeMap(u, _) => Some(*u), // We need to assert that our reference to our rs exists. Value::Oauth2Session(_, m) => Some(m.rs_uuid), _ => None, } } pub fn to_uuid(&self) -> Option<&Uuid> { match &self { Value::Uuid(u) => Some(u), _ => None, } } pub fn to_indextype(&self) -> Option<&IndexType> { match &self { Value::Index(i) => Some(i), _ => None, } } pub fn to_syntaxtype(&self) -> Option<&SyntaxType> { match &self { Value::Syntax(s) => Some(s), _ => None, } } pub fn to_bool(&self) -> Option { match self { // *v is to invoke a copy, but this is cheap af Value::Bool(v) => Some(*v), _ => None, } } pub fn to_uint32(&self) -> Option { match &self { Value::Uint32(v) => Some(*v), _ => None, } } pub fn to_utf8(self) -> Option { match self { Value::Utf8(s) => Some(s), _ => None, } } pub fn to_iutf8(self) -> Option { match self { Value::Iutf8(s) => Some(s), _ => None, } } pub fn to_iname(self) -> Option { match self { Value::Iname(s) => Some(s), _ => None, } } pub fn to_jsonfilt(self) -> Option { match self { Value::JsonFilt(f) => Some(f), _ => None, } } pub fn to_cred(self) -> Option<(String, Credential)> { match self { Value::Cred(tag, c) => Some((tag, c)), _ => None, } } pub fn to_sshkey(self) -> Option<(String, String)> { match self { Value::SshKey(tag, k) => Some((tag, k)), _ => None, } } pub fn to_spn(self) -> Option<(String, String)> { match self { Value::Spn(n, d) => Some((n, d)), _ => None, } } pub fn to_cid(self) -> Option { match self { Value::Cid(s) => Some(s), _ => None, } } pub fn to_nsuniqueid(self) -> Option { match self { Value::Nsuniqueid(s) => Some(s), _ => None, } } pub fn to_emailaddress(self) -> Option { match self { Value::EmailAddress(s, _) => Some(s), _ => None, } } pub fn to_oauthscope(self) -> Option { match self { Value::OauthScope(s) => Some(s), _ => None, } } pub fn to_oauthscopemap(self) -> Option<(Uuid, BTreeSet)> { match self { Value::OauthScopeMap(u, m) => Some((u, m)), _ => None, } } pub fn to_restrictedstring(self) -> Option { match self { Value::RestrictedString(s) => Some(s), _ => None, } } pub fn to_phonenumber(self) -> Option { match self { Value::PhoneNumber(p, _b) => Some(p), _ => None, } } pub fn to_publicbinary(self) -> Option<(String, Vec)> { match self { Value::PublicBinary(t, d) => Some((t, d)), _ => None, } } pub fn to_address(self) -> Option
{ match self { Value::Address(a) => Some(a), _ => None, } } pub fn to_intenttoken(self) -> Option<(String, IntentTokenState)> { match self { Value::IntentToken(u, s) => Some((u, s)), _ => None, } } pub fn to_session(self) -> Option<(Uuid, Session)> { match self { Value::Session(u, s) => Some((u, s)), _ => None, } } pub fn migrate_iutf8_iname(self) -> Option { match self { Value::Iutf8(v) => Some(Value::Iname(v)), _ => None, } } // !!!! This function is being phased out !!! #[allow(clippy::unreachable)] pub(crate) fn to_proto_string_clone(&self) -> String { match &self { Value::Iname(s) => s.clone(), Value::Uuid(u) => u.as_hyphenated().to_string(), // We display the tag and fingerprint. Value::SshKey(tag, key) => // Check it's really an sshkey in the // supplemental data. { match SshPublicKey::from_string(key) { Ok(spk) => { let fp = spk.fingerprint(); format!("{}: {}", tag, fp.hash) } Err(_) => format!("{tag}: corrupted ssh public key"), } } Value::Spn(n, r) => format!("{n}@{r}"), _ => unreachable!(), } } pub(crate) fn validate(&self) -> bool { // Validate that extra-data constraints on the type exist and are // valid. IE json filter is really a filter, or cred types have supplemental // data. match &self { // String security is required here Value::Utf8(s) | Value::Iutf8(s) | Value::Cred(s, _) | Value::PublicBinary(s, _) | Value::IntentToken(s, _) | Value::Passkey(_, s, _) | Value::DeviceKey(_, s, _) | Value::TotpSecret(s, _) => { Value::validate_str_escapes(s) && Value::validate_singleline(s) } Value::Spn(a, b) => { Value::validate_str_escapes(a) && Value::validate_str_escapes(b) && Value::validate_singleline(a) && Value::validate_singleline(b) } Value::Iname(s) => { Value::validate_str_escapes(s) && Value::validate_iname(s) && Value::validate_singleline(s) } Value::SshKey(s, key) => { SshPublicKey::from_string(key).is_ok() && Value::validate_str_escapes(s) && Value::validate_singleline(s) } Value::ApiToken(_, at) => { Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label) } // These have stricter validators so not needed. Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s), Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC, Value::EmailAddress(mail, _) => validator::validate_email(mail.as_str()), Value::OauthScope(s) => OAUTHSCOPE_RE.is_match(s), Value::OauthScopeMap(_, m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)), Value::PhoneNumber(_, _) => true, Value::Address(_) => true, Value::Uuid(_) | Value::Bool(_) | Value::Syntax(_) | Value::Index(_) | Value::Refer(_) | Value::JsonFilt(_) | Value::SecretValue(_) | Value::Uint32(_) | Value::Url(_) | Value::Cid(_) | Value::PrivateBinary(_) | Value::RestrictedString(_) | Value::JwsKeyEs256(_) | Value::Session(_, _) | Value::Oauth2Session(_, _) | Value::JwsKeyRs256(_) | Value::UiHint(_) => true, } } pub(crate) fn validate_iname(s: &str) -> bool { match Uuid::parse_str(s) { // It is a uuid, disallow. Ok(_) => { warn!("iname values may not contain uuids"); false } // Not a uuid, check it against the re. Err(_) => { if !INAME_RE.is_match(s) { warn!("iname values may only contain limited characters - \"{}\" does not pass regex pattern \"{}\"", s, *INAME_RE); false } else if DISALLOWED_NAMES.contains(s) { warn!("iname value \"{}\" is in denied list", s); false } else { true } } } } pub(crate) fn validate_singleline(s: &str) -> bool { if !SINGLELINE_RE.is_match(s) { true } else { warn!( "value contains invalid whitespace chars forbidden by \"{}\"", *SINGLELINE_RE ); false } } pub(crate) fn validate_str_escapes(s: &str) -> bool { // Look for and prevent certain types of string escapes and injections. if !ESCAPES_RE.is_match(s) { true } else { warn!( "value contains invalid escape chars forbidden by \"{}\"", *ESCAPES_RE ); false } } } #[cfg(test)] mod tests { use crate::value::*; #[test] fn test_value_index_tryfrom() { let r1 = IndexType::try_from("EQUALITY"); assert_eq!(r1, Ok(IndexType::Equality)); let r2 = IndexType::try_from("PRESENCE"); assert_eq!(r2, Ok(IndexType::Presence)); let r3 = IndexType::try_from("SUBSTRING"); assert_eq!(r3, Ok(IndexType::SubString)); let r4 = IndexType::try_from("thaoeusaneuh"); assert_eq!(r4, Err(())); } #[test] fn test_value_syntax_tryfrom() { let r1 = SyntaxType::try_from("UTF8STRING"); assert_eq!(r1, Ok(SyntaxType::Utf8String)); let r2 = SyntaxType::try_from("UTF8STRING_INSENSITIVE"); assert_eq!(r2, Ok(SyntaxType::Utf8StringInsensitive)); let r3 = SyntaxType::try_from("BOOLEAN"); assert_eq!(r3, Ok(SyntaxType::Boolean)); let r4 = SyntaxType::try_from("SYNTAX_ID"); assert_eq!(r4, Ok(SyntaxType::SyntaxId)); let r5 = SyntaxType::try_from("INDEX_ID"); assert_eq!(r5, Ok(SyntaxType::IndexId)); let r6 = SyntaxType::try_from("zzzzantheou"); assert_eq!(r6, Err(())); } #[test] fn test_value_sshkey_validation_display() { let ecdsa = concat!("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3B", "tOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81l", "zPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@a", "methyst"); let ed25519 = concat!( "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP", " william@amethyst" ); let rsa = concat!("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTcXpclurQpyOHZBM/cDY9EvInSYkYSGe51by/wJP0Njgi", "GZUJ3HTaPqoGWux0PKd7KJki+onLYt4IwDV1RhV/GtMML2U9v94+pA8RIK4khCxvpUxlM7Kt/svjOzzzqiZfKdV37/", "OUXmM7bwVGOvm3EerDOwmO/QdzNGfkca12aWLoz97YrleXnCoAzr3IN7j3rwmfJGDyuUtGTdmyS/QWhK9FPr8Ic3eM", "QK1JSAQqVfGhA8lLbJHmnQ/b/KMl2lzzp7SXej0wPUfvI/IP3NGb8irLzq8+JssAzXGJ+HMql+mNHiSuPaktbFzZ6y", "ikMR6Rx/psU07nAkxKZDEYpNVv william@amethyst"); let sk1 = Value::new_sshkey_str("tag", ecdsa); assert!(sk1.validate()); // to proto them let psk1 = sk1.to_proto_string_clone(); assert_eq!(psk1, "tag: oMh0SibdRGV2APapEdVojzSySx9PuhcklWny5LP0Mg4"); let sk2 = Value::new_sshkey_str("tag", ed25519); assert!(sk2.validate()); let psk2 = sk2.to_proto_string_clone(); assert_eq!(psk2, "tag: UR7mRCLLXmZNsun+F2lWO3hG3PORk/0JyjxPQxDUcdc"); let sk3 = Value::new_sshkey_str("tag", rsa); assert!(sk3.validate()); let psk3 = sk3.to_proto_string_clone(); assert_eq!(psk3, "tag: sWugDdWeE4LkmKer8hz7ERf+6VttYPIqD0ULXR3EUcU"); let sk4 = Value::new_sshkey_str("tag", "ntaouhtnhtnuehtnuhotnuhtneouhtneouh"); assert!(!sk4.validate()); let sk5 = Value::new_sshkey_str( "tag", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo", ); assert!(!sk5.validate()); } /* #[test] fn test_value_spn() { // Create an spn vale let spnv = Value::new_spn_str("claire", "example.net.au"); // create an spn pv let spnp = PartialValue::new_spn_nrs("claire", "example.net.au"); // check it's indexing output let vidx_key = spnv.generate_idx_eq_keys().pop().unwrap(); let idx_key = spnp.get_idx_eq_key(); assert!(idx_key == vidx_key); // check it can parse from name@realm let spn_parse = PartialValue::new_spn_s("claire@example.net.au").unwrap(); assert!(spn_parse == spnp); // check it can produce name@realm as str from the pv. assert!("claire@example.net.au" == spnv.to_proto_string_clone()); } */ /* #[test] fn test_value_uint32() { assert!(Value::new_uint32_str("test").is_none()); assert!(Value::new_uint32_str("18446744073709551615").is_none()); let u32v = Value::new_uint32_str("4000").unwrap(); let u32pv = PartialValue::new_uint32_str("4000").unwrap(); let idx_key = u32pv.get_idx_eq_key(); let vidx_key = u32v.generate_idx_eq_keys().pop().unwrap(); assert!(idx_key == vidx_key); } */ #[test] fn test_value_cid() { assert!(PartialValue::new_cid_s("_").is_none()); } #[test] fn test_value_iname() { /* * name MUST NOT: * - be a pure int (confusion to gid/uid/linux) * - a uuid (confuses our name mapper) * - contain an @ (confuses SPN) * - can not start with _ (... api end points have _ as a magic char) * - can not have spaces (confuses too many systems :() * - can not have = or , (confuses ldap) * - can not have ., /, \ (path injection attacks) */ let inv1 = Value::new_iname("1234"); let inv2 = Value::new_iname("bc23f637-4439-4c07-b95d-eaed0d9e4b8b"); let inv3 = Value::new_iname("hello@test.com"); let inv4 = Value::new_iname("_bad"); let inv5 = Value::new_iname("no spaces I'm sorry :("); let inv6 = Value::new_iname("bad=equals"); let inv7 = Value::new_iname("bad,comma"); let inv8 = Value::new_iname("123_456"); let inv9 = Value::new_iname("🍿"); let val1 = Value::new_iname("William"); let val2 = Value::new_iname("this_is_okay"); let val3 = Value::new_iname("a123_456"); assert!(!inv1.validate()); assert!(!inv2.validate()); assert!(!inv3.validate()); assert!(!inv4.validate()); assert!(!inv5.validate()); assert!(!inv6.validate()); assert!(!inv7.validate()); assert!(!inv8.validate()); assert!(!inv9.validate()); assert!(val1.validate()); assert!(val2.validate()); assert!(val3.validate()); } #[test] fn test_value_nsuniqueid() { // nsunique // d765e707-48e111e6-8c9ebed8-f7926cc3 // uuid // d765e707-48e1-11e6-8c9e-bed8f7926cc3 let val1 = Value::new_nsuniqueid_s("d765e707-48e111e6-8c9ebed8-f7926cc3"); let val2 = Value::new_nsuniqueid_s("D765E707-48E111E6-8C9EBED8-F7926CC3"); let inv1 = Value::new_nsuniqueid_s("d765e707-48e1-11e6-8c9e-bed8f7926cc3"); let inv2 = Value::new_nsuniqueid_s("xxxx"); assert!(inv1.is_none()); assert!(inv2.is_none()); assert!(val1.unwrap().validate()); assert!(val2.unwrap().validate()); } #[test] fn test_value_datetime() { // Datetimes must always convert to UTC, and must always be rfc3339 let val1 = Value::new_datetime_s("2020-09-25T11:22:02+10:00").expect("Must be valid"); assert!(val1.validate()); let val2 = Value::new_datetime_s("2020-09-25T01:22:02+00:00").expect("Must be valid"); assert!(val2.validate()); assert!(Value::new_datetime_s("2020-09-25T01:22:02").is_none()); assert!(Value::new_datetime_s("2020-09-25").is_none()); assert!(Value::new_datetime_s("2020-09-25T01:22:02+10").is_none()); assert!(Value::new_datetime_s("2020-09-25 01:22:02+00:00").is_none()); // Manually craft let inv1 = Value::DateTime(OffsetDateTime::now_utc().to_offset(time::UtcOffset::east_hours(10))); assert!(!inv1.validate()); let val3 = Value::DateTime(OffsetDateTime::now_utc()); assert!(val3.validate()); } #[test] fn test_value_email_address() { // https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address let val1 = Value::new_email_address_s("william@blackhats.net.au"); let val2 = Value::new_email_address_s("alice@idm.example.com"); let val3 = Value::new_email_address_s("test+mailbox@foo.com"); let inv1 = Value::new_email_address_s("william"); let inv2 = Value::new_email_address_s("test~uuid"); assert!(inv1.is_none()); assert!(inv2.is_none()); assert!(val1.unwrap().validate()); assert!(val2.unwrap().validate()); assert!(val3.unwrap().validate()); } #[test] fn test_value_url() { // https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address let val1 = Value::new_url_s("https://localhost:8000/search?q=text#hello"); let val2 = Value::new_url_s("https://github.com/kanidm/kanidm"); let val3 = Value::new_url_s("ldap://foo.com"); let inv1 = Value::new_url_s("127.0."); let inv2 = Value::new_url_s("🤔"); assert!(inv1.is_none()); assert!(inv2.is_none()); assert!(val1.is_some()); assert!(val2.is_some()); assert!(val3.is_some()); } #[test] fn test_singleline() { assert!(Value::validate_singleline("no new lines")); assert!(!Value::validate_singleline("contains a \n new line")); assert!(!Value::validate_singleline("contains a \r return feed")); assert!(!Value::validate_singleline("contains a \t tab char")); } #[test] fn test_str_escapes() { assert!(Value::validate_str_escapes("safe str")); assert!(Value::validate_str_escapes("🙃 emoji are 👍")); assert!(!Value::validate_str_escapes("naughty \x1b[31mred")); } }