385 lines
15 KiB
Rust
385 lines
15 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* 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,
|
|
},
|
|
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 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 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.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.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));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
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 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(),
|
|
));
|
|
}
|
|
// SPDX-SnippetBegin
|
|
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
// SPDX-License-Identifier: LicenseRef-SEL
|
|
#[cfg(feature = "enterprise")]
|
|
ObjectType::Tenant => {
|
|
// Invalidate all accounts of the tenant
|
|
let tenant_id = linked_object.id().document_id();
|
|
changes.insert(CacheInvalidation::Tenant(tenant_id));
|
|
for account_id in self
|
|
.registry()
|
|
.query::<RoaringBitmap>(
|
|
RegistryQuery::new(ObjectType::Account)
|
|
.with_tenant(tenant_id.into()),
|
|
)
|
|
.await?
|
|
{
|
|
changes.insert(CacheInvalidation::AccessToken(account_id));
|
|
}
|
|
}
|
|
// SPDX-SnippetEnd
|
|
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) {
|
|
self.inner
|
|
.cache
|
|
.emails_negative
|
|
.remove(&EmailAddressRef::new(local_part, domain_id));
|
|
}
|
|
|
|
pub async fn invalidate_local_caches(&self, changes: &[CacheInvalidation]) {
|
|
let cache = &self.inner.cache;
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CacheInvalidation> for CacheInvalidationBuilder {
|
|
fn from(invalidation: CacheInvalidation) -> Self {
|
|
let mut builder = CacheInvalidationBuilder::default();
|
|
builder.invalidate(invalidation);
|
|
builder
|
|
}
|
|
}
|