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:
Vendored
+435
@@ -0,0 +1,435 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{Server, auth::DomainCache, cache::invalidate::CacheInvalidationBuilder};
|
||||
use registry::{
|
||||
schema::{
|
||||
prelude::{Object, ObjectType},
|
||||
structs::{
|
||||
Account, Credential, EmailAlias, GroupAccount, PasswordCredential, Roles, UserAccount,
|
||||
UserRoles,
|
||||
},
|
||||
},
|
||||
types::{datetime::UTCDateTime, id::ObjectId, list::List},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use store::registry::write::{RegistryWrite, RegistryWriteResult};
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
|
||||
pub struct AccountWithId {
|
||||
pub id: u32,
|
||||
pub account: Account,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn synchronize_account(
|
||||
&self,
|
||||
account: directory::Account,
|
||||
) -> trc::Result<AccountWithId> {
|
||||
let (local, domain) = self.validate_address(&account.email).await?;
|
||||
|
||||
match self
|
||||
.account_id_from_parts(local, domain.id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
Some(account_id) => {
|
||||
let current_account = self
|
||||
.registry()
|
||||
.get(ObjectId::new(ObjectType::Account, account_id.into()))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Account ID from directory does not exist in registry")
|
||||
.ctx(trc::Key::AccountName, account.email.clone())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
})?;
|
||||
|
||||
let mut updated_account = Account::from(current_account.clone())
|
||||
.into_user()
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details(
|
||||
"Account ID from directory does not correspond to a user account",
|
||||
)
|
||||
.ctx(trc::Key::AccountName, account.email.clone())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
})?;
|
||||
|
||||
let mut has_changes = false;
|
||||
if let Some(secret) = account.secret
|
||||
&& secret != updated_account.password().unwrap_or_default()
|
||||
{
|
||||
has_changes = true;
|
||||
updated_account.set_password(secret);
|
||||
}
|
||||
if account.description.is_some()
|
||||
&& account.description != updated_account.description
|
||||
{
|
||||
updated_account.description = account.description;
|
||||
has_changes = true;
|
||||
}
|
||||
for alias in account.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
updated_account.aliases.push(EmailAlias {
|
||||
name: local.to_string(),
|
||||
domain_id: Id::from(alias_domain.id),
|
||||
enabled: true,
|
||||
description: None,
|
||||
});
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
if let Some(groups) = account.groups {
|
||||
let mut member_group_ids = Vec::with_capacity(groups.len());
|
||||
for email in groups {
|
||||
member_group_ids.push(
|
||||
self.synchronize_group(directory::Group {
|
||||
email,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if updated_account.member_group_ids.len() != member_group_ids.len()
|
||||
|| !updated_account
|
||||
.member_group_ids
|
||||
.iter()
|
||||
.all(|id| member_group_ids.contains(id))
|
||||
{
|
||||
updated_account.member_group_ids = member_group_ids.into();
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
if has_changes {
|
||||
let updated_account = Object::from(Account::User(updated_account));
|
||||
match self
|
||||
.registry()
|
||||
.write(RegistryWrite::update(
|
||||
Id::from(account_id),
|
||||
&updated_account,
|
||||
¤t_account,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
RegistryWriteResult::Success(id) => {
|
||||
let mut invalidator = CacheInvalidationBuilder::default();
|
||||
invalidator.process_update(id, ¤t_account, &updated_account);
|
||||
self.invalidate_caches(invalidator)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(AccountWithId {
|
||||
id: id.document_id(),
|
||||
account: updated_account.into(),
|
||||
})
|
||||
}
|
||||
failure => Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to synchronize account with directory")
|
||||
.reason(failure)),
|
||||
}
|
||||
} else {
|
||||
Ok(AccountWithId {
|
||||
id: account_id,
|
||||
account: Account::User(updated_account),
|
||||
})
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
||||
let mut aliases = Vec::with_capacity(account.email_aliases.len());
|
||||
for alias in account.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
aliases.push(EmailAlias {
|
||||
name: local.to_string(),
|
||||
domain_id: Id::from(alias_domain.id),
|
||||
enabled: true,
|
||||
description: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut member_group_ids = Vec::new();
|
||||
for email in account.groups.unwrap_or_default() {
|
||||
member_group_ids.push(
|
||||
self.synchronize_group(directory::Group {
|
||||
email,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let account = Object::from(Account::User(UserAccount {
|
||||
name: local.to_string(),
|
||||
domain_id: Id::from(domain.id),
|
||||
aliases: aliases.into(),
|
||||
created_at: UTCDateTime::now(),
|
||||
description: account.description,
|
||||
member_group_ids: member_group_ids.into(),
|
||||
member_tenant_id: domain.id_tenant.map(Id::from),
|
||||
roles: UserRoles::User,
|
||||
credentials: List::from_iter(account.secret.map(|secret| {
|
||||
Credential::Password(PasswordCredential {
|
||||
credential_id: 0u64.into(),
|
||||
secret,
|
||||
..Default::default()
|
||||
})
|
||||
})),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
|
||||
match self
|
||||
.registry()
|
||||
.write(RegistryWrite::insert(&account))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
RegistryWriteResult::Success(id) => {
|
||||
let mut invalidator = CacheInvalidationBuilder::default();
|
||||
invalidator.process_create(&account);
|
||||
self.invalidate_caches(invalidator)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(AccountWithId {
|
||||
id: id.document_id(),
|
||||
account: account.into(),
|
||||
})
|
||||
}
|
||||
failure => Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to create account from directory")
|
||||
.reason(failure)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn synchronize_group(&self, group: directory::Group) -> trc::Result<u32> {
|
||||
let (local, domain) = self.validate_address(&group.email).await?;
|
||||
|
||||
match self
|
||||
.account_id_from_parts(local, domain.id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
Some(account_id) => {
|
||||
let current_account = self
|
||||
.registry()
|
||||
.get(ObjectId::new(ObjectType::Account, account_id.into()))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Account ID from directory does not exist in registry")
|
||||
.ctx(trc::Key::AccountName, group.email.clone())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
})?;
|
||||
|
||||
let mut updated_account = Account::from(current_account.clone())
|
||||
.into_group()
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details(
|
||||
"Account ID from directory does not correspond to a group account",
|
||||
)
|
||||
.ctx(trc::Key::AccountName, group.email.clone())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
})?;
|
||||
|
||||
let mut has_changes = false;
|
||||
if group.description.is_some() && group.description != updated_account.description {
|
||||
updated_account.description = group.description;
|
||||
has_changes = true;
|
||||
}
|
||||
for alias in group.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
updated_account.aliases.push(EmailAlias {
|
||||
name: local.to_string(),
|
||||
domain_id: Id::from(alias_domain.id),
|
||||
enabled: true,
|
||||
description: None,
|
||||
});
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
if has_changes {
|
||||
let updated_account = Object::from(Account::Group(updated_account));
|
||||
match self
|
||||
.registry()
|
||||
.write(RegistryWrite::update(
|
||||
Id::from(account_id),
|
||||
&updated_account,
|
||||
¤t_account,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
RegistryWriteResult::Success(id) => {
|
||||
let mut invalidator = CacheInvalidationBuilder::default();
|
||||
invalidator.process_update(id, ¤t_account, &updated_account);
|
||||
self.invalidate_caches(invalidator)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(id.document_id())
|
||||
}
|
||||
failure => Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to synchronize account with directory")
|
||||
.reason(failure)),
|
||||
}
|
||||
} else {
|
||||
Ok(account_id)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
||||
let mut aliases = Vec::with_capacity(group.email_aliases.len());
|
||||
for alias in group.email_aliases {
|
||||
if let Some((local, alias_domain)) = self.validate_alias(&alias).await?
|
||||
&& alias_domain.id_tenant == domain.id_tenant
|
||||
&& self
|
||||
.rcpt_id_from_parts(local, alias_domain.id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
aliases.push(EmailAlias {
|
||||
name: local.to_string(),
|
||||
domain_id: Id::from(alias_domain.id),
|
||||
enabled: true,
|
||||
description: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let account = Object::from(Account::Group(GroupAccount {
|
||||
name: local.to_string(),
|
||||
domain_id: Id::from(domain.id),
|
||||
aliases: aliases.into(),
|
||||
created_at: UTCDateTime::now(),
|
||||
description: group.description,
|
||||
member_tenant_id: domain.id_tenant.map(Id::from),
|
||||
roles: Roles::Default,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
|
||||
match self
|
||||
.registry()
|
||||
.write(RegistryWrite::insert(&account))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
RegistryWriteResult::Success(id) => {
|
||||
let mut invalidator = CacheInvalidationBuilder::default();
|
||||
invalidator.process_create(&account);
|
||||
self.invalidate_caches(invalidator)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(id.document_id())
|
||||
}
|
||||
failure => Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to create account from directory")
|
||||
.reason(failure)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate_address<'x>(
|
||||
&self,
|
||||
email: &'x str,
|
||||
) -> trc::Result<(&'x str, Arc<DomainCache>)> {
|
||||
if email.is_empty() {
|
||||
return Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Account email cannot be empty"));
|
||||
}
|
||||
match email.rsplit_once('@') {
|
||||
Some((local, domain)) => self
|
||||
.domain(domain)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.map(|domain| (local, domain))
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Account domain does not exist or has been disabled")
|
||||
.ctx(trc::Key::Domain, domain.to_string())
|
||||
}),
|
||||
None => {
|
||||
trc::event!(
|
||||
Auth(trc::AuthEvent::Warning),
|
||||
AccountName = email.to_string().clone(),
|
||||
Details = "Directory account is not an email, appended default domain",
|
||||
);
|
||||
self.domain_by_id(self.core.email.default_domain_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Default domain does not exist or has been disabled")
|
||||
.ctx(trc::Key::Id, self.core.email.default_domain_id)
|
||||
})
|
||||
.map(|domain| (email, domain))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate_alias<'x>(
|
||||
&self,
|
||||
email: &'x str,
|
||||
) -> trc::Result<Option<(&'x str, Arc<DomainCache>)>> {
|
||||
match email.rsplit_once('@') {
|
||||
Some((local, domain)) => self
|
||||
.domain(domain)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|domain| domain.map(|domain| (local, domain))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Server,
|
||||
auth::{EmailAddressRef, EmailCache},
|
||||
ipc::{BroadcastEvent, CacheInvalidation},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use registry::{
|
||||
schema::{
|
||||
prelude::{Object, ObjectInner, ObjectType},
|
||||
structs::{Account, EmailAlias},
|
||||
},
|
||||
types::id::ObjectId,
|
||||
};
|
||||
use store::{registry::RegistryQuery, roaring::RoaringBitmap};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CacheInvalidationBuilder {
|
||||
changes: AHashSet<CacheInvalidation>,
|
||||
}
|
||||
|
||||
impl CacheInvalidationBuilder {
|
||||
pub fn process_update(&mut self, id: Id, current_object: &Object, new_object: &Object) {
|
||||
let id = id.document_id();
|
||||
match (¤t_object.inner, &new_object.inner) {
|
||||
(
|
||||
ObjectInner::Account(Account::User(current)),
|
||||
ObjectInner::Account(Account::User(new)),
|
||||
) => {
|
||||
let was_renamed =
|
||||
(current.name != new.name) || (current.domain_id != new.domain_id);
|
||||
let quota_changed = current.quotas != new.quotas;
|
||||
let permissions_changed = current.permissions != new.permissions;
|
||||
let roles_changed = current.roles != new.roles;
|
||||
let tenant_changed = current.member_tenant_id != new.member_tenant_id;
|
||||
let details_changed =
|
||||
current.locale != new.locale || current.description != new.description;
|
||||
let groups_changed = current.member_group_ids != new.member_group_ids;
|
||||
let aliases_changed = current.aliases != new.aliases;
|
||||
let credentials_changed = current.credentials != new.credentials;
|
||||
let encryption_changed = current.encryption_at_rest != new.encryption_at_rest;
|
||||
|
||||
if was_renamed
|
||||
|| aliases_changed
|
||||
|| tenant_changed
|
||||
|| groups_changed
|
||||
|| quota_changed
|
||||
|| details_changed
|
||||
|| encryption_changed
|
||||
{
|
||||
self.invalidate(CacheInvalidation::Account(id));
|
||||
}
|
||||
|
||||
if was_renamed || aliases_changed {
|
||||
self.invalidate_negative_email(&new_object.inner);
|
||||
}
|
||||
|
||||
if tenant_changed
|
||||
|| groups_changed
|
||||
|| credentials_changed
|
||||
|| roles_changed
|
||||
|| permissions_changed
|
||||
{
|
||||
self.invalidate(CacheInvalidation::AccessToken(id));
|
||||
}
|
||||
|
||||
if was_renamed {
|
||||
self.invalidate(CacheInvalidation::DavResources(id));
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
ObjectInner::Account(Account::Group(current)),
|
||||
ObjectInner::Account(Account::Group(new)),
|
||||
) => {
|
||||
let was_renamed =
|
||||
(current.name != new.name) || (current.domain_id != new.domain_id);
|
||||
let quota_changed = current.quotas != new.quotas;
|
||||
let permissions_changed = current.permissions != new.permissions;
|
||||
let roles_changed = current.roles != new.roles;
|
||||
let tenant_changed = current.member_tenant_id != new.member_tenant_id;
|
||||
let details_changed =
|
||||
current.locale != new.locale || current.description != new.description;
|
||||
let aliases_changed = current.aliases != new.aliases;
|
||||
|
||||
if was_renamed
|
||||
|| aliases_changed
|
||||
|| tenant_changed
|
||||
|| quota_changed
|
||||
|| details_changed
|
||||
{
|
||||
self.invalidate(CacheInvalidation::Account(id));
|
||||
}
|
||||
|
||||
if was_renamed || aliases_changed {
|
||||
self.invalidate_negative_email(&new_object.inner);
|
||||
}
|
||||
|
||||
if tenant_changed || roles_changed || permissions_changed {
|
||||
self.invalidate(CacheInvalidation::AccessToken(id));
|
||||
}
|
||||
|
||||
if was_renamed {
|
||||
self.invalidate(CacheInvalidation::DavResources(id));
|
||||
}
|
||||
}
|
||||
|
||||
(ObjectInner::Domain(current), ObjectInner::Domain(new)) => {
|
||||
if (current.name != new.name)
|
||||
|| (current.aliases != new.aliases)
|
||||
|| (current.directory_id != new.directory_id)
|
||||
|| (current.member_tenant_id != new.member_tenant_id)
|
||||
|| (current.catch_all_address != new.catch_all_address)
|
||||
|| (current.sub_addressing != new.sub_addressing)
|
||||
|| (current.allow_relaying != new.allow_relaying)
|
||||
|| (current.is_enabled != new.is_enabled)
|
||||
{
|
||||
self.invalidate(CacheInvalidation::Domain(id));
|
||||
}
|
||||
|
||||
if (current.name != new.name) || (current.aliases != new.aliases) {
|
||||
self.invalidate(CacheInvalidation::DomainNegative);
|
||||
}
|
||||
|
||||
if current.logo != new.logo {
|
||||
self.invalidate(CacheInvalidation::DomainLogo(id));
|
||||
}
|
||||
}
|
||||
|
||||
(ObjectInner::DkimSignature(current), ObjectInner::DkimSignature(new)) => {
|
||||
let current_domain_id = current.domain_id().document_id();
|
||||
let new_domain_id = new.domain_id().document_id();
|
||||
self.invalidate(CacheInvalidation::DkimSignature(current_domain_id));
|
||||
if current_domain_id != new_domain_id {
|
||||
self.invalidate(CacheInvalidation::DkimSignature(new_domain_id));
|
||||
}
|
||||
}
|
||||
|
||||
(ObjectInner::Tenant(current), ObjectInner::Tenant(new)) => {
|
||||
if (current.permissions != new.permissions)
|
||||
|| (current.roles != new.roles)
|
||||
|| (current.quotas != new.quotas)
|
||||
{
|
||||
self.invalidate(CacheInvalidation::Tenant(id));
|
||||
}
|
||||
|
||||
if current.logo != new.logo {
|
||||
self.invalidate(CacheInvalidation::TenantLogo(id));
|
||||
}
|
||||
}
|
||||
|
||||
(ObjectInner::Role(current), ObjectInner::Role(new))
|
||||
if (current.enabled_permissions != new.enabled_permissions)
|
||||
|| (current.disabled_permissions != new.disabled_permissions)
|
||||
|| (current.member_tenant_id != new.member_tenant_id)
|
||||
|| (current.role_ids != new.role_ids) =>
|
||||
{
|
||||
self.invalidate(CacheInvalidation::Role(id));
|
||||
}
|
||||
|
||||
(ObjectInner::MailingList(current), ObjectInner::MailingList(new))
|
||||
if (current.aliases != new.aliases)
|
||||
|| (current.name != new.name)
|
||||
|| (current.recipients != new.recipients)
|
||||
|| (current.domain_id != new.domain_id) =>
|
||||
{
|
||||
self.invalidate(CacheInvalidation::List(id));
|
||||
if (current.aliases != new.aliases)
|
||||
|| (current.name != new.name)
|
||||
|| (current.domain_id != new.domain_id)
|
||||
{
|
||||
self.invalidate_negative_email(&new_object.inner);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_delete(&mut self, id: Id, object: &Object) {
|
||||
let id = id.document_id();
|
||||
match &object.inner {
|
||||
ObjectInner::Account(_) => {
|
||||
self.invalidate(CacheInvalidation::AccessToken(id));
|
||||
self.invalidate(CacheInvalidation::Account(id));
|
||||
self.invalidate(CacheInvalidation::DavResources(id));
|
||||
}
|
||||
ObjectInner::Domain(_) => {
|
||||
self.invalidate(CacheInvalidation::Domain(id));
|
||||
self.invalidate(CacheInvalidation::DomainLogo(id));
|
||||
}
|
||||
ObjectInner::DkimSignature(object) => {
|
||||
self.invalidate(CacheInvalidation::DkimSignature(
|
||||
object.domain_id().document_id(),
|
||||
));
|
||||
}
|
||||
ObjectInner::Tenant(_) => {
|
||||
self.invalidate(CacheInvalidation::Tenant(id));
|
||||
self.invalidate(CacheInvalidation::TenantLogo(id));
|
||||
}
|
||||
ObjectInner::Role(_) => {
|
||||
self.invalidate(CacheInvalidation::Role(id));
|
||||
}
|
||||
ObjectInner::MailingList(_) => {
|
||||
self.invalidate(CacheInvalidation::List(id));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_create(&mut self, object: &Object) {
|
||||
if matches!(&object.inner, ObjectInner::Domain(_)) {
|
||||
self.invalidate(CacheInvalidation::DomainNegative);
|
||||
}
|
||||
self.invalidate_negative_email(&object.inner);
|
||||
}
|
||||
|
||||
fn invalidate_negative_email(&mut self, object: &ObjectInner) {
|
||||
let (name, domain_id, aliases) = match object {
|
||||
ObjectInner::Account(Account::User(account)) => {
|
||||
(&account.name, account.domain_id, &account.aliases)
|
||||
}
|
||||
ObjectInner::Account(Account::Group(account)) => {
|
||||
(&account.name, account.domain_id, &account.aliases)
|
||||
}
|
||||
ObjectInner::MailingList(list) => (&list.name, list.domain_id, &list.aliases),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
self.invalidate(CacheInvalidation::EmailNegative {
|
||||
domain_id: domain_id.document_id(),
|
||||
local_part_hash: hash_local_part(name),
|
||||
});
|
||||
for alias in aliases.iter().filter(|alias: &&EmailAlias| alias.enabled) {
|
||||
self.invalidate(CacheInvalidation::EmailNegative {
|
||||
domain_id: alias.domain_id.document_id(),
|
||||
local_part_hash: hash_local_part(&alias.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, change: CacheInvalidation) {
|
||||
self.changes.insert(change);
|
||||
}
|
||||
|
||||
pub fn with_invalidation(mut self, change: CacheInvalidation) -> Self {
|
||||
self.invalidate(change);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn invalidate_caches(&self, changes: CacheInvalidationBuilder) -> trc::Result<()> {
|
||||
let mut changes = changes.changes;
|
||||
if changes.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Invalidate objects linking roles
|
||||
let mut role_ids = changes
|
||||
.iter()
|
||||
.filter_map(|change| {
|
||||
if let CacheInvalidation::Role(role_id) = change {
|
||||
Some(*role_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !role_ids.is_empty() {
|
||||
let mut fetched_role_ids = AHashSet::new();
|
||||
|
||||
while let Some(role_id) = role_ids.pop() {
|
||||
if fetched_role_ids.insert(role_id) {
|
||||
let linked_objects = self
|
||||
.registry()
|
||||
.linked_objects(ObjectId::new(ObjectType::Role, role_id.into()))
|
||||
.await?;
|
||||
for linked_object in linked_objects {
|
||||
match linked_object.object() {
|
||||
ObjectType::Account => {
|
||||
changes.insert(CacheInvalidation::AccessToken(
|
||||
linked_object.id().document_id(),
|
||||
));
|
||||
}
|
||||
ObjectType::Role => {
|
||||
role_ids.push(linked_object.id().document_id());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let changes = changes.into_iter().collect::<Vec<_>>();
|
||||
self.invalidate_local_caches(&changes).await;
|
||||
self.cluster_broadcast(BroadcastEvent::CacheInvalidate(changes))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn invalidate_all_local_caches(&self) {
|
||||
self.invalidate_all_local_negative_caches();
|
||||
self.inner.cache.access_tokens.clear();
|
||||
self.inner.cache.domains.clear();
|
||||
self.inner.cache.domain_names.clear();
|
||||
self.inner.cache.emails.clear();
|
||||
self.inner.cache.tenants.clear();
|
||||
self.inner.cache.files.clear();
|
||||
self.inner.cache.contacts.clear();
|
||||
self.inner.cache.events.clear();
|
||||
self.inner.cache.scheduling.clear();
|
||||
self.inner.cache.dkim_signers.clear();
|
||||
self.inner.cache.accounts.clear();
|
||||
self.inner.cache.roles.clear();
|
||||
self.inner.cache.lists.clear();
|
||||
self.inner.data.logos.lock().clear();
|
||||
}
|
||||
|
||||
pub fn invalidate_all_local_negative_caches(&self) {
|
||||
self.inner.cache.domain_names_negative.clear();
|
||||
self.inner.cache.emails_negative.clear();
|
||||
}
|
||||
|
||||
pub fn invalidate_local_negative_account_cache(
|
||||
&self,
|
||||
local_part: &str,
|
||||
domain_id: u32,
|
||||
) -> bool {
|
||||
self.inner
|
||||
.cache
|
||||
.emails_negative
|
||||
.remove(&EmailAddressRef::new(local_part, domain_id))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn invalidate_local_caches(&self, changes: &[CacheInvalidation]) {
|
||||
let cache = &self.inner.cache;
|
||||
let mut negative_emails: AHashSet<(u32, u32)> = AHashSet::new();
|
||||
|
||||
for change in changes {
|
||||
match change {
|
||||
CacheInvalidation::AccessToken(id) => {
|
||||
cache.access_tokens.remove(id);
|
||||
cache.http_auth.inner().retain(|_, v| v.account_id != *id);
|
||||
}
|
||||
CacheInvalidation::DavResources(id) => {
|
||||
cache.files.remove(id);
|
||||
cache.contacts.remove(id);
|
||||
cache.events.remove(id);
|
||||
cache.scheduling.remove(id);
|
||||
}
|
||||
CacheInvalidation::Domain(id) => {
|
||||
cache.domains.remove(id);
|
||||
cache.dkim_signers.remove(id);
|
||||
cache.domain_names.inner().retain(|_, v| v != id);
|
||||
}
|
||||
CacheInvalidation::Account(id) => {
|
||||
cache.accounts.remove(id);
|
||||
cache.emails.inner().retain(|_, v| {
|
||||
!matches!(
|
||||
v,
|
||||
EmailCache::Account(account_id)
|
||||
| EmailCache::DisabledAccountAddress(account_id)
|
||||
if account_id == id
|
||||
)
|
||||
});
|
||||
}
|
||||
CacheInvalidation::DkimSignature(id) => {
|
||||
cache.dkim_signers.remove(id);
|
||||
}
|
||||
CacheInvalidation::Tenant(id) => {
|
||||
cache.tenants.remove(id);
|
||||
}
|
||||
CacheInvalidation::Role(id) => {
|
||||
cache.roles.remove(id);
|
||||
}
|
||||
CacheInvalidation::List(id) => {
|
||||
cache.lists.remove(id);
|
||||
cache.emails.inner().retain(|_, v| {
|
||||
!matches!(
|
||||
v,
|
||||
EmailCache::MailingList(list_id)
|
||||
| EmailCache::DisabledListAddress(list_id)
|
||||
if list_id == id
|
||||
)
|
||||
});
|
||||
}
|
||||
CacheInvalidation::DomainLogo(id) => {
|
||||
self.inner
|
||||
.data
|
||||
.logos
|
||||
.lock()
|
||||
.retain(|_, v| v.domain_id != *id);
|
||||
}
|
||||
CacheInvalidation::TenantLogo(id) => {
|
||||
self.inner
|
||||
.data
|
||||
.logos
|
||||
.lock()
|
||||
.retain(|_, v| v.tenant_id != Some(*id));
|
||||
}
|
||||
CacheInvalidation::EmailNegative {
|
||||
domain_id,
|
||||
local_part_hash,
|
||||
} => {
|
||||
negative_emails.insert((*domain_id, *local_part_hash));
|
||||
}
|
||||
CacheInvalidation::DomainNegative => {
|
||||
cache.domain_names_negative.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !negative_emails.is_empty() {
|
||||
cache.emails_negative.retain(|key| {
|
||||
!negative_emails.contains(&(key.domain_id, hash_local_part(&key.local_part)))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn hash_local_part(local_part: &str) -> u32 {
|
||||
xxhash_rust::xxh3::xxh3_64(local_part.as_bytes()) as u32
|
||||
}
|
||||
|
||||
impl From<CacheInvalidation> for CacheInvalidationBuilder {
|
||||
fn from(invalidation: CacheInvalidation) -> Self {
|
||||
let mut builder = CacheInvalidationBuilder::default();
|
||||
builder.invalidate(invalidation);
|
||||
builder
|
||||
}
|
||||
}
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{DavResources, HttpAuthCache, MailboxCache, MessageStoreCache, UpdateLock};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use tokio::sync::{Semaphore, SemaphorePermit};
|
||||
use utils::cache::CacheItemWeight;
|
||||
|
||||
pub mod directory;
|
||||
pub mod invalidate;
|
||||
pub mod principals;
|
||||
pub mod reload;
|
||||
|
||||
impl MailboxCache {
|
||||
pub fn parent_id(&self) -> Option<u32> {
|
||||
if self.parent_id != u32::MAX {
|
||||
Some(self.parent_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_order(&self) -> Option<u32> {
|
||||
if self.sort_order != u32::MAX {
|
||||
Some(self.sort_order)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.parent_id == u32::MAX
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LockResult<'x> {
|
||||
Acquired(SemaphorePermit<'x>),
|
||||
Stale(SemaphorePermit<'x>),
|
||||
}
|
||||
|
||||
impl UpdateLock {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
semaphore: Semaphore::new(1),
|
||||
revision: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn acquire(&self, current_revision: u64) -> trc::Result<LockResult<'_>> {
|
||||
let permit = self.semaphore.acquire().await.map_err(|err| {
|
||||
trc::EventType::Server(trc::ServerEvent::ThreadError)
|
||||
.reason(err)
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to acquire semaphore permit")
|
||||
})?;
|
||||
|
||||
if self.revision.load(Ordering::Acquire) == current_revision {
|
||||
Ok(LockResult::Acquired(permit))
|
||||
} else {
|
||||
Ok(LockResult::Stale(permit))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_revision(&self, revision: u64) {
|
||||
self.revision.store(revision, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UpdateLock {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for MessageStoreCache {
|
||||
fn weight(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for HttpAuthCache {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<HttpAuthCache>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for DavResources {
|
||||
fn weight(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
+1009
File diff suppressed because it is too large
Load Diff
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Core, Server,
|
||||
config::{
|
||||
server::{Listeners, tls::parse_certificates},
|
||||
storage::Storage,
|
||||
telemetry::Telemetry,
|
||||
},
|
||||
ipc::{QueueEvent, RegistryChange},
|
||||
network::security::{BlockedIps, IpWithTtl},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use directory::Directories;
|
||||
use registry::{
|
||||
schema::{prelude::ObjectType, structs::BlockedIp},
|
||||
types::error::{Error, Warning},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use store::{LookupStores, registry::bootstrap::Bootstrap, write::now};
|
||||
|
||||
pub struct ReloadResult {
|
||||
pub errors: Vec<Error>,
|
||||
pub warnings: Vec<Warning>,
|
||||
pub replaced_core: bool,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn reload_registry(&self, change: RegistryChange) -> trc::Result<ReloadResult> {
|
||||
let mut bootstrap = Bootstrap::new(self.registry().clone()).await;
|
||||
let object = match change {
|
||||
RegistryChange::Insert(id) => {
|
||||
if matches!(id.object(), ObjectType::BlockedIp) {
|
||||
if let Some(ip) = bootstrap.get_infallible::<BlockedIp>(id.id()).await {
|
||||
let expires_at = ip
|
||||
.expires_at
|
||||
.as_ref()
|
||||
.map(|dt| dt.timestamp() as u64)
|
||||
.unwrap_or(u64::MAX);
|
||||
|
||||
if expires_at > now() {
|
||||
let mut ips = self.inner.data.blocked_ips.write();
|
||||
if let Some(ip) = ip.address.try_to_ip() {
|
||||
ips.blocked_ip_addresses
|
||||
.insert(IpWithTtl::new(ip, expires_at));
|
||||
} else {
|
||||
ips.blocked_ip_networks
|
||||
.push(IpWithTtl::new(ip.address, expires_at));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(bootstrap.into());
|
||||
} else {
|
||||
id.object()
|
||||
}
|
||||
}
|
||||
RegistryChange::Delete(id) => id.object(),
|
||||
RegistryChange::Reload(object) => object,
|
||||
};
|
||||
|
||||
match object {
|
||||
ObjectType::Certificate => {
|
||||
let mut certificates = AHashMap::new();
|
||||
parse_certificates(&mut bootstrap, &mut certificates, &mut Default::default())
|
||||
.await;
|
||||
self.inner
|
||||
.data
|
||||
.tls_certificates
|
||||
.store(Arc::new(certificates));
|
||||
}
|
||||
ObjectType::MemoryLookupKey
|
||||
| ObjectType::MemoryLookupKeyValue
|
||||
| ObjectType::HttpLookup
|
||||
| ObjectType::StoreLookup => {
|
||||
let lookup = LookupStores::build(&mut bootstrap).await;
|
||||
|
||||
if bootstrap.errors.is_empty() {
|
||||
self.inner.data.lookup_stores.store(Arc::new(lookup.stores));
|
||||
}
|
||||
}
|
||||
|
||||
ObjectType::BlockedIp => {
|
||||
let blocked_ips = BlockedIps::parse(&mut bootstrap).await;
|
||||
if bootstrap.errors.is_empty() {
|
||||
*self.inner.data.blocked_ips.write() = blocked_ips;
|
||||
}
|
||||
}
|
||||
ObjectType::Application => {
|
||||
self.inner.data.applications.reload(&mut bootstrap).await;
|
||||
if bootstrap.errors.is_empty() {
|
||||
self.inner.data.applications.unpack_all(self, false).await;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Load stores
|
||||
let directory = Directories::build(&mut bootstrap).await;
|
||||
let storage = &self.core.storage;
|
||||
let storage = Storage {
|
||||
registry: storage.registry.clone(),
|
||||
data: storage.data.clone(),
|
||||
blob: storage.blob.clone(),
|
||||
search: storage.search.clone(),
|
||||
metrics: storage.metrics.clone(),
|
||||
tracing: storage.tracing.clone(),
|
||||
memory: storage.memory.clone(),
|
||||
coordinator: storage.coordinator.clone(),
|
||||
directory: directory.default_directory,
|
||||
directories: directory.directories,
|
||||
};
|
||||
|
||||
// Parse tracers
|
||||
let tracers = Telemetry::parse(&mut bootstrap, &storage).await;
|
||||
|
||||
if bootstrap.errors.is_empty() {
|
||||
let core = Box::pin(Core::parse(&mut bootstrap, storage)).await;
|
||||
|
||||
if bootstrap.errors.is_empty() {
|
||||
let mut servers = Listeners::parse(&mut bootstrap).await;
|
||||
servers
|
||||
.parse_tcp_acceptors(&mut bootstrap, self.inner.clone())
|
||||
.await;
|
||||
|
||||
if bootstrap.errors.is_empty() {
|
||||
// Update core
|
||||
self.inner.shared_core.store(core.into());
|
||||
|
||||
// Update tracers
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
tracers.update(false);
|
||||
|
||||
// Reload queue settings
|
||||
self.inner
|
||||
.ipc
|
||||
.queue_tx
|
||||
.send(QueueEvent::ReloadSettings)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
return Ok(ReloadResult {
|
||||
errors: bootstrap.errors,
|
||||
warnings: bootstrap.warnings,
|
||||
replaced_core: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bootstrap.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ReloadResult {
|
||||
pub fn has_errors(&self) -> bool {
|
||||
!self.errors.is_empty()
|
||||
}
|
||||
|
||||
pub fn log(&self) {
|
||||
for error in &self.errors {
|
||||
error.log();
|
||||
}
|
||||
for warning in &self.warnings {
|
||||
warning.log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bootstrap> for ReloadResult {
|
||||
fn from(bootstrap: Bootstrap) -> Self {
|
||||
Self {
|
||||
errors: bootstrap.errors,
|
||||
warnings: bootstrap.warnings,
|
||||
replaced_core: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user