From ddf9cc7ba79ad9caaf1b2a0ce9198df6b79976a4 Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:36:03 +0100 Subject: [PATCH] Registry caching and directory synchronization --- crates/common/src/auth/access_token.rs | 2 +- crates/common/src/auth/authentication.rs | 13 +- crates/common/src/auth/mod.rs | 51 +- crates/common/src/auth/oauth/introspect.rs | 6 +- crates/common/src/auth/oauth/token.rs | 2 +- crates/common/src/auth/permissions.rs | 5 + crates/common/src/cache/directory.rs | 368 ++++++++++- crates/common/src/cache/invalidate.rs | 8 + crates/common/src/cache/principals.rs | 625 ++++++++++++++++-- crates/common/src/cache/reload.rs | 2 +- crates/common/src/config/mailstore/email.rs | 5 +- crates/common/src/config/mod.rs | 2 +- crates/common/src/config/server/tls.rs | 4 +- crates/common/src/config/smtp/auth.rs | 9 +- crates/common/src/config/smtp/queue.rs | 3 +- crates/common/src/enterprise/alerts.rs | 2 +- crates/common/src/enterprise/config.rs | 38 +- crates/common/src/enterprise/mod.rs | 8 +- crates/common/src/lib.rs | 4 +- crates/common/src/manager/boot.rs | 6 +- crates/common/src/network/masked.rs | 27 +- crates/common/src/network/mta.rs | 36 +- crates/common/src/network/security.rs | 37 +- crates/common/src/storage/mod.rs | 15 +- crates/dav/src/calendar/delete.rs | 2 +- crates/dav/src/calendar/scheduling.rs | 2 +- crates/dav/src/calendar/update.rs | 11 +- crates/dav/src/common/propfind.rs | 8 +- crates/dav/src/principal/mod.rs | 4 +- crates/dav/src/principal/propfind.rs | 8 +- crates/directory/src/backend/ldap/lookup.rs | 13 +- crates/directory/src/backend/sql/lookup.rs | 4 + crates/directory/src/core/config.rs | 2 +- crates/directory/src/lib.rs | 2 +- crates/groupware/src/cache/calcard.rs | 4 +- crates/groupware/src/cache/mod.rs | 20 +- crates/groupware/src/calendar/itip.rs | 9 +- crates/groupware/src/calendar/storage.rs | 5 +- .../groupware/src/scheduling/event_cancel.rs | 2 +- .../groupware/src/scheduling/event_create.rs | 2 +- .../groupware/src/scheduling/event_update.rs | 2 +- crates/groupware/src/scheduling/mod.rs | 6 +- crates/groupware/src/scheduling/snapshot.rs | 2 +- crates/http/src/auth/oauth/registration.rs | 8 +- crates/imap/src/core/mailbox.rs | 2 +- crates/jmap-proto/src/object/registry.rs | 5 +- crates/jmap/src/calendar_event/copy.rs | 3 +- crates/jmap/src/calendar_event/get.rs | 8 +- crates/jmap/src/calendar_event/set.rs | 16 +- crates/jmap/src/identity/get.rs | 13 +- crates/jmap/src/identity/set.rs | 6 +- crates/jmap/src/mailbox/set.rs | 2 +- crates/jmap/src/participant_identity/get.rs | 8 +- crates/jmap/src/participant_identity/set.rs | 6 +- crates/jmap/src/principal/availability.rs | 2 +- crates/jmap/src/principal/get.rs | 2 +- crates/jmap/src/share_notification/get.rs | 19 +- crates/managesieve/src/op/putscript.rs | 4 +- crates/registry/src/jmap.rs | 5 +- crates/registry/src/pickle.rs | 4 + crates/registry/src/schema/mod.rs | 10 +- crates/registry/src/types/id.rs | 24 +- crates/registry/src/types/index.rs | 23 +- crates/services/src/broadcast/mod.rs | 10 +- crates/services/src/broadcast/subscriber.rs | 4 +- crates/services/src/task_manager/alarm.rs | 2 +- crates/smtp/src/inbound/auth.rs | 8 +- crates/smtp/src/inbound/mail.rs | 6 +- crates/store/src/build/registry.rs | 61 +- crates/store/src/lib.rs | 8 +- crates/store/src/registry/bootstrap.rs | 77 ++- crates/store/src/registry/get.rs | 88 ++- crates/store/src/registry/local.rs | 124 ++++ crates/store/src/registry/mod.rs | 36 + crates/store/src/registry/query.rs | 44 +- crates/store/src/registry/write.rs | 608 +++++++++++++++-- crates/store/src/write/assert.rs | 2 + crates/store/src/write/key.rs | 43 +- crates/store/src/write/mod.rs | 23 +- crates/utils/src/cache.rs | 5 + 80 files changed, 2238 insertions(+), 467 deletions(-) create mode 100644 crates/store/src/registry/local.rs diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 87902b29..61f401a3 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -328,7 +328,7 @@ impl Server { Err(guard) => { let account = self .registry() - .object::(account_id) + .object::(account_id.into()) .await? .ok_or_else(|| { trc::SecurityEvent::Unauthorized diff --git a/crates/common/src/auth/authentication.rs b/crates/common/src/auth/authentication.rs index b1fc2a78..1b9fc88b 100644 --- a/crates/common/src/auth/authentication.rs +++ b/crates/common/src/auth/authentication.rs @@ -156,7 +156,7 @@ impl Server { { if let Some(account) = self .registry() - .object::(account_id) + .object::(account_id.into()) .await? .and_then(|account| account.into_user()) { @@ -300,7 +300,7 @@ impl Server { ) -> trc::Result { if let Some(account) = self .registry() - .object::(account_id) + .object::(account_id.into()) .await? .and_then(|account| account.into_user()) { @@ -387,7 +387,14 @@ impl Server { AccountName = address.to_string(), Reason = "No domain in username", ); - self.domain_by_id(self.core.email.default_domain_id).await + self.domain_by_id(self.core.email.default_domain_id) + .await? + .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) + }) } } diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index d508ab33..8eb88c66 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -37,16 +37,16 @@ pub const FALLBACK_ADMIN_ID: u32 = u32::MAX; const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::()); pub type Permissions = Bitset; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct EmailAddress { - local_part: Box, - id_domain: u32, + pub local_part: Box, + pub domain_id: u32, } #[derive(Debug, PartialEq, Eq)] pub struct EmailAddressRef<'x> { local_part: &'x str, - id_domain: u32, + domain_id: u32, } #[derive(Debug, Clone, Copy)] @@ -60,8 +60,8 @@ pub struct DomainCache { pub names: Box<[ArcStr]>, pub id: u32, pub id_directory: Option, - pub id_tenant: u32, - pub catch_all: Option, + pub id_tenant: Option, + pub catch_all: Option>, pub sub_addressing_custom: Option>, pub flags: u8, } @@ -71,7 +71,9 @@ pub const DOMAIN_FLAG_SUB_ADDRESSING: u8 = 1 << 1; #[derive(Debug, Clone)] pub struct AccountCache { - pub addresses: Box<[ArcStr]>, + pub name: Box, + pub id: u32, + pub addresses: Box<[EmailAddress]>, pub id_tenant: Option, pub id_member_of: TinyVec<[u32; 3]>, pub quota_disk: u64, @@ -89,7 +91,7 @@ pub struct RoleCache { #[derive(Debug, Clone)] pub struct MailingListCache { - pub addresses: Box<[ArcStr]>, + //pub addresses: Box<[Box]>, pub recipients: Arc<[ArcStr]>, } @@ -146,7 +148,7 @@ pub(crate) struct AccessTo { pub struct AccountInfo { pub(crate) account_id: u32, pub(crate) account: Arc, - pub(crate) member_of: Vec>, + pub(crate) addresses: Vec, } #[derive(Clone, Copy)] @@ -193,28 +195,32 @@ impl CacheItemWeight for DomainCache { impl Equivalent for EmailAddressRef<'_> { fn equivalent(&self, key: &EmailAddress) -> bool { - self.local_part == &*key.local_part && self.id_domain == key.id_domain + self.local_part == &*key.local_part && self.domain_id == key.domain_id } } impl Hash for EmailAddress { fn hash(&self, state: &mut H) { self.local_part.as_ref().hash(state); - self.id_domain.hash(state); + self.domain_id.hash(state); } } impl Hash for EmailAddressRef<'_> { fn hash(&self, state: &mut H) { self.local_part.hash(state); - self.id_domain.hash(state); + self.domain_id.hash(state); } } impl CacheItemWeight for AccountCache { fn weight(&self) -> u64 { std::mem::size_of::() as u64 - + self.addresses.iter().map(|s| s.len() as u64).sum::() + + self + .addresses + .iter() + .map(|s| s.local_part.len() as u64 + std::mem::size_of::() as u64) + .sum::() + self.description.as_ref().map_or(0, |s| s.len() as u64) } } @@ -228,7 +234,7 @@ impl CacheItemWeight for RoleCache { impl CacheItemWeight for MailingListCache { fn weight(&self) -> u64 { std::mem::size_of::() as u64 - + self.addresses.iter().map(|s| s.len() as u64).sum::() + //+ self.addresses.iter().map(|s| s.len() as u64).sum::() + self.recipients.iter().map(|s| s.len() as u64).sum::() } } @@ -259,11 +265,20 @@ impl BuildAccessToken for Arc { } } -impl<'x> EmailAddressRef<'x> { - pub fn new(local_part: &'x str, id_domain: u32) -> Self { +impl EmailAddress { + pub fn new(local_part: impl Into>, domain_id: u32) -> Self { Self { - local_part, - id_domain, + local_part: local_part.into(), + domain_id, + } + } +} + +impl<'x> EmailAddressRef<'x> { + pub fn new(local_part: &'x str, domain_id: u32) -> Self { + Self { + local_part, + domain_id, } } } diff --git a/crates/common/src/auth/oauth/introspect.rs b/crates/common/src/auth/oauth/introspect.rs index 7d6572ee..dff0c82d 100644 --- a/crates/common/src/auth/oauth/introspect.rs +++ b/crates/common/src/auth/oauth/introspect.rs @@ -61,9 +61,9 @@ impl Server { .account(access_token.account_id()) .await .caused_by(trc::location!())? - .addresses - .first() - .map(|v| v.to_string()), + .name() + .to_string() + .into(), token_type: Some("bearer".into()), exp: Some(token_info.expiry as i64), iat: Some(token_info.issued_at as i64), diff --git a/crates/common/src/auth/oauth/token.rs b/crates/common/src/auth/oauth/token.rs index 05c81646..b17a8bed 100644 --- a/crates/common/src/auth/oauth/token.rs +++ b/crates/common/src/auth/oauth/token.rs @@ -218,7 +218,7 @@ impl Server { pub async fn password_hash(&self, account_id: u32) -> trc::Result { if account_id != u32::MAX { self.registry() - .object::(account_id) + .object::(account_id.into()) .await .caused_by(trc::location!())? .and_then(|account| account.into_user().map(|account| account.secret)) diff --git a/crates/common/src/auth/permissions.rs b/crates/common/src/auth/permissions.rs index 6abfdd7a..8c8ebc24 100644 --- a/crates/common/src/auth/permissions.rs +++ b/crates/common/src/auth/permissions.rs @@ -35,6 +35,11 @@ impl Server { } impl PermissionsGroup { + pub fn with_merge(mut self, merge: bool) -> Self { + self.merge = merge; + self + } + pub fn union(&mut self, other: &PermissionsGroup) { self.enabled.union(&other.enabled); self.disabled.union(&other.disabled); diff --git a/crates/common/src/cache/directory.rs b/crates/common/src/cache/directory.rs index 4e9b7362..0c615e01 100644 --- a/crates/common/src/cache/directory.rs +++ b/crates/common/src/cache/directory.rs @@ -4,8 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::Server; -use registry::schema::structs::Account; +use std::sync::Arc; + +use crate::{Server, auth::DomainCache}; +use registry::{ + schema::structs::{Account, EmailAlias, GroupAccount, UserAccount}, + types::datetime::UTCDateTime, +}; +use store::registry::{ + HashedObject, + write::{RegistryWrite, RegistryWriteResult}, +}; +use trc::AddContext; +use types::id::Id; pub(crate) struct AccountWithId { pub id: u32, @@ -17,13 +28,356 @@ impl Server { &self, account: directory::Account, ) -> trc::Result { - todo!() + 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() + .object::>(Id::from(account_id)) + .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 = + current_account.object.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.secret + { + has_changes = true; + updated_account.secret = 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; + } + } + let mut member_group_ids = Vec::with_capacity(account.groups.len()); + for email in account.groups { + member_group_ids.push( + self.synchronize_group(directory::Group { + email, + ..Default::default() + }) + .await + .caused_by(trc::location!())? + .into(), + ); + } + if !member_group_ids.is_empty() + && ((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; + has_changes = true; + } + + if has_changes { + let updated_account = 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) => Ok(AccountWithId { + id: id.document_id(), + account: updated_account, + }), + 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: current_account.object, + }) + } + } + 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::with_capacity(account.groups.len()); + for email in account.groups { + member_group_ids.push( + self.synchronize_group(directory::Group { + email, + ..Default::default() + }) + .await + .caused_by(trc::location!())? + .into(), + ); + } + let account = Account::User(UserAccount { + name: local.to_string(), + domain_id: Id::from(domain.id), + aliases, + created_at: UTCDateTime::now(), + description: account.description, + member_group_ids, + member_tenant_id: domain.id_tenant.map(Id::from), + role_ids: self.core.network.security.default_role_ids_user.clone(), + secret: account.secret.unwrap_or_default(), + ..Default::default() + }); + + match self + .registry() + .write(RegistryWrite::insert(&account)) + .await + .caused_by(trc::location!())? + { + RegistryWriteResult::Success(id) => Ok(AccountWithId { + id: id.document_id(), + account, + }), + failure => Err(trc::AuthEvent::Error + .into_err() + .caused_by(trc::location!()) + .details("Failed to create account from directory") + .reason(failure)), + } + } + } } - pub(crate) async fn synchronize_group( + pub(crate) async fn synchronize_group(&self, group: directory::Group) -> trc::Result { + 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() + .object::>(Id::from(account_id)) + .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 = + current_account.object.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 = 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) => 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 = Account::Group(GroupAccount { + name: local.to_string(), + domain_id: Id::from(domain.id), + aliases, + created_at: UTCDateTime::now(), + description: group.description, + member_tenant_id: domain.id_tenant.map(Id::from), + role_ids: self.core.network.security.default_role_ids_group.clone(), + ..Default::default() + }); + + match self + .registry() + .write(RegistryWrite::insert(&account)) + .await + .caused_by(trc::location!())? + { + RegistryWriteResult::Success(id) => 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, - group: directory::Group, - ) -> trc::Result { - todo!() + email: &'x str, + ) -> trc::Result<(&'x str, Arc)> { + 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)>> { + 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), + } } } diff --git a/crates/common/src/cache/invalidate.rs b/crates/common/src/cache/invalidate.rs index 65253312..66029539 100644 --- a/crates/common/src/cache/invalidate.rs +++ b/crates/common/src/cache/invalidate.rs @@ -6,6 +6,7 @@ use crate::{ Server, + auth::EmailCache, ipc::{BroadcastEvent, CacheInvalidation}, }; @@ -27,9 +28,13 @@ impl Server { 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) if account_id == id), + ); } CacheInvalidation::DkimSignature(id) => { cache.dkim_signers.remove(id); @@ -42,6 +47,9 @@ impl Server { } CacheInvalidation::List(id) => { cache.lists.remove(id); + cache.emails.inner().retain( + |_, v| !matches!(v, EmailCache::MailingList(list_id) if list_id == id), + ); } } } diff --git a/crates/common/src/cache/principals.rs b/crates/common/src/cache/principals.rs index 448a2e7c..1f8ebd3f 100644 --- a/crates/common/src/cache/principals.rs +++ b/crates/common/src/cache/principals.rs @@ -4,27 +4,146 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use p256::elliptic_curve::group; -use registry::schema::enums::Locale; - use crate::{ Server, auth::{ - AccountCache, AccountInfo, AccountTenantIds, DomainCache, EmailCache, MailingListCache, + AccountCache, AccountInfo, AccountTenantIds, DOMAIN_FLAG_RELAY, DOMAIN_FLAG_SUB_ADDRESSING, + DomainCache, EmailAddress, EmailAddressRef, EmailCache, MailingListCache, PermissionsGroup, RoleCache, TenantCache, }, config::smtp::auth::DkimSigner, - storage::ObjectQuota, + expr::if_block::BootstrapExprExt, + network::{masked::MaskedAddress, mta::AddressResolver}, + storage::{ObjectQuota, TenantQuota}, }; -use std::sync::Arc; +use ahash::AHashSet; +use arcstr::ArcStr; +use registry::{ + schema::{ + enums::{Locale, StorageQuota, TenantStorageQuota}, + prelude::{Object, Property}, + structs::{ + Account, DkimSignature, Domain, MailingList, MaskedEmail, Permissions, PermissionsList, + Role, SubAddressing, Tenant, + }, + }, + types::{ + id::ObjectId, + index::{IndexKey, IndexValue}, + }, +}; +use std::{borrow::Cow, sync::Arc}; +use store::{ + registry::{RegistryQuery, bootstrap::Bootstrap}, + write::{RegistryClass, now}, +}; +use trc::AddContext; +use types::id::Id; impl Server { pub async fn domain(&self, domain: &str) -> trc::Result>> { - todo!() + let domain_names = &self.inner.cache.domain_names; + + if let Some(domain_id) = domain_names.get(domain) { + let result = self.domain_by_id(domain_id).await?; + if result.is_none() { + // Domain no longer exists, remove from name cache + domain_names.remove(domain); + } + Ok(result) + } else { + let domain_names_negative = &self.inner.cache.domain_names_negative; + if domain_names_negative.get(domain).is_none() { + if let Some(domain_id) = self + .registry() + .query::>( + RegistryQuery::new(Object::Domain).equal(Property::Name, domain), + ) + .await? + .into_iter() + .next() + { + // Cache positive result + let domain_id = domain_id as u32; + let domain = self.domain_by_id(domain_id).await?; + if let Some(domain) = &domain { + for name in domain.names.iter() { + domain_names.insert(name.clone(), domain_id); + } + } + + Ok(domain) + } else { + // Cache negative result + domain_names_negative.insert( + domain.into(), + (), + self.inner.cache.negative_cache_ttl, + ); + Ok(None) + } + } else { + Ok(None) + } + } } - pub async fn domain_by_id(&self, domain_id: u32) -> trc::Result> { - todo!() + pub async fn domain_by_id(&self, domain_id: u32) -> trc::Result>> { + match self + .inner + .cache + .domains + .get_value_or_guard_async(&domain_id) + .await + { + Ok(domain) => Ok(Some(domain)), + Err(guard) => { + let Some(domain) = self.registry().object::(domain_id.into()).await? else { + return Ok(None); + }; + let mut flags = 0; + if domain.allow_relaying { + flags |= DOMAIN_FLAG_RELAY; + } + let sub_addressing_custom = match domain.sub_addressing { + SubAddressing::Enabled => { + flags |= DOMAIN_FLAG_SUB_ADDRESSING; + None + } + SubAddressing::Custom(custom) => { + flags |= DOMAIN_FLAG_SUB_ADDRESSING; + let mut bp = Bootstrap::new(self.registry().clone()); + let custom = bp.compile_expr( + ObjectId::new(Object::Domain, domain_id.into()), + &custom.ctx_custom_rule(), + ); + if bp.errors.is_empty() { + Some(Box::new(custom)) + } else { + bp.log_errors(); + None + } + } + SubAddressing::Disabled => None, + }; + + let cache = Arc::new(DomainCache { + names: [ArcStr::from(domain.name)] + .into_iter() + .chain(domain.aliases.into_iter().map(ArcStr::from)) + .collect(), + id: domain_id, + id_directory: domain.directory_id.map(|id| id.document_id()), + id_tenant: domain.member_tenant_id.map(|id| id.document_id()), + catch_all: domain.catch_all_address.map(|s| s.into_boxed_str()), + sub_addressing_custom, + flags, + }); + + let _ = guard.insert(cache.clone()); + Ok(Some(cache)) + } + } } pub async fn rcpt_id_from_parts( @@ -32,41 +151,230 @@ impl Server { local_part: &str, domain_id: u32, ) -> trc::Result> { - todo!() + let emails = &self.inner.cache.emails; + + if let Some(email) = emails.get(&EmailAddressRef::new(local_part, domain_id)) { + Ok(Some(email)) + } else { + let emails_negative = &self.inner.cache.emails_negative; + if emails_negative + .get(&EmailAddressRef::new(local_part, domain_id)) + .is_none() + { + let key = IndexKey::Global { + property: Property::Email, + value_1: IndexValue::Text(local_part.into()), + value_2: IndexValue::U64(domain_id.into()), + }; + if let Some(object) = self + .registry() + .validate_primary_key( + RegistryClass::from_index_key(&key, 0, 0), + RegistryClass::from_index_key(&key, u16::MAX, u64::MAX), + None, + ) + .await? + { + let item_id = object.id().document_id(); + let result = match object.object() { + Object::Account => EmailCache::Account(item_id), + Object::MailingList => EmailCache::MailingList(item_id), + _ => { + return Err(trc::AuthEvent::Error + .into_err() + .details( + "Object with email property is not an account or mailing list.", + ) + .ctx(trc::Key::Id, object.to_string()) + .caused_by(trc::location!())); + } + }; + emails.insert(EmailAddress::new(local_part, domain_id), result); + + Ok(Some(result)) + } else { + // Cache negative result + emails_negative.insert( + EmailAddress::new(local_part, domain_id), + (), + self.inner.cache.negative_cache_ttl, + ); + Ok(None) + } + } else { + Ok(None) + } + } } pub async fn rcpt_id_from_email(&self, address: &str) -> trc::Result> { - todo!() + if let Some((local_part, domain)) = address.split_once('@') { + if let Some(domain) = self.domain(domain).await? { + self.rcpt_id_from_parts(local_part, domain.id).await + } else { + Ok(None) + } + } else { + Ok(None) + } } - pub async fn account(&self, id: u32) -> trc::Result> { - /* - - Err(trc::AuthEvent::Error + pub async fn account(&self, account_id: u32) -> trc::Result> { + self.try_account(account_id).await?.ok_or_else(|| { + trc::AuthEvent::Error .into_err() .details("Account not found.") - .caused_by(trc::location!())) - */ - todo!() + .ctx(trc::Key::AccountId, account_id) + .caused_by(trc::location!()) + }) } - pub async fn try_account(&self, id: u32) -> trc::Result>> { - /* + pub async fn try_account(&self, account_id: u32) -> trc::Result>> { + match self + .inner + .cache + .accounts + .get_value_or_guard_async(&account_id) + .await + { + Ok(account) => Ok(Some(account)), + Err(guard) => { + let Some(account) = self.registry().object::(account_id.into()).await? + else { + return Ok(None); + }; - Err(trc::AuthEvent::Error - .into_err() - .details("Account not found.") - .caused_by(trc::location!())) - */ - todo!() + let cache = Arc::new(match account { + Account::User(account) => { + let domain = self + .domain_by_id(account.domain_id.document_id()) + .await? + .ok_or_else(|| { + trc::AuthEvent::Error + .into_err() + .details("Domain not found for user account.") + .ctx(trc::Key::AccountId, account_id) + .ctx(trc::Key::Id, account.domain_id.document_id()) + .caused_by(trc::location!()) + })?; + let mut name = + String::with_capacity(domain.names[0].len() + account.name.len() + 1); + name.push_str(account.name.as_ref()); + name.push('@'); + name.push_str(domain.names[0].as_ref()); + + let mut quota_objects: Option = None; + let mut quota_disk = 0; + for (resource, limit) in account.quotas { + if resource == StorageQuota::MaxDiskQuota { + quota_disk = limit; + } else { + quota_objects + .get_or_insert_with(|| self.core.email.max_objects.clone()) + .set(resource, limit as u32); + } + } + + AccountCache { + id: account_id, + name: name.into_boxed_str(), + addresses: [EmailAddress { + local_part: account.name.into(), + domain_id: account.domain_id.document_id(), + }] + .into_iter() + .chain(account.aliases.into_iter().map(|alias| EmailAddress { + local_part: alias.name.into(), + domain_id: alias.domain_id.document_id(), + })) + .collect(), + id_tenant: account.member_tenant_id.map(|id| id.document_id()), + id_member_of: account + .member_group_ids + .into_iter() + .map(|id| id.document_id()) + .collect(), + quota_disk, + quota_objects: quota_objects.map(Box::new), + description: account.description.map(Into::into), + locale: account.locale, + is_user: true, + } + } + Account::Group(account) => { + let domain = self + .domain_by_id(account.domain_id.document_id()) + .await? + .ok_or_else(|| { + trc::AuthEvent::Error + .into_err() + .details("Domain not found for group account.") + .ctx(trc::Key::AccountId, account_id) + .ctx(trc::Key::Id, account.domain_id.document_id()) + .caused_by(trc::location!()) + })?; + let mut name = + String::with_capacity(domain.names[0].len() + account.name.len() + 1); + name.push_str(account.name.as_ref()); + name.push('@'); + name.push_str(domain.names[0].as_ref()); + + let mut quota_objects: Option = None; + let mut quota_disk = 0; + for (resource, limit) in account.quotas { + if resource == StorageQuota::MaxDiskQuota { + quota_disk = limit; + } else { + quota_objects + .get_or_insert_with(|| self.core.email.max_objects.clone()) + .set(resource, limit as u32); + } + } + + AccountCache { + id: account_id, + name: name.into_boxed_str(), + addresses: [EmailAddress { + local_part: account.name.into(), + domain_id: account.domain_id.document_id(), + }] + .into_iter() + .chain(account.aliases.into_iter().map(|alias| EmailAddress { + local_part: alias.name.into(), + domain_id: alias.domain_id.document_id(), + })) + .collect(), + id_tenant: account.member_tenant_id.map(|id| id.document_id()), + id_member_of: Default::default(), + quota_disk, + quota_objects: quota_objects.map(Box::new), + description: account.description.map(Into::into), + locale: account.locale, + is_user: false, + } + } + }); + + let _ = guard.insert(cache.clone()); + Ok(Some(cache)) + } + } } pub async fn account_id_from_parts( &self, - local: &str, + local_part: &str, domain_id: u32, ) -> trc::Result> { - todo!() + self.rcpt_id_from_parts(local_part, domain_id) + .await + .map(|result| { + if let Some(EmailCache::Account(account_id)) = result { + Some(account_id) + } else { + None + } + }) } pub async fn account_id_from_email( @@ -74,39 +382,246 @@ impl Server { address: &str, resolve: bool, ) -> trc::Result> { - let todo = "resolve subaddressing"; - todo!() + if let Some((local_part, domain)) = address.split_once('@') { + if let Some(domain) = self.domain(domain).await? { + let mut local_part = Cow::Borrowed(local_part); + if resolve { + if domain.flags & DOMAIN_FLAG_SUB_ADDRESSING != 0 { + if let Some(sub_addressing) = &domain.sub_addressing_custom { + // Custom sub-addressing resolution + if let Some(result) = self + .eval_if::( + sub_addressing, + &AddressResolver(local_part.as_ref()), + 0, + ) + .await + { + local_part = Cow::Owned(result); + } + } else if let Some((new_local_part, _)) = address.split_once('+') { + local_part = Cow::Borrowed(new_local_part); + } + } + if let Cow::Borrowed(addr) = &local_part + && let Some(masked_id) = MaskedAddress::parse(addr) + && let Some(masked_entry) = self + .registry() + .object::(Id::new(masked_id)) + .await + .caused_by(trc::location!())? + && masked_entry.enabled + && masked_entry + .expires_at + .is_none_or(|at| at.timestamp() > now() as i64) + { + return Ok(Some(masked_entry.account_id.document_id())); + } + } + + let mut result = self + .rcpt_id_from_parts(local_part.as_ref(), domain.id) + .await?; + if resolve + && result.is_none() + && let Some(catch_all) = &domain.catch_all + { + result = self.rcpt_id_from_email(catch_all).await?; + } + + Ok(result.and_then(|result| { + if let EmailCache::Account(account_id) = result { + Some(account_id) + } else { + None + } + })) + } else { + Ok(None) + } + } else { + Ok(None) + } } pub async fn account_info(&self, id: u32) -> trc::Result { let account = self.account(id).await?; - let mut member_of = Vec::with_capacity(account.id_member_of.len()); - for &group_id in &account.id_member_of { - if let Some(group) = self.try_account(group_id).await? { - member_of.push(group); + let mut addresses = + Vec::with_capacity(account.id_member_of.len() + account.addresses.len()); + for address in account.addresses.iter() { + if let Some(domain) = self.domain_by_id(address.domain_id).await? { + for name in domain.names.iter() { + let mut addr = String::with_capacity(name.len() + address.local_part.len() + 1); + addr.push_str(address.local_part.as_ref()); + addr.push('@'); + addr.push_str(name.as_ref()); + addresses.push(addr); + } } } + + for &group_id in &account.id_member_of { + if let Some(group) = self.try_account(group_id).await? { + for address in group.addresses.iter() { + if let Some(domain) = self.domain_by_id(address.domain_id).await? { + for name in domain.names.iter() { + let mut addr = + String::with_capacity(name.len() + address.local_part.len() + 1); + addr.push_str(address.local_part.as_ref()); + addr.push('@'); + addr.push_str(name.as_ref()); + addresses.push(addr); + } + } + } + } + } + Ok(AccountInfo { account_id: id, account, - member_of, + addresses, }) } pub async fn role(&self, id: u32) -> trc::Result> { - todo!() + let cache = &self.inner.cache.roles; + match cache.get_value_or_guard_async(&id).await { + Ok(role) => Ok(role), + Err(guard) => { + let Some(role) = self.registry().object::(id.into()).await? else { + return Err(trc::AuthEvent::Error + .into_err() + .details("Role not found.") + .ctx(trc::Key::Id, id) + .caused_by(trc::location!())); + }; + + let cache = Arc::new(RoleCache { + id_roles: role + .role_ids + .into_iter() + .map(|id| id.document_id()) + .collect(), + permissions: PermissionsGroup::from(PermissionsList { + permissions: role.permissions, + }), + }); + + let _ = guard.insert(cache.clone()); + Ok(cache) + } + } } pub async fn tenant(&self, id: u32) -> trc::Result> { - todo!() + let cache = &self.inner.cache.tenants; + match cache.get_value_or_guard_async(&id).await { + Ok(tenant) => Ok(tenant), + Err(guard) => { + let Some(tenant) = self.registry().object::(id.into()).await? else { + return Err(trc::AuthEvent::Error + .into_err() + .details("Tenant not found.") + .ctx(trc::Key::Id, id) + .caused_by(trc::location!())); + }; + + let mut quota_objects: Option = None; + let mut quota_disk = 0; + for (resource, limit) in tenant.quotas { + if resource == TenantStorageQuota::MaxDiskQuota { + quota_disk = limit; + } else { + quota_objects + .get_or_insert_default() + .set(resource, limit as u32); + } + } + + // Calculate effective permissions + let permissions = match tenant.permissions { + Permissions::Inherit => None, + Permissions::Merge(permissions) => Some(Box::new( + PermissionsGroup::from(permissions).with_merge(true), + )), + Permissions::Replace(permissions) => Some(Box::new( + PermissionsGroup::from(permissions).with_merge(false), + )), + }; + + let cache = Arc::new(TenantCache { + id_roles: tenant + .role_ids + .into_iter() + .map(|id| id.document_id()) + .collect(), + quota_disk, + quota_objects: quota_objects.map(Box::new), + permissions, + }); + + let _ = guard.insert(cache.clone()); + Ok(cache) + } + } } pub async fn try_list(&self, id: u32) -> trc::Result>> { - todo!() + let cache = &self.inner.cache.lists; + match cache.get_value_or_guard_async(&id).await { + Ok(list) => Ok(Some(list)), + Err(guard) => { + let Some(list) = self.registry().object::(id.into()).await? else { + return Ok(None); + }; + let cache = Arc::new(MailingListCache { + recipients: list.recipients.into_iter().map(Into::into).collect(), + }); + let _ = guard.insert(cache.clone()); + Ok(Some(cache)) + } + } } pub async fn dkim_signers(&self, domain: &str) -> trc::Result>> { - todo!() + let Some(domain) = self.domain(domain).await? else { + return Ok(None); + }; + let cache = &self.inner.cache.dkim_signers; + match cache.get_value_or_guard_async(&domain.id).await { + Ok(signers) => Ok(Some(signers)), + Err(guard) => { + let ids = self + .registry() + .query::>( + RegistryQuery::new(Object::DkimSignature) + .equal(Property::DomainId, domain.id), + ) + .await?; + let mut signatures = Vec::with_capacity(ids.len()); + for id in ids { + if let Some(signature) = + self.registry().object::(id.into()).await? + { + match DkimSigner::new(domain.names[0].to_string(), signature) { + Ok(signer) => signatures.push(signer), + Err(err) => { + trc::error!(err.ctx(trc::Key::Id, id).caused_by(trc::location!())); + } + } + } + } + + if !signatures.is_empty() { + let signatures: Arc<[DkimSigner]> = signatures.into(); + let _ = guard.insert(signatures.clone()); + Ok(Some(signatures)) + } else { + Ok(None) + } + } + } } } @@ -117,11 +632,7 @@ impl AccountInfo { } pub fn name(&self) -> &str { - self.account - .addresses - .first() - .map(|s| s.as_ref()) - .unwrap_or_default() + self.account.name.as_ref() } #[inline(always)] @@ -142,16 +653,8 @@ impl AccountInfo { } } - pub fn addresses(&self) -> impl Iterator { - self.account - .addresses - .iter() - .chain( - self.member_of - .iter() - .flat_map(move |member| member.addresses.iter()), - ) - .map(|a| a.as_str()) + pub fn addresses(&self) -> &[String] { + &self.addresses } #[inline(always)] @@ -171,12 +674,14 @@ impl AccountInfo { } impl AccountCache { + #[inline(always)] + pub fn account_id(&self) -> u32 { + self.id + } + #[inline(always)] pub fn name(&self) -> &str { - self.addresses - .first() - .map(|s| s.as_ref()) - .unwrap_or_default() + self.name.as_ref() } #[inline(always)] @@ -205,9 +710,9 @@ impl AccountCache { } #[inline(always)] - pub fn account_tenant_ids(&self, account_id: u32) -> AccountTenantIds { + pub fn account_tenant_ids(&self) -> AccountTenantIds { AccountTenantIds { - account_id, + account_id: self.id, tenant_id: self.id_tenant, } } diff --git a/crates/common/src/cache/reload.rs b/crates/common/src/cache/reload.rs index 8e5d0a27..27f0ad4d 100644 --- a/crates/common/src/cache/reload.rs +++ b/crates/common/src/cache/reload.rs @@ -28,7 +28,7 @@ pub struct ReloadResult { impl Server { pub async fn reload_registry(&self, change: RegistryChange) -> trc::Result { // TODO: check the different events triggering this, spam filter reload, etc. - let mut bootstrap = Bootstrap::new(self.registry().clone()); + let mut bootstrap = Bootstrap::init(self.registry().clone()).await; let object = match change { RegistryChange::Insert(id) => { if matches!(id.object(), Object::BlockedIp) { diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs index dcaef144..5a2ce3d9 100644 --- a/crates/common/src/config/mailstore/email.rs +++ b/crates/common/src/config/mailstore/email.rs @@ -88,9 +88,8 @@ impl EmailConfig { let auth = bp.setting_infallible::().await; // Obtain default domain name - let default_domain_name = if let Some(default_domain) = bp - .get_infallible::(auth.default_domain_id.id()) - .await + let default_domain_name = if let Some(default_domain) = + bp.get_infallible::(auth.default_domain_id).await { default_domain.name } else { diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 7fb4b7a8..4b868952 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -78,7 +78,7 @@ impl Core { // SPDX-SnippetEnd sieve: Scripting::parse(bp).await, network: Network::parse(bp).await, - smtp: SmtpConfig::parse(bp).await, + smtp: Box::pin(SmtpConfig::parse(bp)).await, jmap: JmapConfig::parse(bp).await, imap: ImapConfig::parse(bp).await, oauth: OAuthConfig::parse(bp).await, diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index 64eb94a3..8d214ca6 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -45,7 +45,7 @@ impl Server { pub async fn build_acme_provider(&self, id: u64) -> trc::Result { if let Some(server) = self .registry() - .object::(id) + .object::(id.into()) .await .caused_by(trc::location!())? { @@ -63,7 +63,7 @@ impl Server { pub async fn build_dns_updater(&self, id: u64) -> trc::Result { let Some(server) = self .registry() - .object::(id) + .object::(id.into()) .await .caused_by(trc::location!())? else { diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index 17770004..70d7c23b 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -115,7 +115,7 @@ impl MailAuthConfig { } impl DkimSigner { - pub fn new(selector: String, domain: String, signature: DkimSignature) -> trc::Result { + pub fn new(domain: String, signature: DkimSignature) -> trc::Result { match signature { DkimSignature::Dkim1Ed25519Sha256(signature) => { let mut errors = vec![]; @@ -143,7 +143,7 @@ impl DkimSigner { })?; Ok(DkimSigner::Ed25519Sha256(build_dkim1_signer( - domain, selector, signature, key, + domain, signature, key, ))) } DkimSignature::Dkim1RsaSha256(signature) => { @@ -179,7 +179,7 @@ impl DkimSigner { })?; Ok(DkimSigner::RsaSha256(build_dkim1_signer( - domain, selector, signature, key, + domain, signature, key, ))) } } @@ -289,13 +289,12 @@ pub fn simple_pem_parse(contents: &str) -> Option> { fn build_dkim1_signer( domain: String, - selector: String, signature: Dkim1Signature, key: T, ) -> mail_auth::dkim::DkimSigner { let mut signer = mail_auth::dkim::DkimSigner::from_key(key) .domain(domain) - .selector(selector) + .selector(signature.selector) .headers(signature.headers) .reporting(signature.report); diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 978b798b..9ff31139 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -245,8 +245,7 @@ impl QueueConfig { // Parse queue strategies for obj in bp.list_infallible::().await { - let virtual_queue = if let Some(name) = queue_id_to_name.get(&obj.object.queue_id.id()) - { + let virtual_queue = if let Some(name) = queue_id_to_name.get(&obj.object.queue_id) { *name } else { bp.build_error( diff --git a/crates/common/src/enterprise/alerts.rs b/crates/common/src/enterprise/alerts.rs index c6974950..eecf9589 100644 --- a/crates/common/src/enterprise/alerts.rs +++ b/crates/common/src/enterprise/alerts.rs @@ -91,7 +91,7 @@ impl Server { AlertMethod::Event { message } => { trc::event!( Telemetry(TelemetryEvent::Alert), - Id = alert.id.id(), + Id = alert.id.id().id(), Details = message.as_ref().map(|m| m.build()) ); diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index 8cacd9e2..c1fc5871 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -20,7 +20,10 @@ use registry::schema::{ structs::{self, AiModel, Alert, CalendarAlarm, CalendarScheduling, DataRetention, SpamLlm}, }; use std::sync::Arc; -use store::registry::bootstrap::Bootstrap; +use store::{ + registry::{HashedObject, RegistryQuery, bootstrap::Bootstrap, write::RegistryWrite}, + roaring::RoaringBitmap, +}; use trc::MetricType; use types::id::Id; use utils::template::Template; @@ -30,8 +33,10 @@ impl Enterprise { let server_hostname = bp.hostname().to_string(); let mut update_license = None; - let mut enterprise = bp.setting_infallible::().await; - let license_result = match (&enterprise.license_key, &enterprise.api_key) { + let mut enterprise = bp + .setting_infallible::>() + .await; + let license_result = match (&enterprise.object.license_key, &enterprise.object.api_key) { (Some(license_key), maybe_api_key) => { match ( LicenseKey::new(license_key, &server_hostname), @@ -79,8 +84,16 @@ impl Enterprise { // Update the license if a new one was obtained if let Some(license) = update_license { - enterprise.license_key = Some(license); - if let Err(err) = bp.registry.update(Id::singleton(), &enterprise).await { + enterprise.object.license_key = Some(license); + if let Err(err) = bp + .registry + .write(RegistryWrite::update( + Id::singleton(), + &enterprise.object, + &enterprise, + )) + .await + { trc::error!( err.caused_by(trc::location!()) .details("Failed to update license key") @@ -88,13 +101,18 @@ impl Enterprise { } } - match bp.registry.count(Object::Account).await { - Ok(total) if total > license.accounts as u64 => { + match bp + .registry + .query::(RegistryQuery::new(Object::Account)) + .await + { + Ok(total) if total.len() > license.accounts as u64 => { bp.build_warning( Object::Enterprise.singleton(), format!( "License key is valid but only allows {} accounts, found {}.", - license.accounts, total + license.accounts, + total.len() ), ); return None; @@ -137,14 +155,14 @@ impl Enterprise { default_temperature: api.temperature, }); ai_apis.insert(api.id.clone(), api.clone()); - ai_apis_ids.insert(id.id(), api); + ai_apis_ids.insert(id.id().id(), api); } // Build the enterprise configuration let mut enterprise = Enterprise { license, undelete_retention: dr.hold_deleted_for.map(|retention| retention.into_inner()), - logo_url: enterprise.logo_url, + logo_url: enterprise.object.logo_url, metrics_alerts: Default::default(), spam_filter_llm: SpamFilterLlmConfig::parse(bp, &ai_apis_ids).await, ai_apis, diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index c7953050..712a46e2 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -172,15 +172,17 @@ impl Server { return Ok(None); }; - let Some(domain_record) = self.registry().object::(domain_id).await? else { + let Some(domain_record) = self.registry().object::(domain_id.into()).await? else { return Ok(None); }; let mut logo = domain_record.logo; - if logo.is_none() && tenant_id != u32::MAX { + if logo.is_none() + && let Some(tenant_id) = tenant_id + { logo = self .registry() - .object::(tenant_id) + .object::(tenant_id.into()) .await? .and_then(|t| t.logo); } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 5045a1c9..8ecf7df6 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -176,9 +176,9 @@ pub struct Caches { pub scheduling: Cache>, pub emails: Cache, - pub emails_negative: CacheWithTtl, + pub emails_negative: CacheWithTtl, pub domain_names: Cache, - pub domain_names_negative: CacheWithTtl, + pub domain_names_negative: CacheWithTtl, ()>, pub domains: Cache>, pub accounts: Cache>, diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index fff22f0c..dd25b72c 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -236,8 +236,10 @@ impl BootManager { } // Initialize registry - let registry = RegistryStore::init(PathBuf::from(config_path.unwrap())); - let mut bootstrap = Bootstrap::new(registry); + let registry = RegistryStore::init(PathBuf::from(config_path.unwrap())) + .await + .failed("⚠️ Startup failed"); + let mut bootstrap = Bootstrap::init(registry).await; // Start listeners let mut servers = Listeners::parse(&mut bootstrap).await; diff --git a/crates/common/src/network/masked.rs b/crates/common/src/network/masked.rs index 8fc007b4..3cf53bd8 100644 --- a/crates/common/src/network/masked.rs +++ b/crates/common/src/network/masked.rs @@ -5,36 +5,29 @@ */ use store::write::now; +use utils::snowflake::SnowflakeIdGenerator; -pub struct MaskedAddress { - pub account_id: u32, - pub address_id: u32, - pub has_expired: bool, -} - -const DEFAULT_EPOCH: u64 = 1632280000; // 52 years after UNIX_EPOCH +pub struct MaskedAddress; impl MaskedAddress { - pub fn parse(local_part: &str) -> Option { + pub fn parse(local_part: &str) -> Option { let mut parts = local_part.split('.'); let _prefix = parts.next().filter(|v| !v.is_empty())?; let ids = parts.next().filter(|v| !v.is_empty())?; if parts.next().is_some() { return None; } - // Format: ... encoded as base36 + // Format: .. encoded as base36 let ids = u128::from_str_radix(ids, 36).ok()?; - let account_id = (ids >> 96) as u32; - let address_id = (ids >> 64) as u32; + let address_id = (ids >> 64) as u64; let expires = (ids >> 32) as u32; let checksum = ids as u32; - if checksum == (account_id ^ address_id ^ expires) { - Some(Self { - account_id, - address_id, - has_expired: (now().saturating_sub(DEFAULT_EPOCH) / 60) > expires as u64, - }) + if checksum == ((address_id as u32) ^ (address_id >> 32) as u32 ^ expires) + && (expires == 0 + || (SnowflakeIdGenerator::to_timestamp(address_id) + expires as u64 > now())) + { + Some(address_id) } else { None } diff --git a/crates/common/src/network/mta.rs b/crates/common/src/network/mta.rs index 26f84587..f5311ed6 100644 --- a/crates/common/src/network/mta.rs +++ b/crates/common/src/network/mta.rs @@ -54,7 +54,11 @@ impl Server { if let Some(sub_addressing) = &domain.sub_addressing_custom { // Custom sub-addressing resolution if let Some(result) = self - .eval_if::(sub_addressing, &Address(local_part.as_ref()), session_id) + .eval_if::( + sub_addressing, + &AddressResolver(local_part.as_ref()), + session_id, + ) .await { local_part = Cow::Owned(result); @@ -66,28 +70,26 @@ impl Server { // Masked email resolution if let Cow::Borrowed(addr) = &local_part - && let Some(masked) = MaskedAddress::parse(addr) + && let Some(masked_id) = MaskedAddress::parse(addr) { - return if !masked.has_expired - && let Some(masked_entry) = self - .registry() - .object::( - Id::from_parts(masked.account_id, masked.account_id).id(), - ) - .await - .caused_by(trc::location!())? + return if let Some(masked_entry) = self + .registry() + .object::(Id::new(masked_id)) + .await + .caused_by(trc::location!())? && masked_entry.enabled && masked_entry .expires_at .is_none_or(|at| at.timestamp() > now() as i64) && let Some(account) = self - .try_account(masked.account_id) + .try_account(masked_entry.account_id.document_id()) .await .caused_by(trc::location!())? - && account.addresses.iter().any(|addr| { - addr.strip_suffix(domain_part) - .is_some_and(|a| a.ends_with('@')) - }) { + && account + .addresses + .iter() + .any(|addr| addr.domain_id == domain.id) + { Ok(RcptResolution::Rewrite(account.name().to_string())) } else { Ok(RcptResolution::UnknownRecipient) @@ -397,9 +399,9 @@ impl Server { } } -struct Address<'x>(&'x str); +pub struct AddressResolver<'x>(pub &'x str); -impl ResolveVariable for Address<'_> { +impl ResolveVariable for AddressResolver<'_> { fn resolve_variable(&'_ self, _: ExpressionVariable) -> crate::expr::Variable<'_> { Variable::from(self.0) } diff --git a/crates/common/src/network/security.rs b/crates/common/src/network/security.rs index 648a654a..3a12a826 100644 --- a/crates/common/src/network/security.rs +++ b/crates/common/src/network/security.rs @@ -20,10 +20,14 @@ use registry::{ }; use std::{fmt::Debug, net::IpAddr}; use store::{ - registry::{bootstrap::Bootstrap, write::RegistryWriteResult}, + registry::{ + bootstrap::Bootstrap, + write::{RegistryWrite, RegistryWriteResult}, + }, write::now, }; use trc::AddContext; +use types::id::Id; use utils::glob::{GlobPattern, MatchType}; #[derive(Debug, Clone)] @@ -41,6 +45,10 @@ pub struct Security { pub auth_fail_rate: Option, pub rcpt_fail_rate: Option, pub loiter_fail_rate: Option, + + pub default_role_ids_user: Vec, + pub default_role_ids_group: Vec, + pub default_role_ids_tenant: Vec, } #[derive(Default)] @@ -74,7 +82,11 @@ impl Security { if !expired_allows.is_empty() { for (id, _) in &expired_allows { - if let Err(err) = bp.registry.delete::(id.id()).await { + if let Err(err) = bp + .registry + .write::(RegistryWrite::delete(id.id())) + .await + { trc::error!( err.details("Failed to delete expired allowed IP from registry.") .caused_by(trc::location!()) @@ -99,9 +111,11 @@ impl Security { } let security = bp.setting_infallible::().await; + let local = bp.setting_infallible::().await; + let auth = bp.setting_infallible::().await; Security { - fallback_admin: bp.local.fallback_admin_user.as_ref().and_then(|user| { - bp.local + fallback_admin: local.fallback_admin_user.as_ref().and_then(|user| { + local .fallback_admin_secret .as_ref() .map(|secret| (user.to_string(), secret.to_string())) @@ -119,6 +133,9 @@ impl Security { .map(|pattern| MatchType::Matches(GlobPattern::compile(pattern, true))) .collect(), scanner_fail_rate: security.scan_ban_rate, + default_role_ids_user: auth.default_user_role_ids, + default_role_ids_group: auth.default_group_role_ids, + default_role_ids_tenant: auth.default_tenant_role_ids, } } } @@ -240,7 +257,7 @@ impl Server { let now = now() as i64; let RegistryWriteResult::Success(id) = self .registry() - .insert(&BlockedIp { + .write(RegistryWrite::insert(&BlockedIp { address: IpAddrOrMask::from_ip(ip), created_at: UTCDateTime::from_timestamp(now), expires_at: self @@ -250,7 +267,7 @@ impl Server { .blocked_ip_expiration .map(|v| UTCDateTime::from_timestamp(now + v as i64)), reason, - }) + })) .await .caused_by(trc::location!())? else { @@ -259,7 +276,7 @@ impl Server { // Increment version self.cluster_broadcast(BroadcastEvent::RegistryChange(RegistryChange::Insert( - Object::BlockedIp.id(id.id()), + Object::BlockedIp.id(id), ))) .await; @@ -317,7 +334,11 @@ impl BlockedIps { if !expired_blocks.is_empty() { for (id, _) in &expired_blocks { - if let Err(err) = bp.registry.delete::(id.id()).await { + if let Err(err) = bp + .registry + .write::(RegistryWrite::delete(id.id())) + .await + { trc::error!( err.details("Failed to delete expired blocked IP from registry.") .caused_by(trc::location!()) diff --git a/crates/common/src/storage/mod.rs b/crates/common/src/storage/mod.rs index a8717fad..fb3b98e3 100644 --- a/crates/common/src/storage/mod.rs +++ b/crates/common/src/storage/mod.rs @@ -14,7 +14,10 @@ use registry::{ types::EnumType, }; use std::sync::Arc; -use store::{BlobStore, InMemoryStore, RegistryStore, SearchStore, Store}; +use store::{ + BlobStore, InMemoryStore, RegistryStore, SearchStore, Store, registry::RegistryQuery, + roaring::RoaringBitmap, +}; pub mod archive; pub mod blob; @@ -87,11 +90,17 @@ impl Server { } pub async fn total_accounts(&self) -> trc::Result { - self.registry().count(Object::Account).await + self.registry() + .query::(RegistryQuery::new(Object::Account)) + .await + .map(|r| r.len()) } pub async fn total_domains(&self) -> trc::Result { - self.registry().count(Object::Domain).await + self.registry() + .query::(RegistryQuery::new(Object::Domain)) + .await + .map(|r| r.len()) } #[cfg(not(feature = "enterprise"))] diff --git a/crates/dav/src/calendar/delete.rs b/crates/dav/src/calendar/delete.rs index 6c74ecea..33a21b3e 100644 --- a/crates/dav/src/calendar/delete.rs +++ b/crates/dav/src/calendar/delete.rs @@ -75,7 +75,7 @@ impl CalendarDeleteRequestHandler for Server { let account_info = self.account_info(access_token.account_id()).await?; let send_itip = self.core.groupware.itip_enabled && !headers.no_schedule_reply - && account_info.addresses().next().is_some() + && !account_info.addresses().is_empty() && access_token.has_permission(Permission::CalendarSchedulingSend); // Fetch entry diff --git a/crates/dav/src/calendar/scheduling.rs b/crates/dav/src/calendar/scheduling.rs index 769e0f35..505df759 100644 --- a/crates/dav/src/calendar/scheduling.rs +++ b/crates/dav/src/calendar/scheduling.rs @@ -368,7 +368,7 @@ impl CalendarEventNotificationHandler for Server { for (email, attendee) in attendees { if let Some(account_id) = self - .account_id_from_email(&email, false) + .account_id_from_email(&email, true) .await .caused_by(trc::location!())? { diff --git a/crates/dav/src/calendar/update.rs b/crates/dav/src/calendar/update.rs index be92920e..8f61a048 100644 --- a/crates/dav/src/calendar/update.rs +++ b/crates/dav/src/calendar/update.rs @@ -116,7 +116,6 @@ impl CalendarUpdateRequestHandler for Server { .account_info(access_token.account_id()) .await .caused_by(trc::location!())?; - let account_emails = account_info.addresses().collect::>(); if let Some(resource) = resources.by_path(resource_name.as_ref()) { if resource.is_container() { @@ -223,7 +222,7 @@ impl CalendarUpdateRequestHandler for Server { // Scheduling let mut itip_messages = None; if self.core.groupware.itip_enabled - && !account_emails.is_empty() + && !account_info.addresses().is_empty() && access_token.has_permission(Permission::CalendarSchedulingSend) && new_event.data.event_range_end() > now { @@ -231,10 +230,10 @@ impl CalendarUpdateRequestHandler for Server { itip_update( &mut new_event.data.event, &old_ical, - account_emails.as_slice(), + account_info.addresses(), ) } else { - itip_create(&mut new_event.data.event, account_emails.as_slice()) + itip_create(&mut new_event.data.event, account_info.addresses()) }; match result { @@ -385,11 +384,11 @@ impl CalendarUpdateRequestHandler for Server { // Scheduling let mut itip_messages = None; if self.core.groupware.itip_enabled - && !account_emails.is_empty() + && !account_info.addresses().is_empty() && access_token.has_permission(Permission::CalendarSchedulingSend) && event.data.event_range_end() > now() as i64 { - match itip_create(&mut event.data.event, account_emails.as_slice()) { + match itip_create(&mut event.data.event, account_info.addresses()) { Ok(messages) => { if messages.iter().map(|r| r.to.len()).sum::() < self.core.groupware.itip_outbound_max_recipients diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index b3df6c0b..d3c61cc8 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -30,7 +30,7 @@ use crate::{ use calcard::{common::timezone::Tz, icalendar::ICalendarComponentType}; use common::{ DavResourcePath, DavResources, Server, - auth::{AccessToken, AccountInfo}, + auth::{AccessToken, AccountCache}, }; use dav_proto::{ Depth, RequestHeaders, @@ -414,7 +414,7 @@ impl PropFindRequestHandler for Server { let is_scheduling = collection_container == Collection::CalendarEventNotification; let account_info = self - .account_info(access_token.account_id()) + .account(access_token.account_id()) .await .caused_by(trc::location!())?; 'outer: for item in paths { @@ -1600,7 +1600,7 @@ impl PropFindData { pub async fn owner( &mut self, server: &Server, - account_info: &AccountInfo, + account_info: &AccountCache, account_id: u32, ) -> trc::Result { let data = self.accounts.entry(account_id).or_default(); @@ -1723,7 +1723,7 @@ async fn add_base_collection_response( let mut fields = Vec::with_capacity(properties.len()); let mut fields_not_found = Vec::new(); let account_info = server - .account_info(access_token.account_id()) + .account(access_token.account_id()) .await .caused_by(trc::location!())?; diff --git a/crates/dav/src/principal/mod.rs b/crates/dav/src/principal/mod.rs index ddec88b1..f6a0bb19 100644 --- a/crates/dav/src/principal/mod.rs +++ b/crates/dav/src/principal/mod.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccountInfo; +use common::auth::AccountCache; use dav_proto::schema::response::Href; use groupware::RFC_3986; @@ -18,7 +18,7 @@ pub trait CurrentUserPrincipal { fn current_user_principal(&self) -> Href; } -impl CurrentUserPrincipal for AccountInfo { +impl CurrentUserPrincipal for AccountCache { fn current_user_principal(&self) -> Href { Href(format!( "{}/{}/", diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index aba2953c..13e726d3 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -11,7 +11,7 @@ use crate::{ }; use common::{ Server, - auth::{AccessToken, AccountInfo}, + auth::{AccessToken, AccountCache}, }; use dav_proto::schema::{ Namespace, @@ -48,7 +48,7 @@ pub(crate) trait PrincipalPropFind: Sync + Send { fn owner_href( &self, - account_info: &AccountInfo, + account_info: &AccountCache, account_id: u32, ) -> impl Future> + Send; } @@ -63,7 +63,7 @@ impl PrincipalPropFind for Server { response: &mut MultiStatus, ) -> crate::Result<()> { let access_account_info = self - .account_info(access_token.account_id()) + .account(access_token.account_id()) .await .caused_by(trc::location!())?; let properties = match request { @@ -375,7 +375,7 @@ impl PrincipalPropFind for Server { Ok(status.response.0.into_iter().next()) } - async fn owner_href(&self, account_info: &AccountInfo, account_id: u32) -> trc::Result { + async fn owner_href(&self, account_info: &AccountCache, account_id: u32) -> trc::Result { if account_info.account_id() == account_id { Ok(account_info.current_user_principal()) } else { diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index f240e1ee..39173810 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -60,7 +60,8 @@ impl LdapDirectory { match result { Ok(Some(mut result)) => { if result.account.email.is_empty() { - result.account.email = username.into(); + result.account.email = + sanitize_email(username).unwrap_or_else(|| username.to_lowercase()); } result.account } @@ -106,7 +107,8 @@ impl LdapDirectory { .is_ok() { if result.account.email.is_empty() { - result.account.email = username.into(); + result.account.email = + sanitize_email(username).unwrap_or_else(|| username.to_lowercase()); } result.account } else { @@ -124,7 +126,7 @@ impl LdapDirectory { } AuthBind::None => { let filter = self.mappings.filter_login.build(username); - if let Some(result) = self.find_object(&mut conn, &filter).await? { + if let Some(mut result) = self.find_object(&mut conn, &filter).await? { if let Some(account_secret) = &result.account.secret { if !verify_secret_hash(account_secret, secret.as_bytes()).await? { return Err(trc::AuthEvent::Failed @@ -138,7 +140,10 @@ impl LdapDirectory { .details("Account does not have a secret") .details(vec![filter])); } - + if result.account.email.is_empty() { + result.account.email = + sanitize_email(username).unwrap_or_else(|| username.to_lowercase()); + } result.account } else { return Err(trc::AuthEvent::Failed diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index 923762f3..0a37845b 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -82,6 +82,10 @@ impl SqlDirectory { ); } + if account.email.is_empty() { + account.email = sanitize_email(username).unwrap_or_else(|| username.to_lowercase()); + } + Ok(account) } diff --git a/crates/directory/src/core/config.rs b/crates/directory/src/core/config.rs index 3a36fee6..1d763b6b 100644 --- a/crates/directory/src/core/config.rs +++ b/crates/directory/src/core/config.rs @@ -31,7 +31,7 @@ impl Directories { match result { Ok(directory) => { - directories.insert(id.id() as u32, Arc::new(directory)); + directories.insert(id.id().id() as u32, Arc::new(directory)); } Err(err) => { bp.build_error(id, err); diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 801ca052..3f388ad2 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -48,7 +48,7 @@ pub struct Account { pub description: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Group { pub email: String, pub email_aliases: Vec, diff --git a/crates/groupware/src/cache/calcard.rs b/crates/groupware/src/cache/calcard.rs index fa767b47..5efd110e 100644 --- a/crates/groupware/src/cache/calcard.rs +++ b/crates/groupware/src/cache/calcard.rs @@ -37,11 +37,11 @@ pub(super) async fn build_calcard_resources( update_lock: Arc, ) -> trc::Result { let is_calendar = matches!(sync_collection, SyncCollection::Calendar); - let owner_account_info = server.account_info(account_id).await?; + let owner_account_info = server.account(account_id).await?; let access_account_info = if account_id == access_account_id { owner_account_info.clone() } else { - server.account_info(access_account_id).await? + server.account(access_account_id).await? }; let mut cache = DavResources { base_path: format!( diff --git a/crates/groupware/src/cache/mod.rs b/crates/groupware/src/cache/mod.rs index 7fdf8cf1..0541143c 100644 --- a/crates/groupware/src/cache/mod.rs +++ b/crates/groupware/src/cache/mod.rs @@ -15,7 +15,9 @@ use calcard::{ build_calcard_resources, build_simple_hierarchy, resource_from_addressbook, resource_from_calendar, resource_from_card, resource_from_event, }; -use common::{DavResource, DavResources, Server, UpdateLock, auth::AccountInfo, cache::LockResult}; +use common::{ + DavResource, DavResources, Server, UpdateLock, auth::AccountCache, cache::LockResult, +}; use file::{build_file_resources, build_nested_hierarchy, resource_from_file}; use std::{sync::Arc, time::Instant}; use store::{ @@ -43,14 +45,14 @@ pub trait GroupwareCache: Sync + Send { fn create_default_addressbook( &self, - account_info_access: &AccountInfo, - account_info_owner: &AccountInfo, + account_info_access: &AccountCache, + account_info_owner: &AccountCache, ) -> impl Future>> + Send; fn create_default_calendar( &self, - account_info_access: &AccountInfo, - account_info_owner: &AccountInfo, + account_info_access: &AccountCache, + account_info_owner: &AccountCache, ) -> impl Future>> + Send; fn get_or_create_default_calendar( @@ -321,8 +323,8 @@ impl GroupwareCache for Server { async fn create_default_addressbook( &self, - account_info_access: &AccountInfo, - account_info_owner: &AccountInfo, + account_info_access: &AccountCache, + account_info_owner: &AccountCache, ) -> trc::Result> { if let Some(name) = &self.core.groupware.default_addressbook_name { let mut batch = BatchBuilder::new(); @@ -364,8 +366,8 @@ impl GroupwareCache for Server { async fn create_default_calendar( &self, - account_info_access: &AccountInfo, - account_info_owner: &AccountInfo, + account_info_access: &AccountCache, + account_info_owner: &AccountCache, ) -> trc::Result> { if let Some(name) = &self.core.groupware.default_calendar_name { let mut batch = BatchBuilder::new(); diff --git a/crates/groupware/src/calendar/itip.rs b/crates/groupware/src/calendar/itip.rs index ddb0635e..f50abb20 100644 --- a/crates/groupware/src/calendar/itip.rs +++ b/crates/groupware/src/calendar/itip.rs @@ -132,8 +132,7 @@ impl ItipIngest for Server { } } - let emails = account_info.addresses().collect::>(); - let itip_snapshots = itip_snapshot(&itip, emails.as_slice(), false)?; + let itip_snapshots = itip_snapshot(&itip, account_info.addresses(), false)?; if !itip_snapshots.sender_is_organizer_or_attendee(sender) { return Err(ItipIngestError::Message( ItipError::SenderIsNotOrganizerNorAttendee, @@ -141,7 +140,7 @@ impl ItipIngest for Server { } // Obtain changedBy - let changed_by = if let Some(id) = self.account_id_from_email(sender, false).await? { + let changed_by = if let Some(id) = self.account_id_from_email(sender, true).await? { ChangedBy::PrincipalId(id) } else { ChangedBy::CalendarAddress(sender.into()) @@ -180,7 +179,7 @@ impl ItipIngest for Server { .caused_by(trc::location!())?; // Process the iTIP message - let snapshots = itip_snapshot(&event.data.event, emails.as_slice(), false)?; + let snapshots = itip_snapshot(&event.data.event, account_info.addresses(), false)?; let is_organizer_update = !itip_snapshots.organizer.email.is_local; match itip_process_message( &event.data.event, @@ -502,7 +501,7 @@ impl ItipIngest for Server { let mut batch = BatchBuilder::new(); new_event .update( - account_info.account_tenant_ids(rsvp.account_id), + account_info.account_tenant_ids(), event, rsvp.account_id, rsvp.document_id, diff --git a/crates/groupware/src/calendar/storage.rs b/crates/groupware/src/calendar/storage.rs index 84726c69..7ae5a799 100644 --- a/crates/groupware/src/calendar/storage.rs +++ b/crates/groupware/src/calendar/storage.rs @@ -137,7 +137,7 @@ impl ItipAutoExpunge for Server { .account(account_id) .await .caused_by(trc::location!())? - .account_tenant_ids(account_id); + .account_tenant_ids(); for document_id in destroy_ids { // Fetch event @@ -480,8 +480,7 @@ impl DestroyArchive> { .deserialize::() .caused_by(trc::location!())?; - let emails = account_info.addresses().collect::>(); - if let Ok(messages) = itip_cancel(&event.data.event, emails.as_slice(), true) { + if let Ok(messages) = itip_cancel(&event.data.event, account_info.addresses(), true) { ItipMessages::new(vec![messages]) .queue(batch) .caused_by(trc::location!())?; diff --git a/crates/groupware/src/scheduling/event_cancel.rs b/crates/groupware/src/scheduling/event_cancel.rs index 76f5dbcd..27eac0a7 100644 --- a/crates/groupware/src/scheduling/event_cancel.rs +++ b/crates/groupware/src/scheduling/event_cancel.rs @@ -21,7 +21,7 @@ use calcard::{ pub fn itip_cancel( ical: &ICalendar, - account_emails: &[&str], + account_emails: &[String], is_deletion: bool, ) -> Result, ItipError> { // Prepare iTIP message diff --git a/crates/groupware/src/scheduling/event_create.rs b/crates/groupware/src/scheduling/event_create.rs index 1b656eca..6dd0b57b 100644 --- a/crates/groupware/src/scheduling/event_create.rs +++ b/crates/groupware/src/scheduling/event_create.rs @@ -12,7 +12,7 @@ use calcard::icalendar::ICalendar; pub fn itip_create( ical: &mut ICalendar, - account_emails: &[&str], + account_emails: &[String], ) -> Result>, ItipError> { let itip = itip_snapshot(ical, account_emails, false)?; if !itip.organizer.is_server_scheduling { diff --git a/crates/groupware/src/scheduling/event_update.rs b/crates/groupware/src/scheduling/event_update.rs index df89e85c..5e97950f 100644 --- a/crates/groupware/src/scheduling/event_update.rs +++ b/crates/groupware/src/scheduling/event_update.rs @@ -13,7 +13,7 @@ use calcard::icalendar::ICalendar; pub fn itip_update( ical: &mut ICalendar, old_ical: &ICalendar, - account_emails: &[&str], + account_emails: &[String], ) -> Result>, ItipError> { let old_itip = itip_snapshot(old_ical, account_emails, false)?; match itip_snapshot(ical, account_emails, false) { diff --git a/crates/groupware/src/scheduling/mod.rs b/crates/groupware/src/scheduling/mod.rs index e3ad2ba3..2e40031b 100644 --- a/crates/groupware/src/scheduling/mod.rs +++ b/crates/groupware/src/scheduling/mod.rs @@ -235,15 +235,15 @@ impl Attendee<'_> { } impl Email { - pub fn new(email: &str, local_addresses: &[&str]) -> Option { + pub fn new(email: &str, local_addresses: &[String]) -> Option { email.contains('@').then(|| { let email = email.trim().trim_start_matches("mailto:").to_lowercase(); - let is_local = local_addresses.contains(&email.as_str()); + let is_local = local_addresses.contains(&email); Email { email, is_local } }) } - pub fn from_uri(uri: &Uri, local_addresses: &[&str]) -> Option { + pub fn from_uri(uri: &Uri, local_addresses: &[String]) -> Option { if let Uri::Location(uri) = uri { Email::new(uri.as_str(), local_addresses) } else { diff --git a/crates/groupware/src/scheduling/snapshot.rs b/crates/groupware/src/scheduling/snapshot.rs index fd66b3ff..e887eb81 100644 --- a/crates/groupware/src/scheduling/snapshot.rs +++ b/crates/groupware/src/scheduling/snapshot.rs @@ -16,7 +16,7 @@ use calcard::icalendar::{ pub fn itip_snapshot<'x, 'y>( ical: &'x ICalendar, - account_emails: &'y [&str], + account_emails: &'y [String], force_add_client_scheduling: bool, ) -> Result, ItipError> { if !ical.components.iter().any(|comp| { diff --git a/crates/http/src/auth/oauth/registration.rs b/crates/http/src/auth/oauth/registration.rs index ecb5b882..de7bc385 100644 --- a/crates/http/src/auth/oauth/registration.rs +++ b/crates/http/src/auth/oauth/registration.rs @@ -27,7 +27,7 @@ use registry::{ use store::{ ahash::AHashSet, rand::{Rng, distr::Alphanumeric, rng}, - registry::RegistryQuery, + registry::{RegistryQuery, write::RegistryWrite}, }; use trc::{AddContext, AuthEvent}; use types::id::Id; @@ -82,7 +82,7 @@ impl ClientRegistrationHandler for Server { .collect::(); self.registry() - .insert(&OAuthClient { + .write(RegistryWrite::insert(&OAuthClient { client_id: client_id.clone(), created_at: UTCDateTime::now(), description: request.client_name.clone(), @@ -91,7 +91,7 @@ impl ClientRegistrationHandler for Server { redirect_uris: request.redirect_uris.clone(), logo: request.logo_uri.clone(), ..Default::default() - }) + })) .await .caused_by(trc::location!())?; @@ -133,7 +133,7 @@ impl ClientRegistrationHandler for Server { if let Some(redirect_uri) = redirect_uri { let client = self .registry() - .object::(*client_id) + .object::(Id::new(*client_id)) .await? .ok_or_else(|| { trc::StoreEvent::UnexpectedError diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 3e6fa842..62c63b3c 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -260,7 +260,7 @@ impl SessionData { "{}/{}", self.server.core.email.shared_folder, self.server - .account_info(account_id) + .account(account_id) .await .caused_by(trc::location!())? .name() diff --git a/crates/jmap-proto/src/object/registry.rs b/crates/jmap-proto/src/object/registry.rs index e7643238..29124fd6 100644 --- a/crates/jmap-proto/src/object/registry.rs +++ b/crates/jmap-proto/src/object/registry.rs @@ -5,16 +5,15 @@ */ use crate::{ - object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref}, + object::{AnyId, JmapObject, JmapObjectId}, request::deserialize::DeserializeArguments, }; -use jmap_tools::{Element, Key}; use registry::{ jmap::RegistryValue, schema::prelude::{Object, Property}, }; use std::borrow::Cow; -use types::{blob::BlobId, id::Id}; +use types::id::Id; #[derive(Debug)] pub struct Registry; diff --git a/crates/jmap/src/calendar_event/copy.rs b/crates/jmap/src/calendar_event/copy.rs index 96ccb8e3..27a9713a 100644 --- a/crates/jmap/src/calendar_event/copy.rs +++ b/crates/jmap/src/calendar_event/copy.rs @@ -111,7 +111,6 @@ impl JmapCalendarEventCopy for Server { .account_info(access_token.account_id()) .await .caused_by(trc::location!())?; - let account_emails = account_info.addresses().collect::>(); // Prepare batch let mut batch = BatchBuilder::new(); @@ -170,7 +169,7 @@ impl JmapCalendarEventCopy for Server { &mut batch, access_token, account_id, - &account_emails, + account_info.addresses(), false, &can_add_calendars, calendar_event.data.event.into_jscalendar(), diff --git a/crates/jmap/src/calendar_event/get.rs b/crates/jmap/src/calendar_event/get.rs index 326d3d32..4c1775b2 100644 --- a/crates/jmap/src/calendar_event/get.rs +++ b/crates/jmap/src/calendar_event/get.rs @@ -270,6 +270,7 @@ impl CalendarEventGet for Server { }) || entry.calendar_address().is_some_and(|addr| { current_account_info .addresses() + .iter() .any(|a| a.eq_ignore_ascii_case(addr)) }) } @@ -482,7 +483,12 @@ impl CalendarEventGet for Server { .find(|c| c.component_type.is_scheduling_object()) .and_then(|c| c.property(&ICalendarProperty::Organizer)) .and_then(|v| v.calendar_address()) - .is_none_or(|v| account.addresses().any(|a| a.eq_ignore_ascii_case(v))) + .is_none_or(|v| { + account + .addresses() + .iter() + .any(|a| a.eq_ignore_ascii_case(v)) + }) }); let jscal = ical diff --git a/crates/jmap/src/calendar_event/set.rs b/crates/jmap/src/calendar_event/set.rs index 0612ec23..b96c3791 100644 --- a/crates/jmap/src/calendar_event/set.rs +++ b/crates/jmap/src/calendar_event/set.rs @@ -66,7 +66,7 @@ pub trait CalendarEventSet: Sync + Send { batch: &mut BatchBuilder, access_token: &AccessToken, account_id: u32, - account_emails: &[&str], + account_emails: &[String], send_scheduling_messages: bool, can_add_calendars: &Option, js_calendar_event: JSCalendar<'_, Id, BlobId>, @@ -93,7 +93,6 @@ impl CalendarEventSet for Server { .account_info(access_token.account_id()) .await .caused_by(trc::location!())?; - let account_emails = account_info.addresses().collect::>(); let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); @@ -125,7 +124,7 @@ impl CalendarEventSet for Server { &mut batch, access_token, account_id, - &account_emails, + account_info.addresses(), send_scheduling_messages, &can_add_calendars, JSCalendar::default(), @@ -314,7 +313,7 @@ impl CalendarEventSet for Server { let mut itip_messages = None; if send_scheduling_messages && self.core.groupware.itip_enabled - && !account_emails.is_empty() + && !account_info.addresses().is_empty() && access_token.has_permission(Permission::CalendarSchedulingSend) && new_calendar_event.data.event_range_end() > now { @@ -325,13 +324,10 @@ impl CalendarEventSet for Server { itip_update( &mut new_calendar_event.data.event, &old_ical, - account_emails.as_slice(), + account_info.addresses(), ) } else { - itip_create( - &mut new_calendar_event.data.event, - account_emails.as_slice(), - ) + itip_create(&mut new_calendar_event.data.event, account_info.addresses()) }; match result { @@ -516,7 +512,7 @@ impl CalendarEventSet for Server { batch: &mut BatchBuilder, access_token: &AccessToken, account_id: u32, - account_emails: &[&str], + account_emails: &[String], send_scheduling_messages: bool, can_add_calendars: &Option, mut js_calendar_group: JSCalendar<'_, Id, BlobId>, diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 952f9164..d7350516 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -157,7 +157,7 @@ impl IdentityGet for Server { } // Obtain account info - let account = self + let account_info = self .account_info(account_id) .await .caused_by(trc::location!())?; @@ -168,14 +168,17 @@ impl IdentityGet for Server { .with_collection(Collection::Identity); // Create identities - let name = account.description().unwrap_or(account.name()); - let emails = account.addresses().collect::>(); + let name = account_info.description().unwrap_or(account_info.name()); let mut next_document_id = self .store() - .assign_document_ids(account_id, Collection::Identity, emails.len() as u64) + .assign_document_ids( + account_id, + Collection::Identity, + account_info.addresses().len() as u64, + ) .await .caused_by(trc::location!())?; - for email in &emails { + for email in account_info.addresses() { let email = sanitize_email(email).unwrap_or_default(); if email.is_empty() || email.starts_with('@') { continue; diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 90b6c4e4..1ff2fd78 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -68,7 +68,11 @@ impl IdentitySet for Server { // Validate email address if !identity.email.is_empty() { - if !account_info.addresses().any(|e| e == identity.email) { + if !account_info + .addresses() + .iter() + .any(|e| e == &identity.email) + { response.not_created.append( id, SetError::invalid_properties() diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index c480fcab..59597e09 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -90,7 +90,7 @@ impl MailboxSet for Server { will_destroy: request.unwrap_destroy().into_valid().collect(), }; let mut change_id = None; - let account_info = self.account_info(account_id).await?; + let account_info = self.account(account_id).await?; // Process creates let mut batch = BatchBuilder::new(); diff --git a/crates/jmap/src/participant_identity/get.rs b/crates/jmap/src/participant_identity/get.rs index 927618ee..197557b8 100644 --- a/crates/jmap/src/participant_identity/get.rs +++ b/crates/jmap/src/participant_identity/get.rs @@ -128,16 +128,16 @@ impl ParticipantIdentityGet for Server { } // Obtain account info - let account = self + let account_info = self .account_info(account_id) .await .caused_by(trc::location!())?; - let name = account.description().unwrap_or(account.name()); - let emails = account.addresses().collect::>(); + let name = account_info.description().unwrap_or(account_info.name()); // Build identities let identities = ParticipantIdentities { - identities: emails + identities: account_info + .addresses() .iter() .enumerate() .map(|(id, email)| ParticipantIdentity { diff --git a/crates/jmap/src/participant_identity/set.rs b/crates/jmap/src/participant_identity/set.rs index 7a8e32eb..d562ce89 100644 --- a/crates/jmap/src/participant_identity/set.rs +++ b/crates/jmap/src/participant_identity/set.rs @@ -57,7 +57,11 @@ impl ParticipantIdentitySet for Server { .caused_by(trc::location!())?; // Obtain allowed emails - let allowed_emails = account_info.addresses().collect::>(); + let allowed_emails = account_info + .addresses() + .iter() + .map(|v| v.as_str()) + .collect::>(); // Process creates let mut has_changes = false; diff --git a/crates/jmap/src/principal/availability.rs b/crates/jmap/src/principal/availability.rs index 8c29f865..f1e5afc8 100644 --- a/crates/jmap/src/principal/availability.rs +++ b/crates/jmap/src/principal/availability.rs @@ -263,7 +263,7 @@ impl PrincipalGetAvailability for Server { ) }) { // Condition: the Principal is a participant of the event, and has a "participationStatus" of "accepted" or "tentative". - if principal_account.addresses().any(|e| e == attendee) { + if principal_account.addresses().contains(&attendee) { busy_status = Some( entry .parameters(&ICalendarParameterName::Partstat) diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index c4b90dc1..99c6d099 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -81,7 +81,7 @@ impl PrincipalGet for Server { continue; }; let principal = self - .account_info(document_id) + .account(document_id) .await .caused_by(trc::location!())?; diff --git a/crates/jmap/src/share_notification/get.rs b/crates/jmap/src/share_notification/get.rs index 32b4f5e9..076ec039 100644 --- a/crates/jmap/src/share_notification/get.rs +++ b/crates/jmap/src/share_notification/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccountInfo, sharing::notification::ShareNotification}; +use common::{Server, auth::AccountCache, sharing::notification::ShareNotification}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::{ @@ -19,7 +19,7 @@ use jmap_proto::{ types::{date::UTCDate, state::State}, }; use jmap_tools::{Key, Map, Value}; -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use store::{ Deserialize, IterateParams, LogKey, U64_LEN, ahash::{AHashMap, AHashSet}, @@ -63,7 +63,7 @@ impl ShareNotificationGet for Server { let mut min_id = u64::MAX; let mut max_id = 0u64; - let mut account_cache: AHashMap = AHashMap::new(); + let mut account_cache: AHashMap> = AHashMap::new(); let mut ids = if let Some(ids) = request.ids.take() { let ids = ids.unwrap(); @@ -150,12 +150,11 @@ impl ShareNotificationGet for Server { if let Some(account) = account_cache.get(¬ification.changed_by) { account.clone() } else { - let account = - if let Ok(account) = self.account_info(notification.changed_by).await { - account - } else { - continue; - }; + let account = if let Ok(account) = self.account(notification.changed_by).await { + account + } else { + continue; + }; account_cache.insert(notification.changed_by, account.clone()); account @@ -184,7 +183,7 @@ impl ShareNotificationGet for Server { fn build_share_notification( id: u64, mut notification: ShareNotification, - changed_by: &AccountInfo, + changed_by: &AccountCache, properties: &[ShareNotificationProperty], ) -> Value<'static, ShareNotificationProperty, ShareNotificationValue> { let mut result = Map::with_capacity(properties.len()); diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs index 84f643c8..3647b566 100644 --- a/crates/managesieve/src/op/putscript.rs +++ b/crates/managesieve/src/op/putscript.rs @@ -146,7 +146,7 @@ impl Session { .with_blob_hash(blob_hash.clone()), ) .with_current(script) - .with_changed_by(account.account_tenant_ids(account_id)), + .with_changed_by(account.account_tenant_ids()), ) .caused_by(trc::location!())? .clear(blob_hold); @@ -189,7 +189,7 @@ impl Session { SieveScript::new(name.clone(), blob_hash.clone()) .with_size(script_size as u32), ) - .with_changed_by(account.account_tenant_ids(account_id)), + .with_changed_by(account.account_tenant_ids()), ) .caused_by(trc::location!())? .clear(blob_hold); diff --git a/crates/registry/src/jmap.rs b/crates/registry/src/jmap.rs index 461770cd..ca407dd5 100644 --- a/crates/registry/src/jmap.rs +++ b/crates/registry/src/jmap.rs @@ -140,8 +140,9 @@ impl jmap_tools::Element for RegistryValue { | Property::DomainId | Property::AccountId | Property::DefaultDomainId - | Property::DefaultUserRoleId - | Property::DefaultTenantRoleId + | Property::DefaultUserRoleIds + | Property::DefaultGroupRoleIds + | Property::DefaultTenantRoleIds | Property::QueueId | Property::ModelId | Property::AcmeProviderId => { diff --git a/crates/registry/src/pickle.rs b/crates/registry/src/pickle.rs index 461dd9ab..5386cb54 100644 --- a/crates/registry/src/pickle.rs +++ b/crates/registry/src/pickle.rs @@ -39,6 +39,10 @@ impl<'x> PickledStream<'x> { pub fn eof(&self) -> bool { self.pos >= self.data.len() } + + pub fn bytes(&self) -> &'x [u8] { + self.data + } } impl Pickle for u16 { diff --git a/crates/registry/src/schema/mod.rs b/crates/registry/src/schema/mod.rs index 9227b023..550bf086 100644 --- a/crates/registry/src/schema/mod.rs +++ b/crates/registry/src/schema/mod.rs @@ -7,7 +7,7 @@ use crate::{ schema::{ enums::{TracingLevel, TracingLevelOpt}, - prelude::{Account, Duration, HttpAuth, NodeRange, Property, UserAccount}, + prelude::{Account, Duration, GroupAccount, HttpAuth, NodeRange, Property, UserAccount}, }, types::EnumType, }; @@ -67,6 +67,14 @@ impl Account { None } } + + pub fn into_group(self) -> Option { + if let Account::Group(group) = self { + Some(group) + } else { + None + } + } } impl HttpAuth { diff --git a/crates/registry/src/types/id.rs b/crates/registry/src/types/id.rs index 6a2afad1..ef73c05a 100644 --- a/crates/registry/src/types/id.rs +++ b/crates/registry/src/types/id.rs @@ -16,49 +16,49 @@ use types::{blob::BlobId, id::Id}; #[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)] pub struct ObjectId { object: Object, - id: u64, + id: Id, } impl ObjectId { - pub fn new(object: Object, id: impl Into) -> Self { - Self { - object, - id: id.into(), - } + pub fn new(object: Object, id: Id) -> Self { + Self { object, id } } - pub fn id(&self) -> u64 { + #[inline(always)] + pub fn id(&self) -> Id { self.id } + #[inline(always)] pub fn object(&self) -> Object { self.object } + #[inline(always)] pub fn is_valid(&self) -> bool { - self.id != u64::MAX + self.id.is_valid() } } impl Display for ObjectId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.object.as_str(), Id::new(self.id)) + write!(f, "{}:{}", self.object.as_str(), self.id) } } impl Object { - pub fn id(&self, id: u64) -> ObjectId { + pub fn id(&self, id: Id) -> ObjectId { ObjectId::new(*self, id) } pub fn singleton(&self) -> ObjectId { - ObjectId::new(*self, 20080258862541u64) + ObjectId::new(*self, Id::singleton()) } } impl Default for ObjectId { fn default() -> Self { - ObjectId::new(Object::Account, u64::MAX) + ObjectId::new(Object::Account, Id::default()) } } diff --git a/crates/registry/src/types/index.rs b/crates/registry/src/types/index.rs index 16909db8..29288691 100644 --- a/crates/registry/src/types/index.rs +++ b/crates/registry/src/types/index.rs @@ -45,7 +45,6 @@ pub enum IndexValue<'x> { Bytes(Vec), U64(u64), I64(i64), - U32(u32), U16(u16), None, } @@ -79,10 +78,10 @@ impl<'x> IndexBuilder<'x> { } pub fn search(&mut self, property: Property, value: impl Into>) { - self.keys.insert(IndexKey::Search { - property, - value: value.into(), - }); + let value = value.into(); + if value != IndexValue::None { + self.keys.insert(IndexKey::Search { property, value }); + } } pub fn text(&mut self, property: Property, value: &'x str) { @@ -131,7 +130,7 @@ impl<'x> IndexBuilder<'x> { pub fn foreign_key(&mut self, object: Object, id: Option, type_filter: Option) { if let Some(id) = id { self.keys.insert(IndexKey::ForeignKey { - object_id: ObjectId::new(object, id.id()), + object_id: ObjectId::new(object, id), type_filter: type_filter.map(IndexValue::U16).unwrap_or(IndexValue::None), }); } @@ -204,3 +203,15 @@ impl<'x> From<&'x Id> for IndexValue<'x> { IndexValue::U64(value.id()) } } + +impl<'x, T> From<&'x Option> for IndexValue<'x> +where + IndexValue<'x>: std::convert::From<&'x T>, +{ + fn from(value: &'x Option) -> Self { + match value { + Some(id) => id.into(), + None => IndexValue::None, + } + } +} diff --git a/crates/services/src/broadcast/mod.rs b/crates/services/src/broadcast/mod.rs index bbe0811f..704b826b 100644 --- a/crates/services/src/broadcast/mod.rs +++ b/crates/services/src/broadcast/mod.rs @@ -12,7 +12,7 @@ use registry::{ types::{EnumType, id::ObjectId}, }; use std::{borrow::Borrow, io::Write}; -use types::type_state::StateChange; +use types::{id::Id, type_state::StateChange}; use utils::{ codec::leb128::{Leb128Iterator, Leb128Writer}, map::bitmap::Bitmap, @@ -80,12 +80,12 @@ impl BroadcastBatch> { RegistryChange::Insert(id) => { serialized.push(4u8); let _ = serialized.write_leb128(id.object().to_id()); - let _ = serialized.write_leb128(id.id()); + let _ = serialized.write_leb128(id.id().id()); } RegistryChange::Delete(id) => { serialized.push(5u8); let _ = serialized.write_leb128(id.object().to_id()); - let _ = serialized.write_leb128(id.id()); + let _ = serialized.write_leb128(id.id().id()); } RegistryChange::Reload(object) => { serialized.push(6u8); @@ -181,7 +181,7 @@ where Ok(Some(BroadcastEvent::RegistryChange( RegistryChange::Insert(ObjectId::new( Object::from_id(object_id).ok_or(())?, - id, + Id::new(id), )), ))) } @@ -191,7 +191,7 @@ where Ok(Some(BroadcastEvent::RegistryChange( RegistryChange::Delete(ObjectId::new( Object::from_id(object_id).ok_or(())?, - id, + Id::new(id), )), ))) } diff --git a/crates/services/src/broadcast/subscriber.rs b/crates/services/src/broadcast/subscriber.rs index 38aeb833..1cbd6125 100644 --- a/crates/services/src/broadcast/subscriber.rs +++ b/crates/services/src/broadcast/subscriber.rs @@ -231,12 +231,12 @@ fn log_event(event: &BroadcastEvent) -> trc::Value { RegistryChange::Insert(id) => trc::Value::Array(vec![ "RegistryInsert".into(), id.object().as_str().into(), - id.id().into(), + id.id().id().into(), ]), RegistryChange::Delete(id) => trc::Value::Array(vec![ "RegistryDelete".into(), id.object().as_str().into(), - id.id().into(), + id.id().id().into(), ]), RegistryChange::Reload(object) => { trc::Value::Array(vec!["RegistryReload".into(), object.as_str().into()]) diff --git a/crates/services/src/task_manager/alarm.rs b/crates/services/src/task_manager/alarm.rs index fe87c565..2a472eec 100644 --- a/crates/services/src/task_manager/alarm.rs +++ b/crates/services/src/task_manager/alarm.rs @@ -543,7 +543,7 @@ async fn build_template( // Validate recipient let rcpt_to = if let Some(rcpt_to) = rcpt_to { if server.core.groupware.alarms_allow_external_recipients - || account_info.addresses().any(|email| email == rcpt_to) + || account_info.addresses().contains(&rcpt_to) { rcpt_to } else { diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index 51080d75..457cade9 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -111,7 +111,11 @@ impl Session { .and_then(|access_token| access_token.assert_has_permission(Permission::EmailSend)); let result = match result { - Ok(access_token) => self.server.account_info(access_token.account_id()).await, + Ok(access_token) => { + self.server + .account_info(access_token.account_id()) + .await + } Err(err) => Err(err), }; @@ -198,7 +202,7 @@ impl Session { self.data.authenticated_as.is_some() } - pub fn authenticated_emails(&self) -> impl Iterator { + pub fn authenticated_emails(&self) -> &[String] { self.data.authenticated_as.as_ref().unwrap().addresses() } } diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs index 116c597b..7dccede0 100644 --- a/crates/smtp/src/inbound/mail.rs +++ b/crates/smtp/src/inbound/mail.rs @@ -245,7 +245,10 @@ impl Session { { let address_lcase = self.data.mail_from.as_ref().unwrap().address_lcase.as_str(); if authenticated_as != address_lcase - && !self.authenticated_emails().any(|e| e == address_lcase) + && !self + .authenticated_emails() + .iter() + .any(|e| e == address_lcase) { trc::event!( Smtp(SmtpEvent::MailFromUnauthorized), @@ -255,6 +258,7 @@ impl Session { .into_iter() .chain( self.authenticated_emails() + .iter() .map(|e| trc::Value::String(e.into())) ) .collect::>() diff --git a/crates/store/src/build/registry.rs b/crates/store/src/build/registry.rs index 20198f13..ea6a2ac9 100644 --- a/crates/store/src/build/registry.rs +++ b/crates/store/src/build/registry.rs @@ -4,26 +4,59 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::RegistryStore; +use crate::{RegistryStore, RegistryStoreInner, Store}; +use registry::{ + schema::{ + prelude::Object, + structs::{DataStore, LocalSettings}, + }, + types::id::ObjectId, +}; use std::path::PathBuf; +use types::id::Id; +use utils::snowflake::SnowflakeIdGenerator; impl RegistryStore { - pub fn init(local: PathBuf) -> Self { + pub async fn init(local: PathBuf) -> Result { let todo = "environment variables and reading from files"; + const ERROR_MSG: &str = "Failed to initialize registry"; - /* + let mut inner = RegistryStoreInner::load(local).await?; + let Some(data_store) = inner + .local_registry + .read() + .get(&ObjectId::new(Object::DataStore, Id::singleton())) + .cloned() + else { + return Err(format!( + "{ERROR_MSG}: Missing \"DataStore\" object definition." + )); + }; + let data_store = serde_json::from_value::(data_store) + .map_err(|err| format!("{ERROR_MSG}: Failed to parse \"DataStore\" object: {err}"))?; + let Some(local_settings) = inner + .local_registry + .read() + .get(&ObjectId::new(Object::LocalSettings, Id::singleton())) + .cloned() + else { + return Err(format!( + "{ERROR_MSG}: Missing \"LocalSettings\" object definition." + )); + }; + let local_settings = + serde_json::from_value::(local_settings).map_err(|err| { + format!("{ERROR_MSG}: Failed to parse \"LocalSettings\" object: {err}") + })?; - match std::fs::read_to_string(&cfg_local_path) { - Ok(value) => { - config.parse(&value).failed("Invalid local registry file"); - } - Err(err) => { - config.new_build_error("*", format!("Could not read registry file: {err}")); - } + inner.store = Store::build(data_store).await?; + inner.node_id = local_settings.node_id; + if inner.node_id == 0 { + return Err(format!( + "{ERROR_MSG}: \"LocalSettings\" object has invalid nodeId of 0." + )); } - - */ - - todo!() + inner.id_generator = SnowflakeIdGenerator::with_node_id(inner.node_id); + Ok(Self(inner.into())) } } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index faa7457f..d9ef77fa 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -19,10 +19,11 @@ use ::registry::{ pub use ahash; pub use blake3; pub use parking_lot; +use parking_lot::RwLock; pub use rand; pub use rkyv; pub use roaring; -use types::id::Id; +use utils::snowflake::SnowflakeIdGenerator; pub use xxhash_rust; use ahash::{AHashMap, AHashSet}; @@ -198,8 +199,11 @@ pub struct RegistryStore(pub(crate) Arc); pub struct RegistryStoreInner { pub(crate) local_path: PathBuf, - pub(crate) local_objects: AHashMap>, + pub(crate) local_registry: RwLock>, + pub(crate) local_objects: AHashSet, pub(crate) store: Store, + pub(crate) node_id: u64, + pub(crate) id_generator: SnowflakeIdGenerator, } #[cfg(feature = "sqlite")] diff --git a/crates/store/src/registry/bootstrap.rs b/crates/store/src/registry/bootstrap.rs index c700ab47..3f870f74 100644 --- a/crates/store/src/registry/bootstrap.rs +++ b/crates/store/src/registry/bootstrap.rs @@ -4,11 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{RegistryStore, Store, registry::RegistryObject}; +use crate::{ + RegistryStore, Store, + registry::{RegistryObject, RegistryQuery}, +}; +use ahash::AHashSet; use registry::{ schema::{ - prelude::Property, - structs::{LocalSettings, Node}, + prelude::{Object, Property}, + structs::Node, }, types::{ EnumType, ObjectType, @@ -16,6 +20,7 @@ use registry::{ id::ObjectId, }, }; +use types::id::Id; pub struct Bootstrap { pub registry: RegistryStore, @@ -24,19 +29,62 @@ pub struct Bootstrap { pub warnings: Vec, pub has_fatal_errors: bool, pub node: Node, - pub local: LocalSettings, } impl Bootstrap { + pub async fn init(registry: RegistryStore) -> Self { + let mut bp = Self::new(registry); + bp.load_node_settings().await; + bp + } + pub fn new(registry: RegistryStore) -> Self { Self { + data_store: registry.0.store.clone(), + node: Node { + node_id: registry.0.node_id, + ..Default::default() + }, registry, errors: Vec::new(), warnings: Vec::new(), has_fatal_errors: false, - node: Node::default(), - local: LocalSettings::default(), - data_store: Store::None, + } + } + + async fn load_node_settings(&mut self) { + let ids = match self + .registry + .query::>( + RegistryQuery::new(Object::Node).equal(Property::NodeId, self.node_id()), + ) + .await + { + Ok(ids) => ids, + Err(err) => { + self.errors.push(Error::Internal { + object_id: None, + error: err, + }); + self.has_fatal_errors = true; + Default::default() + } + }; + let id = ids.into_iter().next(); + if let Some(id) = id + && let Some(node) = self.get_infallible::(Id::new(id)).await + { + self.node = node; + } else { + self.warnings.push(Warning { + object_id: ObjectId::new(Object::Node, id.map(Id::new).unwrap_or(Id::singleton())), + property: Some(Property::NodeId), + message: format!( + "No node configuration found for nodeId {}, using defaults.", + self.node_id() + ), + }); + self.node.hostname = "localhost.localdomain".to_string(); } } @@ -70,8 +118,7 @@ impl Bootstrap { } } - pub async fn get_infallible(&mut self, id: impl Into) -> Option { - let id = id.into(); + pub async fn get_infallible(&mut self, id: Id) -> Option { match self.registry.object::(id).await { Ok(Some(setting)) => { let mut errors = Vec::new(); @@ -162,7 +209,7 @@ impl Bootstrap { } pub fn node_id(&self) -> u64 { - self.node.node_id + self.registry.0.node_id } pub fn hostname(&self) -> &str { @@ -176,7 +223,7 @@ impl Bootstrap { trc::event!( Registry(trc::RegistryEvent::ValidationError), Source = object_id.object().as_str(), - Id = object_id.id(), + Id = object_id.id().id(), Reason = errors .iter() .map(|err| trc::Value::from(err.to_string())) @@ -187,7 +234,7 @@ impl Bootstrap { trc::event!( Registry(trc::RegistryEvent::BuildError), Source = object_id.object().as_str(), - Id = object_id.id(), + Id = object_id.id().id(), Reason = message.clone(), ); } @@ -195,7 +242,7 @@ impl Bootstrap { trc::event!( Registry(trc::RegistryEvent::ReadError), Source = object_id.as_ref().map(|id| id.object().as_str()), - Id = object_id.as_ref().map(|id| id.id()), + Id = object_id.as_ref().map(|id| id.id().id()), CausedBy = error.clone(), ); } @@ -203,7 +250,7 @@ impl Bootstrap { trc::event!( Registry(trc::RegistryEvent::BuildError), Source = object_id.object().as_str(), - Id = object_id.id(), + Id = object_id.id().id(), Reason = "Object not found", ); } @@ -216,7 +263,7 @@ impl Bootstrap { trc::event!( Registry(trc::RegistryEvent::BuildWarning), Source = warning.object_id.object().as_str(), - Id = warning.object_id.id(), + Id = warning.object_id.id().id(), Key = warning.property.map(|key| key.as_str()), Reason = warning.message.clone(), ); diff --git a/crates/store/src/registry/get.rs b/crates/store/src/registry/get.rs index a3a158d4..ac0af843 100644 --- a/crates/store/src/registry/get.rs +++ b/crates/store/src/registry/get.rs @@ -6,43 +6,49 @@ use crate::{ Deserialize, IterateParams, RegistryStore, SUBSPACE_REGISTRY, U16_LEN, U64_LEN, ValueKey, - registry::{RegistryObject, RegistryQuery}, + registry::RegistryObject, write::{AnyClass, RegistryClass, ValueClass, key::KeySerializer}, }; use registry::{ pickle::PickledStream, - schema::prelude::Object, types::{EnumType, ObjectType, id::ObjectId}, }; -use roaring::RoaringBitmap; use trc::AddContext; +use types::id::Id; use utils::codec::leb128::Leb128Reader; impl RegistryStore { - pub async fn object(&self, id: impl Into) -> trc::Result> { - let id = id.into(); + pub async fn object(&self, id: Id) -> trc::Result> { + let item_id = id.id(); let object = T::object(); - if let Some(objects) = self.0.local_objects.get(&object) { - let Some(item) = objects.get(&id) else { + if self.0.local_objects.contains(&object) { + let Some(item) = self + .0 + .local_registry + .read() + .get(&ObjectId::new(object, id)) + .cloned() + else { return Ok(None); }; - serde_json::from_value::(item.clone()) - .map(Some) - .map_err(|err| { - trc::EventType::Registry(trc::RegistryEvent::LocalParseError) - .into_err() - .caused_by(trc::location!()) - .id(id) - .details(object.as_str()) - .reason(err) - }) + serde_json::from_value::(item).map(Some).map_err(|err| { + trc::EventType::Registry(trc::RegistryEvent::LocalParseError) + .into_err() + .caused_by(trc::location!()) + .id(item_id) + .details(object.as_str()) + .reason(err) + }) } else { let Some(bytes) = self .0 .store .get_value::(ValueKey::from(ValueClass::Registry( - RegistryClass::Item(ObjectId::new(object, id)), + RegistryClass::Item { + object_id: object.to_id(), + item_id, + }, ))) .await? else { @@ -53,7 +59,7 @@ impl RegistryStore { trc::EventType::Registry(trc::RegistryEvent::DeserializationError) .into_err() .caused_by(trc::location!()) - .id(id) + .id(item_id) .details(object.as_str()) .ctx(trc::Key::Value, bytes.0) }) @@ -64,22 +70,24 @@ impl RegistryStore { pub async fn list(&self) -> trc::Result>> { let object = T::object(); - if let Some(objects) = self.0.local_objects.get(&object) { - let mut results = Vec::with_capacity(objects.len()); + if self.0.local_objects.contains(&object) { + let mut results = Vec::new(); - for (id, item) in objects { - let item = serde_json::from_value::(item.clone()).map_err(|err| { - trc::EventType::Registry(trc::RegistryEvent::LocalParseError) - .into_err() - .caused_by(trc::location!()) - .id(*id) - .details(object.as_str()) - .reason(err) - })?; - results.push(RegistryObject { - id: ObjectId::new(object, *id), - object: item, - }); + for (id, item) in self.0.local_registry.read().iter() { + if id.object() == object { + let item = serde_json::from_value::(item.clone()).map_err(|err| { + trc::EventType::Registry(trc::RegistryEvent::LocalParseError) + .into_err() + .caused_by(trc::location!()) + .id(id.id().id()) + .details(object.as_str()) + .reason(err) + })?; + results.push(RegistryObject { + id: *id, + object: item, + }); + } } Ok(results) @@ -127,7 +135,7 @@ impl RegistryStore { .ctx(trc::Key::Value, value) })?; results.push(RegistryObject { - id: ObjectId::new(object, id), + id: ObjectId::new(object, Id::new(id)), object: item, }); @@ -140,16 +148,6 @@ impl RegistryStore { Ok(results) } } - - pub async fn count(&self, object: Object) -> trc::Result { - if let Some(objects) = self.0.local_objects.get(&object) { - Ok(objects.len() as u64) - } else { - self.query::(RegistryQuery::new(object)) - .await - .map(|r| r.len()) - } - } } struct PickledBytes(Vec); diff --git a/crates/store/src/registry/local.rs b/crates/store/src/registry/local.rs new file mode 100644 index 00000000..579533c3 --- /dev/null +++ b/crates/store/src/registry/local.rs @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{RegistryStore, RegistryStoreInner, Store}; +use ahash::AHashMap; +use parking_lot::RwLock; +use registry::{ + schema::prelude::{OBJ_SINGLETON, Object}, + types::{EnumType, id::ObjectId}, +}; +use serde_json::{Map, Value, map::Entry}; +use std::path::PathBuf; +use types::id::Id; +use utils::snowflake::SnowflakeIdGenerator; + +impl RegistryStoreInner { + pub async fn load(local_path: PathBuf) -> Result { + let error_msg = format!("Failed to read local registry at {}", local_path.display()); + let contents = tokio::fs::read_to_string(&local_path) + .await + .map_err(|err| format!("{error_msg}: {err}"))?; + let values = serde_json::from_str::(&contents) + .map_err(|err| format!("{error_msg}: {err}"))?; + + let Value::Object(object) = values else { + return Err(format!("{error_msg}: Found invalid JSON structure.")); + }; + + let mut local_registry = AHashMap::new(); + for (key, value) in object.into_iter() { + let object_type = Object::parse(key.as_str()) + .ok_or_else(|| format!("{error_msg}: Unrecognized object {key:?}."))?; + let is_singleton = object_type.flags() & OBJ_SINGLETON != 0; + let Value::Object(object) = value else { + return Err(format!("{error_msg}: Found invalid JSON structure.")); + }; + if !is_singleton { + for (id, value) in object.into_iter() { + let id = id.parse::().map_err(|_| { + format!("{error_msg}: Failed to parse object id {id} for object {key:?}") + })?; + if !matches!(value, Value::Object(_)) { + return Err(format!( + "{error_msg}: Object {key:?} with id {id} is invalid." + )); + } + if local_registry + .insert(ObjectId::new(object_type, Id::new(id)), value) + .is_some() + { + return Err(format!( + "{error_msg}: Object {key:?} with id {id} defined multiple times." + )); + } + } + } else if local_registry + .insert( + ObjectId::new(object_type, Id::singleton()), + Value::Object(object), + ) + .is_some() + { + return Err(format!( + "{error_msg}: Object {key:?} defined multiple times." + )); + } + } + + Ok(RegistryStoreInner { + local_path, + local_registry: RwLock::new(local_registry), + local_objects: Default::default(), + store: Store::None, + id_generator: SnowflakeIdGenerator::new(), + node_id: 0, + }) + } +} + +impl RegistryStore { + pub async fn write_local_registry(&self) -> trc::Result<()> { + let mut map = Map::new(); + + for (id, value) in self.0.local_registry.read().iter() { + let is_singleton = id.object().flags() & OBJ_SINGLETON != 0; + match map.entry(id.object().as_str().to_string()) { + Entry::Vacant(entry) => { + if is_singleton { + entry.insert(value.clone()); + } else { + entry.insert(Map::from_iter([(id.id().to_string(), value.clone())]).into()); + } + } + Entry::Occupied(mut entry) => { + if !is_singleton { + if let Value::Object(map) = entry.get_mut() { + map.insert(id.id().to_string(), value.clone()); + } + } else { + debug_assert!(false, "Unexpected double singleton assignment"); + } + } + } + } + + let json_text = serde_json::to_string(&Value::Object(map)).map_err(|err| { + trc::EventType::Registry(trc::RegistryEvent::LocalWriteError) + .into_err() + .caused_by(trc::location!()) + .reason(err) + })?; + tokio::fs::write(&self.0.local_path, json_text) + .await + .map_err(|err| { + trc::EventType::Registry(trc::RegistryEvent::LocalWriteError) + .into_err() + .caused_by(trc::location!()) + .reason(err) + }) + } +} diff --git a/crates/store/src/registry/mod.rs b/crates/store/src/registry/mod.rs index fdb7a43a..c67d6ed1 100644 --- a/crates/store/src/registry/mod.rs +++ b/crates/store/src/registry/mod.rs @@ -6,14 +6,23 @@ pub mod bootstrap; pub mod get; +pub mod local; pub mod query; pub mod write; use registry::{ + pickle::{Pickle, PickledStream}, schema::prelude::{Object, Property}, types::{ObjectType, id::ObjectId}, }; +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] + +pub struct HashedObject { + pub hash: u64, + pub object: T, +} + pub struct RegistryObject { pub id: ObjectId, pub object: T, @@ -48,3 +57,30 @@ pub enum RegistryFilterValue { U16(u16), Boolean(bool), } + +impl Pickle for HashedObject { + fn pickle(&self, out: &mut Vec) { + self.object.pickle(out); + } + + fn unpickle(stream: &mut PickledStream<'_>) -> Option { + let hash = xxhash_rust::xxh3::xxh3_64(stream.bytes()); + T::unpickle(stream).map(|object| Self { hash, object }) + } +} + +impl ObjectType for HashedObject { + const FLAGS: u64 = T::FLAGS; + + fn object() -> Object { + T::object() + } + + fn validate(&self, errors: &mut Vec) -> bool { + self.object.validate(errors) + } + + fn index<'x>(&'x self, builder: &mut registry::schema::prelude::IndexBuilder<'x>) { + self.object.index(builder) + } +} diff --git a/crates/store/src/registry/query.rs b/crates/store/src/registry/query.rs index 8c261a63..835b2753 100644 --- a/crates/store/src/registry/query.rs +++ b/crates/store/src/registry/query.rs @@ -15,20 +15,27 @@ use crate::{ use ahash::AHashSet; use registry::{ schema::prelude::{OBJ_FILTER_ACCOUNT, OBJ_FILTER_TENANT, OBJ_SINGLETON, Object, Property}, - types::{EnumType, id::ObjectId}, + types::EnumType, }; use roaring::RoaringBitmap; use std::{borrow::Cow, ops::BitAndAssign}; use trc::AddContext; +use types::id::Id; impl RegistryStore { pub async fn query(&self, query: RegistryQuery) -> trc::Result { let flags = query.object_type.flags(); if flags & OBJ_SINGLETON != 0 { - return Err(trc::EventType::Registry(trc::RegistryEvent::NotSupported) - .into_err() - .details("Singletons do not support searching")); - } else if let Some(objects) = self.0.local_objects.get(&query.object_type) { + if query.filters.is_empty() { + let mut results = T::default(); + results.push(Id::singleton().id()); + return Ok(results); + } else { + return Err(trc::EventType::Registry(trc::RegistryEvent::NotSupported) + .into_err() + .details("Singletons do not support searching")); + } + } else if self.0.local_objects.contains(&query.object_type) { if !query.filters.is_empty() { trc::event!( Registry(trc::RegistryEvent::NotSupported), @@ -36,8 +43,10 @@ impl RegistryStore { ); } let mut results = T::default(); - for id in objects.keys() { - results.push(*id); + for id in self.0.local_registry.read().keys() { + if id.object() == query.object_type { + results.push(id.id().id()); + } } return Ok(results); } @@ -67,7 +76,7 @@ impl RegistryStore { all_ids::(&self.0.store, query.object_type).await? }; - if !results.has_items() { + if !results.has_items() || query.filters.is_empty() { return Ok(results); } @@ -357,14 +366,17 @@ impl From for RegistryFilterValue { async fn all_ids(store: &Store, object: Object) -> trc::Result { let mut bm = T::default(); + let object_id = object.to_id(); store .iterate( IterateParams::new( ValueKey::from(ValueClass::Registry(RegistryClass::Id { - item_id: ObjectId::new(object, 0u64), + object_id, + item_id: 0u64, })), ValueKey::from(ValueClass::Registry(RegistryClass::Id { - item_id: ObjectId::new(object, u64::MAX), + object_id, + item_id: u64::MAX, })), ) .no_values() @@ -388,7 +400,8 @@ async fn range_to_set( op: RegistryFilterOp, ) -> trc::Result { let object_id = object.to_id(); - let ((from_value, from_doc_id, from_field), (end_value, end_doc_id, end_field)) = match op { + let ((from_value, from_doc_id, from_index_id), (end_value, end_doc_id, end_index_id)) = match op + { RegistryFilterOp::LowerThan => ((&[][..], 0, object_id), (match_value, 0, object_id)), RegistryFilterOp::LowerEqualThan => { ((&[][..], 0, object_id), (match_value, u64::MAX, object_id)) @@ -412,7 +425,7 @@ async fn range_to_set( key: KeySerializer::new((U16_LEN * 2) + U64_LEN + 1 + from_value.len()) .write(2u8) .write(object_id) - .write(from_field) + .write(from_index_id) .write(from_value) .write(from_doc_id) .finalize(), @@ -422,7 +435,7 @@ async fn range_to_set( key: KeySerializer::new((U16_LEN * 2) + U64_LEN + 1 + end_value.len()) .write(2u8) .write(object_id) - .write(end_field) + .write(end_index_id) .write(end_value) .write(end_doc_id) .finalize(), @@ -465,7 +478,6 @@ async fn range_to_set( }, ) .await - .caused_by(trc::location!())?; - - Ok(bm) + .caused_by(trc::location!()) + .map(|_| bm) } diff --git a/crates/store/src/registry/write.rs b/crates/store/src/registry/write.rs index 85dba827..c31b17e7 100644 --- a/crates/store/src/registry/write.rs +++ b/crates/store/src/registry/write.rs @@ -7,60 +7,377 @@ use crate::{ IterateParams, RegistryStore, SUBSPACE_REGISTRY, SerializeInfallible, U16_LEN, U64_LEN, ValueKey, + registry::HashedObject, write::{ AnyClass, BatchBuilder, RegistryClass, ValueClass, + assert::AssertValue, key::{DeserializeBigEndian, KeySerializer}, }, }; use registry::{ - schema::prelude::{OBJ_SEQ_ID, Object}, + schema::prelude::{ + OBJ_FILTER_ACCOUNT, OBJ_FILTER_TENANT, OBJ_SEQ_ID, OBJ_SINGLETON, Object, Property, + }, types::{ EnumType, ObjectType, - error::Error, + error::ValidationError, id::ObjectId, index::{IndexBuilder, IndexKey, IndexValue}, }, }; +use std::fmt::Display; use trc::AddContext; use types::id::Id; use utils::codec::leb128::Leb128Reader; -pub enum RegistryWriteResult { - Success(T), - CannotDelete { +pub enum RegistryWriteResult { + Success(Id), + CannotDeleteLinked { object_id: ObjectId, linked_objects: Vec, }, + InvalidSingletonId, + CannotDeleteSingleton, NotFound { object_id: ObjectId, }, + InvalidForeignKey { + object_id: ObjectId, + }, + PrimaryKeyConflict { + property: Property, + existing_id: ObjectId, + }, + ValidationError { + errors: Vec, + }, + InvalidTenantId, + InvalidAccountId, + NotSupported, +} + +pub struct RegistryWrite<'x, T: ObjectType> { + op: RegistryWriteOp<'x, T>, + current_tenant_id: Option, + current_account_id: Option, +} + +pub enum RegistryWriteOp<'x, T: ObjectType> { + Insert { + object: &'x T, + id: Option, + }, + Update { + object: &'x T, + id: Id, + old_object: &'x HashedObject, + }, + Delete { + id: Id, + }, } impl RegistryStore { - pub async fn insert(&self, object: &T) -> trc::Result> { - todo!() - } - - pub async fn update( + pub async fn write( &self, - id: Id, - object: &T, - ) -> trc::Result> { - todo!() + write: RegistryWrite<'_, T>, + ) -> trc::Result { + let object_type = T::object(); + let object_flags = T::FLAGS; + let object_id = object_type.to_id(); + let mut set_index = IndexBuilder::default(); + let mut clear_index = IndexBuilder::default(); + + let mut batch = BatchBuilder::new(); + let mut item_id; + let object; + let object_tenant_id; + let mut write_id = true; + let mut generate_id = false; + + match write.op { + RegistryWriteOp::Insert { + object: insert_object, + id, + } => { + object = insert_object; + object.index(&mut set_index); + object_tenant_id = set_index.tenant_id(); + + item_id = if let Some(id) = id { + id.id() + } else if object_flags & OBJ_SINGLETON != 0 { + write_id = false; + Id::singleton().id() + } else if object_flags & OBJ_SEQ_ID != 0 { + generate_id = true; + u64::MAX + } else { + self.0.id_generator.generate() + }; + } + RegistryWriteOp::Update { + object: update_object, + id, + old_object, + } => { + object = update_object; + object.index(&mut set_index); + object_tenant_id = set_index.tenant_id(); + + // Obtain changes + let mut old_index = IndexBuilder::default(); + old_object.object.index(&mut old_index); + for key in &old_index.keys { + set_index.keys.remove(key); + } + clear_index = old_index; + + // Validate singleton + if object_flags & OBJ_SINGLETON != 0 && !id.is_singleton() { + return Ok(RegistryWriteResult::InvalidSingletonId); + } + + // Assert value + item_id = id.id(); + batch.assert_value( + ValueClass::Registry(RegistryClass::Item { object_id, item_id }), + AssertValue::Hash(old_object.hash), + ); + } + RegistryWriteOp::Delete { id } => { + return if object_flags & OBJ_SINGLETON == 0 { + self.delete(write, id).await + } else { + Ok(RegistryWriteResult::CannotDeleteSingleton) + }; + } + } + + // Validate object + let mut errors = Vec::new(); + object.validate(&mut errors); + if !errors.is_empty() { + return Ok(RegistryWriteResult::ValidationError { errors }); + } + + // Validate tenant ownership + if write.current_tenant_id.is_some() + && (object_flags & OBJ_FILTER_TENANT) != 0 + && write.current_tenant_id != object_tenant_id + { + return Ok(RegistryWriteResult::InvalidTenantId); + } + + // Validate tenant and account changes + if let Some(err) = write.validate_owner(&set_index) { + return Ok(err); + } + + // Write to local registry + if self.0.local_objects.contains(&object_type) { + if generate_id { + return Ok(RegistryWriteResult::NotSupported); + } + let id = Id::new(item_id); + self.0.local_registry.write().insert( + ObjectId::new(object_type, id), + serde_json::to_value(object.clone()).map_err(|err| { + trc::EventType::Registry(trc::RegistryEvent::LocalWriteError) + .into_err() + .caused_by(trc::location!()) + .id(item_id) + .details(object_type.as_str()) + .reason(err) + })?, + ); + return self + .write_local_registry() + .await + .map(|_| RegistryWriteResult::Success(id)); + } + + // Validate foreign keys + for key in &set_index.keys { + match key { + IndexKey::ForeignKey { + object_id: foreign_id, + type_filter, + } => { + // Verify that the referenced object exists + let item_id = foreign_id.id().id(); + let object_id = foreign_id.object().to_id(); + let key = if type_filter != &IndexValue::None { + RegistryClass::Index { + index_id: Property::Type.to_id(), + object_id, + item_id, + key: type_filter.serialize(), + } + } else { + RegistryClass::Id { object_id, item_id } + }; + if self + .0 + .store + .get_value::<()>(ValueKey::from(ValueClass::Registry(key))) + .await + .caused_by(trc::location!())? + .is_none() + { + return Ok(RegistryWriteResult::InvalidForeignKey { + object_id: *foreign_id, + }); + } else if let Some(tenant_id) = object_tenant_id + && (object_flags & OBJ_FILTER_TENANT) != 0 + && self + .0 + .store + .get_value::<()>(ValueKey::from(ValueClass::Registry( + RegistryClass::Index { + index_id: Property::MemberTenantId.to_id(), + object_id, + item_id, + key: IndexValue::U64(tenant_id as u64).serialize(), + }, + ))) + .await + .caused_by(trc::location!())? + .is_none() + { + return Ok(RegistryWriteResult::InvalidForeignKey { + object_id: *foreign_id, + }); + } else if let Some(account_id) = write.current_account_id + && (object_flags & OBJ_FILTER_ACCOUNT) != 0 + && self + .0 + .store + .get_value::<()>(ValueKey::from(ValueClass::Registry( + RegistryClass::Index { + index_id: Property::AccountId.to_id(), + object_id, + item_id, + key: IndexValue::U64(account_id as u64).serialize(), + }, + ))) + .await + .caused_by(trc::location!())? + .is_none() + { + return Ok(RegistryWriteResult::InvalidForeignKey { + object_id: *foreign_id, + }); + } + } + IndexKey::Search { .. } => {} + IndexKey::Unique { property, .. } => { + let from_key = RegistryClass::from_index_key(key, object_id, 0); + let to_key = RegistryClass::from_index_key(key, object_id, u64::MAX); + if let Some(existing_id) = self + .validate_primary_key(from_key, to_key, Some(object_type)) + .await? + && existing_id.id().id() != item_id + { + return Ok(RegistryWriteResult::PrimaryKeyConflict { + existing_id, + property: *property, + }); + } + } + IndexKey::Global { property, .. } => { + let from_key = RegistryClass::from_index_key(key, 0, 0); + let to_key = RegistryClass::from_index_key(key, u16::MAX, u64::MAX); + + if let Some(existing_id) = + self.validate_primary_key(from_key, to_key, None).await? + && existing_id.id().id() != item_id + { + return Ok(RegistryWriteResult::PrimaryKeyConflict { + existing_id, + property: *property, + }); + } + } + } + } + + // Assign id + if generate_id { + let mut id_batch = BatchBuilder::new(); + id_batch.add_and_get( + ValueClass::Registry(RegistryClass::IdCounter { object_id }), + 1, + ); + item_id = self + .0 + .store + .write(id_batch.build_all()) + .await + .and_then(|v| v.last_counter_id())? as u64; + } + + // It's pickle time! + let mut out = Vec::with_capacity(256); + object.pickle(&mut out); + + // Build batch + if write_id { + batch.set( + ValueClass::Registry(RegistryClass::Id { object_id, item_id }), + vec![], + ); + } + batch.registry_index(object_id, item_id, set_index.keys.iter(), true); + batch.registry_index(object_id, item_id, clear_index.keys.iter(), false); + batch.set( + ValueClass::Registry(RegistryClass::Item { object_id, item_id }), + out, + ); + + Ok(RegistryWriteResult::Success(Id::new(item_id))) } - pub async fn delete(&self, id: u64) -> trc::Result> { + async fn delete( + &self, + write: RegistryWrite<'_, T>, + id: Id, + ) -> trc::Result { let object_type = T::object(); - let object_id = ObjectId::new(object_type, id); + let object_id = object_type.to_id(); + let item_id = id.id(); - let todo = "local registry"; + if self.0.local_objects.contains(&object_type) { + let object = ObjectId::new(object_type, id); + return if self.0.local_registry.write().remove(&object).is_some() { + self.write_local_registry() + .await + .map(|_| RegistryWriteResult::Success(id)) + } else { + Ok(RegistryWriteResult::NotFound { object_id: object }) + }; + } + + // Fetch object + let Some(object) = self.object::>(id).await? else { + return Ok(RegistryWriteResult::NotFound { + object_id: ObjectId::new(object_type, id), + }); + }; + + // Validate tenant and account changes + let mut clear_index = IndexBuilder::default(); + object.object.index(&mut clear_index); + if let Some(err) = write.validate_owner(&clear_index) { + return Ok(err); + } // Validate relationships let mut linked = Vec::new(); let key = KeySerializer::new(U64_LEN + U16_LEN + 1) .write(1u8) - .write(object_type.to_id()) - .write(id) + .write(object_id) + .write(item_id) .finalize(); let prefix_len = key.len(); let from_key = ValueKey::from(ValueClass::Any(AnyClass { @@ -69,8 +386,8 @@ impl RegistryStore { })); let key = KeySerializer::new((U64_LEN * 2) + U16_LEN + 1) .write(1u8) - .write(object_type.to_id()) - .write(id) + .write(object_id) + .write(item_id) .write(u64::MAX) .finalize(); let to_key = ValueKey::from(ValueClass::Any(AnyClass { @@ -100,7 +417,7 @@ impl RegistryStore { .details(object.as_str()) .ctx(trc::Key::Key, key) })?; - linked.push(ObjectId::new(object, id)); + linked.push(ObjectId::new(object, Id::new(id))); Ok(true) }, @@ -109,49 +426,92 @@ impl RegistryStore { .caused_by(trc::location!())?; if !linked.is_empty() { - return Ok(RegistryWriteResult::CannotDelete { + return Ok(RegistryWriteResult::CannotDeleteLinked { object_id: ObjectId::new(object_type, id), linked_objects: linked, }); } - let Some(object) = self.object::(id).await? else { - return Ok(RegistryWriteResult::NotFound { - object_id: ObjectId::new(object_type, id), - }); - }; - // Build deletion batch let mut batch = BatchBuilder::new(); - batch.clear(ValueClass::Registry(RegistryClass::Item(object_id))); - if object_type.flags() & OBJ_SEQ_ID != 0 { - batch.clear(ValueClass::Registry(RegistryClass::Id { - item_id: object_id, - })); - } - let mut index = IndexBuilder::default(); - object.index(&mut index); - batch.registry_index(object_id, index.keys.iter(), false); + batch + .assert_value( + ValueClass::Registry(RegistryClass::Item { object_id, item_id }), + AssertValue::Hash(object.hash), + ) + .clear(ValueClass::Registry(RegistryClass::Item { + object_id, + item_id, + })) + .clear(ValueClass::Registry(RegistryClass::Id { + object_id, + item_id, + })) + .registry_index(object_id, item_id, clear_index.keys.iter(), false); self.0 .store .write(batch.build_all()) .await - .map(|_| RegistryWriteResult::Success(())) + .map(|_| RegistryWriteResult::Success(Id::from(item_id))) .caused_by(trc::location!()) } + + pub async fn validate_primary_key( + &self, + from_key: RegistryClass, + to_key: RegistryClass, + object: Option, + ) -> trc::Result> { + let from_key = ValueKey::from(from_key); + let to_key = ValueKey::from(to_key); + let key_len = from_key.class.serialized_size() - 1; + + let mut result = None; + self.0 + .store + .iterate( + IterateParams::new(from_key, to_key).no_values().ascending(), + |key, _| { + if key.len() == key_len { + let item_id = key.deserialize_be_u64(key.len() - U64_LEN)?; + let object = if let Some(object) = object { + object + } else { + let object_id = + key.deserialize_be_u16(key.len() - U64_LEN - U16_LEN)?; + Object::from_id(object_id).ok_or_else(|| { + trc::EventType::Registry(trc::RegistryEvent::DeserializationError) + .into_err() + .caused_by(trc::location!()) + .ctx(trc::Key::Key, key) + })? + }; + + result = Some(ObjectId::new(object, Id::new(item_id))); + } + + Ok(false) + }, + ) + .await + .caused_by(trc::location!()) + .map(|_| result) + } } impl RegistryClass { - fn from_index_key(key: &IndexKey<'_>, item_id: ObjectId) -> Self { + pub fn from_index_key(key: &IndexKey<'_>, object_id: u16, item_id: u64) -> Self { match key { IndexKey::Unique { property, value } => RegistryClass::Index { index_id: property.to_id(), + object_id, item_id, key: value.serialize(), }, IndexKey::Search { property, value } => RegistryClass::Index { index_id: property.to_id(), + object_id, item_id, key: value.serialize(), }, @@ -161,12 +521,18 @@ impl RegistryClass { value_2, } => RegistryClass::IndexGlobal { index_id: property.to_id(), + object_id, item_id, key: serialize_composite_key(value_1, value_2), }, - IndexKey::ForeignKey { object_id, .. } => RegistryClass::Reference { - to: *object_id, - from: item_id, + IndexKey::ForeignKey { + object_id: to_object_id, + .. + } => RegistryClass::Reference { + to_object_id: to_object_id.object().to_id(), + to_item_id: to_object_id.id().id(), + from_item_id: item_id, + from_object_id: object_id, }, } } @@ -175,19 +541,20 @@ impl RegistryClass { impl BatchBuilder { fn registry_index<'x>( &mut self, - item_id: ObjectId, + object_id: u16, + item_id: u64, index_keys: impl Iterator>, is_set: bool, ) { for key in index_keys { if is_set { self.set( - ValueClass::Registry(RegistryClass::from_index_key(key, item_id)), + ValueClass::Registry(RegistryClass::from_index_key(key, object_id, item_id)), vec![], ); } else { self.clear(ValueClass::Registry(RegistryClass::from_index_key( - key, item_id, + key, object_id, item_id, ))); } } @@ -196,13 +563,11 @@ impl BatchBuilder { fn serialize_composite_key(value_1: &IndexValue<'_>, value_2: &IndexValue<'_>) -> Vec { let mut key = value_1.serialize(); - match value_2 { IndexValue::Text(text) => key.extend_from_slice(text.as_bytes()), IndexValue::Bytes(bytes) => key.extend_from_slice(bytes), IndexValue::U64(num) => key.extend_from_slice(&num.to_be_bytes()), IndexValue::I64(num) => key.extend_from_slice(&num.to_be_bytes()), - IndexValue::U32(num) => key.extend_from_slice(&num.to_be_bytes()), IndexValue::U16(num) => key.extend_from_slice(&num.to_be_bytes()), IndexValue::None => {} } @@ -216,9 +581,154 @@ impl SerializeInfallible for IndexValue<'_> { IndexValue::Bytes(bytes) => bytes.clone(), IndexValue::U64(num) => num.to_be_bytes().to_vec(), IndexValue::I64(num) => num.to_be_bytes().to_vec(), - IndexValue::U32(num) => num.to_be_bytes().to_vec(), IndexValue::U16(num) => num.to_be_bytes().to_vec(), IndexValue::None => vec![], } } } + +impl<'x, T: ObjectType> RegistryWrite<'x, T> { + pub fn insert(object: &'x T) -> Self { + Self { + op: RegistryWriteOp::Insert { object, id: None }, + current_tenant_id: None, + current_account_id: None, + } + } + + pub fn insert_with_id(id: Id, object: &'x T) -> Self { + Self { + op: RegistryWriteOp::Insert { + object, + id: Some(id), + }, + current_tenant_id: None, + current_account_id: None, + } + } + + pub fn update(id: Id, object: &'x T, old_object: &'x HashedObject) -> Self { + Self { + op: RegistryWriteOp::Update { + object, + id, + old_object, + }, + current_tenant_id: None, + current_account_id: None, + } + } + + pub fn delete(id: Id) -> Self { + Self { + op: RegistryWriteOp::Delete { id }, + current_tenant_id: None, + current_account_id: None, + } + } + + pub fn with_current_tenant_id(mut self, tenant_id: u32) -> Self { + self.current_tenant_id = Some(tenant_id); + self + } + + pub fn with_current_account_id(mut self, account_id: u32) -> Self { + self.current_account_id = Some(account_id); + self + } + + fn validate_owner(&self, builder: &IndexBuilder<'_>) -> Option { + // Validate tenant and account changes + if let Some(tenant_id) = self.current_tenant_id { + for key in &builder.keys { + if let IndexKey::Search { + property: Property::MemberTenantId, + value, + } = key + && value != &IndexValue::U64(tenant_id as u64) + { + return Some(RegistryWriteResult::InvalidTenantId); + } + } + } + if let Some(account_id) = self.current_account_id { + for key in &builder.keys { + if let IndexKey::Search { + property: Property::AccountId, + value, + } = key + && value != &IndexValue::U64(account_id as u64) + { + return Some(RegistryWriteResult::InvalidTenantId); + } + } + } + + None + } +} + +trait FindTenantId { + fn tenant_id(&self) -> Option; +} + +impl FindTenantId for IndexBuilder<'_> { + fn tenant_id(&self) -> Option { + self.keys.iter().find_map(|key| { + if let IndexKey::Search { + property: Property::MemberTenantId, + value: IndexValue::U64(tenant_id), + } = key + { + Some(*tenant_id as u32) + } else { + None + } + }) + } +} + +impl Display for RegistryWriteResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RegistryWriteResult::Success(id) => write!(f, "Success: {}", id), + RegistryWriteResult::CannotDeleteLinked { + object_id, + linked_objects, + } => { + write!(f, "Cannot delete {} because it is linked to: ", object_id)?; + for linked in linked_objects { + write!(f, "{}, ", linked)?; + } + Ok(()) + } + RegistryWriteResult::InvalidSingletonId => write!(f, "Invalid singleton id"), + RegistryWriteResult::CannotDeleteSingleton => write!(f, "Cannot delete singleton"), + RegistryWriteResult::NotFound { object_id } => write!(f, "Not found: {}", object_id), + RegistryWriteResult::InvalidForeignKey { object_id } => { + write!(f, "Invalid foreign key: {}", object_id) + } + RegistryWriteResult::PrimaryKeyConflict { + property, + existing_id, + } => { + write!( + f, + "Primary key conflict on property {:?} with existing object {}", + property.as_str(), + existing_id + ) + } + RegistryWriteResult::ValidationError { errors } => { + write!(f, "Validation error: ")?; + for error in errors { + write!(f, "{}, ", error)?; + } + Ok(()) + } + RegistryWriteResult::InvalidTenantId => write!(f, "Invalid tenant id"), + RegistryWriteResult::InvalidAccountId => write!(f, "Invalid account id"), + RegistryWriteResult::NotSupported => write!(f, "Operation not supported"), + } + } +} diff --git a/crates/store/src/write/assert.rs b/crates/store/src/write/assert.rs index eedeebe6..0920a25d 100644 --- a/crates/store/src/write/assert.rs +++ b/crates/store/src/write/assert.rs @@ -12,6 +12,7 @@ pub enum AssertValue { U32(u32), U64(u64), Archive(ArchiveVersion), + Hash(u64), Some, None, } @@ -77,6 +78,7 @@ impl AssertValue { }, AssertValue::None => false, AssertValue::Some => true, + AssertValue::Hash(v) => xxhash_rust::xxh3::xxh3_64(bytes) == *v, } } diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index bfab1f4f..73c43669 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -19,7 +19,6 @@ use crate::{ BlobLink, IndexPropertyClass, RegistryClass, SearchIndex, SearchIndexId, SearchIndexType, }, }; -use registry::types::EnumType; use std::convert::TryInto; use types::{ blob_hash::BLOB_HASH_LEN, @@ -375,41 +374,47 @@ impl ValueClass { InMemoryClass::Counter(key) => serializer.write(key.as_slice()), }, ValueClass::Registry(registry) => match registry { - RegistryClass::Item(object_id) => serializer + RegistryClass::Item { object_id, item_id } => serializer .write(0u8) - .write(object_id.object().to_id()) - .write_leb128(object_id.id()), - RegistryClass::Reference { to, from } => serializer + .write(*object_id) + .write_leb128(*item_id), + RegistryClass::Reference { + to_object_id, + to_item_id, + from_object_id, + from_item_id, + } => serializer .write(1u8) - .write(to.object().to_id()) - .write(to.id()) - .write(from.object().to_id()) - .write_leb128(from.id()), + .write(*to_object_id) + .write_leb128(*to_item_id) + .write(*from_object_id) + .write_leb128(*from_item_id), RegistryClass::Index { index_id, + object_id, item_id, key, } => serializer .write(2u8) - .write(item_id.object().to_id()) + .write(*object_id) .write(*index_id) .write(key.as_slice()) - .write(item_id.id()), + .write(*item_id), RegistryClass::IndexGlobal { index_id, + object_id, item_id, key, } => serializer .write(3u8) .write(*index_id) .write(key.as_slice()) - .write(item_id.object().to_id()) - .write(item_id.id()), - RegistryClass::Id { item_id } => serializer - .write(4u8) - .write(item_id.object().to_id()) - .write(item_id.id()), - RegistryClass::IdCounter { object } => serializer.write(object.to_id()), + .write(*object_id) + .write(*item_id), + RegistryClass::Id { object_id, item_id } => { + serializer.write(4u8).write(*object_id).write(*item_id) + } + RegistryClass::IdCounter { object_id } => serializer.write(*object_id), }, ValueClass::Queue(queue) => match queue { QueueClass::Message(queue_id) => serializer.write(*queue_id), @@ -598,7 +603,7 @@ impl ValueClass { ValueClass::Acl(_) => U32_LEN * 3 + 2, ValueClass::InMemory(InMemoryClass::Counter(v) | InMemoryClass::Key(v)) => v.len(), ValueClass::Registry(registry) => match registry { - RegistryClass::Item(_) => U16_LEN + U64_LEN + 2, + RegistryClass::Item { .. } => U16_LEN + U64_LEN + 2, RegistryClass::Reference { .. } => ((U16_LEN + U64_LEN) * 2) + 2, RegistryClass::Index { key, .. } | RegistryClass::IndexGlobal { key, .. } => { (U16_LEN * 2) + U64_LEN + key.len() + 2 diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index f2f22563..44b5d970 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -8,7 +8,6 @@ use self::assert::AssertValue; use crate::backend::MAX_TOKEN_LENGTH; use log::ChangeLogBuilder; use nlp::tokenizers::word::WordTokenizer; -use registry::{schema::prelude::Object, types::id::ObjectId}; use rkyv::util::AlignedVec; use std::{ collections::HashSet, @@ -271,26 +270,34 @@ pub enum InMemoryClass { #[derive(Debug, PartialEq, Clone, Eq, Hash)] pub enum RegistryClass { - Item(ObjectId), + Item { + object_id: u16, + item_id: u64, + }, Reference { - to: ObjectId, - from: ObjectId, + to_object_id: u16, + to_item_id: u64, + from_object_id: u16, + from_item_id: u64, }, Index { index_id: u16, - item_id: ObjectId, + object_id: u16, + item_id: u64, key: Vec, }, IndexGlobal { index_id: u16, - item_id: ObjectId, + object_id: u16, + item_id: u64, key: Vec, }, Id { - item_id: ObjectId, + object_id: u16, + item_id: u64, }, IdCounter { - object: Object, + object_id: u16, }, } diff --git a/crates/utils/src/cache.rs b/crates/utils/src/cache.rs index 45303460..04cacceb 100644 --- a/crates/utils/src/cache.rs +++ b/crates/utils/src/cache.rs @@ -99,6 +99,11 @@ impl Cache { pub fn clear(&self) { self.0.clear(); } + + #[inline(always)] + pub fn inner(&self) -> &quick_cache::sync::Cache { + &self.0 + } } impl CacheWithTtl {