mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
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.
This commit is contained in:
parent
7da4fa9d7e
commit
ea34dc08a9
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -829,6 +829,17 @@ impl KanidmAsyncClient {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_add_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
attr: &str,
|
||||
values: &[&str],
|
||||
) -> Result<bool, ClientError> {
|
||||
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,
|
||||
|
|
|
@ -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<bool, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_add_attr(id, attr, values))
|
||||
}
|
||||
|
||||
pub fn idm_account_set_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
|
|
|
@ -98,7 +98,8 @@ fn is_attr_writable(rsclient: &KanidmClient, id: &str, attr: &str) -> Option<boo
|
|||
),
|
||||
entry => {
|
||||
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(),
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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" ]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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<usize> 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() {
|
||||
|
|
Loading…
Reference in a new issue