From 0af735540dc34db311707bbe076ede72bf697650 Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:55:16 +0100 Subject: [PATCH] Authenticate using registry - part 1 --- Cargo.lock | 8 + crates/common/Cargo.toml | 4 +- crates/common/src/addresses.rs | 1 - crates/common/src/auth/access_token.rs | 5 +- crates/common/src/auth/authentication.rs | 261 ++++++++++ crates/common/src/auth/mod.rs | 450 +++++++----------- crates/common/src/auth/rate_limit.rs | 1 + crates/common/src/auth/roles.rs | 67 +-- crates/common/src/config/inner.rs | 25 +- crates/common/src/config/mailstore/email.rs | 2 +- .../common/src/config/mailstore/spamfilter.rs | 17 +- crates/common/src/config/server/tls.rs | 30 +- crates/common/src/config/smtp/auth.rs | 12 - crates/common/src/config/smtp/queue.rs | 22 +- crates/common/src/config/smtp/report.rs | 23 +- crates/common/src/config/smtp/resolver.rs | 10 +- crates/common/src/config/smtp/session.rs | 21 +- crates/common/src/config/storage.rs | 13 +- crates/common/src/config/telemetry.rs | 2 +- crates/common/src/core.rs | 45 +- crates/common/src/expr/functions/asynch.rs | 2 - crates/common/src/expr/functions/mod.rs | 4 +- crates/common/src/expr/if_block.rs | 23 +- crates/common/src/expr/mod.rs | 20 +- crates/common/src/expr/tokenizer.rs | 5 +- crates/common/src/lib.rs | 33 +- crates/common/src/listener/acme/cache.rs | 1 - crates/common/src/listener/limiter.rs | 48 +- crates/directory/src/core/config.rs | 23 +- crates/directory/src/lib.rs | 1 + crates/email/src/sieve/ingest.rs | 62 ++- crates/migration/src/mailbox.rs | 2 +- crates/registry/src/types/mod.rs | 2 + crates/utils/src/cache.rs | 12 + 34 files changed, 670 insertions(+), 587 deletions(-) create mode 100644 crates/common/src/auth/authentication.rs diff --git a/Cargo.lock b/Cargo.lock index ffe1eb4c..7a40cdcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" + [[package]] name = "argon2" version = "0.5.3" @@ -1060,6 +1066,7 @@ dependencies = [ "aes-gcm-siv", "ahash", "arc-swap", + "arcstr", "base64 0.22.1", "bincode 2.0.1", "biscuit", @@ -1089,6 +1096,7 @@ dependencies = [ "mail-send", "md5 0.8.0", "nlp", + "nohash-hasher", "num_cpus", "opentelemetry", "opentelemetry-otlp", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 0eb7f99b..26c80b5c 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -73,10 +73,12 @@ num_cpus = "1.13.1" hashify = "0.2" rkyv = { version = "0.8.10", features = ["little_endian"] } indexmap = "2.7.1" -tinyvec = "1.9.0" +tinyvec = { version = "1.10.0", features = ["alloc"] } compact_str = { version = "0.9.0", features = ["rkyv", "serde"] } lz4_flex = { version = "0.12", features = ["frame"], default-features = false } hickory-proto = "0.24" +arcstr = "1.2.0" +nohash-hasher = "0.2.0" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index 65a055c8..7aec8d11 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -7,7 +7,6 @@ use directory::Directory; use registry::schema::enums::ExpressionVariable; use std::borrow::Cow; -use utils::config::{Config, utils::AsKey}; use crate::{ Server, diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 4b1e97ff..42f286f4 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -4,13 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{AccessToken, ResourceToken, TenantInfo, roles::RolePermissions}; +use super::{AccessToken, ResourceToken, TenantInfo, roles::PermissionsGroup}; use crate::{ Server, ipc::BroadcastEvent, listener::limiter::{ConcurrencyLimiter, LimiterResult}, }; use ahash::AHashSet; +use registry::schema::enums::Permission; use std::{ hash::{DefaultHasher, Hash, Hasher}, sync::Arc, @@ -34,7 +35,7 @@ impl Server { principal: Principal, revision: u64, ) -> trc::Result { - let mut role_permissions = RolePermissions::default(); + let mut role_permissions = PermissionsGroup::default(); // Extract data let mut object_quota = self.core.email.max_objects; diff --git a/crates/common/src/auth/authentication.rs b/crates/common/src/auth/authentication.rs new file mode 100644 index 00000000..5e956bfd --- /dev/null +++ b/crates/common/src/auth/authentication.rs @@ -0,0 +1,261 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +impl Server { + pub async fn authenticate(&self, req: &AuthRequest<'_>) -> trc::Result> { + // Resolve directory + let directory = req.directory.unwrap_or(&self.core.storage.directory); + + // Validate credentials + match &req.credentials { + Credentials::OAuthBearer { token } if !directory.has_bearer_token_support() => { + match self + .validate_access_token(GrantType::AccessToken.into(), token) + .await + { + Ok(token_into) => self.get_access_token(token_into.account_id).await, + Err(err) => Err(err), + } + } + _ => match self.authenticate_credentials(req, directory).await { + Ok(principal) => self.get_access_token(principal).await, + Err(err) => Err(err), + }, + } + .and_then(|token| { + token + .assert_has_permission(Permission::Authenticate) + .map(|_| token) + }) + } + + async fn authenticate_credentials( + &self, + req: &AuthRequest<'_>, + directory: &Directory, + ) -> trc::Result { + // First try to authenticate the user against the default directory + let result = match directory + .query( + QueryParams::credentials(&req.credentials) + .with_return_member_of(req.return_member_of), + ) + .await + { + Ok(Some(principal)) => { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = principal.name().to_string(), + AccountId = principal.id(), + SpanId = req.session_id, + ); + + return Ok(principal); + } + Ok(None) => Ok(()), + Err(err) => { + if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) { + return Err(err); + } else { + Err(err) + } + } + }; + + match &req.credentials { + Credentials::Plain { username, secret } => { + // Then check if the credentials match the fallback admin or master user + let master_user: Option<(String, String)> = None; + let todo = "implement master"; + match (&self.core.network.security.fallback_admin, &master_user) { + (Some((fallback_admin, fallback_pass)), _) if username == fallback_admin => { + if verify_secret_hash(fallback_pass, secret).await? { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = username.clone(), + SpanId = req.session_id, + ); + + return Ok(Principal::fallback_admin(fallback_pass)); + } + } + (_, Some((master_user, master_pass))) if username.ends_with(master_user) => { + if verify_secret_hash(master_pass, secret).await? { + let username = username.strip_suffix(master_user).unwrap(); + let username = username.strip_suffix('%').unwrap_or(username); + + if let Some(principal) = directory + .query( + QueryParams::name(username) + .with_return_member_of(req.return_member_of), + ) + .await? + { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = username.to_string(), + SpanId = req.session_id, + AccountId = principal.id(), + Type = principal.typ().description(), + ); + + return Ok(principal); + } + } + } + _ => { + // Validate API credentials + if req.allow_api_access + && let Ok(Some(principal)) = self + .store() + .query( + QueryParams::credentials(&req.credentials) + .with_return_member_of(req.return_member_of), + ) + .await + && principal.typ == Type::ApiKey + { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = principal.name().to_string(), + AccountId = principal.id(), + SpanId = req.session_id, + ); + + return Ok(principal); + } + } + } + } + Credentials::OAuthBearer { token } if directory.has_bearer_token_support() => { + // Check for bearer tokens issued locally + if let Ok(token_info) = self + .validate_access_token(GrantType::AccessToken.into(), token) + .await + { + let principal = if token_info.account_id != FALLBACK_ADMIN_ID { + directory + .query( + QueryParams::id(token_info.account_id) + .with_return_member_of(req.return_member_of), + ) + .await + .unwrap_or_default() + } else if let Some((_, fallback_pass)) = + &self.core.network.security.fallback_admin + { + Principal::fallback_admin(fallback_pass).into() + } else { + None + }; + if let Some(principal) = principal { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = principal.name().to_string(), + AccountId = principal.id(), + SpanId = req.session_id, + ); + + return Ok(principal); + } + } + } + _ => (), + }; + + if let Err(err) = result { + Err(err) + } else if self.has_auth_fail2ban() { + let login = req.credentials.login(); + if self.is_auth_fail2banned(req.remote_ip, login).await? { + Err(trc::SecurityEvent::AuthenticationBan + .into_err() + .ctx(trc::Key::RemoteIp, req.remote_ip) + .ctx_opt(trc::Key::AccountName, login.map(|s| s.to_string()))) + } else { + Err(trc::AuthEvent::Failed + .ctx(trc::Key::RemoteIp, req.remote_ip) + .ctx_opt(trc::Key::AccountName, login.map(|s| s.to_string()))) + } + } else { + Err(trc::AuthEvent::Failed + .ctx(trc::Key::RemoteIp, req.remote_ip) + .ctx_opt( + trc::Key::AccountName, + req.credentials.login().map(|s| s.to_string()), + )) + } + } +} + +impl<'x> AuthRequest<'x> { + pub fn from_credentials( + credentials: Credentials, + session_id: u64, + remote_ip: IpAddr, + ) -> Self { + Self { + credentials, + session_id, + remote_ip, + return_member_of: true, + directory: None, + allow_api_access: false, + } + } + + pub fn from_plain( + user: impl Into, + pass: impl Into, + session_id: u64, + remote_ip: IpAddr, + ) -> Self { + Self::from_credentials( + Credentials::Plain { + username: user.into(), + secret: pass.into(), + }, + session_id, + remote_ip, + ) + } + + pub fn without_members(mut self) -> Self { + self.return_member_of = false; + self + } + + pub fn with_directory(mut self, directory: &'x Directory) -> Self { + self.directory = Some(directory); + self + } + + pub fn with_api_access(mut self, allow_api_access: bool) -> Self { + self.allow_api_access = allow_api_access; + self + } +} + +impl CacheItemWeight for AccessToken { + fn weight(&self) -> u64 { + self.obj_size + } +} + +pub(crate) trait CredentialsUsername { + fn login(&self) -> Option<&str>; +} + +impl CredentialsUsername for Credentials { + fn login(&self) -> Option<&str> { + match self { + Credentials::Plain { username, .. } | Credentials::XOauth2 { username, .. } => { + username.as_str().into() + } + Credentials::OAuthBearer { .. } => None, + } + } +} diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 9948406b..ad213b19 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -4,35 +4,183 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{Server, listener::limiter::ConcurrencyLimiter}; -use mail_send::Credentials; -use oauth::GrantType; -use std::{net::IpAddr, sync::Arc}; -use types::collection::Collection; -use utils::{ - cache::CacheItemWeight, - map::{bitmap::Bitmap, vec_map::VecMap}, +use crate::{expr::if_block::IfBlock, listener::limiter::ConcurrencyLimiter}; +use ahash::AHashMap; +use arc_swap::ArcSwap; +use arcstr::ArcStr; +use directory::Credentials; +use registry::{ + schema::enums::{Locale, Permission, StorageObject}, + types::EnumType, }; +use std::{collections::HashMap, net::IpAddr, sync::Arc}; +use tinyvec::TinyVec; +use trc::ipc::bitset::Bitset; +use types::collection::Collection; +use utils::{cache::CacheItemWeight, map::bitmap::Bitmap}; pub mod access_token; +pub mod authentication; pub mod oauth; pub mod rate_limit; pub mod roles; pub mod sasl; +const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::()); +pub type Permissions = Bitset; +pub type ObjectQuota = [u32; StorageObject::COUNT - 1]; +pub type IdMap = HashMap>; + +pub struct DirectoryEntries { + pub emails: ArcSwap, + pub domains: ArcSwap, + pub accounts: ArcSwap, + pub groups: ArcSwap, + pub roles: ArcSwap, + pub mailing_lists: ArcSwap, + pub tenants: ArcSwap, + pub api_keys: ArcSwap, +} + +#[derive(Debug, Clone)] +pub struct EmailEntries { + pub addresses: AHashMap, +} + +#[derive(Debug, Clone)] +pub struct EmailEntry { + pub id: u32, + pub flags: u8, +} + +pub const EMAIL_FLAG_ACCOUNT: u8 = 1; +pub const EMAIL_FLAG_GROUP: u8 = 1 << 1; +pub const EMAIL_FLAG_MAILING_LIST: u8 = 1 << 2; +pub const EMAIL_FLAG_ALIAS: u8 = 1 << 3; +pub const EMAIL_FLAG_EXPIRES: u8 = 1 << 4; + +#[derive(Debug, Clone)] +pub struct DomainEntries { + pub names: AHashMap, + pub entries: IdMap, + pub default: u32, +} + +#[derive(Debug, Clone)] +pub struct AccountEntries { + pub entries: IdMap, +} + +#[derive(Debug, Clone)] +pub struct GroupEntries { + pub entries: IdMap, +} + +#[derive(Debug, Clone)] +pub struct RoleEntries { + pub entries: IdMap, +} + +#[derive(Debug, Clone)] +pub struct MailingListEntries { + pub entries: IdMap, +} + +#[derive(Debug, Clone)] +pub struct TenantEntries { + pub entries: IdMap, +} + +#[derive(Debug, Clone)] +pub struct ApiKeyEntries { + pub entries: AHashMap, +} + +#[derive(Debug, Clone)] +pub struct DomainEntry { + pub name: ArcStr, + pub id_alias_of: u32, + pub id_tenant: u32, + pub id_directory: u32, + pub catch_all: Option, + pub sub_addressing_custom: Option>, + pub flags: u8, +} + +pub const DOMAIN_FLAG_LOCAL: u8 = 1; +pub const DOMAIN_FLAG_DEFAULT: u8 = 1 << 1; +pub const DOMAIN_FLAG_SUB_ADDRESSING: u8 = 1 << 2; +pub const DOMAIN_FLAG_WILDCARD: u8 = 1 << 3; +pub const DOMAIN_FLAG_ALIAS_LOGIN: u8 = 1 << 4; + +#[derive(Debug, Clone)] +pub struct AccountEntry { + pub addresses: Arc<[ArcStr]>, + pub id_tenant: u32, + pub description: Option, + pub locale: Locale, +} + +#[derive(Debug, Clone)] +pub struct GroupEntry { + pub addresses: Arc<[ArcStr]>, + pub id_member_of: TinyVec<[u32; 3]>, + pub id_tenant: u32, + pub id_roles: TinyVec<[u32; 3]>, + pub quota_disk: u64, + pub quota_objects: Option>, + pub permissions: Option>, +} + +#[derive(Debug, Clone)] +pub struct RoleEntry { + pub id_tenant: u32, + pub id_roles: TinyVec<[u32; 3]>, + pub permissions: Permissions, +} + +#[derive(Debug, Clone)] +pub struct MailingListEntry { + pub addresses: Arc<[ArcStr]>, + pub id_tenant: u32, + pub recipients: Arc<[ArcStr]>, +} + +#[derive(Debug, Clone)] +pub struct TenantEntry { + pub id_roles: TinyVec<[u32; 3]>, + pub quota_disk: u64, + pub quota_objects: Option>, + pub permissions: Option>, +} + +#[derive(Debug, Clone)] +pub struct ApiKeyEntry { + pub id: u32, + pub id_tenant: u32, + pub id_roles: TinyVec<[u32; 3]>, + pub permissions: Option>, + pub expires_at: u64, +} + +#[derive(Debug, Clone, Default)] +pub struct PermissionsGroup { + pub enabled: Permissions, + pub disabled: Permissions, + pub merge: bool, +} + #[derive(Debug, Default)] pub struct AccessToken { + pub addresses: Arc<[ArcStr]>, pub primary_id: u32, - pub member_of: Vec, - pub access_to: VecMap>, - pub name: String, - pub description: Option, - pub locale: Option, - pub emails: Vec, - pub quota: u64, - pub object_quota: [u32; Collection::MAX], + pub member_of: TinyVec<[u32; 3]>, + pub access_to: Box<[AccessTo]>, + pub quota_disk: u64, + pub quota_disk_tenant: u64, + pub quota_disk_domain: u64, + pub quota_objects: ObjectQuota, pub permissions: Permissions, - pub tenant: Option, pub concurrent_http_requests: Option, pub concurrent_imap_requests: Option, pub concurrent_uploads: Option, @@ -40,261 +188,18 @@ pub struct AccessToken { pub obj_size: u64, } -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct TenantInfo { - pub id: u32, - pub quota: u64, -} - -#[derive(Debug, Clone, Default)] -pub struct ResourceToken { +#[derive(Debug, Default)] +pub struct AccessTo { pub account_id: u32, - pub quota: u64, - pub tenant: Option, + pub collections: Bitmap, } -pub struct AuthRequest<'x> { - credentials: Credentials, +pub struct AuthRequest { + credentials: Credentials, session_id: u64, remote_ip: IpAddr, return_member_of: bool, allow_api_access: bool, - directory: Option<&'x Directory>, -} - -impl Server { - pub async fn authenticate(&self, req: &AuthRequest<'_>) -> trc::Result> { - // Resolve directory - let directory = req.directory.unwrap_or(&self.core.storage.directory); - - // Validate credentials - match &req.credentials { - Credentials::OAuthBearer { token } if !directory.has_bearer_token_support() => { - match self - .validate_access_token(GrantType::AccessToken.into(), token) - .await - { - Ok(token_into) => self.get_access_token(token_into.account_id).await, - Err(err) => Err(err), - } - } - _ => match self.authenticate_credentials(req, directory).await { - Ok(principal) => self.get_access_token(principal).await, - Err(err) => Err(err), - }, - } - .and_then(|token| { - token - .assert_has_permission(Permission::Authenticate) - .map(|_| token) - }) - } - - async fn authenticate_credentials( - &self, - req: &AuthRequest<'_>, - directory: &Directory, - ) -> trc::Result { - // First try to authenticate the user against the default directory - let result = match directory - .query( - QueryParams::credentials(&req.credentials) - .with_return_member_of(req.return_member_of), - ) - .await - { - Ok(Some(principal)) => { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = principal.name().to_string(), - AccountId = principal.id(), - SpanId = req.session_id, - ); - - return Ok(principal); - } - Ok(None) => Ok(()), - Err(err) => { - if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) { - return Err(err); - } else { - Err(err) - } - } - }; - - match &req.credentials { - Credentials::Plain { username, secret } => { - // Then check if the credentials match the fallback admin or master user - let master_user: Option<(String, String)> = None; - let todo = "implement master"; - match (&self.core.network.security.fallback_admin, &master_user) { - (Some((fallback_admin, fallback_pass)), _) if username == fallback_admin => { - if verify_secret_hash(fallback_pass, secret).await? { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = username.clone(), - SpanId = req.session_id, - ); - - return Ok(Principal::fallback_admin(fallback_pass)); - } - } - (_, Some((master_user, master_pass))) if username.ends_with(master_user) => { - if verify_secret_hash(master_pass, secret).await? { - let username = username.strip_suffix(master_user).unwrap(); - let username = username.strip_suffix('%').unwrap_or(username); - - if let Some(principal) = directory - .query( - QueryParams::name(username) - .with_return_member_of(req.return_member_of), - ) - .await? - { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = username.to_string(), - SpanId = req.session_id, - AccountId = principal.id(), - Type = principal.typ().description(), - ); - - return Ok(principal); - } - } - } - _ => { - // Validate API credentials - if req.allow_api_access - && let Ok(Some(principal)) = self - .store() - .query( - QueryParams::credentials(&req.credentials) - .with_return_member_of(req.return_member_of), - ) - .await - && principal.typ == Type::ApiKey - { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = principal.name().to_string(), - AccountId = principal.id(), - SpanId = req.session_id, - ); - - return Ok(principal); - } - } - } - } - Credentials::OAuthBearer { token } if directory.has_bearer_token_support() => { - // Check for bearer tokens issued locally - if let Ok(token_info) = self - .validate_access_token(GrantType::AccessToken.into(), token) - .await - { - let principal = if token_info.account_id != FALLBACK_ADMIN_ID { - directory - .query( - QueryParams::id(token_info.account_id) - .with_return_member_of(req.return_member_of), - ) - .await - .unwrap_or_default() - } else if let Some((_, fallback_pass)) = - &self.core.network.security.fallback_admin - { - Principal::fallback_admin(fallback_pass).into() - } else { - None - }; - if let Some(principal) = principal { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = principal.name().to_string(), - AccountId = principal.id(), - SpanId = req.session_id, - ); - - return Ok(principal); - } - } - } - _ => (), - }; - - if let Err(err) = result { - Err(err) - } else if self.has_auth_fail2ban() { - let login = req.credentials.login(); - if self.is_auth_fail2banned(req.remote_ip, login).await? { - Err(trc::SecurityEvent::AuthenticationBan - .into_err() - .ctx(trc::Key::RemoteIp, req.remote_ip) - .ctx_opt(trc::Key::AccountName, login.map(|s| s.to_string()))) - } else { - Err(trc::AuthEvent::Failed - .ctx(trc::Key::RemoteIp, req.remote_ip) - .ctx_opt(trc::Key::AccountName, login.map(|s| s.to_string()))) - } - } else { - Err(trc::AuthEvent::Failed - .ctx(trc::Key::RemoteIp, req.remote_ip) - .ctx_opt( - trc::Key::AccountName, - req.credentials.login().map(|s| s.to_string()), - )) - } - } -} - -impl<'x> AuthRequest<'x> { - pub fn from_credentials( - credentials: Credentials, - session_id: u64, - remote_ip: IpAddr, - ) -> Self { - Self { - credentials, - session_id, - remote_ip, - return_member_of: true, - directory: None, - allow_api_access: false, - } - } - - pub fn from_plain( - user: impl Into, - pass: impl Into, - session_id: u64, - remote_ip: IpAddr, - ) -> Self { - Self::from_credentials( - Credentials::Plain { - username: user.into(), - secret: pass.into(), - }, - session_id, - remote_ip, - ) - } - - pub fn without_members(mut self) -> Self { - self.return_member_of = false; - self - } - - pub fn with_directory(mut self, directory: &'x Directory) -> Self { - self.directory = Some(directory); - self - } - - pub fn with_api_access(mut self, allow_api_access: bool) -> Self { - self.allow_api_access = allow_api_access; - self - } } impl CacheItemWeight for AccessToken { @@ -302,18 +207,3 @@ impl CacheItemWeight for AccessToken { self.obj_size } } - -pub(crate) trait CredentialsUsername { - fn login(&self) -> Option<&str>; -} - -impl CredentialsUsername for Credentials { - fn login(&self) -> Option<&str> { - match self { - Credentials::Plain { username, .. } | Credentials::XOauth2 { username, .. } => { - username.as_str().into() - } - Credentials::OAuthBearer { .. } => None, - } - } -} diff --git a/crates/common/src/auth/rate_limit.rs b/crates/common/src/auth/rate_limit.rs index ffa5efad..5a637799 100644 --- a/crates/common/src/auth/rate_limit.rs +++ b/crates/common/src/auth/rate_limit.rs @@ -9,6 +9,7 @@ use crate::{ KV_RATE_LIMIT_HTTP_ANONYMOUS, KV_RATE_LIMIT_HTTP_AUTHENTICATED, Server, ip_to_bytes, listener::limiter::{InFlight, LimiterResult}, }; +use registry::schema::enums::Permission; use std::net::IpAddr; use trc::AddContext; diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs index b5aa7c51..5f1ff422 100644 --- a/crates/common/src/auth/roles.rs +++ b/crates/common/src/auth/roles.rs @@ -4,25 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::Server; +use crate::{ + Server, + auth::{Permissions, PermissionsGroup}, +}; use ahash::AHashSet; use std::sync::{Arc, LazyLock}; use trc::AddContext; use utils::cache::CacheItemWeight; -#[derive(Debug, Clone, Default)] -pub struct RolePermissions { - pub enabled: Permissions, - pub disabled: Permissions, -} - -static USER_PERMISSIONS: LazyLock> = LazyLock::new(user_permissions); -static ADMIN_PERMISSIONS: LazyLock> = LazyLock::new(admin_permissions); -static TENANT_ADMIN_PERMISSIONS: LazyLock> = - LazyLock::new(tenant_admin_permissions); - impl Server { - pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result> { + pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result> { match role_id { ROLE_USER => Ok(USER_PERMISSIONS.clone()), ROLE_ADMIN => Ok(ADMIN_PERMISSIONS.clone()), @@ -46,11 +38,11 @@ impl Server { } } - async fn build_role_permissions(&self, role_id: u32) -> trc::Result> { + async fn build_role_permissions(&self, role_id: u32) -> trc::Result> { let mut role_ids = vec![role_id].into_iter(); let mut role_ids_stack = vec![]; let mut fetched_role_ids = AHashSet::new(); - let mut return_permissions = RolePermissions::default(); + let mut return_permissions = PermissionsGroup::default(); 'outer: loop { if let Some(role_id) = role_ids.next() { @@ -86,7 +78,7 @@ impl Server { if let Some(role_permissions) = self.inner.cache.permissions.get(&role_id) { return_permissions.union(role_permissions.as_ref()); } else { - let mut role_permissions = RolePermissions::default(); + let mut role_permissions = PermissionsGroup::default(); // Obtain principal let principal = self @@ -143,8 +135,8 @@ impl Server { } } -impl RolePermissions { - pub fn union(&mut self, other: &RolePermissions) { +impl PermissionsGroup { + pub fn union(&mut self, other: &PermissionsGroup) { self.enabled.union(&other.enabled); self.disabled.union(&other.disabled); } @@ -160,42 +152,3 @@ impl RolePermissions { enabled } } - -fn tenant_admin_permissions() -> Arc { - let mut permissions = RolePermissions::default(); - - for permission_id in 0..Permission::COUNT { - let permission = Permission::from_id(permission_id as u32).unwrap(); - if permission.is_tenant_admin_permission() { - permissions.enabled.set(permission_id); - } - } - - Arc::new(permissions) -} - -fn user_permissions() -> Arc { - let mut permissions = RolePermissions::default(); - - for permission_id in 0..Permission::COUNT { - let permission = Permission::from_id(permission_id as u32).unwrap(); - if permission.is_user_permission() { - permissions.enabled.set(permission_id); - } - } - - Arc::new(permissions) -} - -fn admin_permissions() -> Arc { - Arc::new(RolePermissions { - enabled: Permissions::all(), - disabled: Permissions::new(), - }) -} - -impl CacheItemWeight for RolePermissions { - fn weight(&self) -> u64 { - std::mem::size_of::() as u64 - } -} diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 52b37387..6a7b0101 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -8,7 +8,7 @@ use super::server::tls::build_self_signed_cert; use crate::{ CacheSwap, Caches, Data, DavResource, DavResources, MailboxCache, MessageStoreCache, MessageUidCache, TlsConnectors, - auth::{AccessToken, roles::RolePermissions}, + auth::AccessToken, config::{ mailstore::spamfilter::SpamClassifier, server::tls::parse_certificates, @@ -40,7 +40,7 @@ impl Data { let mut subject_names = AHashSet::new(); parse_certificates(bp, &mut certificates, &mut subject_names); if subject_names.is_empty() { - subject_names.insert("localhost".to_string()); + subject_names.insert("localhost".into()); } // Build and test snowflake id generator @@ -56,7 +56,10 @@ impl Data { spam_classifier: ArcSwap::from_pointee(SpamClassifier::default()), tls_certificates: ArcSwap::from_pointee(certificates), tls_self_signed_cert: build_self_signed_cert( - subject_names.into_iter().collect::>(), + subject_names + .into_iter() + .map(Into::into) + .collect::>(), ) .or_else(|err| { bp.build_error( @@ -79,6 +82,7 @@ impl Data { logos: Default::default(), smtp_connectors: TlsConnectors::default(), asn_geo_data: Default::default(), + lookup_stores: Default::default(), } } } @@ -93,10 +97,6 @@ impl Caches { (std::mem::size_of::() + 255) as u64, ), http_auth: Cache::new(cache.http_auth, (50 + std::mem::size_of::()) as u64), - permissions: Cache::new( - cache.permissions, - std::mem::size_of::() as u64, - ), messages: Cache::new( cache.messages, (std::mem::size_of::() @@ -155,11 +155,11 @@ impl Caches { ) -> Parameters< '_, T, - CacheWithTtl, - CacheWithTtl>>, - CacheWithTtl>>, - CacheWithTtl>>, - CacheWithTtl>>, + CacheWithTtl, Txt>, + CacheWithTtl, Arc>>, + CacheWithTtl, Arc>>, + CacheWithTtl, Arc>>, + CacheWithTtl]>>>, > { Parameters { params, @@ -187,6 +187,7 @@ impl Default for Data { logos: Default::default(), smtp_connectors: Default::default(), asn_geo_data: Default::default(), + lookup_stores: Default::default(), } } } diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs index 19cbb3d6..bf841003 100644 --- a/crates/common/src/config/mailstore/email.rs +++ b/crates/common/src/config/mailstore/email.rs @@ -22,7 +22,7 @@ use store::{ write::SearchIndex, }; use types::{collection::Collection, special_use::SpecialUse}; -use utils::config::cron::SimpleCron; +use utils::cron::SimpleCron; #[derive(Clone)] pub struct EmailConfig { diff --git a/crates/common/src/config/mailstore/spamfilter.rs b/crates/common/src/config/mailstore/spamfilter.rs index 58430028..d025b00e 100644 --- a/crates/common/src/config/mailstore/spamfilter.rs +++ b/crates/common/src/config/mailstore/spamfilter.rs @@ -26,7 +26,7 @@ use std::{ }; use store::registry::{RegistryObject, bootstrap::Bootstrap}; use tokio::net::lookup_host; -use utils::{cache::CacheItemWeight, config::utils::ParseValue, glob::GlobMap}; +use utils::{cache::CacheItemWeight, glob::GlobMap}; #[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default)] pub enum SpamClassifier { @@ -522,21 +522,6 @@ impl SpamClassifier { } } -impl ParseValue for Element { - fn parse_value(value: &str) -> utils::config::Result { - match value { - "url" => Ok(Element::Url), - "domain" => Ok(Element::Domain), - "email" => Ok(Element::Email), - "ip" => Ok(Element::Ip), - "header" => Ok(Element::Header), - "body" => Ok(Element::Body), - "any" | "message" => Ok(Element::Any), - other => Err(format!("Invalid type {other:?}.",)), - } - } -} - impl Location { pub fn as_str(&self) -> &'static str { match self { diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index c1df0a06..6516ed47 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -197,8 +197,8 @@ impl Server { pub(crate) async fn parse_certificates( bp: &mut Bootstrap, - certificates: &mut AHashMap>, - subject_names: &mut AHashSet, + certificates: &mut AHashMap, Arc>, + subject_names: &mut AHashSet>, ) { // Parse certificates for cert_obj in bp.list_infallible::().await { @@ -216,10 +216,10 @@ pub(crate) async fn parse_certificates( }) { Ok((_, parsed)) => { // Add CNs and SANs to the list of names - let mut names = AHashSet::new(); + let mut names: AHashSet> = AHashSet::new(); for name in parsed.subject().iter_common_name() { if let Ok(name) = name.as_str() { - names.insert(name.to_string()); + names.insert(name.into()); } } for ext in parsed.extensions() { @@ -227,14 +227,16 @@ pub(crate) async fn parse_certificates( ext.parsed_extension() { for name in &san.general_names { - let name = match name { - GeneralName::DNSName(name) => name.to_string(), + let name: Box = match name { + GeneralName::DNSName(name) => (*name).into(), GeneralName::IPAddress(ip) => match ip.len() { 4 => Ipv4Addr::from(<[u8; 4]>::try_from(*ip).unwrap()) - .to_string(), + .to_string() + .into(), 16 => { Ipv6Addr::from(<[u8; 16]>::try_from(*ip).unwrap()) .to_string() + .into() } _ => continue, }, @@ -248,7 +250,13 @@ pub(crate) async fn parse_certificates( } // Add custom SNIs - names.extend(cert_obj.object.subject_alternative_names); + names.extend( + cert_obj + .object + .subject_alternative_names + .into_iter() + .map(Into::into), + ); // Add domain names subject_names.extend(names.iter().cloned()); @@ -257,16 +265,14 @@ pub(crate) async fn parse_certificates( let cert = Arc::new(cert); for name in names { certificates.insert( - name.strip_prefix("*.") - .map(|name| name.to_string()) - .unwrap_or(name), + name.strip_prefix("*.").map(Into::into).unwrap_or(name), cert.clone(), ); } // Add default certificate if cert_obj.object.default { - certificates.insert("*".to_string(), cert.clone()); + certificates.insert("*".into(), cert.clone()); } } Err(err) => { diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index 5362ca08..a39eefef 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -23,7 +23,6 @@ use registry::{ }; use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, pem::PemObject}; use store::registry::bootstrap::Bootstrap; -use utils::config::utils::ParseValue; #[derive(Clone)] pub struct MailAuthConfig { @@ -419,14 +418,3 @@ impl VerifyStrategy { matches!(self, VerifyStrategy::Strict) } } - -impl ParseValue for VerifyStrategy { - fn parse_value(value: &str) -> Result { - match value { - "relaxed" => Ok(VerifyStrategy::Relaxed), - "strict" => Ok(VerifyStrategy::Strict), - "disable" | "disabled" | "never" | "none" => Ok(VerifyStrategy::Disable), - _ => Err(format!("Invalid value {:?}.", value)), - } - } -} diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index d337aa49..3f3445cd 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -30,7 +30,6 @@ use std::{ net::IpAddr, time::Duration, }; -use utils::config::utils::ParseValue; #[derive( Debug, @@ -600,7 +599,14 @@ impl<'x> TryFrom> for IpLookupStrategy { _ => Err(()), }, Variable::String(value) => { - IpLookupStrategy::parse_value(value.as_str()).map_err(|_| ()) + match value.as_str() { + "ipv4_only" => Ok(IpLookupStrategy::Ipv4Only), + "ipv6_only" => Ok(IpLookupStrategy::Ipv6Only), + //"ipv4_and_ipv6" => IpLookupStrategy::Ipv4AndIpv6, + "ipv6_then_ipv4" => Ok(IpLookupStrategy::Ipv6thenIpv4), + "ipv4_then_ipv6" => Ok(IpLookupStrategy::Ipv4thenIpv6), + _ => Err(()), + } } _ => Err(()), } @@ -726,18 +732,6 @@ impl Default for QueueName { } } -impl ParseValue for QueueName { - fn parse_value(value: &str) -> Result { - if let Some(name) = QueueName::new(value.trim().as_bytes()) { - Ok(name) - } else { - Err(format!( - "Queue name '{value}' is too long. Maximum length is 8 bytes." - )) - } - } -} - impl Display for QueueName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.as_str().fmt(f) diff --git a/crates/common/src/config/smtp/report.rs b/crates/common/src/config/smtp/report.rs index 02d6eb5c..4057d4f1 100644 --- a/crates/common/src/config/smtp/report.rs +++ b/crates/common/src/config/smtp/report.rs @@ -17,8 +17,7 @@ use registry::schema::{ TlsReportSettings, }, }; -use std::time::Duration; -use utils::config::utils::ParseValue; +use std::{str::FromStr, time::Duration}; #[derive(Clone)] pub struct ReportConfig { @@ -93,7 +92,7 @@ impl ReportConfig { addresses: report .inbound_report_addresses .iter() - .filter_map(|addr| AddressMatch::parse_value(addr).ok()) + .filter_map(|addr| AddressMatch::from_str(addr).ok()) .collect(), forward: report.inbound_report_forwarding, store: dr.hold_mta_reports_for.map(|d| d.into_inner()), @@ -215,18 +214,6 @@ impl ReportConfig { } } -impl ParseValue for AggregateFrequency { - fn parse_value(value: &str) -> Result { - match value { - "daily" | "day" => Ok(AggregateFrequency::Daily), - "hourly" | "hour" => Ok(AggregateFrequency::Hourly), - "weekly" | "week" => Ok(AggregateFrequency::Weekly), - "never" | "disable" | "false" => Ok(AggregateFrequency::Never), - _ => Err(format!("Invalid aggregate frequency value {:?}.", value,)), - } - } -} - impl<'x> TryFrom> for AggregateFrequency { type Error = (); @@ -241,8 +228,10 @@ impl<'x> TryFrom> for AggregateFrequency { } } -impl ParseValue for AddressMatch { - fn parse_value(value: &str) -> Result { +impl FromStr for AddressMatch { + type Err = String; + + fn from_str(value: &str) -> Result { if let Some(value) = value.strip_prefix('*').map(|v| v.trim()) { if !value.is_empty() { return Ok(AddressMatch::EndsWith(value.to_lowercase())); diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index b4648009..0587b75c 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -24,10 +24,11 @@ use std::{ fmt::Display, hash::{DefaultHasher, Hash, Hasher}, net::SocketAddr, + str::FromStr, sync::Arc, }; use store::registry::bootstrap::Bootstrap; -use utils::{cache::CacheItemWeight, config::utils::ParseValue}; +use utils::cache::CacheItemWeight; pub struct Resolvers { pub dns: MessageAuthenticator, @@ -74,7 +75,7 @@ pub enum MxPattern { pub struct Policy { pub id: String, pub mode: Mode, - pub mx: Vec, + pub mx: Box<[MxPattern]>, pub max_age: u64, } @@ -304,8 +305,9 @@ impl Server { } } -impl ParseValue for Mode { - fn parse_value(value: &str) -> Result { +impl FromStr for Mode { + type Err = String; + fn from_str(value: &str) -> Result { match value { "enforce" => Ok(Self::Enforce), "testing" | "test" => Ok(Self::Testing), diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index b3ec6ea4..565df7f2 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -20,9 +20,9 @@ use registry::schema::{ use smtp_proto::*; use std::{ net::{SocketAddr, ToSocketAddrs}, + str::FromStr, time::Duration, }; -use utils::config::utils::ParseValue; #[derive(Clone)] pub struct SessionConfig { @@ -386,8 +386,10 @@ impl SessionConfig { #[derive(Default)] pub struct Mechanism(u64); -impl ParseValue for Mechanism { - fn parse_value(value: &str) -> Result { +impl FromStr for Mechanism { + type Err = String; + + fn from_str(value: &str) -> Result { Ok(Mechanism(match value.to_ascii_uppercase().as_str() { "LOGIN" => AUTH_LOGIN, "PLAIN" => AUTH_PLAIN, @@ -496,7 +498,18 @@ impl<'x> TryFrom> for MtPriority { ExpressionConstant::Nsep => Ok(MtPriority::Nsep), _ => Err(()), }, - Variable::String(value) => MtPriority::parse_value(value.as_str()).map_err(|_| ()), + Variable::String(value) => { + let value = value.as_str(); + if value.eq_ignore_ascii_case("MIXER") { + Ok(MtPriority::Mixer) + } else if value.eq_ignore_ascii_case("STANAG4406") { + Ok(MtPriority::Stanag4406) + } else if value.eq_ignore_ascii_case("NSEP") { + Ok(MtPriority::Nsep) + } else { + Err(()) + } + } _ => Err(()), } } diff --git a/crates/common/src/config/storage.rs b/crates/common/src/config/storage.rs index afafffc2..fb91345d 100644 --- a/crates/common/src/config/storage.rs +++ b/crates/common/src/config/storage.rs @@ -4,11 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use ahash::AHashMap; +use crate::auth::IdMap; use coordinator::Coordinator; use directory::Directory; use std::sync::Arc; -use store::{BlobStore, InMemoryStore, PurgeSchedule, RegistryStore, SearchStore, Store}; +use store::{BlobStore, InMemoryStore, RegistryStore, SearchStore, Store}; #[derive(Clone)] pub struct Storage { @@ -18,12 +18,5 @@ pub struct Storage { pub fts: SearchStore, pub lookup: InMemoryStore, pub pubsub: Coordinator, - pub directory: Arc, - pub directories: AHashMap>, - pub purge_schedules: Vec, - - pub stores: AHashMap, - pub blobs: AHashMap, - pub lookups: AHashMap, - pub ftss: AHashMap, + pub directories: IdMap>, } diff --git a/crates/common/src/config/telemetry.rs b/crates/common/src/config/telemetry.rs index ebcc2f28..4d6022ad 100644 --- a/crates/common/src/config/telemetry.rs +++ b/crates/common/src/config/telemetry.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use ahash::{AHashMap, AHashSet, HashSet}; +use ahash::{AHashMap, AHashSet}; use base64::{Engine, engine::general_purpose::STANDARD}; use hyper::HeaderMap; use opentelemetry::{InstrumentationScope, KeyValue, logs::LoggerProvider}; diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index 86b316de..34a8646b 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -6,11 +6,11 @@ use crate::{ Inner, Server, - auth::{AccessToken, ResourceToken, TenantInfo}, + auth::AccessToken, config::{ mailstore::spamfilter::SpamClassifier, smtp::{ - auth::{ArcSealer, DkimSigner}, + auth::DkimSigner, queue::{ ConnectionStrategy, DEFAULT_QUEUE_NAME, MxConfig, QueueExpiry, QueueName, QueueStrategy, RequireOptional, RoutingStrategy, TlsStrategy, VirtualQueue, @@ -100,47 +100,6 @@ impl Server { self.core.storage.lookups.get(name) } - pub fn get_in_memory_store_or_default(&self, name: &str, session_id: u64) -> &InMemoryStore { - self.core.storage.lookups.get(name).unwrap_or_else(|| { - if !name.is_empty() { - trc::event!( - Eval(trc::EvalEvent::StoreNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - } - - &self.core.storage.lookup - }) - } - - pub fn get_data_store(&self, name: &str, session_id: u64) -> &Store { - self.core.storage.stores.get(name).unwrap_or_else(|| { - if !name.is_empty() { - trc::event!( - Eval(trc::EvalEvent::StoreNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - } - - &self.core.storage.data - }) - } - - pub fn get_arc_sealer(&self, name: &str, session_id: u64) -> Option> { - todo!() - /*self.resolve_signature(name).map(|s| s.sealer).or_else(|| { - trc::event!( - Arc(trc::ArcEvent::SealerNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - - None - })*/ - } - pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option> { todo!() /*self.resolve_signature(name).map(|s| s.signer).or_else(|| { diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index cfe9f8f6..51e029ae 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -26,7 +26,6 @@ impl Server { match fnc_id { F_IS_LOCAL_DOMAIN => { - let directory = params.next_as_string(); let domain = params.next_as_string(); self.get_directory_or_default(directory.as_ref(), session_id) @@ -36,7 +35,6 @@ impl Server { .map(|v| v.into()) } F_IS_LOCAL_ADDRESS => { - let directory = params.next_as_string(); let address = params.next_as_string(); self.get_directory_or_default(directory.as_ref(), session_id) diff --git a/crates/common/src/expr/functions/mod.rs b/crates/common/src/expr/functions/mod.rs index 9299dc3c..0f09cf1d 100644 --- a/crates/common/src/expr/functions/mod.rs +++ b/crates/common/src/expr/functions/mod.rs @@ -93,8 +93,8 @@ pub const F_SQL_QUERY: u32 = 7; pub const F_DNS_QUERY: u32 = 8; pub const ASYNC_FUNCTIONS: &[(&str, u32, u32)] = &[ - ("is_local_domain", F_IS_LOCAL_DOMAIN, 2), - ("is_local_address", F_IS_LOCAL_ADDRESS, 2), + ("is_local_domain", F_IS_LOCAL_DOMAIN, 1), + ("is_local_address", F_IS_LOCAL_ADDRESS, 1), ("key_get", F_KEY_GET, 2), ("key_exists", F_KEY_EXISTS, 2), ("key_set", F_KEY_SET, 3), diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index 3c7d4471..3e113563 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -30,7 +30,7 @@ pub struct IfThen { pub struct IfBlock { pub id: Id, pub property: Property, - pub if_then: Vec, + pub if_then: Box<[IfThen]>, pub default: Expression, } @@ -121,13 +121,9 @@ impl BootstrapExprExt for Bootstrap { expr: &structs::Expression, ) -> Option { // Parse conditions - let mut if_block = IfBlock { - id, - property: expr_ctx.property, - if_then: Vec::with_capacity(expr.match_.len()), - default: Expression { - items: Default::default(), - }, + let mut if_then = Vec::with_capacity(expr.match_.len()); + let mut default = Expression { + items: Default::default(), }; if expr.else_.is_empty() { @@ -160,7 +156,7 @@ impl BootstrapExprExt for Bootstrap { match ExpressionParser::new(Tokenizer::new(&expr.else_, &token_map)).parse() { Ok(expr) => { - if_block.default = expr; + default = expr; } Err(err) => { self.invalid_property( @@ -177,7 +173,7 @@ impl BootstrapExprExt for Bootstrap { Ok(if_expr) => { match ExpressionParser::new(Tokenizer::new(&match_.then, &token_map)).parse() { Ok(then_expr) => { - if_block.if_then.push(IfThen { + if_then.push(IfThen { expr: if_expr, then: then_expr, }); @@ -211,7 +207,12 @@ impl BootstrapExprExt for Bootstrap { } } - Some(if_block) + Some(IfBlock { + id, + property: expr_ctx.property, + if_then: if_then.into_boxed_slice(), + default, + }) } } diff --git a/crates/common/src/expr/mod.rs b/crates/common/src/expr/mod.rs index 83c87220..ed22f972 100644 --- a/crates/common/src/expr/mod.rs +++ b/crates/common/src/expr/mod.rs @@ -6,15 +6,18 @@ use compact_str::CompactString; use regex::Regex; -use registry::schema::enums::{ExpressionConstant, ExpressionVariable}; +use registry::schema::{ + enums::{ExpressionConstant, ExpressionVariable}, + structs::Rate, +}; use std::{ borrow::Cow, fmt::{Display, Formatter}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, time::Duration, }; use trc::MetricType; -use utils::config::{Rate, utils::ParseValue}; pub mod eval; pub mod functions; @@ -23,8 +26,9 @@ pub mod parser; pub mod tokenizer; #[derive(Debug, PartialEq, Eq, Clone, Default)] +#[repr(transparent)] pub struct Expression { - pub items: Vec, + pub items: Box<[ExpressionItem]>, } #[derive(Debug, Clone)] @@ -246,7 +250,7 @@ impl From for Variable<'_> { impl> From for Expression { fn from(value: T) -> Self { Expression { - items: vec![ExpressionItem::Constant(value.into())], + items: Box::new([ExpressionItem::Constant(value.into())]), } } } @@ -336,7 +340,9 @@ impl<'x> TryFrom> for Duration { Variable::Integer(value) if value > 0 => Ok(Duration::from_millis(value as u64)), Variable::Float(value) if value > 0.0 => Ok(Duration::from_millis(value as u64)), Variable::String(value) if !value.is_empty() => { - Duration::parse_value(value.as_str()).map_err(|_| ()) + registry::types::duration::Duration::from_str(value.as_str()) + .map(|v| v.into_inner()) + .map_err(|_| ()) } _ => Err(()), } @@ -433,8 +439,8 @@ impl<'x> TryFrom> for Rate { if requests > 0 && period > 0 { Ok(Rate { - requests: requests as u64, - period: Duration::from_millis(period as u64), + count: requests as u64, + period: registry::types::duration::Duration::from_millis(period as u64), }) } else { Err(()) diff --git a/crates/common/src/expr/tokenizer.rs b/crates/common/src/expr/tokenizer.rs index fc21f27e..45ac1fb0 100644 --- a/crates/common/src/expr/tokenizer.rs +++ b/crates/common/src/expr/tokenizer.rs @@ -11,9 +11,8 @@ use super::{ use ahash::AHashSet; use regex::Regex; use registry::{schema::enums::ExpressionConstant, types::EnumType}; -use std::{borrow::Cow, iter::Peekable, slice::Iter, time::Duration}; +use std::{borrow::Cow, iter::Peekable, slice::Iter}; use trc::MetricType; -use utils::config::utils::ParseValue; pub struct Tokenizer<'x> { pub(crate) iter: Peekable>, @@ -376,7 +375,7 @@ impl<'x> Tokenizer<'x> { } else { Err(format!("Constant {:?} not allowed in this context", buf)) } - } else if let Ok(duration) = Duration::parse_value(&buf) { + } else if let Ok(duration) = registry::types::duration::Duration::from_str(&buf) { Ok(Token::Constant(Constant::Integer( duration.as_millis() as i64 ))) diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 9277bc67..e3f23ac5 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -7,6 +7,7 @@ #![warn(clippy::large_futures)] use crate::{ + auth::DirectoryEntries, config::mailstore::{ email::EmailConfig, imap::ImapConfig, @@ -18,7 +19,7 @@ use crate::{ }; use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; -use auth::{AccessToken, oauth::config::OAuthConfig, roles::RolePermissions}; +use auth::{AccessToken, oauth::config::OAuthConfig}; use calcard::common::timezone::Tz; use config::{ groupware::GroupwareConfig, @@ -43,7 +44,10 @@ use std::{ sync::{Arc, atomic::AtomicBool}, time::{Duration, Instant}, }; -use store::rand::{Rng, distr::Alphanumeric}; +use store::{ + InMemoryStore, + rand::{Rng, distr::Alphanumeric}, +}; use tinyvec::TinyVec; use tokio::sync::{Notify, Semaphore, mpsc}; use tokio_rustls::TlsConnector; @@ -134,6 +138,7 @@ pub struct Server { pub struct Inner { pub shared_core: ArcSwap, pub data: Data, + pub directory: DirectoryEntries, pub cache: Caches, pub ipc: Ipc, } @@ -141,10 +146,11 @@ pub struct Inner { pub struct Data { pub spam_classifier: ArcSwap, - pub tls_certificates: ArcSwap>>, + pub tls_certificates: ArcSwap, Arc>>, pub tls_self_signed_cert: Option>, pub blocked_ips: RwLock, + pub lookup_stores: ArcSwap, InMemoryStore>>, pub asn_geo_data: AsnGeoLookupData, @@ -154,15 +160,14 @@ pub struct Data { pub queue_status: AtomicBool, pub webadmin: WebAdminManager, - pub logos: Mutex>>>>, + pub logos: Mutex, Option>>>>, pub smtp_connectors: TlsConnectors, } pub struct Caches { pub access_tokens: Cache>, - pub http_auth: Cache, - pub permissions: Cache>, + pub http_auth: Cache, HttpAuthCache>, pub messages: Cache>, pub files: Cache>, @@ -170,14 +175,14 @@ pub struct Caches { pub events: Cache>, pub scheduling: Cache>, - pub dns_txt: CacheWithTtl, - pub dns_mx: CacheWithTtl>>, - pub dns_ptr: CacheWithTtl>>, - pub dns_ipv4: CacheWithTtl>>, - pub dns_ipv6: CacheWithTtl>>, - pub dns_tlsa: CacheWithTtl>, - pub dns_mta_sts: CacheWithTtl>, - pub dns_rbl: CacheWithTtl>>, + pub dns_txt: CacheWithTtl, Txt>, + pub dns_mx: CacheWithTtl, Arc>>, + pub dns_ptr: CacheWithTtl]>>>, + pub dns_ipv4: CacheWithTtl, Arc>>, + pub dns_ipv6: CacheWithTtl, Arc>>, + pub dns_tlsa: CacheWithTtl, Arc>, + pub dns_mta_sts: CacheWithTtl, Arc>, + pub dns_rbl: CacheWithTtl, Option>>, } #[derive(Debug, Clone)] diff --git a/crates/common/src/listener/acme/cache.rs b/crates/common/src/listener/acme/cache.rs index c61e0b6b..f7ee4cce 100644 --- a/crates/common/src/listener/acme/cache.rs +++ b/crates/common/src/listener/acme/cache.rs @@ -8,7 +8,6 @@ use super::AcmeProvider; use crate::Server; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use trc::AddContext; -use utils::config::ConfigKey; impl Server { pub(crate) async fn load_cert(&self, provider: &AcmeProvider) -> trc::Result>> { diff --git a/crates/common/src/listener/limiter.rs b/crates/common/src/listener/limiter.rs index a5803f55..b25d2eef 100644 --- a/crates/common/src/listener/limiter.rs +++ b/crates/common/src/listener/limiter.rs @@ -10,63 +10,63 @@ use std::sync::{ }; #[derive(Debug, Clone)] -pub struct ConcurrencyLimiter { - pub max_concurrent: u64, - pub concurrent: Arc, +#[repr(transparent)] +pub struct ConcurrencyLimiter(Arc); + +#[derive(Debug)] +pub struct ConcurrencyLimiterInner { + max_concurrent: u64, + concurrent: AtomicU64, } #[derive(Default)] -pub struct InFlight { - concurrent: Arc, -} - -pub enum LimiterResult { - Allowed(InFlight), - Forbidden, - Disabled, -} +pub struct InFlight(Arc); impl Drop for InFlight { fn drop(&mut self) { - self.concurrent.fetch_sub(1, Ordering::Relaxed); + self.0.concurrent.fetch_sub(1, Ordering::Relaxed); } } impl ConcurrencyLimiter { pub fn new(max_concurrent: u64) -> Self { - ConcurrencyLimiter { + ConcurrencyLimiter(Arc::new(ConcurrencyLimiterInner { max_concurrent, - concurrent: Arc::new(0.into()), - } + concurrent: AtomicU64::new(0), + })) } pub fn is_allowed(&self) -> LimiterResult { - if self.concurrent.load(Ordering::Relaxed) < self.max_concurrent { + if self.0.concurrent.load(Ordering::Relaxed) < self.0.max_concurrent { // Return in-flight request - self.concurrent.fetch_add(1, Ordering::Relaxed); - LimiterResult::Allowed(InFlight { - concurrent: self.concurrent.clone(), - }) + self.0.concurrent.fetch_add(1, Ordering::Relaxed); + LimiterResult::Allowed(InFlight(self.0.clone())) } else { LimiterResult::Forbidden } } pub fn check_is_allowed(&self) -> bool { - self.concurrent.load(Ordering::Relaxed) < self.max_concurrent + self.0.concurrent.load(Ordering::Relaxed) < self.0.max_concurrent } pub fn is_active(&self) -> bool { - self.concurrent.load(Ordering::Relaxed) > 0 + self.0.concurrent.load(Ordering::Relaxed) > 0 } } impl InFlight { pub fn num_concurrent(&self) -> u64 { - self.concurrent.load(Ordering::Relaxed) + self.0.concurrent.load(Ordering::Relaxed) } } +pub enum LimiterResult { + Allowed(InFlight), + Forbidden, + Disabled, +} + impl From for Option { fn from(result: LimiterResult) -> Self { match result { diff --git a/crates/directory/src/core/config.rs b/crates/directory/src/core/config.rs index 68074ec7..c3141457 100644 --- a/crates/directory/src/core/config.rs +++ b/crates/directory/src/core/config.rs @@ -9,7 +9,10 @@ use crate::{ backend::{ldap::LdapDirectory, oidc::OpenIdDirectory, sql::SqlDirectory}, }; use ahash::AHashMap; -use registry::schema::structs; +use registry::schema::{ + prelude::Object, + structs::{self, Authentication}, +}; use std::sync::Arc; use store::registry::bootstrap::Bootstrap; @@ -37,6 +40,22 @@ impl Directories { } } - Directories { directories } + let mut default_directory = None; + let auth = bp.setting_infallible::().await; + if let Some(id) = auth.directory_id { + if let Some(directory) = directories.get(&id) { + default_directory = Some(directory.clone()); + } else { + bp.build_error( + Object::Authentication.singleton(), + format!("Default directory with id {} not found", id), + ); + } + } + + Directories { + default_directory, + directories, + } } } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 05fb5e12..89f52f65 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -54,6 +54,7 @@ pub struct Group { #[derive(Default, Clone, Debug)] pub struct Directories { + pub default_directory: Option>, pub directories: AHashMap>, } diff --git a/crates/email/src/sieve/ingest.rs b/crates/email/src/sieve/ingest.rs index aaf3e8d3..f3854dcb 100644 --- a/crates/email/src/sieve/ingest.rs +++ b/crates/email/src/sieve/ingest.rs @@ -36,7 +36,6 @@ use types::{ keyword::Keyword, special_use::SpecialUse, }; -use utils::config::utils::ParseValue; struct SieveMessage<'x> { pub raw_message: Cow<'x, [u8]>, @@ -198,20 +197,15 @@ impl SieveScriptIngest for Server { } => { if !mailboxes.is_empty() { let mut special_use_ids = Vec::with_capacity(special_use.len()); - for role in special_use { - special_use_ids.push(if role.eq_ignore_ascii_case("inbox") { - INBOX_ID - } else if role.eq_ignore_ascii_case("trash") { - TRASH_ID - } else { - let mut mailbox_id = u32::MAX; - if let Ok(role) = SpecialUse::parse_value(&role) - && let Some(m) = cache.mailbox_by_role(&role) - { - mailbox_id = m.document_id; - } - - mailbox_id + for role in special_use.iter().map(|v| SpecialUse::parse(v)) { + special_use_ids.push(match role { + Some(SpecialUse::Inbox) => INBOX_ID, + Some(SpecialUse::Trash) => TRASH_ID, + Some(role) => cache + .mailbox_by_role(&role) + .map(|m| m.document_id) + .unwrap_or(u32::MAX), + None => u32::MAX, }); } @@ -244,14 +238,11 @@ impl SieveScriptIngest for Server { } else if !special_use.is_empty() { let mut result = true; - for role in special_use { - if !role.eq_ignore_ascii_case("inbox") - && !role.eq_ignore_ascii_case("trash") - { - let role = SpecialUse::parse_value(&role); - if role.is_err() - || cache.mailbox_by_role(&role.unwrap()).is_none() - { + for role in special_use.iter().map(|v| SpecialUse::parse(v)) { + match role { + Some(SpecialUse::Inbox | SpecialUse::Trash) => {} + Some(other) if cache.mailbox_by_role(&other).is_some() => {} + _ => { result = false; break; } @@ -334,17 +325,22 @@ impl SieveScriptIngest for Server { } // Find mailbox by role - if let Some(special_use) = special_use - && target_id == u32::MAX + if target_id == u32::MAX + && let Some(special_use) = + special_use.as_deref().and_then(SpecialUse::parse) { - if special_use.eq_ignore_ascii_case("inbox") { - target_id = INBOX_ID; - } else if special_use.eq_ignore_ascii_case("trash") { - target_id = TRASH_ID; - } else if let Ok(role) = SpecialUse::parse_value(&special_use) - && let Some(item) = cache.mailbox_by_role(&role) - { - target_id = item.document_id; + match special_use { + SpecialUse::Inbox => { + target_id = INBOX_ID; + } + SpecialUse::Trash => { + target_id = TRASH_ID; + } + role => { + if let Some(item) = cache.mailbox_by_role(&role) { + target_id = item.document_id; + } + } } } diff --git a/crates/migration/src/mailbox.rs b/crates/migration/src/mailbox.rs index e478d34c..6170c700 100644 --- a/crates/migration/src/mailbox.rs +++ b/crates/migration/src/mailbox.rs @@ -144,7 +144,7 @@ impl FromLegacy for Mailbox { role: legacy .get(&Property::Role) .as_string() - .and_then(|r| SpecialUse::parse_value(r).ok()) + .and_then(SpecialUse::parse) .unwrap_or(SpecialUse::None), parent_id: legacy .get(&Property::ParentId) diff --git a/crates/registry/src/types/mod.rs b/crates/registry/src/types/mod.rs index 7e3e06c4..b8508871 100644 --- a/crates/registry/src/types/mod.rs +++ b/crates/registry/src/types/mod.rs @@ -15,6 +15,8 @@ pub mod ipmask; pub mod socketaddr; pub trait EnumType: Sized { + const COUNT: usize; + fn parse(s: &str) -> Option; fn as_str(&self) -> &'static str; fn from_id(id: u16) -> Option; diff --git a/crates/utils/src/cache.rs b/crates/utils/src/cache.rs index 9c7bac46..ca1d26b6 100644 --- a/crates/utils/src/cache.rs +++ b/crates/utils/src/cache.rs @@ -214,6 +214,18 @@ impl CacheItemWeight for String { } } +impl CacheItemWeight for Box { + fn weight(&self) -> u64 { + self.len() as u64 + std::mem::size_of::>() as u64 + } +} + +impl CacheItemWeight for Box<[T]> { + fn weight(&self) -> u64 { + (self.len() * std::mem::size_of::()) as u64 + std::mem::size_of::>() as u64 + } +} + impl CacheItemWeight for u32 { fn weight(&self) -> u64 { std::mem::size_of::() as u64