Authenticate using registry - part 1

This commit is contained in:
mdecimus
2026-02-04 17:55:16 +01:00
parent c6495f6fed
commit 0af735540d
34 changed files with 670 additions and 587 deletions

8
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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,

View File

@@ -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<AccessToken> {
let mut role_permissions = RolePermissions::default();
let mut role_permissions = PermissionsGroup::default();
// Extract data
let mut object_quota = self.core.email.max_objects;

View File

@@ -0,0 +1,261 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
impl Server {
pub async fn authenticate(&self, req: &AuthRequest<'_>) -> trc::Result<Arc<AccessToken>> {
// 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<Principal> {
// 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<String>,
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<String>,
pass: impl Into<String>,
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<String> {
fn login(&self) -> Option<&str> {
match self {
Credentials::Plain { username, .. } | Credentials::XOauth2 { username, .. } => {
username.as_str().into()
}
Credentials::OAuthBearer { .. } => None,
}
}
}

View File

@@ -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::<usize>());
pub type Permissions = Bitset<PERMISSIONS_BITSET_SIZE>;
pub type ObjectQuota = [u32; StorageObject::COUNT - 1];
pub type IdMap<V> = HashMap<u32, V, nohash_hasher::BuildNoHashHasher<u32>>;
pub struct DirectoryEntries {
pub emails: ArcSwap<EmailEntries>,
pub domains: ArcSwap<DomainEntries>,
pub accounts: ArcSwap<AccountEntries>,
pub groups: ArcSwap<GroupEntries>,
pub roles: ArcSwap<RoleEntries>,
pub mailing_lists: ArcSwap<MailingListEntries>,
pub tenants: ArcSwap<TenantEntries>,
pub api_keys: ArcSwap<ApiKeyEntries>,
}
#[derive(Debug, Clone)]
pub struct EmailEntries {
pub addresses: AHashMap<ArcStr, EmailEntry>,
}
#[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<ArcStr, u32>,
pub entries: IdMap<DomainEntry>,
pub default: u32,
}
#[derive(Debug, Clone)]
pub struct AccountEntries {
pub entries: IdMap<AccountEntry>,
}
#[derive(Debug, Clone)]
pub struct GroupEntries {
pub entries: IdMap<GroupEntry>,
}
#[derive(Debug, Clone)]
pub struct RoleEntries {
pub entries: IdMap<RoleEntry>,
}
#[derive(Debug, Clone)]
pub struct MailingListEntries {
pub entries: IdMap<MailingListEntry>,
}
#[derive(Debug, Clone)]
pub struct TenantEntries {
pub entries: IdMap<TenantEntry>,
}
#[derive(Debug, Clone)]
pub struct ApiKeyEntries {
pub entries: AHashMap<ArcStr, ApiKeyEntry>,
}
#[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<ArcStr>,
pub sub_addressing_custom: Option<Arc<IfBlock>>,
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<ArcStr>,
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<Arc<ObjectQuota>>,
pub permissions: Option<Arc<PermissionsGroup>>,
}
#[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<Arc<ObjectQuota>>,
pub permissions: Option<Arc<PermissionsGroup>>,
}
#[derive(Debug, Clone)]
pub struct ApiKeyEntry {
pub id: u32,
pub id_tenant: u32,
pub id_roles: TinyVec<[u32; 3]>,
pub permissions: Option<Arc<PermissionsGroup>>,
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<u32>,
pub access_to: VecMap<u32, Bitmap<Collection>>,
pub name: String,
pub description: Option<String>,
pub locale: Option<String>,
pub emails: Vec<String>,
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<TenantInfo>,
pub concurrent_http_requests: Option<ConcurrencyLimiter>,
pub concurrent_imap_requests: Option<ConcurrencyLimiter>,
pub concurrent_uploads: Option<ConcurrencyLimiter>,
@@ -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<TenantInfo>,
pub collections: Bitmap<Collection>,
}
pub struct AuthRequest<'x> {
credentials: Credentials<String>,
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<Arc<AccessToken>> {
// 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<Principal> {
// 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<String>,
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<String>,
pass: impl Into<String>,
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<String> {
fn login(&self) -> Option<&str> {
match self {
Credentials::Plain { username, .. } | Credentials::XOauth2 { username, .. } => {
username.as_str().into()
}
Credentials::OAuthBearer { .. } => None,
}
}
}

View File

@@ -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;

View File

@@ -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<Arc<RolePermissions>> = LazyLock::new(user_permissions);
static ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> = LazyLock::new(admin_permissions);
static TENANT_ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> =
LazyLock::new(tenant_admin_permissions);
impl Server {
pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> {
pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result<Arc<PermissionsGroup>> {
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<Arc<RolePermissions>> {
async fn build_role_permissions(&self, role_id: u32) -> trc::Result<Arc<PermissionsGroup>> {
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<RolePermissions> {
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<RolePermissions> {
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<RolePermissions> {
Arc::new(RolePermissions {
enabled: Permissions::all(),
disabled: Permissions::new(),
})
}
impl CacheItemWeight for RolePermissions {
fn weight(&self) -> u64 {
std::mem::size_of::<RolePermissions>() as u64
}
}

View File

@@ -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::<Vec<_>>(),
subject_names
.into_iter()
.map(Into::into)
.collect::<Vec<_>>(),
)
.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::<AccessToken>() + 255) as u64,
),
http_auth: Cache::new(cache.http_auth, (50 + std::mem::size_of::<u32>()) as u64),
permissions: Cache::new(
cache.permissions,
std::mem::size_of::<RolePermissions>() as u64,
),
messages: Cache::new(
cache.messages,
(std::mem::size_of::<u32>()
@@ -155,11 +155,11 @@ impl Caches {
) -> Parameters<
'_,
T,
CacheWithTtl<String, Txt>,
CacheWithTtl<String, Arc<Vec<MX>>>,
CacheWithTtl<String, Arc<Vec<Ipv4Addr>>>,
CacheWithTtl<String, Arc<Vec<Ipv6Addr>>>,
CacheWithTtl<IpAddr, Arc<Vec<String>>>,
CacheWithTtl<Box<str>, Txt>,
CacheWithTtl<Box<str>, Arc<Box<[MX]>>>,
CacheWithTtl<Box<str>, Arc<Box<[Ipv4Addr]>>>,
CacheWithTtl<Box<str>, Arc<Box<[Ipv6Addr]>>>,
CacheWithTtl<IpAddr, Arc<Box<[Box<str>]>>>,
> {
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(),
}
}
}

View File

@@ -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 {

View File

@@ -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<Self> {
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 {

View File

@@ -197,8 +197,8 @@ impl Server {
pub(crate) async fn parse_certificates(
bp: &mut Bootstrap,
certificates: &mut AHashMap<String, Arc<CertifiedKey>>,
subject_names: &mut AHashSet<String>,
certificates: &mut AHashMap<Box<str>, Arc<CertifiedKey>>,
subject_names: &mut AHashSet<Box<str>>,
) {
// Parse certificates
for cert_obj in bp.list_infallible::<Certificate>().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<Box<str>> = 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<str> = 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) => {

View File

@@ -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<Self, String> {
match value {
"relaxed" => Ok(VerifyStrategy::Relaxed),
"strict" => Ok(VerifyStrategy::Strict),
"disable" | "disabled" | "never" | "none" => Ok(VerifyStrategy::Disable),
_ => Err(format!("Invalid value {:?}.", value)),
}
}
}

View File

@@ -30,7 +30,6 @@ use std::{
net::IpAddr,
time::Duration,
};
use utils::config::utils::ParseValue;
#[derive(
Debug,
@@ -600,7 +599,14 @@ impl<'x> TryFrom<Variable<'x>> 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<Self, String> {
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)

View File

@@ -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<Self, String> {
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<Variable<'x>> for AggregateFrequency {
type Error = ();
@@ -241,8 +228,10 @@ impl<'x> TryFrom<Variable<'x>> for AggregateFrequency {
}
}
impl ParseValue for AddressMatch {
fn parse_value(value: &str) -> Result<Self, String> {
impl FromStr for AddressMatch {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if let Some(value) = value.strip_prefix('*').map(|v| v.trim()) {
if !value.is_empty() {
return Ok(AddressMatch::EndsWith(value.to_lowercase()));

View File

@@ -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<MxPattern>,
pub mx: Box<[MxPattern]>,
pub max_age: u64,
}
@@ -304,8 +305,9 @@ impl Server {
}
}
impl ParseValue for Mode {
fn parse_value(value: &str) -> Result<Self, String> {
impl FromStr for Mode {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"enforce" => Ok(Self::Enforce),
"testing" | "test" => Ok(Self::Testing),

View File

@@ -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<Self, String> {
impl FromStr for Mechanism {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Mechanism(match value.to_ascii_uppercase().as_str() {
"LOGIN" => AUTH_LOGIN,
"PLAIN" => AUTH_PLAIN,
@@ -496,7 +498,18 @@ impl<'x> TryFrom<Variable<'x>> 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(()),
}
}

View File

@@ -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<Directory>,
pub directories: AHashMap<String, Arc<Directory>>,
pub purge_schedules: Vec<PurgeSchedule>,
pub stores: AHashMap<String, Store>,
pub blobs: AHashMap<String, BlobStore>,
pub lookups: AHashMap<String, InMemoryStore>,
pub ftss: AHashMap<String, SearchStore>,
pub directories: IdMap<Arc<Directory>>,
}

View File

@@ -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};

View File

@@ -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<Arc<ArcSealer>> {
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<Arc<DkimSigner>> {
todo!()
/*self.resolve_signature(name).map(|s| s.signer).or_else(|| {

View File

@@ -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)

View File

@@ -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),

View File

@@ -30,7 +30,7 @@ pub struct IfThen {
pub struct IfBlock {
pub id: Id,
pub property: Property,
pub if_then: Vec<IfThen>,
pub if_then: Box<[IfThen]>,
pub default: Expression,
}
@@ -121,13 +121,9 @@ impl BootstrapExprExt for Bootstrap {
expr: &structs::Expression,
) -> Option<IfBlock> {
// Parse conditions
let mut if_block = IfBlock {
id,
property: expr_ctx.property,
if_then: Vec::with_capacity(expr.match_.len()),
default: Expression {
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,
})
}
}

View File

@@ -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<ExpressionItem>,
pub items: Box<[ExpressionItem]>,
}
#[derive(Debug, Clone)]
@@ -246,7 +250,7 @@ impl From<bool> for Variable<'_> {
impl<T: Into<Constant>> From<T> 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<Variable<'x>> 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<Variable<'x>> 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(())

View File

@@ -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<Iter<'x, u8>>,
@@ -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
)))

View File

@@ -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<Core>,
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<SpamClassifier>,
pub tls_certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>,
pub tls_certificates: ArcSwap<AHashMap<Box<str>, Arc<CertifiedKey>>>,
pub tls_self_signed_cert: Option<Arc<CertifiedKey>>,
pub blocked_ips: RwLock<BlockedIps>,
pub lookup_stores: ArcSwap<AHashMap<Box<str>, InMemoryStore>>,
pub asn_geo_data: AsnGeoLookupData,
@@ -154,15 +160,14 @@ pub struct Data {
pub queue_status: AtomicBool,
pub webadmin: WebAdminManager,
pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>,
pub logos: Mutex<AHashMap<Box<str>, Option<Resource<Vec<u8>>>>>,
pub smtp_connectors: TlsConnectors,
}
pub struct Caches {
pub access_tokens: Cache<u32, Arc<AccessToken>>,
pub http_auth: Cache<String, HttpAuthCache>,
pub permissions: Cache<u32, Arc<RolePermissions>>,
pub http_auth: Cache<Box<str>, HttpAuthCache>,
pub messages: Cache<u32, CacheSwap<MessageStoreCache>>,
pub files: Cache<u32, CacheSwap<DavResources>>,
@@ -170,14 +175,14 @@ pub struct Caches {
pub events: Cache<u32, CacheSwap<DavResources>>,
pub scheduling: Cache<u32, CacheSwap<DavResources>>,
pub dns_txt: CacheWithTtl<String, Txt>,
pub dns_mx: CacheWithTtl<String, Arc<Vec<MX>>>,
pub dns_ptr: CacheWithTtl<IpAddr, Arc<Vec<String>>>,
pub dns_ipv4: CacheWithTtl<String, Arc<Vec<Ipv4Addr>>>,
pub dns_ipv6: CacheWithTtl<String, Arc<Vec<Ipv6Addr>>>,
pub dns_tlsa: CacheWithTtl<String, Arc<Tlsa>>,
pub dns_mta_sts: CacheWithTtl<String, Arc<Policy>>,
pub dns_rbl: CacheWithTtl<String, Option<Arc<IpResolver>>>,
pub dns_txt: CacheWithTtl<Box<str>, Txt>,
pub dns_mx: CacheWithTtl<Box<str>, Arc<Box<[MX]>>>,
pub dns_ptr: CacheWithTtl<IpAddr, Arc<Box<[Box<str>]>>>,
pub dns_ipv4: CacheWithTtl<Box<str>, Arc<Box<[Ipv4Addr]>>>,
pub dns_ipv6: CacheWithTtl<Box<str>, Arc<Box<[Ipv6Addr]>>>,
pub dns_tlsa: CacheWithTtl<Box<str>, Arc<Tlsa>>,
pub dns_mta_sts: CacheWithTtl<Box<str>, Arc<Policy>>,
pub dns_rbl: CacheWithTtl<Box<str>, Option<Arc<IpResolver>>>,
}
#[derive(Debug, Clone)]

View File

@@ -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<Option<Vec<u8>>> {

View File

@@ -10,63 +10,63 @@ use std::sync::{
};
#[derive(Debug, Clone)]
pub struct ConcurrencyLimiter {
pub max_concurrent: u64,
pub concurrent: Arc<AtomicU64>,
#[repr(transparent)]
pub struct ConcurrencyLimiter(Arc<ConcurrencyLimiterInner>);
#[derive(Debug)]
pub struct ConcurrencyLimiterInner {
max_concurrent: u64,
concurrent: AtomicU64,
}
#[derive(Default)]
pub struct InFlight {
concurrent: Arc<AtomicU64>,
}
pub enum LimiterResult {
Allowed(InFlight),
Forbidden,
Disabled,
}
pub struct InFlight(Arc<ConcurrencyLimiterInner>);
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<LimiterResult> for Option<InFlight> {
fn from(result: LimiterResult) -> Self {
match result {

View File

@@ -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::<Authentication>().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,
}
}
}

View File

@@ -54,6 +54,7 @@ pub struct Group {
#[derive(Default, Clone, Debug)]
pub struct Directories {
pub default_directory: Option<Arc<Directory>>,
pub directories: AHashMap<Id, Arc<Directory>>,
}

View File

@@ -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,19 +325,24 @@ 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") {
match special_use {
SpecialUse::Inbox => {
target_id = INBOX_ID;
} else if special_use.eq_ignore_ascii_case("trash") {
}
SpecialUse::Trash => {
target_id = TRASH_ID;
} else if let Ok(role) = SpecialUse::parse_value(&special_use)
&& let Some(item) = cache.mailbox_by_role(&role)
{
}
role => {
if let Some(item) = cache.mailbox_by_role(&role) {
target_id = item.document_id;
}
}
}
}
// Find mailbox by name
if target_id == u32::MAX {

View File

@@ -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)

View File

@@ -15,6 +15,8 @@ pub mod ipmask;
pub mod socketaddr;
pub trait EnumType: Sized {
const COUNT: usize;
fn parse(s: &str) -> Option<Self>;
fn as_str(&self) -> &'static str;
fn from_id(id: u16) -> Option<Self>;

View File

@@ -214,6 +214,18 @@ impl CacheItemWeight for String {
}
}
impl CacheItemWeight for Box<str> {
fn weight(&self) -> u64 {
self.len() as u64 + std::mem::size_of::<Box<str>>() as u64
}
}
impl<T> CacheItemWeight for Box<[T]> {
fn weight(&self) -> u64 {
(self.len() * std::mem::size_of::<T>()) as u64 + std::mem::size_of::<Box<[T]>>() as u64
}
}
impl CacheItemWeight for u32 {
fn weight(&self) -> u64 {
std::mem::size_of::<u32>() as u64