From ea34dc08a9d8369b7ab7a49b9facdc5716ceb808 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Sat, 12 Jun 2021 10:01:44 +1000 Subject: [PATCH] Add email syntax (#465) Part one of #461 - this adds the syntax to support email addresses and validation of their content, and a method to serialise to the DB that can be extended with attribute tagging in the future. Part two will address administration of these values. --- Cargo.lock | 23 ++++ kanidm_client/src/asynchronous.rs | 11 ++ kanidm_client/src/lib.rs | 9 ++ kanidm_client/tests/default_entries.rs | 3 +- kanidm_client/tests/proto_v1_test.rs | 11 ++ kanidmd/Cargo.toml | 2 + kanidmd/src/lib/be/dbvalue.rs | 6 + kanidmd/src/lib/constants/schema.rs | 5 +- kanidmd/src/lib/core/https.rs | 4 +- kanidmd/src/lib/idm/authsession.rs | 2 +- kanidmd/src/lib/schema.rs | 184 ++++--------------------- kanidmd/src/lib/server.rs | 2 + kanidmd/src/lib/value.rs | 116 ++++++++++++---- 13 files changed, 189 insertions(+), 189 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b411e5bc8..7c1c4a8e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1778,6 +1778,7 @@ dependencies = [ "url", "users", "uuid", + "validator", "webauthn-authenticator-rs", "webauthn-rs", "zxcvbn", @@ -3609,6 +3610,28 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be110dc66fa015b8b1d2c4eae40c495a27fae55f82b9cae3efb8178241ed20eb" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9680608df133af2c1ddd5eaf1ddce91d60d61b6bc51494ef326458365a470a" + [[package]] name = "value-bag" version = "1.0.0-alpha.6" diff --git a/kanidm_client/src/asynchronous.rs b/kanidm_client/src/asynchronous.rs index 62d02548f..802e41d6e 100644 --- a/kanidm_client/src/asynchronous.rs +++ b/kanidm_client/src/asynchronous.rs @@ -829,6 +829,17 @@ impl KanidmAsyncClient { .await } + pub async fn idm_account_add_attr( + &self, + id: &str, + attr: &str, + values: &[&str], + ) -> Result { + let msg: Vec<_> = values.iter().map(|v| (*v).to_string()).collect(); + self.perform_post_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str(), msg) + .await + } + pub async fn idm_account_set_attr( &self, id: &str, diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index fee745070..45f86eef4 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -571,6 +571,15 @@ impl KanidmClient { tokio_block_on(self.asclient.idm_account_purge_attr(id, attr)) } + pub fn idm_account_add_attr( + &self, + id: &str, + attr: &str, + values: &[&str], + ) -> Result { + tokio_block_on(self.asclient.idm_account_add_attr(id, attr, values)) + } + pub fn idm_account_set_attr( &self, id: &str, diff --git a/kanidm_client/tests/default_entries.rs b/kanidm_client/tests/default_entries.rs index cd6feb64c..b510ca50c 100644 --- a/kanidm_client/tests/default_entries.rs +++ b/kanidm_client/tests/default_entries.rs @@ -98,7 +98,8 @@ fn is_attr_writable(rsclient: &KanidmClient, id: &str, attr: &str) -> Option { let new_value = match entry { - "acp_receiver" => "{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000011\"]}".to_string(), + "mail" => format!("{}@example.com", id), + "acp_receiver" => r#"{"eq":["memberof","00000000-0000-0000-0000-000000000011"]}"#.to_string(), "acp_targetscope" => "{\"and\": [{\"eq\": [\"class\",\"access_control_profile\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}".to_string(), _ => id.to_string(), }; diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidm_client/tests/proto_v1_test.rs index d983ea581..0d7abfe49 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidm_client/tests/proto_v1_test.rs @@ -425,6 +425,17 @@ fn test_server_rest_account_lifecycle() { .idm_account_set_displayname("demo_account", "Demo Account") .unwrap(); + // Test adding some mail addrs + rsclient + .idm_account_add_attr("demo_account", "mail", &["demo@example.com"]) + .unwrap(); + + let r = rsclient + .idm_account_get_attr("demo_account", "mail") + .unwrap(); + + assert!(r == Some(vec!["demo@example.com".to_string()])); + // Delete the account rsclient.idm_account_delete("demo_account").unwrap(); }); diff --git a/kanidmd/Cargo.toml b/kanidmd/Cargo.toml index a73a745d6..51fdcab5d 100644 --- a/kanidmd/Cargo.toml +++ b/kanidmd/Cargo.toml @@ -86,6 +86,8 @@ users = "0.11" smartstring = { version = "0.2", features = ["serde"] } +validator = { version = "0.13" } + [features] simd_support = [ "concread/simd_support" ] # default = [ "libsqlite3-sys/bundled", "openssl/vendored" ] diff --git a/kanidmd/src/lib/be/dbvalue.rs b/kanidmd/src/lib/be/dbvalue.rs index 8ad91f088..b007425c0 100644 --- a/kanidmd/src/lib/be/dbvalue.rs +++ b/kanidmd/src/lib/be/dbvalue.rs @@ -80,6 +80,11 @@ pub struct DbValueTaggedStringV1 { pub d: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct DbValueEmailAddressV1 { + pub d: String, +} + #[derive(Serialize, Deserialize, Debug)] pub enum DbValueV1 { U8(String), @@ -99,6 +104,7 @@ pub enum DbValueV1 { CI(DbCidV1), NU(String), DT(String), + EM(DbValueEmailAddressV1), } #[cfg(test)] diff --git a/kanidmd/src/lib/constants/schema.rs b/kanidmd/src/lib/constants/schema.rs index 944941b95..42924ea5e 100644 --- a/kanidmd/src/lib/constants/schema.rs +++ b/kanidmd/src/lib/constants/schema.rs @@ -56,7 +56,7 @@ pub const JSON_SCHEMA_ATTR_MAIL: &str = r#" "mail" ], "syntax": [ - "UTF8STRING" + "EMAIL_ADDRESS" ], "uuid": [ "00000000-0000-0000-0000-ffff00000041" @@ -561,7 +561,8 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#" "ssh_publickey", "radius_secret", "account_expire", - "account_valid_from" + "account_valid_from", + "mail" ], "systemmust": [ "displayname", diff --git a/kanidmd/src/lib/core/https.rs b/kanidmd/src/lib/core/https.rs index c5c1e5c8a..5bd589ea4 100644 --- a/kanidmd/src/lib/core/https.rs +++ b/kanidmd/src/lib/core/https.rs @@ -1204,7 +1204,9 @@ pub fn create_https_server( tserver.at("/status").get(self::status); let mut well_known = tserver.at("/well-known"); - well_known.at("/openid-configuration").get(get_openid_configuration); + well_known + .at("/openid-configuration") + .get(get_openid_configuration); let mut raw_route = tserver.at("/v1/raw"); raw_route.at("/create").post(create); diff --git a/kanidmd/src/lib/idm/authsession.rs b/kanidmd/src/lib/idm/authsession.rs index bff3eed36..76ff45cfc 100644 --- a/kanidmd/src/lib/idm/authsession.rs +++ b/kanidmd/src/lib/idm/authsession.rs @@ -73,7 +73,7 @@ struct CredWebauthn { /// The current active handler for this authentication session. This is determined from what credentials /// are possible from the account, and what the user selected as the preferred authentication -/// mechanism. +/// mechanism. #[derive(Clone, Debug)] enum CredHandler { Anonymous, diff --git a/kanidmd/src/lib/schema.rs b/kanidmd/src/lib/schema.rs index d54708270..82b96a3ed 100644 --- a/kanidmd/src/lib/schema.rs +++ b/kanidmd/src/lib/schema.rs @@ -200,6 +200,7 @@ impl SchemaAttribute { SyntaxType::Cid => v.is_cid(), SyntaxType::NsUniqueId => v.is_nsuniqueid(), SyntaxType::DateTime => v.is_datetime(), + SyntaxType::EmailAddress => v.is_email_address(), }; if r { Ok(()) @@ -229,161 +230,30 @@ impl SchemaAttribute { return Err(SchemaError::InvalidAttributeSyntax(a.to_string())); }; // If syntax, check the type is correct - match self.syntax { - SyntaxType::Boolean => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_bool() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::SYNTAX_ID => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_syntax() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::Uuid => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_uuid() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - // This is the same as a UUID, refint is a plugin - SyntaxType::REFERENCE_UUID => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_refer() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::INDEX_ID => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_index() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::Utf8StringInsensitive => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_insensitive_utf8() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::Utf8StringIname => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_iname() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::UTF8STRING => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_utf8() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::JSON_FILTER => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_json_filter() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::Credential => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_credential() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::RadiusUtf8String => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_radius_string() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::SshKey => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_sshkey() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::SecurityPrincipalName => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_spn() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::UINT32 => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_uint32() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::Cid => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_cid() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::NsUniqueId => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_nsuniqueid() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), - SyntaxType::DateTime => ava.iter().fold(Ok(()), |acc, v| { - acc.and_then(|_| { - if v.is_datetime() { - Ok(()) - } else { - Err(SchemaError::InvalidAttributeSyntax(a.to_string())) - } - }) - }), + let valid = match self.syntax { + SyntaxType::Boolean => ava.iter().all(Value::is_bool), + SyntaxType::SYNTAX_ID => ava.iter().all(Value::is_syntax), + SyntaxType::Uuid => ava.iter().all(Value::is_uuid), + SyntaxType::REFERENCE_UUID => ava.iter().all(Value::is_refer), + SyntaxType::INDEX_ID => ava.iter().all(Value::is_index), + SyntaxType::Utf8StringInsensitive => ava.iter().all(Value::is_insensitive_utf8), + SyntaxType::Utf8StringIname => ava.iter().all(Value::is_iname), + SyntaxType::UTF8STRING => ava.iter().all(Value::is_utf8), + SyntaxType::JSON_FILTER => ava.iter().all(Value::is_json_filter), + SyntaxType::Credential => ava.iter().all(Value::is_credential), + SyntaxType::RadiusUtf8String => ava.iter().all(Value::is_radius_string), + SyntaxType::SshKey => ava.iter().all(Value::is_sshkey), + SyntaxType::SecurityPrincipalName => ava.iter().all(Value::is_spn), + SyntaxType::UINT32 => ava.iter().all(Value::is_uint32), + SyntaxType::Cid => ava.iter().all(Value::is_cid), + SyntaxType::NsUniqueId => ava.iter().all(Value::is_nsuniqueid), + SyntaxType::DateTime => ava.iter().all(Value::is_datetime), + SyntaxType::EmailAddress => ava.iter().all(Value::is_email_address), + }; + if valid { + Ok(()) + } else { + Err(SchemaError::InvalidAttributeSyntax(a.to_string())) } } } @@ -1206,7 +1076,7 @@ impl<'a> SchemaWriteTransaction<'a> { unique: false, phantom: true, index: vec![], - syntax: SyntaxType::UTF8STRING, + syntax: SyntaxType::EmailAddress, }, ); self.attributes.insert( @@ -1219,7 +1089,7 @@ impl<'a> SchemaWriteTransaction<'a> { unique: false, phantom: true, index: vec![], - syntax: SyntaxType::UTF8STRING, + syntax: SyntaxType::EmailAddress, }, ); self.attributes.insert( diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index 3878a1654..fca0cfd0a 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -537,6 +537,7 @@ pub trait QueryServerTransaction<'a> { SyntaxType::NsUniqueId => Ok(Value::new_nsuniqueid_s(value)), SyntaxType::DateTime => Value::new_datetime_s(value) .ok_or_else(|| OperationError::InvalidAttribute("Invalid DateTime (rfc3339) syntax".to_string())), + SyntaxType::EmailAddress => Ok(Value::new_email_address_s(value)), } } None => { @@ -629,6 +630,7 @@ pub trait QueryServerTransaction<'a> { "Invalid DateTime (rfc3339) syntax".to_string(), ) }), + SyntaxType::EmailAddress => Ok(PartialValue::new_email_address_s(value)), } } None => { diff --git a/kanidmd/src/lib/value.rs b/kanidmd/src/lib/value.rs index ecdfe7ef6..a1e59838f 100644 --- a/kanidmd/src/lib/value.rs +++ b/kanidmd/src/lib/value.rs @@ -3,7 +3,9 @@ //! 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`]. -use crate::be::dbvalue::{DbCidV1, DbValueCredV1, DbValueTaggedStringV1, DbValueV1}; +use crate::be::dbvalue::{ + DbCidV1, DbValueCredV1, DbValueEmailAddressV1, DbValueTaggedStringV1, DbValueV1, +}; use crate::credential::Credential; use crate::repl::cid::Cid; use kanidm_proto::v1::Filter as ProtoFilter; @@ -133,6 +135,7 @@ pub enum SyntaxType { Cid, NsUniqueId, DateTime, + EmailAddress, } impl TryFrom<&str> for SyntaxType { @@ -158,6 +161,7 @@ impl TryFrom<&str> for SyntaxType { "CID" => Ok(SyntaxType::Cid), "NSUNIQUEID" => Ok(SyntaxType::NsUniqueId), "DATETIME" => Ok(SyntaxType::DateTime), + "EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress), _ => Err(()), } } @@ -185,6 +189,7 @@ impl TryFrom for SyntaxType { 14 => Ok(SyntaxType::Utf8StringIname), 15 => Ok(SyntaxType::NsUniqueId), 16 => Ok(SyntaxType::DateTime), + 17 => Ok(SyntaxType::EmailAddress), _ => Err(()), } } @@ -210,35 +215,33 @@ impl SyntaxType { SyntaxType::Utf8StringIname => 14, SyntaxType::NsUniqueId => 15, SyntaxType::DateTime => 16, + SyntaxType::EmailAddress => 17, } } } impl fmt::Display for SyntaxType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - match self { - SyntaxType::UTF8STRING => "UTF8STRING", - SyntaxType::Utf8StringInsensitive => "UTF8STRING_INSENSITIVE", - SyntaxType::Utf8StringIname => "UTF8STRING_INAME", - SyntaxType::Uuid => "UUID", - SyntaxType::Boolean => "BOOLEAN", - SyntaxType::SYNTAX_ID => "SYNTAX_ID", - SyntaxType::INDEX_ID => "INDEX_ID", - SyntaxType::REFERENCE_UUID => "REFERENCE_UUID", - SyntaxType::JSON_FILTER => "JSON_FILTER", - SyntaxType::Credential => "CREDENTIAL", - SyntaxType::RadiusUtf8String => "RADIUS_UTF8STRING", - SyntaxType::SshKey => "SSHKEY", - SyntaxType::SecurityPrincipalName => "SECURITY_PRINCIPAL_NAME", - SyntaxType::UINT32 => "UINT32", - SyntaxType::Cid => "CID", - SyntaxType::NsUniqueId => "NSUNIQUEID", - SyntaxType::DateTime => "DATETIME", - } - ) + f.write_str(match self { + SyntaxType::UTF8STRING => "UTF8STRING", + SyntaxType::Utf8StringInsensitive => "UTF8STRING_INSENSITIVE", + SyntaxType::Utf8StringIname => "UTF8STRING_INAME", + SyntaxType::Uuid => "UUID", + SyntaxType::Boolean => "BOOLEAN", + SyntaxType::SYNTAX_ID => "SYNTAX_ID", + SyntaxType::INDEX_ID => "INDEX_ID", + SyntaxType::REFERENCE_UUID => "REFERENCE_UUID", + SyntaxType::JSON_FILTER => "JSON_FILTER", + SyntaxType::Credential => "CREDENTIAL", + SyntaxType::RadiusUtf8String => "RADIUS_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", + }) } } @@ -259,6 +262,12 @@ impl std::fmt::Debug for DataValue { } } +/// 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), @@ -281,6 +290,7 @@ pub enum PartialValue { Cid(Cid), Nsuniqueid(String), DateTime(OffsetDateTime), + EmailAddress(String), } impl PartialValue { @@ -510,6 +520,14 @@ impl PartialValue { 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 to_str(&self) -> Option<&str> { match self { PartialValue::Utf8(s) => Some(s.as_str()), @@ -541,7 +559,8 @@ impl PartialValue { PartialValue::Utf8(s) | PartialValue::Iutf8(s) | PartialValue::Iname(s) - | PartialValue::Nsuniqueid(s) => s.clone(), + | PartialValue::Nsuniqueid(s) + | PartialValue::EmailAddress(s) => s.clone(), PartialValue::Refer(u) | PartialValue::Uuid(u) => u.to_hyphenated_ref().to_string(), PartialValue::Bool(b) => b.to_string(), PartialValue::Syntax(syn) => syn.to_string(), @@ -571,6 +590,12 @@ impl PartialValue { } } +/// 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 struct Value { pv: PartialValue, @@ -1006,6 +1031,17 @@ impl Value { self.pv.is_datetime() } + pub fn new_email_address_s(s: &str) -> Self { + Value { + pv: PartialValue::new_email_address_s(s), + data: None, + } + } + + pub fn is_email_address(&self) -> bool { + self.pv.is_email_address() + } + pub fn contains(&self, s: &PartialValue) -> bool { self.pv.contains(s) } @@ -1101,6 +1137,10 @@ impl Value { DbValueV1::DT(s) => PartialValue::new_datetime_s(&s) .ok_or(()) .map(|pv| Value { pv, data: None }), + DbValueV1::EM(DbValueEmailAddressV1 { d }) => Ok(Value { + pv: PartialValue::EmailAddress(d), + data: None, + }), } } @@ -1172,6 +1212,9 @@ impl Value { debug_assert!(odt.offset() == time::UtcOffset::UTC); DbValueV1::DT(odt.format(time::Format::Rfc3339)) } + PartialValue::EmailAddress(mail) => { + DbValueV1::EM(DbValueEmailAddressV1 { d: mail.clone() }) + } } } @@ -1258,7 +1301,8 @@ impl Value { PartialValue::Utf8(s) | PartialValue::Iutf8(s) | PartialValue::Iname(s) - | PartialValue::Nsuniqueid(s) => s.clone(), + | PartialValue::Nsuniqueid(s) + | PartialValue::EmailAddress(s) => s.clone(), PartialValue::Uuid(u) => u.to_hyphenated_ref().to_string(), PartialValue::Bool(b) => b.to_string(), PartialValue::Syntax(syn) => syn.to_string(), @@ -1339,6 +1383,7 @@ impl Value { }, PartialValue::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s), PartialValue::DateTime(odt) => odt.offset() == time::UtcOffset::UTC, + PartialValue::EmailAddress(mail) => validator::validate_email(mail.as_str()), _ => true, } } @@ -1349,7 +1394,8 @@ impl Value { PartialValue::Utf8(s) | PartialValue::Iutf8(s) | PartialValue::Iname(s) - | PartialValue::Nsuniqueid(s) => vec![s.clone()], + | PartialValue::Nsuniqueid(s) + | PartialValue::EmailAddress(s) => vec![s.clone()], PartialValue::Refer(u) | PartialValue::Uuid(u) => { vec![u.to_hyphenated_ref().to_string()] } @@ -1590,6 +1636,22 @@ mod tests { 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.validate()); + assert!(!inv2.validate()); + assert!(val1.validate()); + assert!(val2.validate()); + assert!(val3.validate()); + } + /* #[test] fn test_schema_syntax_json_filter() {