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:
Firstyear 2021-06-12 10:01:44 +10:00 committed by GitHub
parent 7da4fa9d7e
commit ea34dc08a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 189 additions and 189 deletions

23
Cargo.lock generated
View file

@ -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"

View file

@ -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,

View file

@ -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,

View file

@ -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(),
};

View file

@ -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();
});

View file

@ -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" ]

View file

@ -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)]

View file

@ -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",

View file

@ -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);

View file

@ -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,

View file

@ -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(

View file

@ -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 => {

View file

@ -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() {