Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{http::HttpRequest, server::TestServerBuilder};
use registry::{
schema::structs::{Directory, OidcDirectory},
types::map::Map,
};
const EXTERNAL_ENDPOINT: &str =
"http://localhost:9080/realms/stalwart/protocol/openid-connect/auth";
const INTERNAL_ENDPOINT: &str = "https://127.0.0.1:8899/login";
pub async fn test() {
println!("Running OIDC account discovery tests...");
crate::utils::containers::ensure_keycloak().await;
let test = TestServerBuilder::new("directory_discovery_test")
.await
.with_default_listeners()
.await
.disable_services()
.with_object(Directory::Oidc(oidc_test_directory()))
.await
.build()
.await;
assert!(
test.server
.get_default_directory()
.and_then(|directory| directory.oidc_discovery_document())
.is_some_and(|discovery| discovery.document.authorization_endpoint
== EXTERNAL_ENDPOINT),
"The default directory is not the test OpenID Connect provider"
);
let http = HttpRequest::new();
for (account_name, expected) in [
("[email protected]", EXTERNAL_ENDPOINT),
("[email protected]", EXTERNAL_ENDPOINT),
(
"[email protected]%[email protected]",
EXTERNAL_ENDPOINT,
),
("admin", INTERNAL_ENDPOINT),
("[email protected]%admin", INTERNAL_ENDPOINT),
("[email protected]%Admin", INTERNAL_ENDPOINT),
("john.doe@", INTERNAL_ENDPOINT),
] {
assert_eq!(
authorization_endpoint(&http, account_name).await,
expected,
"Unexpected discovery document for {account_name:?}"
);
}
}
async fn authorization_endpoint(http: &HttpRequest, account_name: &str) -> String {
http.get::<serde_json::Value>(&format!(
"/api/discover/{}",
account_name.replace('%', "%25")
))
.await
.unwrap()
.get("authorization_endpoint")
.and_then(|endpoint| endpoint.as_str())
.unwrap_or_else(|| panic!("No authorization endpoint returned for {account_name:?}"))
.to_string()
}
fn oidc_test_directory() -> OidcDirectory {
OidcDirectory {
description: "Test OIDC directory".to_string(),
issuer_url: "http://localhost:9080/realms/stalwart".to_string(),
claim_username: "preferred_username".to_string(),
claim_name: Some("name".to_string()),
claim_groups: Some("groups".to_string()),
require_audience: Some("stalwart".to_string()),
require_scopes: Map::new(vec![
"email".to_string(),
"profile".to_string(),
"openid".to_string(),
]),
..Default::default()
}
}
+232
View File
@@ -0,0 +1,232 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
directory::ldap::ldap_test_directory,
utils::{server::TestServerBuilder, smtp::SmtpConnection},
};
use ahash::AHashMap;
use common::auth::{AuthRequest, RECOVERY_ADMIN_ID};
use email::cache::MessageCacheFetch;
use registry::schema::structs::{Account, AccountSettings, Directory};
use std::net::IpAddr;
use types::id::Id;
pub async fn test() {
println!("Running directory integration tests...");
crate::utils::containers::ensure_openldap().await;
let test = TestServerBuilder::new("directory_integration_test")
.await
.with_default_listeners()
.await
.with_object(Directory::Ldap(ldap_test_directory()))
.await
.build()
.await;
let admin = test.account("admin");
admin.mta_no_auth().await;
admin.mta_disable_spam_filter().await;
admin.reload_settings().await;
// Test account creation by login
let account = crate::utils::account::Account::new(
"[email protected]",
"this is John's LDAP password",
&[],
"",
Id::from(u32::MAX),
);
assert_eq!(
account
.registry_get::<AccountSettings>(Id::singleton())
.await
.description
.as_deref(),
Some("John Doe")
);
// Test account creation by rcpt
let mut lmtp = SmtpConnection::connect().await;
for rcpt in [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
] {
lmtp.ingest(
"[email protected]",
&[rcpt],
&TEST_EMAIL.replace("$RCPT", rcpt),
)
.await;
}
// Fetch all accounts
let mut accounts = admin
.registry_get_all::<Account>()
.await
.into_iter()
.map(|(id, account)| {
(
match &account {
Account::User(user_account) => user_account.name.clone(),
Account::Group(group_account) => group_account.name.clone(),
},
(account, id),
)
})
.collect::<AHashMap<_, _>>();
assert_eq!(accounts.len(), 5, "Got: {accounts:#?}");
// Validate accounts
for (name, description, secret, groups, aliases) in [
(
"john.doe",
"John Doe",
"$app$8958830913002348890$",
&["sales"][..],
&["john"][..],
),
(
"jane.smith",
"Jane Smith",
"$app$4096614298472586996$",
&["sales", "corporate"][..],
&[][..],
),
(
"bill.foobar",
"Bill Foobar",
"",
&["corporate"][..],
&["bill"][..],
),
] {
let (account, id) = accounts
.remove(name)
.map(|(account, id)| (account.into_user().unwrap(), id))
.unwrap();
assert_eq!(account.description.as_deref(), Some(description));
if !secret.is_empty() {
assert_eq!(
test.server
.registry()
.object::<Account>(id)
.await
.unwrap()
.unwrap()
.into_user()
.unwrap()
.credentials
.values()
.next()
.and_then(|v| v.as_main_credential())
.map(|v| v.secret.as_str()),
Some(secret)
);
}
for group in groups {
let id = accounts.get(*group).unwrap().1;
assert!(
account
.member_group_ids
.iter()
.any(|group_id| group_id == &id),
"Account {name} is not a member of group {group}"
);
}
for alias in aliases {
assert!(
account
.aliases
.iter()
.any(|account_alias| account_alias.name == *alias),
"Account {name} does not have alias {alias}"
);
}
assert_eq!(
test.server
.get_cached_messages(id.document_id())
.await
.unwrap()
.emails
.index
.len(),
1
);
}
// Validate groups
for (name, description, aliases) in [
("sales", "sales", &[][..]),
("corporate", "corporate", &["everyone"][..]),
] {
let (account, id) = accounts
.remove(name)
.map(|(account, id)| (account.into_group().unwrap(), id))
.unwrap();
assert_eq!(account.description.as_deref(), Some(description));
for alias in aliases {
assert!(
account
.aliases
.iter()
.any(|account_alias| account_alias.name == *alias),
"Group {name} does not have alias {alias}"
);
}
assert_eq!(
test.server
.get_cached_messages(id.document_id())
.await
.unwrap()
.emails
.index
.len(),
1
);
}
// Test recovery admin impersonation of an account that has not logged in before
assert!(
test.server
.account_id_from_email("[email protected]", false)
.await
.unwrap()
.is_none(),
"Account [email protected] exists before impersonation"
);
let access_token = test
.server
.authenticate(&AuthRequest::from_plain(
"[email protected]%admin",
admin.secret(),
0,
IpAddr::from([127, 0, 0, 1]),
))
.await
.unwrap_or_else(|err| panic!("Failed to impersonate [email protected]: {err:?}"));
assert_ne!(access_token.account_id(), RECOVERY_ADMIN_ID);
assert_eq!(
test.server
.registry()
.object::<Account>(access_token.account_id().into())
.await
.unwrap()
.and_then(|account| account.into_user())
.map(|account| account.name),
Some("multi.mail".to_string())
);
}
const TEST_EMAIL: &str = r#"From: [email protected]
To: $RCPT
Subject: TPS Report for $RCPT
I'm going to need those TPS reports ASAP. So, if you could do that, that'd be great.
"#;
+243
View File
@@ -0,0 +1,243 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use directory::{Account, Credentials, Group, Recipient, backend::ldap::LdapDirectory};
use registry::{
schema::structs::{self, SecretKeyOptional, SecretKeyValue},
types::map::Map,
};
pub async fn test() {
println!("Running LDAP directory tests...");
crate::utils::containers::ensure_openldap().await;
let mut config = ldap_test_directory();
// Test bind authentication
let ldap = LdapDirectory::open(config.clone()).await.unwrap();
assert_eq!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "this is John's LDAP password".into(),
mfa_token: None,
})
.await
.unwrap(),
Account {
email: "[email protected]".into(),
email_aliases: vec!["[email protected]".into()],
secret: Some("$app$8958830913002348890$".into()),
groups: Some(vec!["[email protected]".into()]),
description: Some("John Doe".into()),
}
);
assert_eq!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "this is Jane's LDAP password".into(),
mfa_token: None,
})
.await
.unwrap(),
Account {
email: "[email protected]".into(),
email_aliases: vec![],
secret: Some("$app$4096614298472586996$".into()),
groups: Some(vec![
"[email protected]".into(),
"[email protected]".into()
]),
description: Some("Jane Smith".into()),
}
);
assert!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "this is a wrong LDAP password".into(),
mfa_token: None,
})
.await
.is_err()
);
assert!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "".into(),
mfa_token: None,
})
.await
.is_err(),
"Empty password accepted during bind authentication"
);
// Test direct authentication (without bind)
config.attr_secret = Map::new(vec!["userPassword".to_string()]);
config.attr_secret_changed = Map::new(vec![]);
config.bind_authentication = false;
let ldap = LdapDirectory::open(config.clone()).await.unwrap();
assert_eq!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "this is John's LDAP password".into(),
mfa_token: None,
})
.await
.unwrap(),
Account {
email: "[email protected]".into(),
email_aliases: vec!["[email protected]".into()],
secret: Some("this is John's LDAP password".into()),
groups: Some(vec!["[email protected]".into()]),
description: Some("John Doe".into()),
}
);
assert!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "this is a wrong LDAP password".into(),
mfa_token: None,
})
.await
.is_err()
);
assert!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "".into(),
mfa_token: None,
})
.await
.is_err(),
"Empty password accepted during direct authentication"
);
// Test recipient lookup
assert_eq!(
ldap.recipient("[email protected]").await.unwrap(),
Recipient::Account(Account {
email: "[email protected]".into(),
email_aliases: vec!["[email protected]".into()],
secret: Some("this is John's LDAP password".into()),
groups: Some(vec!["[email protected]".into()]),
description: Some("John Doe".into())
})
);
assert_eq!(
ldap.recipient("[email protected]").await.unwrap(),
Recipient::Account(Account {
email: "[email protected]".into(),
email_aliases: vec![],
secret: Some("this is Jane's LDAP password".into()),
groups: Some(vec![
"[email protected]".into(),
"[email protected]".into()
]),
description: Some("Jane Smith".into())
})
);
assert_eq!(
ldap.recipient("[email protected]").await.unwrap(),
Recipient::Group(Group {
email: "[email protected]".into(),
email_aliases: vec![],
description: Some("sales".into())
})
);
assert_eq!(
ldap.recipient("[email protected]").await.unwrap(),
Recipient::Group(Group {
email: "[email protected]".into(),
email_aliases: vec!["[email protected]".into()],
description: Some("corporate".into())
})
);
assert_eq!(
ldap.recipient("[email protected]").await.unwrap(),
Recipient::Invalid
);
const MULTI_MAIL: &[&str] = &[
"[email protected]",
"[email protected]",
"[email protected]",
];
let mut config = ldap_test_directory();
config.attr_secret = Map::new(vec!["userPassword".to_string()]);
config.attr_secret_changed = Map::new(vec![]);
config.bind_authentication = false;
let ldap_dedicated_attr = LdapDirectory::open(config.clone()).await.unwrap();
config.attr_email_alias = Map::new(vec!["mail".to_string()]);
let ldap_overloaded_attr = LdapDirectory::open(config).await.unwrap();
for address in MULTI_MAIL {
let Recipient::Account(account) = ldap_dedicated_attr.recipient(address).await.unwrap()
else {
panic!("Expected an account for {address}");
};
assert!(
MULTI_MAIL.contains(&account.email.as_str()),
"Unexpected primary address {:?}",
account.email
);
assert!(
account.email_aliases.is_empty(),
"Expected no aliases, got {:?}",
account.email_aliases
);
let Recipient::Account(account) = ldap_overloaded_attr.recipient(address).await.unwrap()
else {
panic!("Expected an account for {address}");
};
assert_eq!(sorted_addresses(&account), MULTI_MAIL);
}
let account = ldap_overloaded_attr
.authenticate(&Credentials::Basic {
username: "[email protected]".into(),
secret: "this is Multi's LDAP password".into(),
mfa_token: None,
})
.await
.unwrap();
assert_eq!(sorted_addresses(&account), MULTI_MAIL);
}
fn sorted_addresses(account: &Account) -> Vec<String> {
let mut addresses = account.email_aliases.clone();
addresses.push(account.email.clone());
addresses.sort_unstable();
addresses
}
pub fn ldap_test_directory() -> structs::LdapDirectory {
structs::LdapDirectory {
url: "ldap://localhost".into(),
use_tls: false,
attr_class: Map::new(vec!["objectClass".to_string()]),
attr_description: Map::new(vec!["cn".to_string()]),
attr_email: Map::new(vec!["mail".to_string()]),
attr_email_alias: Map::new(vec!["mailAlias".to_string()]),
attr_member_of: Map::new(vec!["memberOf".to_string()]),
attr_secret: Map::new(vec![]),
attr_secret_changed: Map::new(vec!["shadowLastChange".to_string()]),
base_dn: "dc=stalwart,dc=test".into(),
bind_dn: "cn=admin,dc=stalwart,dc=test".to_string().into(),
bind_secret: SecretKeyOptional::Value(SecretKeyValue {
secret: "admin".into(),
}),
filter_member_of: "(&(objectClass=groupOfNames)(member=?))".to_string().into(),
filter_login: "(&(objectClass=inetOrgPerson)(mail=?))".into(),
filter_mailbox: concat!(
"(|(&(objectClass=inetOrgPerson)(|(mail=?)(mailAlias=?)))",
"(&(objectClass=groupOfNames)(|(mail=?)(mailAlias=?))))"
)
.into(),
group_class: "groupOfNames".into(),
bind_authentication: true,
description: "Test LDAP directory".into(),
..Default::default()
}
}
+25
View File
@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod discovery;
pub mod integration;
pub mod ldap;
#[cfg(feature = "sqlite")]
pub mod sql;
pub mod synchronization;
pub mod unavailable;
#[tokio::test(flavor = "multi_thread")]
pub async fn directory_tests() {
ldap::test().await;
oidc::test().await;
unavailable::test().await;
discovery::test().await;
#[cfg(feature = "sqlite")]
sql::test().await;
synchronization::test().await;
integration::test().await;
}
+204
View File
@@ -0,0 +1,204 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use directory::{Account, Credentials, Group, Recipient, backend::sql::SqlDirectory};
use registry::schema::structs::{self, SqlAuthStore};
use store::{Store, backend::sqlite::SqliteStore};
pub async fn test() {
println!("Running SQL directory tests...");
let sql_store = Store::SQLite(SqliteStore::open_memory().unwrap().into());
// Create test directory
for query in [
concat!(
"CREATE TABLE accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT,",
" type TEXT NOT NULL, active BOOLEAN DEFAULT TRUE)"
),
concat!(
"CREATE TABLE group_members (name TEXT NOT NULL, member_of ",
"TEXT NOT NULL, PRIMARY KEY (name, member_of))"
),
concat!(
"CREATE TABLE emails (name TEXT NOT NULL, address TEXT NOT",
" NULL, PRIMARY KEY (name, address))"
),
concat!(
"INSERT INTO accounts (name, secret, description, type) ",
"VALUES ('[email protected]', 'john secret', 'John Doe', 'individual')"
),
concat!(
"INSERT INTO accounts (name, secret, description, type) ",
"VALUES ('[email protected]', 'jane secret', 'Jane Doe', 'individual')"
),
concat!(
"INSERT INTO accounts (name, secret, description, type) ",
"VALUES ('[email protected]', 'bob secret', '', 'individual')"
),
concat!(
"INSERT INTO accounts (name, secret, description, type) ",
"VALUES ('[email protected]', '', '', 'individual')"
),
concat!(
"INSERT INTO accounts (name, secret, description, type) ",
"VALUES ('[email protected]', NULL, 'Sales Team', 'group')"
),
concat!(
"INSERT INTO accounts (name, secret, description, type) ",
"VALUES ('[email protected]', NULL, '', 'group')"
),
concat!(
"INSERT INTO group_members (name, member_of) VALUES ",
"('[email protected]', '[email protected]')"
),
concat!(
"INSERT INTO group_members (name, member_of) VALUES ",
"('[email protected]', '[email protected]')"
),
concat!(
"INSERT INTO emails (name, address) VALUES ",
"('[email protected]', '[email protected]')"
),
] {
sql_store
.sql_query::<usize>(query, vec![])
.await
.unwrap_or_else(|_| panic!("failed for {query}"));
}
let config = structs::SqlDirectory {
description: "Test SQL directory".to_string(),
query_login: concat!(
"SELECT name, secret, description, type FROM accounts ",
"WHERE name = $1 AND active = true"
)
.into(),
query_recipient: concat!(
"SELECT name, secret, description, type FROM accounts ",
"WHERE name = $1 AND active = true"
)
.into(),
query_email_aliases: concat!("SELECT address FROM emails ", "WHERE name = $1")
.to_string()
.into(),
query_member_of: concat!("SELECT member_of FROM group_members ", "WHERE name = $1")
.to_string()
.into(),
column_class: "type".to_string().into(),
column_description: "description".to_string().into(),
column_email: "name".into(),
column_secret: "secret".into(),
store: SqlAuthStore::Default,
member_tenant_id: None,
};
// Test authentication
let sql = SqlDirectory::open(config, &sql_store).await.unwrap();
assert_eq!(
sql.authenticate(&Credentials::Basic {
username: "[email protected]".to_string(),
secret: "john secret".to_string(),
mfa_token: None,
})
.await
.unwrap(),
Account {
email: "[email protected]".to_string(),
email_aliases: vec!["[email protected]".to_string(),],
secret: Some("john secret".to_string()),
groups: Some(vec!["[email protected]".to_string()]),
description: Some("John Doe".to_string()),
}
);
assert!(
sql.authenticate(&Credentials::Basic {
username: "[email protected]".to_string(),
secret: "wrong secret".to_string(),
mfa_token: None,
})
.await
.is_err()
);
// Empty columns are treated as missing values
assert_eq!(
sql.authenticate(&Credentials::Basic {
username: "[email protected]".to_string(),
secret: "bob secret".to_string(),
mfa_token: None,
})
.await
.unwrap(),
Account {
email: "[email protected]".to_string(),
email_aliases: vec![],
secret: Some("bob secret".to_string()),
groups: Some(vec![]),
description: None,
}
);
assert!(
sql.authenticate(&Credentials::Basic {
username: "[email protected]".to_string(),
secret: "".to_string(),
mfa_token: None,
})
.await
.is_err()
);
assert_eq!(
sql.recipient("[email protected]").await.unwrap(),
Recipient::Account(Account {
email: "[email protected]".to_string(),
email_aliases: vec![],
secret: None,
groups: Some(vec![]),
description: None,
})
);
assert_eq!(
sql.recipient("[email protected]").await.unwrap(),
Recipient::Group(Group {
email: "[email protected]".to_string(),
email_aliases: vec![],
description: None
})
);
// Test recipient lookup
assert_eq!(
sql.recipient("[email protected]").await.unwrap(),
Recipient::Account(Account {
email: "[email protected]".to_string(),
email_aliases: vec!["[email protected]".to_string()],
secret: Some("john secret".to_string()),
groups: Some(vec!["[email protected]".to_string()]),
description: Some("John Doe".to_string()),
})
);
assert_eq!(
sql.recipient("[email protected]").await.unwrap(),
Recipient::Account(Account {
email: "[email protected]".to_string(),
email_aliases: vec![],
secret: Some("jane secret".to_string()),
groups: Some(vec!["[email protected]".to_string()]),
description: Some("Jane Doe".to_string()),
})
);
assert_eq!(
sql.recipient("[email protected]").await.unwrap(),
Recipient::Group(Group {
email: "[email protected]".to_string(),
email_aliases: vec![],
description: Some("Sales Team".to_string())
})
);
assert_eq!(
sql.recipient("[email protected]").await.unwrap(),
Recipient::Invalid
);
}
+305
View File
@@ -0,0 +1,305 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServerBuilder;
use registry::schema::{
prelude::ObjectType,
structs::{Account, Domain, EmailAlias},
};
use types::id::Id;
pub async fn test() {
println!("Running directory synchronization tests...");
let test = TestServerBuilder::new("directory_synchronization_test")
.await
.with_default_listeners()
.await
.disable_services()
.build()
.await;
let admin = test.account("admin");
// Synchronizing an account with an unknown domain should fail
assert!(
test.server
.synchronize_account(directory::Account {
email: "[email protected]".to_string(),
email_aliases: vec![],
secret: "supersecret".to_string().into(),
groups: Some(vec![]),
description: "John Doe".to_string().into(),
})
.await
.is_err()
);
// Initial account synchronization
let mut account_in = directory::Account {
email: "[email protected]".to_string(),
email_aliases: vec![
"[email protected]".to_string(),
"[email protected]".to_string(),
],
secret: "supersecret".to_string().into(),
groups: Some(vec![
"[email protected]".to_string(),
"[email protected]".to_string(),
]),
description: "John Doe".to_string().into(),
};
let result = test
.server
.synchronize_account(account_in.clone())
.await
.unwrap();
let account_id = Id::from(result.id);
let account_out = test
.server
.registry()
.object::<Account>(account_id)
.await
.unwrap()
.unwrap()
.into_user()
.unwrap();
let domain_id = account_out.domain_id;
assert_eq!(
admin.registry_get::<Domain>(domain_id).await.name,
"example.org"
);
assert_eq!(account_out.name, "john");
assert_eq!(account_out.description.as_deref(), Some("John Doe"));
assert_eq!(
account_out
.credentials
.values()
.next()
.and_then(|v| v.as_main_credential())
.map(|c| c.secret.as_str()),
Some("supersecret")
);
assert_eq!(account_out.aliases.len(), 2);
let aliases = account_out.aliases.iter().collect::<Vec<_>>();
assert_eq!(
aliases[0],
&EmailAlias {
description: None,
domain_id,
enabled: true,
name: "john.doe".to_string(),
}
);
assert_eq!(
aliases[1],
&EmailAlias {
description: None,
domain_id,
enabled: true,
name: "j.doe".to_string(),
}
);
assert_eq!(account_out.member_group_ids.len(), 2);
for (idx, group_id) in account_out.member_group_ids.iter().enumerate() {
let group = admin
.registry_get::<Account>(*group_id)
.await
.into_group()
.unwrap();
assert_eq!(group.name, if idx == 0 { "corporate" } else { "sales" });
assert_eq!(group.domain_id, domain_id);
}
assert_eq!(
test.server
.registry()
.count_object(ObjectType::Account)
.await
.unwrap(),
3
);
assert_eq!(
test.server
.registry()
.count_object(ObjectType::Domain)
.await
.unwrap(),
1
);
// No changes should not cause any updates
assert_eq!(
test.server
.synchronize_account(account_in.clone())
.await
.unwrap()
.id,
account_id.document_id()
);
assert_eq!(
test.server
.registry()
.object::<Account>(account_id)
.await
.unwrap()
.unwrap()
.into_user()
.unwrap(),
account_out
);
assert_eq!(
test.server
.registry()
.count_object(ObjectType::Account)
.await
.unwrap(),
3
);
// Make some changes and synchronize again
account_in.description = "Johnathan Doe".to_string().into();
account_in
.email_aliases
.push("[email protected]".to_string());
let groups = account_in.groups.get_or_insert_default();
groups.pop();
groups.push("[email protected]".to_string());
account_in.secret = "evenmoresecret".to_string().into();
assert_eq!(
test.server
.synchronize_account(account_in.clone())
.await
.unwrap()
.id,
account_id.document_id()
);
let account_out = test
.server
.registry()
.object::<Account>(account_id)
.await
.unwrap()
.unwrap()
.into_user()
.unwrap();
assert_eq!(
account_out
.credentials
.values()
.next()
.and_then(|v| v.as_main_credential())
.map(|c| c.secret.as_str()),
Some("evenmoresecret")
);
assert_eq!(account_out.description.as_deref(), Some("Johnathan Doe"));
assert_eq!(account_out.aliases.len(), 3);
let aliases = account_out.aliases.iter().collect::<Vec<_>>();
assert_eq!(
aliases[2],
&EmailAlias {
description: None,
domain_id,
enabled: true,
name: "johnny".to_string(),
}
);
assert_eq!(account_out.member_group_ids.len(), 2);
let account_groups = account_out
.member_group_ids
.iter()
.copied()
.collect::<Vec<_>>();
for (idx, group_id) in account_groups.iter().enumerate() {
let group = admin
.registry_get::<Account>(*group_id)
.await
.into_group()
.unwrap();
assert_eq!(group.name, if idx == 0 { "corporate" } else { "support" });
assert_eq!(group.domain_id, domain_id);
}
assert_eq!(
test.server
.registry()
.count_object(ObjectType::Account)
.await
.unwrap(),
4
);
account_in.groups = None;
test.server
.synchronize_account(account_in.clone())
.await
.unwrap();
let account_out = test
.server
.registry()
.object::<Account>(account_id)
.await
.unwrap()
.unwrap()
.into_user()
.unwrap();
assert_eq!(account_out.member_group_ids.len(), 2);
account_in.groups = Some(vec![]);
test.server
.synchronize_account(account_in.clone())
.await
.unwrap();
let account_out = test
.server
.registry()
.object::<Account>(account_id)
.await
.unwrap()
.unwrap()
.into_user()
.unwrap();
assert_eq!(account_out.member_group_ids.len(), 0);
// Synchronize a group
assert_eq!(
test.server
.synchronize_group(directory::Group {
email: "[email protected]".to_string(),
email_aliases: vec!["[email protected]".to_string()],
description: "Corporate Group".to_string().into(),
})
.await
.unwrap(),
account_groups[0].document_id()
);
let group_out = test
.server
.registry()
.object::<Account>(account_groups[0])
.await
.unwrap()
.unwrap()
.into_group()
.unwrap();
assert_eq!(group_out.name, "corporate");
assert_eq!(group_out.description.as_deref(), Some("Corporate Group"));
assert_eq!(group_out.aliases.len(), 1);
let aliases = group_out.aliases.iter().collect::<Vec<_>>();
assert_eq!(
aliases[0],
&EmailAlias {
description: None,
domain_id,
enabled: true,
name: "everyone".to_string(),
}
);
assert_eq!(
test.server
.registry()
.count_object(ObjectType::Account)
.await
.unwrap(),
4
);
}
+78
View File
@@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use directory::{Credentials, Directory, UnavailableDirectory, backend::oidc::OpenIdDirectory};
use registry::{
schema::{enums::DirectoryType, structs},
types::map::Map,
};
use std::time::{Duration, Instant};
const DISCOVERY_RETRY_FOR: Duration = Duration::from_secs(30);
pub async fn test() {
println!("Running unavailable directory tests...");
let started_at = Instant::now();
let error = OpenIdDirectory::open(structs::OidcDirectory {
description: "Unreachable OIDC directory".to_string(),
issuer_url: "http://localhost:59999/realms/stalwart".to_string(),
claim_username: "preferred_username".to_string(),
claim_name: None,
claim_groups: None,
username_domain: None,
require_audience: None,
require_scopes: Map::new(vec![]),
member_tenant_id: None,
})
.await
.expect_err("Discovery against an unreachable issuer must fail");
let elapsed = started_at.elapsed();
assert!(
elapsed >= DISCOVERY_RETRY_FOR,
"Discovery gave up after {elapsed:?}: {error}"
);
let oidc = Directory::Unavailable(UnavailableDirectory::new(DirectoryType::Oidc, error));
assert!(
oidc.authenticate(&Credentials::Basic {
username: "[email protected]".to_string(),
secret: "this is an OIDC password".to_string(),
mfa_token: None,
})
.await
.is_err(),
"Password authentication must not fall back to internal credentials"
);
assert!(
oidc.authenticate(&Credentials::Bearer {
username: None,
token: "not a token".to_string(),
})
.await
.is_err()
);
assert!(oidc.has_bearer_token_support());
assert!(!oidc.can_lookup_recipients());
assert!(oidc.oidc_discovery_document().is_none());
let ldap = Directory::Unavailable(UnavailableDirectory::new(
DirectoryType::Ldap,
"LDAP bind password is required when bind DN is set",
));
assert!(
ldap.authenticate(&Credentials::Basic {
username: "[email protected]".to_string(),
secret: "this is John's LDAP password".to_string(),
mfa_token: None,
})
.await
.is_err()
);
assert!(ldap.recipient("[email protected]").await.is_err());
assert!(ldap.can_lookup_recipients());
assert!(!ldap.has_bearer_token_support());
}