Directory implementation - part 3

This commit is contained in:
Mauro D
2023-06-01 17:08:48 +00:00
parent beffa408e6
commit 93e925a635
18 changed files with 1212 additions and 139 deletions

View File

@@ -18,15 +18,15 @@ use super::dummy_tls_acceptor;
#[tokio::test]
async fn imap_directory() {
// Enable logging
tracing::subscriber::set_global_default(
/*tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::DEBUG)
.finish(),
)
.unwrap();
.unwrap();*/
// Obtain directory handle
let handle = parse_config().remove("imap").unwrap();
let handle = parse_config().directories.remove("imap").unwrap();
// Spawn mock LMTP server
let shutdown = spawn_mock_imap_server(5);

View File

@@ -0,0 +1,195 @@
use std::fmt::Debug;
use directory::{Principal, Type};
use mail_send::Credentials;
use crate::directory::parse_config;
#[tokio::test]
async fn ldap_directory() {
// Enable logging
tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::DEBUG)
.finish(),
)
.unwrap();
// Obtain directory handle
let handle = parse_config().directories.remove("ldap").unwrap();
// Test authentication
assert_eq!(
handle
.authenticate(&Credentials::Plain {
username: "john".to_string(),
secret: "12345".to_string()
})
.await
.unwrap()
.unwrap(),
Principal {
id: 2,
name: "john".to_string(),
description: "John Doe".to_string().into(),
secrets: vec!["12345".to_string()],
typ: Type::Individual,
member_of: vec!["ou=sales,ou=groups,dc=example,dc=org".to_string()],
..Default::default()
}
);
assert_eq!(
handle
.authenticate(&Credentials::Plain {
username: "bill".to_string(),
secret: "password".to_string()
})
.await
.unwrap()
.unwrap(),
Principal {
id: 4,
name: "bill".to_string(),
description: "Bill Foobar".to_string().into(),
secrets: vec![
"$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".to_string()
],
typ: Type::Individual,
quota: 500000,
..Default::default()
}
);
assert!(handle
.authenticate(&Credentials::Plain {
username: "bill".to_string(),
secret: "invalid".to_string()
})
.await
.unwrap()
.is_none());
// Get by id
assert_eq!(
handle.principal_by_id(2).await.unwrap().unwrap(),
Principal {
id: 2,
name: "john".to_string(),
description: "John Doe".to_string().into(),
typ: Type::Individual,
secrets: vec!["12345".to_string()],
member_of: vec!["ou=sales,ou=groups,dc=example,dc=org".to_string()],
..Default::default()
}
);
// Get user by name
let mut principal = handle.principal_by_name("jane").await.unwrap().unwrap();
principal.member_of.sort_unstable();
assert_eq!(
principal,
Principal {
id: 3,
name: "jane".to_string(),
description: "Jane Doe".to_string().into(),
typ: Type::Individual,
secrets: vec!["abcde".to_string()],
member_of: vec![
"ou=sales,ou=groups,dc=example,dc=org".to_string(),
"support".to_string()
],
..Default::default()
}
);
// Get group by name
assert_eq!(
handle.principal_by_name("sales").await.unwrap().unwrap(),
Principal {
id: 5,
name: "sales".to_string(),
description: "sales".to_string().into(),
typ: Type::Group,
..Default::default()
}
);
// Member of
compare_sorted(
handle
.member_of(&handle.principal_by_name("john").await.unwrap().unwrap())
.await
.unwrap(),
vec![5],
);
compare_sorted(
handle
.member_of(&handle.principal_by_name("jane").await.unwrap().unwrap())
.await
.unwrap(),
vec![5, 6],
);
// Emails by id
compare_sorted(
handle.emails_by_id(2).await.unwrap(),
vec![
"john@example.org".to_string(),
"john.doe@example.org".to_string(),
],
);
compare_sorted(
handle.emails_by_id(4).await.unwrap(),
vec!["bill@example.org".to_string()],
);
// Ids by email
compare_sorted(
handle.ids_by_email("jane@example.org").await.unwrap(),
vec![3],
);
compare_sorted(
handle.ids_by_email("info@example.org").await.unwrap(),
vec![2, 3, 4],
);
// RCPT TO
assert!(handle.rcpt("jane@example.org").await.unwrap());
assert!(handle.rcpt("info@example.org").await.unwrap());
assert!(!handle.rcpt("invalid@example.org").await.unwrap());
// VRFY
compare_sorted(
handle.vrfy("jane").await.unwrap(),
vec!["jane@example.org".to_string()],
);
compare_sorted(
handle.vrfy("john").await.unwrap(),
vec!["john@example.org".to_string()],
);
compare_sorted(handle.vrfy("info").await.unwrap(), Vec::<String>::new());
compare_sorted(handle.vrfy("invalid").await.unwrap(), Vec::<String>::new());
// EXPN
compare_sorted(
handle.expn("info@example.org").await.unwrap(),
vec![
"bill@example.org".to_string(),
"jane@example.org".to_string(),
"john@example.org".to_string(),
],
);
compare_sorted(
handle.expn("john@example.org").await.unwrap(),
Vec::<String>::new(),
);
}
fn compare_sorted<T: Eq + Debug>(v1: Vec<T>, v2: Vec<T>) {
for val in v1.iter() {
assert!(v2.contains(val), "{v1:?} != {v2:?}");
}
for val in v2.iter() {
assert!(v1.contains(val), "{v1:?} != {v2:?}");
}
}

View File

@@ -3,8 +3,7 @@ pub mod ldap;
pub mod smtp;
pub mod sql;
use ahash::AHashMap;
use directory::{config::ConfigDirectory, Directory};
use directory::{config::ConfigDirectory, DirectoryConfig};
use mail_send::Credentials;
use rustls::{Certificate, PrivateKey, ServerConfig};
use rustls_pemfile::{certs, pkcs8_private_keys};
@@ -16,15 +15,18 @@ const CONFIG: &str = r#"
protocol = "sql"
address = "sqlite::memory:"
[directory."sql".pool]
max-connections = 1
[directory."sql".query]
login = "SELECT id, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'"
name = "SELECT id, type, description, quota FROM accounts WHERE name = ?"
id = "SELECT name, type, description, quota FROM accounts WHERE id = ?"
login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'"
name = "SELECT id, name, type, description, quota FROM accounts WHERE name = ?"
id = "SELECT id, name, type, description, quota FROM accounts WHERE id = ?"
members = "SELECT gid FROM group_members WHERE uid = ?"
recipients = "SELECT id FROM emails WHERE address = ?"
emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC"
verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type != 'list' LIMIT 5"
expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' LIMIT 50"
emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC"
verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5"
expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50"
[directory."sql".columns]
name = "name"
@@ -35,35 +37,41 @@ email = "address"
quota = "quota"
type = "type"
[directory."sql".lookup]
domains = "SELECT name FROM domains WHERE name = ?"
[directory."ldap"]
protocol = "ldap"
address = "ldap://localhost:3893"
base-dn = "dc=example,dc=com"
base-dn = "dc=example,dc=org"
[directory."ldap".bind]
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=com"
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"
secret = "mysecret"
[directory."ldap".filter]
login = "(&(objectClass=posixAccount)(accountStatus=active)(cn=?))"
name = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))"
email = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(!(mail=?)(mailAliases=?)(mailLists=?)))"
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))"
email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(givenName=?)(sn=?)))"
id = "(|(&(objectClass=posixAccount)(uidNumber=?))(&(objectClass=posixGroup)(gidNumber=?)))"
verify = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(!(mail=*?*)(mailAliases=*?*)))"
expand = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(mailLists=?))"
verify = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*?*)(givenName=*?*)))"
expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(sn=?))"
[directory."ldap".object-classes]
user = "posixAccount"
group = "posixGroup"
# Glauth does not support searchable custom attributes so
# 'sn' and 'givenName' are used to search for aliases/lists.
[directory."ldap".attributes]
name = "cn"
description = "description"
description = ["principalName", "description"]
secret = "userPassword"
groups = "memberOf"
groups = ["memberOf", "otherGroups"]
id = ["uidNumber", "gidNumber"]
email = "mail"
email-alias = "mailAliases"
email-alias = "givenName"
quota = "diskQuota"
[directory."imap"]
@@ -94,9 +102,47 @@ max-connections = 5
implicit = true
allow-invalid-certs = true
[directory."local"]
protocol = "memory"
[[directory."local".users]]
name = "john"
description = "John Doe"
secret = "12345"
email = ["john@example.org", "jdoe@example.org", "john.doe@example.org"]
email-list = ["info@example.org"]
member-of = ["sales"]
[[directory."local".users]]
name = "jane"
description = "Jane Doe"
secret = "abcde"
email = "jane@example.org"
email-list = ["info@example.org"]
member-of = ["sales", "support"]
[[directory."local".users]]
name = "bill"
description = "Bill Foobar"
secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"
quota = 500000
email = "bill@example.org"
email-list = ["info@example.org"]
[[directory."local".groups]]
name = "sales"
description = "Sales Team"
[[directory."local".groups]]
name = "support"
description = "Support Team"
[directory."local".lookup]
domains = ["example.org"]
"#;
pub fn parse_config() -> AHashMap<String, Arc<dyn Directory>> {
pub fn parse_config() -> DirectoryConfig {
utils::config::Config::parse(CONFIG)
.unwrap()
.parse_directory()

View File

@@ -22,7 +22,7 @@ async fn smtp_directory() {
let shutdown = spawn_mock_lmtp_server(5);
// Obtain directory handle
let handle = parse_config().remove("smtp").unwrap();
let handle = parse_config().directories.remove("smtp").unwrap();
// Basic lookup
let tests = vec![

View File

@@ -1,12 +1,221 @@
use directory::Directory;
use directory::{Directory, Principal, Type};
use jmap_proto::types::id::Id;
use mail_send::Credentials;
use crate::directory::parse_config;
#[tokio::test]
async fn sql_directory() {
// Enable logging
/*tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::DEBUG)
.finish(),
)
.unwrap();*/
// Obtain directory handle
let handle = parse_config().directories.remove("sql").unwrap();
// Create tables
create_test_directory(handle.as_ref()).await;
// Create test users
create_test_user(handle.as_ref(), "john", "12345", "John Doe").await;
create_test_user(handle.as_ref(), "jane", "abcde", "Jane Doe").await;
create_test_user(
handle.as_ref(),
"bill",
"$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe",
"Bill Foobar",
)
.await;
set_test_quota(handle.as_ref(), "bill", 500000).await;
// Create test groups
create_test_group(handle.as_ref(), "sales", "Sales Team").await;
create_test_group(handle.as_ref(), "support", "Support Team").await;
// Link users to groups
add_to_group(handle.as_ref(), "john", "sales").await;
add_to_group(handle.as_ref(), "jane", "sales").await;
add_to_group(handle.as_ref(), "jane", "support").await;
// Add email addresses
link_test_address(handle.as_ref(), "john", "john@example.org", "primary").await;
link_test_address(handle.as_ref(), "jane", "jane@example.org", "primary").await;
link_test_address(handle.as_ref(), "bill", "bill@example.org", "primary").await;
// Add aliases and lists
link_test_address(handle.as_ref(), "john", "john.doe@example.org", "alias").await;
link_test_address(handle.as_ref(), "john", "jdoe@example.org", "alias").await;
link_test_address(handle.as_ref(), "john", "info@example.org", "list").await;
link_test_address(handle.as_ref(), "jane", "info@example.org", "list").await;
link_test_address(handle.as_ref(), "bill", "info@example.org", "list").await;
// Test authentication
assert_eq!(
handle
.authenticate(&Credentials::Plain {
username: "john".to_string(),
secret: "12345".to_string()
})
.await
.unwrap()
.unwrap(),
Principal {
id: 2,
name: "john".to_string(),
description: "John Doe".to_string().into(),
secrets: vec!["12345".to_string()],
typ: Type::Individual,
..Default::default()
}
);
assert_eq!(
handle
.authenticate(&Credentials::Plain {
username: "bill".to_string(),
secret: "password".to_string()
})
.await
.unwrap()
.unwrap(),
Principal {
id: 4,
name: "bill".to_string(),
description: "Bill Foobar".to_string().into(),
secrets: vec![
"$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".to_string()
],
typ: Type::Individual,
quota: 500000,
..Default::default()
}
);
assert!(handle
.authenticate(&Credentials::Plain {
username: "bill".to_string(),
secret: "invalid".to_string()
})
.await
.unwrap()
.is_none());
// Get by id
assert_eq!(
handle.principal_by_id(2).await.unwrap().unwrap(),
Principal {
id: 2,
name: "john".to_string(),
description: "John Doe".to_string().into(),
typ: Type::Individual,
..Default::default()
}
);
// Get user by name
assert_eq!(
handle.principal_by_name("jane").await.unwrap().unwrap(),
Principal {
id: 3,
name: "jane".to_string(),
description: "Jane Doe".to_string().into(),
typ: Type::Individual,
..Default::default()
}
);
// Get group by name
assert_eq!(
handle.principal_by_name("sales").await.unwrap().unwrap(),
Principal {
id: 5,
name: "sales".to_string(),
description: "Sales Team".to_string().into(),
typ: Type::Group,
..Default::default()
}
);
// Member of
assert_eq!(
handle
.member_of(&handle.principal_by_name("john").await.unwrap().unwrap())
.await
.unwrap(),
vec![5]
);
assert_eq!(
handle
.member_of(&handle.principal_by_name("jane").await.unwrap().unwrap())
.await
.unwrap(),
vec![5, 6]
);
// Emails by id
assert_eq!(
handle.emails_by_id(2).await.unwrap(),
vec![
"john@example.org".to_string(),
"jdoe@example.org".to_string(),
"john.doe@example.org".to_string(),
]
);
assert_eq!(
handle.emails_by_id(4).await.unwrap(),
vec!["bill@example.org".to_string(),]
);
// Ids by email
assert_eq!(
handle.ids_by_email("jane@example.org").await.unwrap(),
vec![3]
);
assert_eq!(
handle.ids_by_email("info@example.org").await.unwrap(),
vec![2, 3, 4]
);
// RCPT TO
assert!(handle.rcpt("jane@example.org").await.unwrap());
assert!(handle.rcpt("info@example.org").await.unwrap());
assert!(!handle.rcpt("invalid@example.org").await.unwrap());
// VRFY
assert_eq!(
handle.vrfy("jane").await.unwrap(),
vec!["jane@example.org".to_string()]
);
assert_eq!(
handle.vrfy("john").await.unwrap(),
vec!["john@example.org".to_string()]
);
assert_eq!(handle.vrfy("info").await.unwrap(), Vec::<String>::new());
assert_eq!(handle.vrfy("invalid").await.unwrap(), Vec::<String>::new());
// EXPN
assert_eq!(
handle.expn("info@example.org").await.unwrap(),
vec![
"bill@example.org".to_string(),
"jane@example.org".to_string(),
"john@example.org".to_string()
]
);
assert_eq!(
handle.expn("john@example.org").await.unwrap(),
Vec::<String>::new()
);
}
pub async fn create_test_directory(handle: &dyn Directory) {
// Create tables
for query in [
"CREATE TABLE accounts (name TEXT, id INTEGER PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER, active BOOLEAN DEFAULT 1)",
"CREATE TABLE accounts (name TEXT, id INTEGER PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)",
"CREATE TABLE group_members (uid INTEGER, gid INTEGER, PRIMARY KEY (uid, gid))",
"CREATE TABLE emails (id INTEGER NOT NULL, email TEXT NOT NULL, type TEXT, PRIMARY KEY (id, email))",
"CREATE TABLE emails (id INTEGER NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (id, address))",
"INSERT INTO accounts (name, secret, type) VALUES ('admin', 'secret', 'individual')",
] {
handle.query(query, &[]).await.unwrap_or_else(|_| panic!("failed for {query}"));
@@ -16,13 +225,13 @@ pub async fn create_test_directory(handle: &dyn Directory) {
pub async fn create_test_user(handle: &dyn Directory, login: &str, secret: &str, name: &str) -> Id {
handle
.query(
"INSERT OR IGNORE INTO users (name, secret, description, type, is_active) VALUES (?, ?, ?, 'individual', true)",
"INSERT OR IGNORE INTO accounts (name, secret, description, type, active) VALUES (?, ?, ?, 'individual', true)",
&[login, secret, name],
)
.await
.unwrap();
Id::from(handle.principal_by_name(login).await.unwrap().unwrap().id)
Id::from(get_principal_id(handle, login).await)
}
pub async fn create_test_user_with_email(
@@ -39,34 +248,27 @@ pub async fn create_test_user_with_email(
pub async fn create_test_group(handle: &dyn Directory, login: &str, name: &str) -> Id {
handle
.query(
"INSERT OR IGNORE INTO users (name, description, type, is_active) VALUES (?, ?, 'group', true)",
"INSERT OR IGNORE INTO accounts (name, description, type, active) VALUES (?, ?, 'group', true)",
&[login, name],
)
.await
.unwrap();
let id = handle.principal_by_name(login).await.unwrap().unwrap().id;
Id::from(get_principal_id(handle, login).await)
}
handle
.query(
&format!(
"INSERT OR IGNORE INTO emails (id, email, type) VALUES ({}, ?, 'primary')",
id
),
&[login],
)
.await
.unwrap();
Id::from(id)
pub async fn create_test_group_with_email(handle: &dyn Directory, login: &str, name: &str) -> Id {
let id = create_test_group(handle, login, name).await;
link_test_address(handle, login, login, "primary").await;
id
}
pub async fn link_test_address(handle: &dyn Directory, login: &str, address: &str, typ: &str) {
let id = handle.principal_by_name(login).await.unwrap().unwrap().id;
let id = get_principal_id(handle, login).await;
handle
.query(
&format!(
"INSERT OR IGNORE INTO emails (id, email, type) VALUES ({}, ?, ?)",
"INSERT OR IGNORE INTO emails (id, address, type) VALUES ({}, ?, ?)",
id,
),
&[address, typ],
@@ -75,7 +277,28 @@ pub async fn link_test_address(handle: &dyn Directory, login: &str, address: &st
.unwrap();
}
pub async fn add_to_group(handle: &dyn Directory, uid: u32, gid: u32) {
pub async fn set_test_quota(handle: &dyn Directory, login: &str, quota: u32) {
let id = get_principal_id(handle, login).await;
handle
.query(
&format!("UPDATE accounts SET quota = {} where id = {}", quota, id,),
&[],
)
.await
.unwrap();
}
pub async fn add_to_group(handle: &dyn Directory, login: &str, group: &str) {
let user = handle.principal_by_name(login).await.unwrap().unwrap();
let group = handle.principal_by_name(group).await.unwrap().unwrap();
let uid = user.id;
let gid = group.id;
assert_ne!(uid, gid, "{user:?} {group:?}");
assert_ne!(uid, u32::MAX, "{user:?} {group:?}");
assert_ne!(gid, u32::MAX, "{user:?} {group:?}");
handle
.query(
&format!(
@@ -99,12 +322,18 @@ pub async fn remove_from_group(handle: &dyn Directory, uid: u32, gid: u32) {
}
pub async fn remove_test_alias(handle: &dyn Directory, login: &str, alias: &str) {
let id = handle.principal_by_name(login).await.unwrap().unwrap().id;
let id = get_principal_id(handle, login).await;
handle
.query(
&format!("DELETE FROM emails WHERE id = {} AND email = ?", id),
&format!("DELETE FROM emails WHERE id = {} AND address = ?", id),
&[alias],
)
.await
.unwrap();
}
async fn get_principal_id(handle: &dyn Directory, name: &str) -> u32 {
let p = handle.principal_by_name(name).await.unwrap().unwrap();
assert_ne!(p.id, u32::MAX, "{name} {p:#?}");
p.id
}