From 7c8be27fcf1856e815c0f37e0bc55bfa0bd20f1e Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:23:05 +0100 Subject: [PATCH] Bootstrap from registry - part 7 --- Cargo.lock | 6 +- crates/common/src/addresses.rs | 2 +- crates/common/src/auth/access_token.rs | 7 - crates/common/src/auth/mod.rs | 4 - crates/common/src/auth/oauth/config.rs | 7 +- crates/common/src/auth/oauth/token.rs | 1 - crates/common/src/auth/rate_limit.rs | 7 +- crates/common/src/auth/roles.rs | 4 - crates/common/src/config/groupware.rs | 2 +- crates/common/src/config/inner.rs | 6 +- .../src/config/mailstore/capabilities.rs | 3 +- crates/common/src/config/mailstore/email.rs | 2 +- crates/common/src/config/mailstore/imap.rs | 2 +- crates/common/src/config/mailstore/jmap.rs | 2 +- crates/common/src/config/mailstore/scripts.rs | 4 +- .../common/src/config/mailstore/spamfilter.rs | 21 +- crates/common/src/config/mod.rs | 5 +- crates/common/src/config/network.rs | 2 +- crates/common/src/config/server/listener.rs | 9 +- crates/common/src/config/server/tls.rs | 160 +- crates/common/src/config/smtp/auth.rs | 11 +- crates/common/src/config/smtp/mod.rs | 3 +- crates/common/src/config/smtp/queue.rs | 5 +- crates/common/src/config/smtp/report.rs | 5 +- crates/common/src/config/smtp/resolver.rs | 3 +- crates/common/src/config/smtp/session.rs | 2 +- crates/common/src/config/telemetry.rs | 2 +- crates/common/src/core.rs | 1 - crates/common/src/enterprise/config.rs | 2 +- crates/common/src/enterprise/mod.rs | 4 - crates/common/src/expr/functions/asynch.rs | 1 - crates/common/src/expr/if_block.rs | 25 +- crates/common/src/listener/blocked.rs | 4 +- crates/common/src/listener/listen.rs | 2 +- crates/common/src/manager/boot.rs | 5 +- crates/common/src/manager/reload.rs | 1 - crates/common/src/manager/restore.rs | 3 +- .../common/src/scripts/plugins/llm_prompt.rs | 4 +- crates/common/src/sharing/acl.rs | 4 - crates/coordinator/Cargo.toml | 1 + crates/coordinator/src/backend/kafka/mod.rs | 61 +- .../coordinator/src/backend/kafka/pubsub.rs | 5 +- crates/coordinator/src/backend/nats/mod.rs | 99 +- crates/coordinator/src/backend/nats/pubsub.rs | 2 +- crates/coordinator/src/backend/redis/mod.rs | 100 +- .../coordinator/src/backend/redis/pubsub.rs | 105 + crates/coordinator/src/backend/zenoh/mod.rs | 27 +- .../coordinator/src/backend/zenoh/pubsub.rs | 2 +- crates/coordinator/src/bootstrap.rs | 70 + crates/coordinator/src/dispatch.rs | 6 +- crates/coordinator/src/lib.rs | 5 +- crates/directory/Cargo.toml | 8 +- crates/directory/src/backend/imap/client.rs | 207 -- crates/directory/src/backend/imap/config.rs | 57 - crates/directory/src/backend/imap/lookup.rs | 82 - crates/directory/src/backend/imap/mod.rs | 72 - crates/directory/src/backend/imap/pool.rs | 51 - crates/directory/src/backend/imap/tls.rs | 92 - .../directory/src/backend/internal/lookup.rs | 188 -- .../directory/src/backend/internal/manage.rs | 2851 ----------------- crates/directory/src/backend/internal/mod.rs | 290 -- crates/directory/src/backend/ldap/config.rs | 254 +- crates/directory/src/backend/ldap/lookup.rs | 655 ++-- crates/directory/src/backend/ldap/mod.rs | 31 +- crates/directory/src/backend/memory/config.rs | 158 - crates/directory/src/backend/memory/lookup.rs | 99 - crates/directory/src/backend/memory/mod.rs | 28 - crates/directory/src/backend/mod.rs | 22 - crates/directory/src/backend/oidc/config.rs | 100 +- crates/directory/src/backend/oidc/lookup.rs | 363 +-- crates/directory/src/backend/oidc/mod.rs | 51 +- crates/directory/src/backend/smtp/config.rs | 72 - crates/directory/src/backend/smtp/lookup.rs | 133 - crates/directory/src/backend/smtp/mod.rs | 37 - crates/directory/src/backend/smtp/pool.rs | 53 - crates/directory/src/backend/sql/config.rs | 103 +- crates/directory/src/backend/sql/lookup.rs | 406 +-- crates/directory/src/backend/sql/mod.rs | 17 +- crates/directory/src/core/cache.rs | 67 - crates/directory/src/core/config.rs | 146 +- crates/directory/src/core/dispatch.rs | 130 +- crates/directory/src/core/mod.rs | 349 -- crates/directory/src/core/principal.rs | 1738 ---------- crates/directory/src/core/secret.rs | 263 -- crates/directory/src/lib.rs | 573 +--- crates/main/Cargo.toml | 6 +- crates/store/src/backend/mysql/main.rs | 2 +- crates/store/src/backend/postgres/main.rs | 4 +- crates/store/src/bootstrap/blob.rs | 2 +- crates/store/src/bootstrap/memory.rs | 4 +- crates/store/src/bootstrap/search.rs | 4 +- crates/store/src/registry/bootstrap.rs | 4 +- tests/Cargo.toml | 6 +- 93 files changed, 1206 insertions(+), 9398 deletions(-) create mode 100644 crates/coordinator/src/backend/redis/pubsub.rs create mode 100644 crates/coordinator/src/bootstrap.rs delete mode 100644 crates/directory/src/backend/imap/client.rs delete mode 100644 crates/directory/src/backend/imap/config.rs delete mode 100644 crates/directory/src/backend/imap/lookup.rs delete mode 100644 crates/directory/src/backend/imap/mod.rs delete mode 100644 crates/directory/src/backend/imap/pool.rs delete mode 100644 crates/directory/src/backend/imap/tls.rs delete mode 100644 crates/directory/src/backend/internal/lookup.rs delete mode 100644 crates/directory/src/backend/internal/manage.rs delete mode 100644 crates/directory/src/backend/internal/mod.rs delete mode 100644 crates/directory/src/backend/memory/config.rs delete mode 100644 crates/directory/src/backend/memory/lookup.rs delete mode 100644 crates/directory/src/backend/memory/mod.rs delete mode 100644 crates/directory/src/backend/smtp/config.rs delete mode 100644 crates/directory/src/backend/smtp/lookup.rs delete mode 100644 crates/directory/src/backend/smtp/mod.rs delete mode 100644 crates/directory/src/backend/smtp/pool.rs delete mode 100644 crates/directory/src/core/cache.rs delete mode 100644 crates/directory/src/core/principal.rs delete mode 100644 crates/directory/src/core/secret.rs diff --git a/Cargo.lock b/Cargo.lock index a455f4e5..ffe1eb4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1233,6 +1233,7 @@ dependencies = [ "futures", "rdkafka", "redis", + "registry", "store", "tokio", "trc", @@ -1754,9 +1755,6 @@ dependencies = [ "deadpool 0.10.0", "futures", "ldap3", - "mail-builder", - "mail-parser", - "mail-send", "md5 0.8.0", "nlp", "password-hash", @@ -1764,6 +1762,7 @@ dependencies = [ "proc_macros", "pwhash", "regex", + "registry", "reqwest", "rkyv", "rustls 0.23.36", @@ -1773,7 +1772,6 @@ dependencies = [ "serde_json", "sha1", "sha2 0.10.9", - "smtp-proto", "store", "tokio", "tokio-rustls 0.26.4", diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index 812515de..65a055c8 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::{Directory, backend::RcptType}; +use directory::Directory; use registry::schema::enums::ExpressionVariable; use std::borrow::Cow; use utils::config::{Config, utils::AsKey}; diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 5f94cbcb..4b1e97ff 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -11,13 +11,6 @@ use crate::{ listener::limiter::{ConcurrencyLimiter, LimiterResult}, }; use ahash::AHashSet; -use directory::{ - Permission, Principal, PrincipalData, QueryParams, Type, - backend::internal::{ - lookup::DirectoryStore, - manage::{ChangedPrincipals, ManageDirectory}, - }, -}; use std::{ hash::{DefaultHasher, Hash, Hasher}, sync::Arc, diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 9cf4d3d0..9948406b 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -5,10 +5,6 @@ */ use crate::{Server, listener::limiter::ConcurrencyLimiter}; -use directory::{ - Directory, FALLBACK_ADMIN_ID, Permission, Permissions, Principal, QueryParams, Type, - backend::internal::lookup::DirectoryStore, core::secret::verify_secret_hash, -}; use mail_send::Credentials; use oauth::GrantType; use std::{net::IpAddr, sync::Arc}; diff --git a/crates/common/src/auth/oauth/config.rs b/crates/common/src/auth/oauth/config.rs index af15bf2a..ad2f279f 100644 --- a/crates/common/src/auth/oauth/config.rs +++ b/crates/common/src/auth/oauth/config.rs @@ -6,7 +6,7 @@ use crate::{ config::{build_ecdsa_pem, build_rsa_keypair}, - manager::{bootstrap::Bootstrap, webadmin::Resource}, + manager::webadmin::Resource, }; use biscuit::{ jwa::{Algorithm, SignatureAlgorithm}, @@ -20,7 +20,10 @@ use biscuit::{ use registry::schema::{enums::JwtSignatureAlgorithm, prelude::Object, structs::Authentication}; use ring::signature::{self, KeyPair}; use rsa::{RsaPublicKey, pkcs1::DecodeRsaPublicKey, traits::PublicKeyParts}; -use store::rand::{Rng, distr::Alphanumeric, rng}; +use store::{ + rand::{Rng, distr::Alphanumeric, rng}, + registry::bootstrap::Bootstrap, +}; use x509_parser::num_bigint::BigUint; #[derive(Clone)] diff --git a/crates/common/src/auth/oauth/token.rs b/crates/common/src/auth/oauth/token.rs index 8e984805..ad062bde 100644 --- a/crates/common/src/auth/oauth/token.rs +++ b/crates/common/src/auth/oauth/token.rs @@ -6,7 +6,6 @@ use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncrypt}; use crate::Server; -use directory::{PrincipalData, QueryParams}; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; use std::time::SystemTime; diff --git a/crates/common/src/auth/rate_limit.rs b/crates/common/src/auth/rate_limit.rs index 6fb9ed31..ffa5efad 100644 --- a/crates/common/src/auth/rate_limit.rs +++ b/crates/common/src/auth/rate_limit.rs @@ -4,17 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::net::IpAddr; - +use crate::auth::AccessToken; use crate::{ KV_RATE_LIMIT_HTTP_ANONYMOUS, KV_RATE_LIMIT_HTTP_AUTHENTICATED, Server, ip_to_bytes, listener::limiter::{InFlight, LimiterResult}, }; -use directory::Permission; +use std::net::IpAddr; use trc::AddContext; -use crate::auth::AccessToken; - impl Server { pub async fn is_http_authenticated_request_allowed( &self, diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs index da5433a7..b5aa7c51 100644 --- a/crates/common/src/auth/roles.rs +++ b/crates/common/src/auth/roles.rs @@ -6,10 +6,6 @@ use crate::Server; use ahash::AHashSet; -use directory::{ - Permission, Permissions, QueryParams, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, - backend::internal::lookup::DirectoryStore, -}; use std::sync::{Arc, LazyLock}; use trc::AddContext; use utils::cache::CacheItemWeight; diff --git a/crates/common/src/config/groupware.rs b/crates/common/src/config/groupware.rs index 8bb08440..91e58819 100644 --- a/crates/common/src/config/groupware.rs +++ b/crates/common/src/config/groupware.rs @@ -4,12 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::manager::bootstrap::Bootstrap; use registry::schema::structs::{ AddressBook, Calendar, CalendarAlarm, CalendarScheduling, DataRetention, FileStorage, Sharing, WebDav, }; use std::str::FromStr; +use store::registry::bootstrap::Bootstrap; use utils::template::Template; #[derive(Debug, Clone, Default)] diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 0ef690b8..52b37387 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -11,10 +11,11 @@ use crate::{ auth::{AccessToken, roles::RolePermissions}, config::{ mailstore::spamfilter::SpamClassifier, + server::tls::parse_certificates, smtp::resolver::{Policy, Tlsa}, }, listener::blocked::BlockedIps, - manager::{bootstrap::Bootstrap, webadmin::WebAdminManager}, + manager::webadmin::WebAdminManager, }; use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; @@ -26,6 +27,7 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::Arc, }; +use store::registry::bootstrap::Bootstrap; use utils::{ cache::{Cache, CacheWithTtl}, snowflake::SnowflakeIdGenerator, @@ -36,7 +38,7 @@ impl Data { // Parse certificates let mut certificates = AHashMap::new(); let mut subject_names = AHashSet::new(); - bp.parse_certificates(&mut certificates, &mut subject_names); + parse_certificates(bp, &mut certificates, &mut subject_names); if subject_names.is_empty() { subject_names.insert("localhost".to_string()); } diff --git a/crates/common/src/config/mailstore/capabilities.rs b/crates/common/src/config/mailstore/capabilities.rs index e2fde2c0..fe4b971a 100644 --- a/crates/common/src/config/mailstore/capabilities.rs +++ b/crates/common/src/config/mailstore/capabilities.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{config::mailstore::jmap::JmapConfig, manager::bootstrap::Bootstrap}; +use crate::config::mailstore::jmap::JmapConfig; use ahash::AHashSet; use calcard::icalendar::ICalendarDuration; use chrono::{DateTime, Utc}; @@ -22,6 +22,7 @@ use registry::{ schema::structs::{Calendar, Email, SieveUserInterpreter}, types::EnumType, }; +use store::registry::bootstrap::Bootstrap; use types::type_state::DataType; use utils::map::vec_map::VecMap; diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs index 0625925f..19cbb3d6 100644 --- a/crates/common/src/config/mailstore/email.rs +++ b/crates/common/src/config/mailstore/email.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::manager::bootstrap::Bootstrap; use ahash::{AHashMap, AHashSet}; use nlp::language::Language; use registry::{ @@ -18,6 +17,7 @@ use registry::{ }; use std::time::Duration; use store::{ + registry::bootstrap::Bootstrap, search::{CalendarSearchField, ContactSearchField, EmailSearchField, SearchField}, write::SearchIndex, }; diff --git a/crates/common/src/config/mailstore/imap.rs b/crates/common/src/config/mailstore/imap.rs index caf63a15..cda5e589 100644 --- a/crates/common/src/config/mailstore/imap.rs +++ b/crates/common/src/config/mailstore/imap.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::manager::bootstrap::Bootstrap; use registry::schema::structs::{Imap, Rate}; use std::time::Duration; +use store::registry::bootstrap::Bootstrap; #[derive(Default, Clone)] pub struct ImapConfig { diff --git a/crates/common/src/config/mailstore/jmap.rs b/crates/common/src/config/mailstore/jmap.rs index 548ab9da..060ca147 100644 --- a/crates/common/src/config/mailstore/jmap.rs +++ b/crates/common/src/config/mailstore/jmap.rs @@ -4,10 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::manager::bootstrap::Bootstrap; use jmap_proto::request::capability::BaseCapabilities; use registry::schema::structs::Jmap; use std::time::Duration; +use store::registry::bootstrap::Bootstrap; #[derive(Default, Clone)] pub struct JmapConfig { diff --git a/crates/common/src/config/mailstore/scripts.rs b/crates/common/src/config/mailstore/scripts.rs index ee4d6912..3caade72 100644 --- a/crates/common/src/config/mailstore/scripts.rs +++ b/crates/common/src/config/mailstore/scripts.rs @@ -6,8 +6,7 @@ use crate::{ VERSION_PUBLIC, - expr::if_block::IfBlock, - manager::bootstrap::Bootstrap, + expr::if_block::{BootstrapExprExt, IfBlock}, scripts::{ functions::{register_functions_trusted, register_functions_untrusted}, plugins::RegisterSievePlugins, @@ -25,6 +24,7 @@ use registry::{ }; use sieve::{Compiler, Runtime, Sieve, compiler::grammar::Capability}; use std::sync::Arc; +use store::registry::bootstrap::Bootstrap; pub struct Scripting { pub untrusted_compiler: Compiler, diff --git a/crates/common/src/config/mailstore/spamfilter.rs b/crates/common/src/config/mailstore/spamfilter.rs index 9528203e..58430028 100644 --- a/crates/common/src/config/mailstore/spamfilter.rs +++ b/crates/common/src/config/mailstore/spamfilter.rs @@ -4,9 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - expr::{Variable, functions::ResolveVariable, if_block::IfBlock}, - manager::bootstrap::Bootstrap, +use crate::expr::{ + Variable, + functions::ResolveVariable, + if_block::{BootstrapExprExt, IfBlock}, }; use ahash::AHashSet; use mail_auth::common::resolver::ToReverseName; @@ -23,7 +24,7 @@ use std::{ net::{IpAddr, SocketAddr}, time::Duration, }; -use store::registry::RegistryObject; +use store::registry::{RegistryObject, bootstrap::Bootstrap}; use tokio::net::lookup_host; use utils::{cache::CacheItemWeight, config::utils::ParseValue, glob::GlobMap}; @@ -376,15 +377,19 @@ impl SpamFilterLists { match tag.object { SpamTag::Score(tag) => lists .scores - .insert(&tag.tag, SpamFilterAction::Allow(tag.score as f32)), - SpamTag::Discard(tag) => lists.scores.insert(&tag.tag, SpamFilterAction::Discard), - SpamTag::Reject(tag) => lists.scores.insert(&tag.tag, SpamFilterAction::Reject), + .insert_pattern(&tag.tag, SpamFilterAction::Allow(tag.score as f32)), + SpamTag::Discard(tag) => lists + .scores + .insert_pattern(&tag.tag, SpamFilterAction::Discard), + SpamTag::Reject(tag) => lists + .scores + .insert_pattern(&tag.tag, SpamFilterAction::Reject), } } for ext in bp.list_infallible::().await { let ext = ext.object; - lists.file_extensions.insert( + lists.file_extensions.insert_pattern( &ext.extension, FileExtension { known_types: ext.content_types.into_iter().collect(), diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 2df0c265..41e4b89f 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -10,7 +10,6 @@ use crate::{ auth::oauth::config::OAuthConfig, config::mailstore::{imap::ImapConfig, scripts::Scripting, spamfilter::SpamFilterConfig}, expr::*, - manager::bootstrap::Bootstrap, }; use arc_swap::ArcSwap; use coordinator::Coordinator; @@ -19,7 +18,7 @@ use groupware::GroupwareConfig; use hyper::HeaderMap; use ring::signature::{EcdsaKeyPair, RsaKeyPair}; use std::sync::Arc; -use store::{BlobBackend, BlobStore, InMemoryStore, SearchStore, Store, Stores}; +use store::{BlobStore, InMemoryStore, SearchStore, Store, registry::bootstrap::Bootstrap}; use telemetry::Metrics; pub mod groupware; @@ -32,7 +31,7 @@ pub mod storage; pub mod telemetry; impl Core { - pub async fn parse(bp: &mut Bootstrap, mut stores: Stores) -> Self { + pub async fn parse(bp: &mut Bootstrap) -> Self { todo!() /*let mut data = config .value_require("storage.data") diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index cc7516a3..84cdb343 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -5,7 +5,7 @@ */ use super::*; -use crate::{expr::if_block::IfBlock, manager::bootstrap::Bootstrap}; +use crate::expr::if_block::{BootstrapExprExt, IfBlock}; use ahash::AHashMap; use registry::{ schema::{ diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index de706a3e..810fa09a 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -11,7 +11,6 @@ use super::{ use crate::{ Inner, listener::{TcpAcceptor, tls::CertificateResolver}, - manager::bootstrap::Bootstrap, }; use registry::schema::{ enums::{NetworkListenerProtocol, TlsCipherSuite, TlsVersion}, @@ -22,7 +21,7 @@ use rustls::{ crypto::ring::{ALL_CIPHER_SUITES, cipher_suite::*, default_provider}, }; use std::sync::Arc; -use store::registry::RegistryObject; +use store::registry::{RegistryObject, bootstrap::Bootstrap}; use tokio::net::TcpSocket; use tokio_rustls::TlsAcceptor; use utils::snowflake::SnowflakeIdGenerator; @@ -107,8 +106,8 @@ impl Listeners { } } - if let Some(tos) = listener.socket_tos { - if let Err(err) = socket.set_tos(tos as u32) { + if let Some(tos) = listener.socket_tos_v4 { + if let Err(err) = socket.set_tos_v4(tos as u32) { bp.build_error(id, format!("Failed to set IP_TOS: {err}")); return; } @@ -152,7 +151,7 @@ impl Listeners { let listener = listener.object; // Build TLS config - let acceptor = if listener.tls_enable { + let acceptor = if listener.use_tls { // Parse protocol versions let mut tls_v2 = true; let mut tls_v3 = true; diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index c86e2ca4..c1df0a06 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{Server, listener::acme::AcmeProvider, manager::bootstrap::Bootstrap}; +use crate::{Server, listener::acme::AcmeProvider}; use ahash::{AHashMap, AHashSet}; use dns_update::{ Algorithm, DnsUpdater, TsigAlgorithm, @@ -33,7 +33,7 @@ use std::{ net::{Ipv4Addr, Ipv6Addr, SocketAddr}, sync::Arc, }; -use store::registry::RegistryObject; +use store::registry::{RegistryObject, bootstrap::Bootstrap}; use trc::AddContext; use x509_parser::{ certificate::X509Certificate, @@ -195,91 +195,87 @@ impl Server { } } -impl Bootstrap { - pub(crate) async fn parse_certificates( - &mut self, - certificates: &mut AHashMap>, - subject_names: &mut AHashSet, - ) { - // Parse certificates - for cert_obj in self.list_infallible::().await { - match build_certified_key( - cert_obj.object.certificate.into_bytes(), - cert_obj.object.private_key.into_bytes(), - ) { - Ok(cert) => { - match cert - .end_entity_cert() - .map_err(|err| format!("Failed to obtain end entity cert: {err}")) - .and_then(|cert| { - X509Certificate::from_der(cert.as_ref()) - .map_err(|err| format!("Failed to parse end entity cert: {err}")) - }) { - Ok((_, parsed)) => { - // Add CNs and SANs to the list of names - let mut names = AHashSet::new(); - for name in parsed.subject().iter_common_name() { - if let Ok(name) = name.as_str() { - names.insert(name.to_string()); - } - } - for ext in parsed.extensions() { - if let ParsedExtension::SubjectAlternativeName(san) = - ext.parsed_extension() - { - for name in &san.general_names { - let name = match name { - GeneralName::DNSName(name) => name.to_string(), - GeneralName::IPAddress(ip) => match ip.len() { - 4 => Ipv4Addr::from( - <[u8; 4]>::try_from(*ip).unwrap(), - ) - .to_string(), - 16 => Ipv6Addr::from( - <[u8; 16]>::try_from(*ip).unwrap(), - ) - .to_string(), - _ => continue, - }, - _ => { - continue; - } - }; - names.insert(name); - } - } - } - - // Add custom SNIs - names.extend(cert_obj.object.subject_alternative_names); - - // Add domain names - subject_names.extend(names.iter().cloned()); - - // Add certificates - let cert = Arc::new(cert); - for name in names { - certificates.insert( - name.strip_prefix("*.") - .map(|name| name.to_string()) - .unwrap_or(name), - cert.clone(), - ); - } - - // Add default certificate - if cert_obj.object.default { - certificates.insert("*".to_string(), cert.clone()); +pub(crate) async fn parse_certificates( + bp: &mut Bootstrap, + certificates: &mut AHashMap>, + subject_names: &mut AHashSet, +) { + // Parse certificates + for cert_obj in bp.list_infallible::().await { + match build_certified_key( + cert_obj.object.certificate.into_bytes(), + cert_obj.object.private_key.into_bytes(), + ) { + Ok(cert) => { + match cert + .end_entity_cert() + .map_err(|err| format!("Failed to obtain end entity cert: {err}")) + .and_then(|cert| { + X509Certificate::from_der(cert.as_ref()) + .map_err(|err| format!("Failed to parse end entity cert: {err}")) + }) { + Ok((_, parsed)) => { + // Add CNs and SANs to the list of names + let mut names = AHashSet::new(); + for name in parsed.subject().iter_common_name() { + if let Ok(name) = name.as_str() { + names.insert(name.to_string()); } } - Err(err) => { - self.build_error(cert_obj.id, format!("Invalid certificate: {err}")); + for ext in parsed.extensions() { + if let ParsedExtension::SubjectAlternativeName(san) = + ext.parsed_extension() + { + for name in &san.general_names { + let name = match name { + GeneralName::DNSName(name) => name.to_string(), + GeneralName::IPAddress(ip) => match ip.len() { + 4 => Ipv4Addr::from(<[u8; 4]>::try_from(*ip).unwrap()) + .to_string(), + 16 => { + Ipv6Addr::from(<[u8; 16]>::try_from(*ip).unwrap()) + .to_string() + } + _ => continue, + }, + _ => { + continue; + } + }; + names.insert(name); + } + } + } + + // Add custom SNIs + names.extend(cert_obj.object.subject_alternative_names); + + // Add domain names + subject_names.extend(names.iter().cloned()); + + // Add certificates + let cert = Arc::new(cert); + for name in names { + certificates.insert( + name.strip_prefix("*.") + .map(|name| name.to_string()) + .unwrap_or(name), + cert.clone(), + ); + } + + // Add default certificate + if cert_obj.object.default { + certificates.insert("*".to_string(), cert.clone()); } } + Err(err) => { + bp.build_error(cert_obj.id, format!("Invalid certificate: {err}")); + } } - Err(err) => { - self.build_error(cert_obj.id, format!("Invalid certificate: {err}")); - } + } + Err(err) => { + bp.build_error(cert_obj.id, format!("Invalid certificate: {err}")); } } } diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index 7e2eac82..5362ca08 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - expr::{self, if_block::IfBlock}, - manager::bootstrap::Bootstrap, +use crate::expr::{ + self, + if_block::{BootstrapExprExt, IfBlock}, }; use mail_auth::{ common::crypto::{Ed25519Key, HashAlgorithm, RsaKey, Sha256, SigningKey}, @@ -22,6 +22,7 @@ use registry::{ types::ObjectType, }; use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, pem::PemObject}; +use store::registry::bootstrap::Bootstrap; use utils::config::utils::ParseValue; #[derive(Clone)] @@ -43,7 +44,7 @@ pub struct DkimAuthConfig { #[derive(Clone)] pub struct ArcAuthConfig { pub verify: IfBlock, - pub seal: IfBlock, + //pub seal: IfBlock, } #[derive(Clone)] @@ -92,7 +93,7 @@ impl MailAuthConfig { }, arc: ArcAuthConfig { verify: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_arc_verify()), - seal: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_arc_seal_domain()), + //seal: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_arc_seal_domain()), }, spf: SpfAuthConfig { verify_ehlo: bp diff --git a/crates/common/src/config/smtp/mod.rs b/crates/common/src/config/smtp/mod.rs index e9ae765a..9a510533 100644 --- a/crates/common/src/config/smtp/mod.rs +++ b/crates/common/src/config/smtp/mod.rs @@ -15,8 +15,9 @@ use self::{ session::SessionConfig, }; use super::*; -use crate::{expr::Expression, manager::bootstrap::Bootstrap}; +use crate::expr::Expression; use registry::schema::structs::Rate; +use store::registry::bootstrap::Bootstrap; #[derive(Clone)] pub struct SmtpConfig { diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 3aff964b..d337aa49 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -7,7 +7,10 @@ use super::*; use crate::{ config::server::ServerProtocol, - expr::{if_block::IfBlock, *}, + expr::{ + if_block::{BootstrapExprExt, IfBlock}, + *, + }, }; use ahash::AHashMap; use mail_auth::IpLookupStrategy; diff --git a/crates/common/src/config/smtp/report.rs b/crates/common/src/config/smtp/report.rs index 31c72a58..02d6eb5c 100644 --- a/crates/common/src/config/smtp/report.rs +++ b/crates/common/src/config/smtp/report.rs @@ -5,7 +5,10 @@ */ use super::*; -use crate::expr::{Variable, if_block::IfBlock}; +use crate::expr::{ + Variable, + if_block::{BootstrapExprExt, IfBlock}, +}; use registry::schema::{ enums::ExpressionConstant, prelude::Object, diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index f46c576a..b4648009 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{Server, manager::bootstrap::Bootstrap}; +use crate::Server; use mail_auth::{ MessageAuthenticator, hickory_resolver::{ @@ -26,6 +26,7 @@ use std::{ net::SocketAddr, sync::Arc, }; +use store::registry::bootstrap::Bootstrap; use utils::{cache::CacheItemWeight, config::utils::ParseValue}; pub struct Resolvers { diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 27fe4b41..b3ec6ea4 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -6,7 +6,7 @@ use self::resolver::Policy; use super::*; -use crate::expr::if_block::IfBlock; +use crate::expr::if_block::{BootstrapExprExt, IfBlock}; use ahash::AHashSet; use hyper::HeaderMap; use registry::schema::{ diff --git a/crates/common/src/config/telemetry.rs b/crates/common/src/config/telemetry.rs index bcb62fa3..ebcc2f28 100644 --- a/crates/common/src/config/telemetry.rs +++ b/crates/common/src/config/telemetry.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::manager::bootstrap::Bootstrap; use ahash::{AHashMap, AHashSet, HashSet}; use base64::{Engine, engine::general_purpose::STANDARD}; use hyper::HeaderMap; @@ -24,6 +23,7 @@ use registry::schema::{ structs::{self, EventTracingLevel, MetricsPrometheus, Tracer, WebHook}, }; use std::{collections::HashMap, sync::Arc, time::Duration}; +use store::registry::bootstrap::Bootstrap; use trc::{EventType, Level, MetricType, TelemetryEvent, ipc::subscriber::Interests}; #[derive(Debug)] diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index 667d7f4e..86b316de 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -20,7 +20,6 @@ use crate::{ ipc::{BroadcastEvent, PushEvent, PushNotification}, manager::SPAM_CLASSIFIER_KEY, }; -use directory::{Directory, QueryParams, Type, backend::internal::manage::ManageDirectory}; use mail_auth::IpLookupStrategy; use sieve::Sieve; use std::{ diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index 600f5227..295e44f6 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -12,7 +12,7 @@ use super::{ AlertContent, AlertContentToken, AlertMethod, Enterprise, MetricAlert, MetricStore, SpamFilterLlmConfig, TraceStore, Undelete, license::LicenseKey, llm::AiApiConfig, }; -use crate::{enterprise::llm::ApiType, manager::bootstrap::Bootstrap}; +use crate::enterprise::llm::ApiType; use ahash::AHashMap; use registry::{ schema::{ diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index 70f71535..6514c3a6 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -19,10 +19,6 @@ use crate::{ manager::webadmin::Resource, }; use ahash::{AHashMap, AHashSet}; -use directory::{ - QueryParams, Type, - backend::internal::{lookup::DirectoryStore, manage::ManageDirectory}, -}; use license::LicenseKey; use llm::AiApiConfig; use mail_parser::DateTime; diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index 2bd810ff..cfe9f8f6 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -7,7 +7,6 @@ use std::{cmp::Ordering, net::IpAddr, vec::IntoIter}; use compact_str::{CompactString, ToCompactString}; -use directory::backend::RcptType; use mail_auth::IpLookupStrategy; use store::{Deserialize, Rows, Value, dispatch::lookup::KeyValue}; use trc::AddContext; diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index f6d51fbd..3c7d4471 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -9,10 +9,7 @@ use super::{ parser::ExpressionParser, tokenizer::{TokenMap, Tokenizer}, }; -use crate::{ - expr::{Constant, Expression}, - manager::bootstrap::Bootstrap, -}; +use crate::expr::{Constant, Expression}; use compact_str::CompactString; use registry::{ schema::{ @@ -21,6 +18,7 @@ use registry::{ }, types::id::Id, }; +use store::registry::bootstrap::Bootstrap; #[derive(Debug, Clone, PartialEq, Eq)] pub struct IfThen { @@ -83,8 +81,19 @@ impl Expression { } } -impl Bootstrap { - pub fn compile_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock { +pub(crate) trait BootstrapExprExt { + fn compile_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock; + fn compile_default_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock; + fn try_compile_expr( + &mut self, + id: Id, + expr_ctx: &ExpressionContext<'_>, + expr: &structs::Expression, + ) -> Option; +} + +impl BootstrapExprExt for Bootstrap { + fn compile_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock { if expr_ctx.expr.else_.is_empty() && expr_ctx.expr.match_.is_empty() { return IfBlock::empty(id, expr_ctx.property); } @@ -96,7 +105,7 @@ impl Bootstrap { } } - pub fn compile_default_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock { + fn compile_default_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock { if let Some(default) = &expr_ctx.default { self.try_compile_expr(id, expr_ctx, default) .expect("Valid default expression") @@ -105,7 +114,7 @@ impl Bootstrap { } } - pub fn try_compile_expr( + fn try_compile_expr( &mut self, id: Id, expr_ctx: &ExpressionContext<'_>, diff --git a/crates/common/src/listener/blocked.rs b/crates/common/src/listener/blocked.rs index 54216d78..f81afd40 100644 --- a/crates/common/src/listener/blocked.rs +++ b/crates/common/src/listener/blocked.rs @@ -6,7 +6,7 @@ use crate::{ KV_RATE_LIMIT_AUTH, KV_RATE_LIMIT_LOITER, KV_RATE_LIMIT_RCPT, KV_RATE_LIMIT_SCAN, Server, - ip_to_bytes, ipc::BroadcastEvent, manager::bootstrap::Bootstrap, + ip_to_bytes, ipc::BroadcastEvent, }; use ahash::AHashSet; use registry::{ @@ -17,7 +17,7 @@ use registry::{ types::{datetime::UTCDateTime, ipmask::IpAddrOrMask}, }; use std::{fmt::Debug, net::IpAddr}; -use store::write::now; +use store::{registry::bootstrap::Bootstrap, write::now}; use trc::AddContext; use utils::glob::{GlobPattern, MatchType}; diff --git a/crates/common/src/listener/listen.rs b/crates/common/src/listener/listen.rs index b0860d3c..5f288953 100644 --- a/crates/common/src/listener/listen.rs +++ b/crates/common/src/listener/listen.rs @@ -12,7 +12,6 @@ use crate::{ Inner, Server, config::server::{Listener, Listeners, ServerProtocol, TcpListener}, core::BuildServer, - manager::bootstrap::Bootstrap, }; use proxy_header::io::ProxiedStream; use rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256; @@ -21,6 +20,7 @@ use std::{ sync::Arc, time::Duration, }; +use store::registry::bootstrap::Bootstrap; use tokio::{net::TcpStream, sync::watch}; use tokio_rustls::server::TlsStream; use trc::{EventType, HttpEvent, ImapEvent, ManageSieveEvent, Pop3Event, SmtpEvent}; diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index 20a9add1..6baf7f8e 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -21,10 +21,7 @@ use std::{ path::PathBuf, sync::Arc, }; -use store::{ - Stores, - rand::{Rng, distr::Alphanumeric, rng}, -}; +use store::rand::{Rng, distr::Alphanumeric, rng}; use tokio::sync::{Notify, mpsc}; use utils::{ UnwrapFailure, diff --git a/crates/common/src/manager/reload.rs b/crates/common/src/manager/reload.rs index ec08cdbb..2f58619d 100644 --- a/crates/common/src/manager/reload.rs +++ b/crates/common/src/manager/reload.rs @@ -11,7 +11,6 @@ use crate::{ }; use ahash::AHashMap; use arc_swap::ArcSwap; -use store::Stores; use utils::config::Config; pub struct ReloadResult { diff --git a/crates/common/src/manager/restore.rs b/crates/common/src/manager/restore.rs index efa2acb8..116e29fb 100644 --- a/crates/common/src/manager/restore.rs +++ b/crates/common/src/manager/restore.rs @@ -7,6 +7,7 @@ use super::backup::MAGIC_MARKER; use crate::{Core, DATABASE_SCHEMA_VERSION}; use lz4_flex::frame::FrameDecoder; +use registry::schema::enums::CompressionAlgo; use std::{ fs::File, io::{BufReader, ErrorKind, Read}, @@ -56,7 +57,7 @@ async fn restore_file(store: Store, blob_store: BlobStore, path: &Path) { SUBSPACE_BLOBS => { while let Some((key, value)) = reader.next() { blob_store - .put_blob(&key, &value) + .put_blob(&key, &value, CompressionAlgo::Lz4) .await .failed("Failed to write blob"); } diff --git a/crates/common/src/scripts/plugins/llm_prompt.rs b/crates/common/src/scripts/plugins/llm_prompt.rs index fbd01252..21a0ec0d 100644 --- a/crates/common/src/scripts/plugins/llm_prompt.rs +++ b/crates/common/src/scripts/plugins/llm_prompt.rs @@ -4,10 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Instant; - -use directory::Permission; use sieve::{FunctionMap, compiler::Number, runtime::Variable}; +use std::time::Instant; use trc::{AiEvent, SecurityEvent}; use super::PluginContext; diff --git a/crates/common/src/sharing/acl.rs b/crates/common/src/sharing/acl.rs index 93b631e7..3b921abe 100644 --- a/crates/common/src/sharing/acl.rs +++ b/crates/common/src/sharing/acl.rs @@ -5,10 +5,6 @@ */ use crate::Server; -use directory::{ - Type, - backend::internal::{PrincipalField, manage::ChangedPrincipals}, -}; use types::acl::{AclGrant, ArchivedAclGrant}; impl Server { diff --git a/crates/coordinator/Cargo.toml b/crates/coordinator/Cargo.toml index 1b14ddd7..56b5323a 100644 --- a/crates/coordinator/Cargo.toml +++ b/crates/coordinator/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] utils = { path = "../utils" } store = { path = "../store" } +registry = { path = "../registry" } trc = { path = "../trc" } futures = { version = "0.3", optional = true } tokio = { version = "1.47", features = ["sync", "fs", "io-util"] } diff --git a/crates/coordinator/src/backend/kafka/mod.rs b/crates/coordinator/src/backend/kafka/mod.rs index db8ef919..306ee3d0 100644 --- a/crates/coordinator/src/backend/kafka/mod.rs +++ b/crates/coordinator/src/backend/kafka/mod.rs @@ -4,14 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use std::sync::Arc; + +use crate::Coordinator; use rdkafka::{ ClientConfig, ClientContext, TopicPartitionList, consumer::{BaseConsumer, ConsumerContext, Rebalance, StreamConsumer}, error::KafkaResult, producer::FutureProducer, }; -use std::{fmt::Debug, time::Duration}; -use utils::config::{Config, utils::AsKey}; +use registry::schema::structs::KafkaCoordinator; pub mod pubsub; @@ -23,70 +25,41 @@ pub struct KafkaPubSub { } impl KafkaPubSub { - pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option { - let prefix = prefix.as_key(); - let brokers = config - .values((&prefix, "brokers")) - .map(|(_, v)| v.to_string()) - .collect::>(); - if brokers.is_empty() { - config.new_build_error((&prefix, "brokers"), "No Kafka brokers specified"); - return None; + pub async fn open(config: KafkaCoordinator) -> Result { + if config.brokers.is_empty() { + return Err("No Kafka brokers specified".to_string()); } + let brokers = config.brokers.join(","); let mut consumer_builder = ClientConfig::new(); consumer_builder - .set( - "group.id", - config.value_require_non_empty((&prefix, "group-id"))?, - ) - .set( - "bootstrap.servers", - config.value_require_non_empty((&prefix, "brokers"))?, - ) + .set("group.id", config.group_id) + .set("bootstrap.servers", &brokers) .set("enable.partition.eof", "false") .set( "session.timeout.ms", - config - .property_or_default((&prefix, "timeout.session"), "5s") - .unwrap_or(Duration::from_secs(5)) - .as_millis() - .to_string(), + config.timeout_session.as_millis().to_string(), ) .set("enable.auto.commit", "true"); let producer = ClientConfig::new() - .set( - "bootstrap.servers", - config.value_require_non_empty((&prefix, "brokers"))?, - ) + .set("bootstrap.servers", brokers) .set( "message.timeout.ms", - config - .property_or_default((&prefix, "timeout.message"), "5s") - .unwrap_or(Duration::from_secs(5)) - .as_millis() - .to_string(), + config.timeout_message.as_millis().to_string(), ) .create() - .map_err(|err| { - config.new_build_error( - (&prefix, "config"), - format!("Failed to create Kafka producer: {}", err), - ); - }) - .ok()?; + .map_err(|err| format!("Failed to create Kafka producer: {}", err))?; - KafkaPubSub { + Ok(Coordinator::Kafka(Arc::new(KafkaPubSub { consumer_builder, producer, - } - .into() + }))) } } -impl Debug for KafkaPubSub { +impl std::fmt::Debug for KafkaPubSub { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("KafkaPubSub").finish() } diff --git a/crates/coordinator/src/backend/kafka/pubsub.rs b/crates/coordinator/src/backend/kafka/pubsub.rs index 53056bc6..00b77e71 100644 --- a/crates/coordinator/src/backend/kafka/pubsub.rs +++ b/crates/coordinator/src/backend/kafka/pubsub.rs @@ -4,15 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; - use super::{CustomContext, KafkaPubSub, LoggingConsumer}; -use crate::dispatch::pubsub::{Msg, PubSubStream}; +use crate::{Msg, PubSubStream}; use rdkafka::{ Message, consumer::{CommitMode, Consumer, StreamConsumer}, producer::FutureRecord, }; +use std::time::Duration; use trc::{ClusterEvent, Error, EventType}; pub struct KafkaPubSubStream { diff --git a/crates/coordinator/src/backend/nats/mod.rs b/crates/coordinator/src/backend/nats/mod.rs index f9e37213..d1c5e2c5 100644 --- a/crates/coordinator/src/backend/nats/mod.rs +++ b/crates/coordinator/src/backend/nats/mod.rs @@ -4,10 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; +use std::sync::Arc; +use crate::Coordinator; use async_nats::Client; -use utils::config::{Config, utils::AsKey}; +use registry::schema::structs::NatsCoordinator; pub mod pubsub; @@ -17,92 +18,36 @@ pub struct NatsPubSub { } impl NatsPubSub { - pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option { - let prefix = prefix.as_key(); - let urls = config - .values((&prefix, "address")) - .map(|(_, v)| v.to_string()) - .collect::>(); - if urls.is_empty() { - config.new_build_error((&prefix, "address"), "No Nats addresses specified"); - return None; + pub async fn open(config: NatsCoordinator) -> Result { + if config.addresses.is_empty() { + return Err("No Nats addresses specified".to_string()); } let mut opts = async_nats::ConnectOptions::new() - .max_reconnects( - config - .property_or_default::>((&prefix, "max-reconnects"), "false") - .unwrap_or_default(), - ) - .connection_timeout( - config - .property_or_default((&prefix, "timeout.connection"), "5s") - .unwrap_or_else(|| Duration::from_secs(5)), - ) - .request_timeout( - config - .property_or_default::>((&prefix, "timeout.request"), "10s") - .unwrap_or_else(|| Some(Duration::from_secs(10))), - ) - .ping_interval( - config - .property_or_default((&prefix, "ping-interval"), "60s") - .unwrap_or_else(|| Duration::from_secs(5)), - ) - .client_capacity( - config - .property_or_default((&prefix, "capacity.client"), "2048") - .unwrap_or(2048), - ) - .subscription_capacity( - config - .property_or_default((&prefix, "capacity.subscription"), "65536") - .unwrap_or(65536), - ) - .read_buffer_capacity( - config - .property_or_default((&prefix, "capacity.read-buffer"), "65535") - .unwrap_or(65535), - ) - .require_tls( - config - .property_or_default((&prefix, "tls.enable"), "false") - .unwrap_or_default(), - ); + .max_reconnects(config.max_reconnects.map(|v| v as usize)) + .connection_timeout(config.timeout_connection.into_inner()) + .request_timeout(config.timeout_request.into_inner().into()) + .ping_interval(config.ping_interval.into_inner()) + .client_capacity(config.capacity_client as usize) + .subscription_capacity(config.capacity_subscription as usize) + .read_buffer_capacity(config.capacity_read_buffer as u16) + .require_tls(config.use_tls); - if config - .property_or_default((&prefix, "no-echo"), "true") - .unwrap_or(true) - { + if config.no_echo { opts = opts.no_echo(); } - if let (Some(user), Some(pass)) = ( - config.value((&prefix, "user")), - config.value((&prefix, "password")), - ) { + if let (Some(user), Some(pass)) = (config.auth_username, config.auth_secret) { opts = opts.user_and_password(user.to_string(), pass.to_string()); - } else if let Some(credentials) = config.value((&prefix, "credentials")) { + } else if let Some(credentials) = config.credentials { opts = opts - .credentials(credentials) - .map_err(|err| { - config.new_build_error( - (&prefix, "credentials"), - format!("Failed to parse Nats credentials: {}", err), - ); - }) - .ok()?; + .credentials(&credentials) + .map_err(|err| format!("Failed to parse Nats credentials: {}", err))?; } - async_nats::connect_with_options(urls, opts) + async_nats::connect_with_options(config.addresses, opts) .await - .map_err(|err| { - config.new_build_error( - (&prefix, "urls"), - format!("Failed to connect to Nats: {}", err), - ); - }) - .map(|client| NatsPubSub { client }) - .ok() + .map(|client| Coordinator::Nats(Arc::new(NatsPubSub { client }))) + .map_err(|err| format!("Failed to connect to Nats: {}", err)) } } diff --git a/crates/coordinator/src/backend/nats/pubsub.rs b/crates/coordinator/src/backend/nats/pubsub.rs index 46f24bc1..3b1d8766 100644 --- a/crates/coordinator/src/backend/nats/pubsub.rs +++ b/crates/coordinator/src/backend/nats/pubsub.rs @@ -5,7 +5,7 @@ */ use super::NatsPubSub; -use crate::dispatch::pubsub::{Msg, PubSubStream}; +use crate::{Msg, PubSubStream}; use futures::StreamExt; use trc::{ClusterEvent, Error, EventType}; diff --git a/crates/coordinator/src/backend/redis/mod.rs b/crates/coordinator/src/backend/redis/mod.rs index ec5e9322..02693767 100644 --- a/crates/coordinator/src/backend/redis/mod.rs +++ b/crates/coordinator/src/backend/redis/mod.rs @@ -4,102 +4,4 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{Msg, PubSubStream}; -use futures::StreamExt; -use redis::{AsyncCommands, PushInfo, cluster::ClusterConfig, cluster_async::ClusterConnection}; -use std::fmt::Display; -use store::backend::redis::{RedisPool, RedisStore}; -use tokio::sync::mpsc::UnboundedReceiver; - -pub struct RedisPubSubStream { - stream: redis::aio::PubSubStream, -} - -pub struct RedisClusterPubSubStream { - _conn: ClusterConnection, - rx: UnboundedReceiver, -} - -pub(crate) async fn redis_publish( - redis: &RedisStore, - topic: &'static str, - message: Vec, -) -> trc::Result<()> { - match &redis.pool { - RedisPool::Single(pool) => pool - .get() - .await - .map_err(into_error)? - .as_mut() - .publish(topic, message) - .await - .map_err(into_error), - RedisPool::Cluster(pool) => pool - .get() - .await - .map_err(into_error)? - .as_mut() - .publish(topic, message) - .await - .map_err(into_error), - } -} - -pub(crate) async fn redis_subscribe( - redis: &RedisStore, - topic: &'static str, -) -> trc::Result { - match &redis.pool { - RedisPool::Single(pool) => { - let mut pubsub = pool - .manager() - .client - .get_async_pubsub() - .await - .map_err(into_error)?; - pubsub.subscribe(topic).await.map_err(into_error)?; - - Ok(PubSubStream::Redis(RedisPubSubStream { - stream: pubsub.into_on_message(), - })) - } - RedisPool::Cluster(pool) => { - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - - let mut _conn = pool - .manager() - .client - .get_async_connection_with_config(ClusterConfig::default().set_push_sender(tx)) - .await - .map_err(into_error)?; - - _conn.subscribe(topic).await.map_err(into_error)?; - - Ok(PubSubStream::RedisCluster(RedisClusterPubSubStream { - _conn, - rx, - })) - } - } -} - -impl RedisPubSubStream { - pub async fn next(&mut self) -> Option { - self.stream.next().await.map(Msg::Redis) - } -} - -impl RedisClusterPubSubStream { - pub async fn next(&mut self) -> Option { - loop { - if let Some(msg) = redis::Msg::from_push_info(self.rx.recv().await?) { - return Some(Msg::Redis(msg)); - } - } - } -} - -#[inline(always)] -fn into_error(err: impl Display) -> trc::Error { - trc::StoreEvent::RedisError.reason(err) -} +pub mod pubsub; diff --git a/crates/coordinator/src/backend/redis/pubsub.rs b/crates/coordinator/src/backend/redis/pubsub.rs new file mode 100644 index 00000000..ec5e9322 --- /dev/null +++ b/crates/coordinator/src/backend/redis/pubsub.rs @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{Msg, PubSubStream}; +use futures::StreamExt; +use redis::{AsyncCommands, PushInfo, cluster::ClusterConfig, cluster_async::ClusterConnection}; +use std::fmt::Display; +use store::backend::redis::{RedisPool, RedisStore}; +use tokio::sync::mpsc::UnboundedReceiver; + +pub struct RedisPubSubStream { + stream: redis::aio::PubSubStream, +} + +pub struct RedisClusterPubSubStream { + _conn: ClusterConnection, + rx: UnboundedReceiver, +} + +pub(crate) async fn redis_publish( + redis: &RedisStore, + topic: &'static str, + message: Vec, +) -> trc::Result<()> { + match &redis.pool { + RedisPool::Single(pool) => pool + .get() + .await + .map_err(into_error)? + .as_mut() + .publish(topic, message) + .await + .map_err(into_error), + RedisPool::Cluster(pool) => pool + .get() + .await + .map_err(into_error)? + .as_mut() + .publish(topic, message) + .await + .map_err(into_error), + } +} + +pub(crate) async fn redis_subscribe( + redis: &RedisStore, + topic: &'static str, +) -> trc::Result { + match &redis.pool { + RedisPool::Single(pool) => { + let mut pubsub = pool + .manager() + .client + .get_async_pubsub() + .await + .map_err(into_error)?; + pubsub.subscribe(topic).await.map_err(into_error)?; + + Ok(PubSubStream::Redis(RedisPubSubStream { + stream: pubsub.into_on_message(), + })) + } + RedisPool::Cluster(pool) => { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let mut _conn = pool + .manager() + .client + .get_async_connection_with_config(ClusterConfig::default().set_push_sender(tx)) + .await + .map_err(into_error)?; + + _conn.subscribe(topic).await.map_err(into_error)?; + + Ok(PubSubStream::RedisCluster(RedisClusterPubSubStream { + _conn, + rx, + })) + } + } +} + +impl RedisPubSubStream { + pub async fn next(&mut self) -> Option { + self.stream.next().await.map(Msg::Redis) + } +} + +impl RedisClusterPubSubStream { + pub async fn next(&mut self) -> Option { + loop { + if let Some(msg) = redis::Msg::from_push_info(self.rx.recv().await?) { + return Some(Msg::Redis(msg)); + } + } + } +} + +#[inline(always)] +fn into_error(err: impl Display) -> trc::Error { + trc::StoreEvent::RedisError.reason(err) +} diff --git a/crates/coordinator/src/backend/zenoh/mod.rs b/crates/coordinator/src/backend/zenoh/mod.rs index 3adf6bab..17b5a960 100644 --- a/crates/coordinator/src/backend/zenoh/mod.rs +++ b/crates/coordinator/src/backend/zenoh/mod.rs @@ -4,7 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use utils::config::{Config, utils::AsKey}; +use registry::schema::structs::ZenohCoordinator; + +use crate::Coordinator; pub mod pubsub; #[derive(Debug)] @@ -13,26 +15,13 @@ pub struct ZenohPubSub { } impl ZenohPubSub { - pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option { - let prefix = prefix.as_key(); - let zenoh_config = - zenoh::Config::from_json5(config.value_require_non_empty((&prefix, "config"))?) - .map_err(|err| { - config.new_build_error( - (&prefix, "config"), - format!("Invalid zenoh config: {}", err), - ); - }) - .ok()?; + pub async fn open(config: ZenohCoordinator) -> Result { + let zenoh_config = zenoh::Config::from_json5(&config.config) + .map_err(|err| format!("Invalid Zenoh config: {}", err))?; zenoh::open(zenoh_config) .await - .map_err(|err| { - config.new_build_error( - (&prefix, "config"), - format!("Failed to create zenoh session: {}", err), - ); - }) + .map_err(|err| format!("Failed to create Zenoh session: {}", err)) .map(|session| ZenohPubSub { session }) - .ok() + .map(|store| Coordinator::Zenoh(std::sync::Arc::new(store))) } } diff --git a/crates/coordinator/src/backend/zenoh/pubsub.rs b/crates/coordinator/src/backend/zenoh/pubsub.rs index b19455c3..6650edda 100644 --- a/crates/coordinator/src/backend/zenoh/pubsub.rs +++ b/crates/coordinator/src/backend/zenoh/pubsub.rs @@ -5,7 +5,7 @@ */ use super::ZenohPubSub; -use crate::dispatch::pubsub::{Msg, PubSubStream}; +use crate::{Msg, PubSubStream}; use trc::{ClusterEvent, Error, EventType}; pub struct ZenohPubSubStream { diff --git a/crates/coordinator/src/bootstrap.rs b/crates/coordinator/src/bootstrap.rs new file mode 100644 index 00000000..3ec480e7 --- /dev/null +++ b/crates/coordinator/src/bootstrap.rs @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::Coordinator; +use registry::schema::{prelude::Object, structs}; +use store::{InMemoryStore, registry::bootstrap::Bootstrap}; + +#[allow(unreachable_patterns)] +impl Coordinator { + pub async fn build(bp: &mut Bootstrap, in_memory: &InMemoryStore) -> Option { + let result = match bp.setting_infallible::().await { + structs::Coordinator::Disabled => Ok(Coordinator::None), + #[cfg(feature = "redis")] + structs::Coordinator::Default => { + if let InMemoryStore::Redis(redis) = &in_memory { + Ok(Coordinator::Redis(redis.clone())) + } else { + Err( + "Default coordinator requires Redis or Redis Cluster in-memory backend" + .to_string(), + ) + } + } + #[cfg(feature = "kafka")] + structs::Coordinator::Kafka(kafka_coordinator) => { + crate::backend::kafka::KafkaPubSub::open(kafka_coordinator).await + } + #[cfg(feature = "nats")] + structs::Coordinator::Nats(nats_coordinator) => { + crate::backend::nats::NatsPubSub::open(nats_coordinator).await + } + #[cfg(feature = "zenoh")] + structs::Coordinator::Zenoh(zenoh_coordinator) => { + crate::backend::zenoh::ZenohPubSub::open(zenoh_coordinator).await + } + #[cfg(feature = "redis")] + structs::Coordinator::Redis(redis_store) => { + store::backend::redis::RedisStore::open_single(redis_store) + .await + .map(unwrap_redis) + } + #[cfg(feature = "redis")] + structs::Coordinator::RedisCluster(redis_cluster_store) => { + store::backend::redis::RedisStore::open_cluster(redis_cluster_store) + .await + .map(unwrap_redis) + } + _ => Err("Binary was not compiled with the selected coordinator backend".to_string()), + }; + + match result { + Ok(store) => Some(store), + Err(err) => { + bp.build_error(Object::Coordinator.singleton(), err); + None + } + } + } +} + +fn unwrap_redis(store: InMemoryStore) -> Coordinator { + if let InMemoryStore::Redis(redis) = store { + Coordinator::Redis(redis) + } else { + unreachable!() + } +} diff --git a/crates/coordinator/src/dispatch.rs b/crates/coordinator/src/dispatch.rs index e3c22ca7..bd84b67c 100644 --- a/crates/coordinator/src/dispatch.rs +++ b/crates/coordinator/src/dispatch.rs @@ -12,7 +12,7 @@ impl Coordinator { match self { #[cfg(feature = "redis")] Coordinator::Redis(store) => { - crate::backend::redis::redis_publish(store, topic, message).await + crate::backend::redis::pubsub::redis_publish(store, topic, message).await } #[cfg(feature = "nats")] Coordinator::Nats(store) => store.publish(topic, message).await, @@ -27,7 +27,9 @@ impl Coordinator { pub async fn subscribe(&self, topic: &'static str) -> trc::Result { match self { #[cfg(feature = "redis")] - Coordinator::Redis(store) => crate::backend::redis::redis_subscribe(store, topic).await, + Coordinator::Redis(store) => { + crate::backend::redis::pubsub::redis_subscribe(store, topic).await + } #[cfg(feature = "nats")] Coordinator::Nats(store) => store.subscribe(topic).await, #[cfg(feature = "zenoh")] diff --git a/crates/coordinator/src/lib.rs b/crates/coordinator/src/lib.rs index 4a8760fe..5a3cbf1f 100644 --- a/crates/coordinator/src/lib.rs +++ b/crates/coordinator/src/lib.rs @@ -8,6 +8,7 @@ use std::sync::Arc; pub mod backend; +pub mod bootstrap; pub mod dispatch; #[derive(Clone, Default)] @@ -26,9 +27,9 @@ pub enum Coordinator { pub enum PubSubStream { #[cfg(feature = "redis")] - Redis(crate::backend::redis::RedisPubSubStream), + Redis(crate::backend::redis::pubsub::RedisPubSubStream), #[cfg(feature = "redis")] - RedisCluster(crate::backend::redis::RedisClusterPubSubStream), + RedisCluster(crate::backend::redis::pubsub::RedisClusterPubSubStream), #[cfg(feature = "nats")] Nats(crate::backend::nats::pubsub::NatsPubSubStream), #[cfg(feature = "zenoh")] diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index 7b2e7e72..affb27e3 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -10,10 +10,7 @@ store = { path = "../store" } trc = { path = "../trc" } nlp = { path = "../nlp" } types = { path = "../types" } -smtp-proto = { version = "0.2" } -mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } -mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } -mail-builder = { version = "0.4" } +registry = { path = "../registry" } tokio = { version = "1.47", features = ["net"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] } rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] } @@ -46,3 +43,6 @@ tokio = { version = "1.47", features = ["full"] } [features] test_mode = [] enterprise = [] +mysql = [] +postgres = [] +sqlite = [] diff --git a/crates/directory/src/backend/imap/client.rs b/crates/directory/src/backend/imap/client.rs deleted file mode 100644 index ac3c8a3e..00000000 --- a/crates/directory/src/backend/imap/client.rs +++ /dev/null @@ -1,207 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use mail_send::Credentials; -use smtp_proto::{ - AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2, IntoString, - request::{AUTH, parser::Rfc5321Parser}, - response::generate::BitToString, -}; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; - -use super::{ImapClient, ImapError}; - -impl ImapClient { - pub async fn authenticate( - &mut self, - mechanism: u64, - credentials: &Credentials, - ) -> Result<(), ImapError> { - if (mechanism & (AUTH_PLAIN | AUTH_XOAUTH2 | AUTH_OAUTHBEARER)) != 0 { - self.write( - format!( - "C3 AUTHENTICATE {} {}\r\n", - mechanism.to_mechanism(), - credentials - .encode(mechanism, "") - .map_err(|err| ImapError::InvalidChallenge(err.to_string()))? - ) - .as_bytes(), - ) - .await?; - } else { - self.write(format!("C3 AUTHENTICATE {}\r\n", mechanism.to_mechanism()).as_bytes()) - .await?; - } - let mut line = self.read_line().await?; - - for _ in 0..3 { - if matches!(line.first(), Some(b'+')) { - self.write( - format!( - "{}\r\n", - credentials - .encode( - mechanism, - std::str::from_utf8(line.get(2..).unwrap_or_default()) - .unwrap_or_default() - ) - .map_err(|err| ImapError::InvalidChallenge(err.to_string()))? - ) - .as_bytes(), - ) - .await?; - line = self.read_line().await?; - } else if matches!(line.get(..5), Some(b"C3 OK")) { - return Ok(()); - } else if matches!(line.get(..5), Some(b"C3 NO")) - || matches!(line.get(..6), Some(b"C3 BAD")) - { - return Err(ImapError::AuthenticationFailed); - } else { - return Err(ImapError::InvalidResponse(line.into_string())); - } - } - - Err(ImapError::InvalidResponse(line.into_string())) - } - - pub async fn authentication_mechanisms(&mut self) -> Result { - tokio::time::timeout(self.timeout, async { - self.write(b"C0 CAPABILITY\r\n").await?; - - let mut line = self.read_line().await?.into_string(); - if !line.starts_with("* CAPABILITY") { - return Err(ImapError::InvalidResponse(line)); - } - while !line.contains("C0 ") { - line.push_str(&self.read_line().await?.into_string()); - } - - let mut line_iter = line.as_bytes().iter(); - let mut parser = Rfc5321Parser::new(&mut line_iter); - let mut mechanisms = 0; - - 'outer: while let Ok(ch) = parser.read_char() { - if ch == b' ' { - loop { - if parser.hashed_value().unwrap_or(0) == AUTH && parser.stop_char == b'=' { - if let Ok(Some(mechanism)) = parser.mechanism() { - mechanisms |= mechanism; - } - match parser.stop_char { - b' ' => (), - b'\n' => break 'outer, - _ => break, - } - } - } - } else if ch == b'\n' { - break; - } - } - - Ok(mechanisms) - }) - .await - .map_err(|_| ImapError::Timeout)? - } - - pub async fn noop(&mut self) -> Result<(), ImapError> { - tokio::time::timeout(self.timeout, async { - self.write(b"C8 NOOP\r\n").await?; - self.read_line().await?; - Ok(()) - }) - .await - .map_err(|_| ImapError::Timeout)? - } - - pub async fn logout(&mut self) -> Result<(), ImapError> { - tokio::time::timeout(self.timeout, async { - self.write(b"C9 LOGOUT\r\n").await?; - Ok(()) - }) - .await - .map_err(|_| ImapError::Timeout)? - } - - pub async fn expect_greeting(&mut self) -> Result<(), ImapError> { - tokio::time::timeout(self.timeout, async { - let line = self.read_line().await?; - if matches!(line.get(..4), Some(b"* OK")) { - Ok(()) - } else { - Err(ImapError::InvalidResponse(line.into_string())) - } - }) - .await - .map_err(|_| ImapError::Timeout)? - } - - pub async fn read_line(&mut self) -> Result, ImapError> { - let mut buf = vec![0u8; 1024]; - let mut buf_extended = Vec::with_capacity(0); - - loop { - let br = self.stream.read(&mut buf).await?; - - if br > 0 { - if matches!(buf.get(br - 1), Some(b'\n')) { - //println!("{:?}", std::str::from_utf8(&buf[..br]).unwrap()); - return Ok(if buf_extended.is_empty() { - buf.truncate(br); - buf - } else { - buf_extended.extend_from_slice(&buf[..br]); - buf_extended - }); - } else if buf_extended.is_empty() { - buf_extended = buf[..br].to_vec(); - } else { - buf_extended.extend_from_slice(&buf[..br]); - } - } else { - return Err(ImapError::Disconnected); - } - } - } - - pub async fn write(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> { - self.stream.write_all(bytes).await?; - self.stream.flush().await - } -} - -#[cfg(test)] -mod test { - use mail_send::smtp::tls::build_tls_connector; - use smtp_proto::{AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH, AUTH_XOAUTH2}; - use std::time::Duration; - - use crate::backend::imap::ImapClient; - - #[ignore] - #[tokio::test] - async fn imap_auth() { - let connector = build_tls_connector(false); - - let mut client = ImapClient::connect( - "imap.gmail.com:993", - Duration::from_secs(5), - &connector, - "imap.gmail.com", - true, - ) - .await - .unwrap(); - assert_eq!( - AUTH_PLAIN | AUTH_XOAUTH | AUTH_XOAUTH2 | AUTH_OAUTHBEARER, - client.authentication_mechanisms().await.unwrap() - ); - client.logout().await.unwrap(); - } -} diff --git a/crates/directory/src/backend/imap/config.rs b/crates/directory/src/backend/imap/config.rs deleted file mode 100644 index f78e6880..00000000 --- a/crates/directory/src/backend/imap/config.rs +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::time::Duration; - -use mail_send::smtp::tls::build_tls_connector; -use utils::config::{Config, utils::AsKey}; - -use crate::core::config::build_pool; - -use super::{ImapConnectionManager, ImapDirectory}; - -impl ImapDirectory { - pub fn from_config(config: &mut Config, prefix: impl AsKey) -> Option { - let prefix = prefix.as_key(); - let address = config.value_require((&prefix, "host"))?.to_string(); - let tls_implicit: bool = config - .property_or_default((&prefix, "tls.enable"), "false") - .unwrap_or_default(); - let port: u16 = config - .property_or_default((&prefix, "port"), if tls_implicit { "993" } else { "143" }) - .unwrap_or(if tls_implicit { 993 } else { 143 }); - - let manager = ImapConnectionManager { - addr: format!("{address}:{port}"), - timeout: config - .property_or_default((&prefix, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)), - tls_connector: build_tls_connector( - config - .property_or_default((&prefix, "tls.allow-invalid-certs"), "false") - .unwrap_or_default(), - ), - tls_hostname: address.to_string(), - tls_implicit, - mechanisms: 0.into(), - }; - - Some(ImapDirectory { - pool: build_pool(config, &prefix, manager) - .map_err(|e| { - config.new_parse_error( - prefix.as_str(), - format!("Failed to build IMAP pool: {e:?}"), - ) - }) - .ok()?, - domains: config - .values((&prefix, "lookup.domains")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - }) - } -} diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs deleted file mode 100644 index b9dfe488..00000000 --- a/crates/directory/src/backend/imap/lookup.rs +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use mail_send::Credentials; -use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; - -use crate::{IntoError, Principal, QueryBy, Type, backend::RcptType}; - -use super::{ImapDirectory, ImapError}; - -impl ImapDirectory { - pub async fn query(&self, query: QueryBy<'_>) -> trc::Result> { - if let QueryBy::Credentials(credentials) = query { - let mut client = self - .pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - let mechanism = match credentials { - Credentials::Plain { .. } - if (client.mechanisms & (AUTH_PLAIN | AUTH_LOGIN | AUTH_CRAM_MD5)) != 0 => - { - if client.mechanisms & AUTH_CRAM_MD5 != 0 { - AUTH_CRAM_MD5 - } else if client.mechanisms & AUTH_PLAIN != 0 { - AUTH_PLAIN - } else { - AUTH_LOGIN - } - } - Credentials::OAuthBearer { .. } if client.mechanisms & AUTH_OAUTHBEARER != 0 => { - AUTH_OAUTHBEARER - } - Credentials::XOauth2 { .. } if client.mechanisms & AUTH_XOAUTH2 != 0 => { - AUTH_XOAUTH2 - } - _ => { - trc::bail!(trc::StoreEvent::NotSupported.ctx( - trc::Key::Reason, - "IMAP server does not offer any supported auth mechanisms." - )); - } - }; - - match client.authenticate(mechanism, credentials).await { - Ok(_) => { - client.is_valid = false; - Ok(Some(Principal::new(u32::MAX, Type::Individual))) - } - Err(err) => match &err { - ImapError::AuthenticationFailed => Ok(None), - _ => Err(err.into_error()), - }, - } - } else { - Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) - } - } - - pub async fn email_to_id(&self, _address: &str) -> trc::Result> { - Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) - } - - pub async fn rcpt(&self, _address: &str) -> trc::Result { - Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) - } - - pub async fn vrfy(&self, _address: &str) -> trc::Result> { - Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) - } - - pub async fn expn(&self, _address: &str) -> trc::Result> { - Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) - } - - pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - Ok(self.domains.contains(domain)) - } -} diff --git a/crates/directory/src/backend/imap/mod.rs b/crates/directory/src/backend/imap/mod.rs deleted file mode 100644 index 13b76761..00000000 --- a/crates/directory/src/backend/imap/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -pub mod client; -pub mod config; -pub mod lookup; -pub mod pool; -pub mod tls; - -use std::{fmt::Display, sync::atomic::AtomicU64, time::Duration}; - -use ahash::AHashSet; -use deadpool::managed::Pool; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio_rustls::TlsConnector; - -pub struct ImapDirectory { - pool: Pool, - domains: AHashSet, -} - -pub struct ImapConnectionManager { - addr: String, - timeout: Duration, - tls_connector: TlsConnector, - tls_hostname: String, - tls_implicit: bool, - mechanisms: AtomicU64, -} - -pub struct ImapClient { - stream: T, - mechanisms: u64, - is_valid: bool, - timeout: Duration, -} - -#[derive(Debug)] -pub enum ImapError { - Io(std::io::Error), - Timeout, - InvalidResponse(String), - InvalidChallenge(String), - AuthenticationFailed, - TLSInvalidName, - Disconnected, -} - -impl From for ImapError { - fn from(error: std::io::Error) -> Self { - ImapError::Io(error) - } -} - -impl Display for ImapError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ImapError::Io(io) => write!(f, "I/O error: {io}"), - ImapError::Timeout => f.write_str("Connection time-out"), - ImapError::InvalidResponse(response) => write!(f, "Unexpected response: {response:?}"), - ImapError::InvalidChallenge(response) => { - write!(f, "Invalid auth challenge: {response}") - } - ImapError::TLSInvalidName => f.write_str("Invalid TLS name"), - ImapError::Disconnected => f.write_str("Connection disconnected by peer"), - ImapError::AuthenticationFailed => f.write_str("Authentication failed"), - } - } -} diff --git a/crates/directory/src/backend/imap/pool.rs b/crates/directory/src/backend/imap/pool.rs deleted file mode 100644 index bec73fec..00000000 --- a/crates/directory/src/backend/imap/pool.rs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::sync::atomic::Ordering; - -use async_trait::async_trait; -use deadpool::managed; -use tokio::net::TcpStream; -use tokio_rustls::client::TlsStream; - -use super::{ImapClient, ImapConnectionManager, ImapError}; - -#[async_trait] -impl managed::Manager for ImapConnectionManager { - type Type = ImapClient>; - type Error = ImapError; - - async fn create(&self) -> Result>, ImapError> { - let mut conn = ImapClient::connect( - &self.addr, - self.timeout, - &self.tls_connector, - &self.tls_hostname, - self.tls_implicit, - ) - .await?; - - // Obtain the list of supported authentication mechanisms. - conn.mechanisms = self.mechanisms.load(Ordering::Relaxed); - if conn.mechanisms == 0 { - conn.mechanisms = conn.authentication_mechanisms().await?; - self.mechanisms.store(conn.mechanisms, Ordering::Relaxed); - } - - Ok(conn) - } - - async fn recycle( - &self, - conn: &mut ImapClient>, - _: &managed::Metrics, - ) -> managed::RecycleResult { - conn.noop() - .await - .map(|_| ()) - .map_err(managed::RecycleError::Backend) - } -} diff --git a/crates/directory/src/backend/imap/tls.rs b/crates/directory/src/backend/imap/tls.rs deleted file mode 100644 index d6017277..00000000 --- a/crates/directory/src/backend/imap/tls.rs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::time::Duration; - -use rustls_pki_types::ServerName; -use smtp_proto::IntoString; -use tokio::net::{TcpStream, ToSocketAddrs}; -use tokio_rustls::{TlsConnector, client::TlsStream}; - -use super::{ImapClient, ImapError}; - -impl ImapClient { - async fn start_tls( - mut self, - tls_connector: &TlsConnector, - tls_hostname: &str, - ) -> Result>, ImapError> { - let line = tokio::time::timeout(self.timeout, async { - self.write(b"C7 STARTTLS\r\n").await?; - - self.read_line().await - }) - .await - .map_err(|_| ImapError::Timeout)??; - - if matches!(line.get(..5), Some(b"C7 OK")) { - self.into_tls(tls_connector, tls_hostname).await - } else { - Err(ImapError::InvalidResponse(line.into_string())) - } - } - - async fn into_tls( - self, - tls_connector: &TlsConnector, - tls_hostname: &str, - ) -> Result>, ImapError> { - tokio::time::timeout(self.timeout, async { - Ok(ImapClient { - stream: tls_connector - .connect( - ServerName::try_from(tls_hostname.to_string()) - .map_err(|_| ImapError::TLSInvalidName)?, - self.stream, - ) - .await?, - timeout: self.timeout, - mechanisms: self.mechanisms, - is_valid: true, - }) - }) - .await - .map_err(|_| ImapError::Timeout)? - } -} - -impl ImapClient> { - pub async fn connect( - addr: impl ToSocketAddrs, - timeout: Duration, - tls_connector: &TlsConnector, - tls_hostname: &str, - tls_implicit: bool, - ) -> Result { - let mut client: ImapClient = tokio::time::timeout(timeout, async { - match TcpStream::connect(addr).await { - Ok(stream) => Ok(ImapClient { - stream, - timeout, - mechanisms: 0, - is_valid: true, - }), - Err(err) => Err(ImapError::Io(err)), - } - }) - .await - .map_err(|_| ImapError::Timeout)??; - - if tls_implicit { - let mut client = client.into_tls(tls_connector, tls_hostname).await?; - client.expect_greeting().await?; - Ok(client) - } else { - client.expect_greeting().await?; - client.start_tls(tls_connector, tls_hostname).await - } - } -} diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs deleted file mode 100644 index a34ae573..00000000 --- a/crates/directory/src/backend/internal/lookup.rs +++ /dev/null @@ -1,188 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::{PrincipalInfo, manage::ManageDirectory}; -use crate::{Principal, PrincipalData, QueryBy, QueryParams, Type, backend::RcptType}; -use mail_send::Credentials; -use store::{ - Deserialize, IterateParams, Store, ValueKey, - write::{DirectoryClass, ValueClass}, -}; -use trc::AddContext; -use utils::DomainPart; - -#[allow(async_fn_in_trait)] -pub trait DirectoryStore: Sync + Send { - async fn query(&self, by: QueryParams<'_>) -> trc::Result>; - async fn email_to_id(&self, address: &str) -> trc::Result>; - async fn is_local_domain(&self, domain: &str) -> trc::Result; - async fn rcpt(&self, address: &str) -> trc::Result; - async fn vrfy(&self, address: &str) -> trc::Result>; - async fn expn(&self, address: &str) -> trc::Result>; - async fn expn_by_id(&self, id: u32) -> trc::Result>; -} - -impl DirectoryStore for Store { - async fn query(&self, by: QueryParams<'_>) -> trc::Result> { - let (account_id, secret) = match by.by { - QueryBy::Name(name) => (self.get_principal_id(name).await?, None), - QueryBy::Id(account_id) => (account_id.into(), None), - QueryBy::Credentials(credentials) => match credentials { - Credentials::Plain { username, secret } => ( - self.get_principal_id(username).await?, - secret.as_str().into(), - ), - Credentials::OAuthBearer { token } => { - (self.get_principal_id(token).await?, token.as_str().into()) - } - Credentials::XOauth2 { username, secret } => ( - self.get_principal_id(username).await?, - secret.as_str().into(), - ), - }, - }; - - if let Some(account_id) = account_id - && let Some(mut principal) = self.get_principal(account_id).await? - { - if let Some(secret) = secret - && !principal - .verify_secret(secret, by.only_app_pass, true) - .await? - { - return Ok(None); - } - - if by.return_member_of { - for member in self.get_member_of(principal.id).await? { - match member.typ { - Type::List => principal - .data - .push(PrincipalData::List(member.principal_id)), - Type::Role => principal - .data - .push(PrincipalData::Role(member.principal_id)), - _ => principal - .data - .push(PrincipalData::MemberOf(member.principal_id)), - } - } - } - return Ok(Some(principal)); - } - Ok(None) - } - - async fn email_to_id(&self, address: &str) -> trc::Result> { - self.get_value::(ValueKey::from(ValueClass::Directory( - DirectoryClass::EmailToId(address.as_bytes().to_vec()), - ))) - .await - .map(|ptype| ptype.map(|ptype| ptype.id)) - } - - async fn is_local_domain(&self, domain: &str) -> trc::Result { - self.get_value::(ValueKey::from(ValueClass::Directory( - DirectoryClass::NameToId(domain.as_bytes().to_vec()), - ))) - .await - .map(|p| p.is_some_and(|p| p.typ == Type::Domain)) - } - - async fn rcpt(&self, address: &str) -> trc::Result { - if let Some(pinfo) = self - .get_value::(ValueKey::from(ValueClass::Directory( - DirectoryClass::EmailToId(address.as_bytes().to_vec()), - ))) - .await? - { - if pinfo.typ != Type::List { - Ok(RcptType::Mailbox) - } else { - self.expn_by_id(pinfo.id).await.map(RcptType::List) - } - } else { - Ok(RcptType::Invalid) - } - } - - async fn vrfy(&self, address: &str) -> trc::Result> { - let mut results = Vec::new(); - let address = address.try_local_part().unwrap_or(address); - if address.len() > 3 { - self.iterate( - IterateParams::new( - ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId(vec![0u8]))), - ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId( - vec![u8::MAX; 10], - ))), - ), - |key, value| { - let key = - std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default(); - if key.try_local_part().unwrap_or(key).contains(address) - && PrincipalInfo::deserialize(value) - .caused_by(trc::location!())? - .typ - != Type::List - { - results.push(key.into()); - } - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - } - - Ok(results) - } - - async fn expn(&self, address: &str) -> trc::Result> { - if let Some(ptype) = self - .get_value::(ValueKey::from(ValueClass::Directory( - DirectoryClass::EmailToId(address.as_bytes().to_vec()), - ))) - .await? - .filter(|p| p.typ == Type::List) - { - self.expn_by_id(ptype.id).await - } else { - Ok(vec![]) - } - } - - async fn expn_by_id(&self, list_id: u32) -> trc::Result> { - let mut results = Vec::new(); - for account_id in self.get_members(list_id).await? { - if let Some(email) = self.get_principal(account_id).await?.and_then(|p| { - p.data.into_iter().find_map(|data| { - if let PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) = - data - { - Some(email) - } else { - None - } - }) - }) { - results.push(email); - } - } - - if let Some(principal) = self.get_principal(list_id).await? { - results.extend(principal.data.into_iter().filter_map(|data| { - if let PrincipalData::ExternalMember(member) = data { - Some(member) - } else { - None - } - })); - } - - Ok(results) - } -} diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs deleted file mode 100644 index 86f106aa..00000000 --- a/crates/directory/src/backend/internal/manage.rs +++ /dev/null @@ -1,2851 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::{ - PrincipalAction, PrincipalField, PrincipalInfo, PrincipalSet, PrincipalUpdate, PrincipalValue, - SpecialSecrets, lookup::DirectoryStore, -}; -use crate::{ - ArchivedPrincipalData, FALLBACK_ADMIN_ID, MemberOf, Permission, PermissionGrant, Permissions, - Principal, PrincipalData, QueryBy, QueryParams, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, Type, - backend::RcptType, core::principal::build_search_index, -}; -use ahash::{AHashMap, AHashSet}; -use compact_str::CompactString; -use nlp::tokenizers::word::WordTokenizer; -use store::{ - Deserialize, IterateParams, Serialize, SerializeInfallible, Store, U32_LEN, ValueKey, - backend::MAX_TOKEN_LENGTH, - roaring::RoaringBitmap, - write::{ - AlignedBytes, Archive, Archiver, BatchBuilder, DirectoryClass, ValueClass, - key::DeserializeBigEndian, - }, -}; -use trc::AddContext; -use types::{ - collection::Collection, - field::{self}, -}; -use utils::{DomainPart, sanitize_email}; - -#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct PrincipalList { - pub items: Vec, - pub total: u64, -} - -pub struct UpdatePrincipal<'x> { - query: QueryBy<'x>, - allowed_permissions: Option<&'x Permissions>, - changes: Vec, - tenant_id: Option, - create_domains: bool, -} - -#[derive(Debug, Default, PartialEq, Eq)] -#[repr(transparent)] -pub struct ChangedPrincipals(AHashMap); - -#[derive(Debug, Default, PartialEq, Eq)] -pub struct ChangedPrincipal { - pub typ: Type, - pub name_change: bool, - pub member_change: bool, -} - -#[derive(Debug, Default, PartialEq, Eq)] -pub struct CreatedPrincipal { - pub id: u32, - pub changed_principals: ChangedPrincipals, -} - -#[allow(async_fn_in_trait)] -pub trait ManageDirectory: Sized { - async fn get_principal_id(&self, name: &str) -> trc::Result>; - async fn get_principal_info(&self, name: &str) -> trc::Result>; - async fn get_or_create_principal_id(&self, name: &str, typ: Type) -> trc::Result; - async fn get_principal(&self, principal_id: u32) -> trc::Result>; - async fn get_principal_name(&self, principal_id: u32) -> trc::Result>; - async fn get_member_of(&self, principal_id: u32) -> trc::Result>; - async fn get_members(&self, principal_id: u32) -> trc::Result>; - async fn create_principal( - &self, - principal: PrincipalSet, - tenant_id: Option, - allowed_permissions: Option<&Permissions>, - ) -> trc::Result; - async fn update_principal(&self, params: UpdatePrincipal<'_>) - -> trc::Result; - async fn delete_principal(&self, by: QueryBy<'_>) -> trc::Result; - async fn list_principals( - &self, - filter: Option<&str>, - tenant_id: Option, - types: &[Type], - fetch: bool, - page: usize, - limit: usize, - ) -> trc::Result>; - async fn count_principals( - &self, - filter: Option<&str>, - typ: Option, - tenant_id: Option, - ) -> trc::Result; - async fn principal_ids( - &self, - typ: Option, - tenant_id: Option, - ) -> trc::Result; - async fn map_principal( - &self, - principal: Principal, - fields: &[PrincipalField], - ) -> trc::Result; -} - -#[allow(async_fn_in_trait)] -trait ValidateDirectory: Sized { - async fn validate_email( - &self, - email: &str, - tenant_id: Option, - create_if_missing: bool, - ) -> trc::Result<()>; -} - -impl ManageDirectory for Store { - async fn get_principal(&self, principal_id: u32) -> trc::Result> { - let archive = self - .get_value::>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(principal_id), - ))) - .await - .caused_by(trc::location!())?; - - if let Some(archive) = archive { - let mut principal = archive - .deserialize::() - .caused_by(trc::location!())?; - principal.id = principal_id; - Ok(Some(principal)) - } else { - Ok(None) - } - } - - async fn get_principal_name(&self, principal_id: u32) -> trc::Result> { - let archive = self - .get_value::>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(principal_id), - ))) - .await - .caused_by(trc::location!())?; - - if let Some(archive) = archive { - let principal = archive - .unarchive::() - .caused_by(trc::location!())?; - Ok(Some(principal.name.as_str().into())) - } else { - Ok(None) - } - } - - async fn get_principal_id(&self, name: &str) -> trc::Result> { - self.get_principal_info(name).await.map(|v| v.map(|v| v.id)) - } - - async fn get_principal_info(&self, name: &str) -> trc::Result> { - self.get_value::(ValueKey::from(ValueClass::Directory( - DirectoryClass::NameToId(name.as_bytes().to_vec()), - ))) - .await - .caused_by(trc::location!()) - } - - // Used by all directories except internal - async fn get_or_create_principal_id(&self, name: &str, typ: Type) -> trc::Result { - let mut try_count = 0; - let name = name.to_lowercase(); - let mut principal_id = None; - - loop { - // Try to obtain ID - if let Some(principal_id) = self - .get_principal_id(&name) - .await - .caused_by(trc::location!())? - { - return Ok(principal_id); - } - - let principal_id = if let Some(principal_id) = principal_id { - principal_id - } else { - let principal_id_ = self - .assign_document_ids(u32::MAX, Collection::Principal, 1) - .await - .caused_by(trc::location!())?; - if principal_id_ == FALLBACK_ADMIN_ID { - return Err(trc::StoreEvent::UnexpectedError - .into_err() - .details("ID assignment failed") - .caused_by(trc::location!())); - } - principal_id = Some(principal_id_); - principal_id_ - }; - - // Prepare principal - let mut principal = Principal::new(principal_id, typ); - principal.name = name.as_str().into(); - - // Write principal ID - let name_key = - ValueClass::Directory(DirectoryClass::NameToId(name.as_bytes().to_vec())); - let mut batch = BatchBuilder::new(); - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal) - .assert_value(name_key.clone(), ()) - .with_document(principal_id); - build_search_index(&mut batch, principal_id, None, Some(&principal)); - principal.sort(); - batch - .set( - name_key, - PrincipalInfo::new(principal_id, typ, None).serialize(), - ) - .set( - ValueClass::Directory(DirectoryClass::Principal(principal_id)), - Archiver::new(principal) - .serialize() - .caused_by(trc::location!())?, - ); - - // Add default user role - if typ == Type::Individual { - batch - .set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: ROLE_USER, - }), - vec![Type::Role as u8], - ) - .set( - ValueClass::Directory(DirectoryClass::Members { - principal_id: ROLE_USER, - has_member: principal_id, - }), - vec![], - ); - } - - match self.write(batch.build_all()).await { - Ok(_) => { - return Ok(principal_id); - } - Err(err) => { - if err.is_assertion_failure() && try_count < 3 { - try_count += 1; - continue; - } else { - return Err(err.caused_by(trc::location!())); - } - } - } - } - } - - async fn create_principal( - &self, - mut principal_set: PrincipalSet, - mut tenant_id: Option, - allowed_permissions: Option<&Permissions>, - ) -> trc::Result { - // Make sure the principal has a name - let name = principal_set.name().to_lowercase(); - if name.is_empty() { - return Err(err_missing(PrincipalField::Name)); - } - let mut valid_domains: AHashSet = AHashSet::new(); - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - // Validate tenant - #[cfg(feature = "enterprise")] - if let Some(tenant_id) = tenant_id { - let tenant = self - .query(crate::QueryParams::id(tenant_id).with_return_member_of(false)) - .await? - .ok_or_else(|| { - trc::ManageEvent::NotFound - .into_err() - .id(tenant_id) - .details("Tenant not found") - .caused_by(trc::location!()) - })?; - - // Enforce tenant quotas - if let Some(limit) = tenant - .directory_quota(&principal_set.typ()) - .filter(|q| *q > 0) - { - // Obtain number of principals - let total = self - .count_principals(None, principal_set.typ().into(), tenant_id.into()) - .await - .caused_by(trc::location!())? as u32; - - if total >= limit { - trc::bail!( - trc::LimitEvent::TenantQuota - .into_err() - .details("Tenant principal quota exceeded") - .ctx(trc::Key::Details, principal_set.typ().description()) - .ctx(trc::Key::Limit, limit) - .ctx(trc::Key::Total, total) - ); - } - } - } - - // SPDX-SnippetEnd - - // Make sure new name is not taken - if self - .get_principal_id(&name) - .await - .caused_by(trc::location!())? - .is_some() - { - return Err(err_exists(PrincipalField::Name, name)); - } - - let mut create_principal = Principal::new(0, principal_set.typ()); - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - // Obtain tenant id, only if no default tenant is provided - #[cfg(feature = "enterprise")] - if let (Some(tenant_name), None) = - (principal_set.take_str(PrincipalField::Tenant), tenant_id) - { - tenant_id = self - .get_principal_info(&tenant_name) - .await - .caused_by(trc::location!())? - .filter(|v| v.typ == Type::Tenant) - .ok_or_else(|| not_found(tenant_name.clone()))? - .id - .into(); - } - - // Tenants must provide principal names including a valid domain - #[cfg(feature = "enterprise")] - if let Some(tenant_id) = tenant_id { - if matches!(principal_set.typ, Type::Tenant) { - return Err(error( - "Invalid field", - "Tenants cannot contain a tenant field".into(), - )); - } - - create_principal.data.push(PrincipalData::Tenant(tenant_id)); - - if !matches!(create_principal.typ, Type::Tenant | Type::Domain) { - if let Some(domain) = name.try_domain_part() - && self - .get_principal_info(domain) - .await - .caused_by(trc::location!())? - .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id.into())) - .is_some() - { - valid_domains.insert(domain.into()); - } - - if valid_domains.is_empty() { - return Err(error( - "Invalid principal name", - "Principal name must include a valid domain assigned to the tenant".into(), - )); - } - } - } - // SPDX-SnippetEnd - - // Set fields - create_principal.name = name; - let mut has_secret = false; - for secret in principal_set - .take_str_array(PrincipalField::Secrets) - .unwrap_or_default() - { - if secret.is_otp_secret() { - create_principal.data.push(PrincipalData::OtpAuth(secret)); - } else if secret.is_app_secret() { - create_principal - .data - .push(PrincipalData::AppPassword(secret)); - } else if !has_secret { - has_secret = true; - create_principal.data.push(PrincipalData::Password(secret)); - } - } - - if let Some(description) = principal_set.take_str(PrincipalField::Description) { - create_principal - .data - .push(PrincipalData::Description(description)); - } - - if let Some(picture) = principal_set.take_str(PrincipalField::Picture) { - create_principal.data.push(PrincipalData::Picture(picture)); - } - if let Some(picture) = principal_set.take_str(PrincipalField::Locale) { - create_principal.data.push(PrincipalData::Locale(picture)); - } - for url in principal_set - .take_str_array(PrincipalField::Urls) - .unwrap_or_default() - { - create_principal.data.push(PrincipalData::Url(url)); - } - for member in principal_set - .take_str_array(PrincipalField::ExternalMembers) - .unwrap_or_default() - { - create_principal - .data - .push(PrincipalData::ExternalMember(member)); - } - if let Some(quotas) = principal_set.take_int_array(PrincipalField::Quota) { - for (idx, quota) in quotas.into_iter().take(Type::MAX_ID + 2).enumerate() { - if quota != 0 { - if idx != 0 { - create_principal.data.push(PrincipalData::DirectoryQuota { - quota: quota as u32, - typ: Type::from_u8((idx - 1) as u8), - }); - } else { - create_principal.data.push(PrincipalData::DiskQuota(quota)); - } - } - } - } - - // Map member names - let mut members = Vec::new(); - let mut member_of = Vec::new(); - let mut changed_principals = ChangedPrincipals::default(); - for (field, expected_type) in [ - (PrincipalField::Members, None), - (PrincipalField::MemberOf, Some(Type::Group)), - (PrincipalField::Lists, Some(Type::List)), - (PrincipalField::Roles, Some(Type::Role)), - ] { - if let Some(names) = principal_set.take_str_array(field) { - let list = if field == PrincipalField::Members { - &mut members - } else { - &mut member_of - }; - - for name in names { - let item = match ( - self.get_principal_info(&name) - .await - .caused_by(trc::location!())? - .filter(|v| { - expected_type.is_none_or(|t| v.typ == t) - && v.has_tenant_access(tenant_id) - }), - field.map_internal_roles(&name), - ) { - (_, Some(v)) => v, - (Some(v), _) => { - if field == PrincipalField::Members { - // Update principal members - changed_principals.add_change( - v.id, - v.typ, - PrincipalField::MemberOf, - ); - } - v - } - _ => { - return Err(not_found(name)); - } - }; - - list.push(item); - } - } - } - - // Map permissions - let mut permissions = AHashMap::new(); - for field in [ - PrincipalField::EnabledPermissions, - PrincipalField::DisabledPermissions, - ] { - let is_disabled = field == PrincipalField::DisabledPermissions; - if let Some(names) = principal_set.take_str_array(field) { - for name in names { - let permission = Permission::from_name(&name).ok_or_else(|| { - error( - format!("Invalid {} value", field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })?; - - if !permissions.contains_key(&permission) { - if allowed_permissions - .as_ref() - .is_none_or(|p| p.get(permission as usize)) - || is_disabled - { - permissions.insert(permission, is_disabled); - } else { - return Err(error( - "Invalid permission", - format!("Your account cannot grant the {name:?} permission").into(), - )); - } - } - } - } - } - if !permissions.is_empty() { - for (permission, v) in permissions { - create_principal.data.push(PrincipalData::Permission { - permission_id: permission.id(), - grant: !v, - }); - } - } - - // Make sure the e-mail is not taken and validate domain - if create_principal.typ != Type::OauthClient { - for (idx, email) in principal_set - .take_str_array(PrincipalField::Emails) - .unwrap_or_default() - .into_iter() - .enumerate() - { - let email = email.to_lowercase(); - if self.rcpt(&email).await.caused_by(trc::location!())? != RcptType::Invalid { - return Err(err_exists(PrincipalField::Emails, email.to_string())); - } - if let Some(domain) = email.try_domain_part() - && valid_domains.insert(domain.into()) - { - self.get_principal_info(domain) - .await - .caused_by(trc::location!())? - .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id)) - .ok_or_else(|| not_found(domain.to_string()))?; - } - if idx == 0 { - create_principal - .data - .push(PrincipalData::PrimaryEmail(email)); - } else { - create_principal.data.push(PrincipalData::EmailAlias(email)); - } - } - } - - // Write principal - let principal_id = self - .assign_document_ids(u32::MAX, Collection::Principal, 1) - .await - .caused_by(trc::location!())?; - if principal_id == FALLBACK_ADMIN_ID { - return Err(trc::StoreEvent::UnexpectedError - .into_err() - .details("ID assignment failed") - .caused_by(trc::location!())); - } - create_principal.id = principal_id; - let mut batch = BatchBuilder::new(); - let pinfo_name = PrincipalInfo::new(principal_id, create_principal.typ, tenant_id); - let pinfo_email = PrincipalInfo::new(principal_id, create_principal.typ, None); - - // Validate object size - if create_principal.object_size() > 100_000 { - return Err(error( - "Invalid parameter", - "Principal object size exceeds 100kb safety limit.".into(), - )); - } - - // Serialize - create_principal.sort(); - let archiver = Archiver::new(create_principal); - let principal_bytes = archiver.serialize().caused_by(trc::location!())?; - let create_principal = archiver.into_inner(); - - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal) - .with_document(principal_id) - .assert_value( - ValueClass::Directory(DirectoryClass::NameToId( - create_principal.name().as_bytes().to_vec(), - )), - (), - ); - build_search_index(&mut batch, principal_id, None, Some(&create_principal)); - batch - .set( - ValueClass::Directory(DirectoryClass::Principal(principal_id)), - principal_bytes, - ) - .set( - ValueClass::Directory(DirectoryClass::NameToId( - create_principal.name.as_bytes().to_vec(), - )), - pinfo_name.serialize(), - ); - - // Write email to id mapping - for email in create_principal.email_addresses() { - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId(email.as_bytes().to_vec())), - pinfo_email.serialize(), - ); - } - - // Write membership - for member_of in member_of { - batch.set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: member_of.id, - }), - vec![member_of.typ as u8], - ); - batch.set( - ValueClass::Directory(DirectoryClass::Members { - principal_id: member_of.id, - has_member: principal_id, - }), - vec![], - ); - } - for member in members { - batch.set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: member.id, - member_of: principal_id, - }), - vec![create_principal.typ as u8], - ); - batch.set( - ValueClass::Directory(DirectoryClass::Members { - principal_id, - has_member: member.id, - }), - vec![], - ); - } - - self.write(batch.build_all()) - .await - .map(|_| CreatedPrincipal { - id: principal_id, - changed_principals, - }) - } - - async fn delete_principal(&self, by: QueryBy<'_>) -> trc::Result { - // Obtain principal - let principal_id = match by { - QueryBy::Name(name) => self - .get_principal_id(name) - .await - .caused_by(trc::location!())? - .ok_or_else(|| not_found(name.to_string()))?, - QueryBy::Id(principal_id) => principal_id, - QueryBy::Credentials(_) => unreachable!(), - }; - - let principal_ = self - .get_value::>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(principal_id), - ))) - .await - .caused_by(trc::location!())? - .ok_or_else(|| not_found(principal_id.to_string()))?; - let principal = principal_ - .unarchive::() - .caused_by(trc::location!())?; - let typ = Type::from(&principal.typ); - - let mut batch = BatchBuilder::new(); - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal); - - let tenant = principal.data.iter().find_map(|data| { - if let ArchivedPrincipalData::Tenant(tenant_id) = data { - Some(tenant_id.to_native()) - } else { - None - } - }); - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - // Make sure tenant has no data - #[cfg(feature = "enterprise")] - match typ { - Type::Individual | Type::Group => { - // Update tenant quota - if let Some(tenant_id) = tenant { - let quota = self - .get_counter(DirectoryClass::UsedQuota(principal_id)) - .await - .caused_by(trc::location!())?; - if quota > 0 { - batch.add(DirectoryClass::UsedQuota(tenant_id), -quota); - } - } - } - Type::Tenant => { - let tenant_members = self - .list_principals( - None, - principal_id.into(), - &[ - Type::Individual, - Type::Group, - Type::Role, - Type::List, - Type::Resource, - Type::Other, - Type::Location, - Type::Domain, - Type::ApiKey, - ], - false, - 0, - 0, - ) - .await - .caused_by(trc::location!())?; - - if tenant_members.total > 0 { - let mut message = - String::from("Tenant must have no members to be deleted: Found: "); - - for (num, principal) in tenant_members.items.iter().enumerate() { - if num > 0 { - message.push_str(", "); - } - message.push_str(principal.name()); - } - - if tenant_members.total > 5 { - message.push_str(" and "); - message.push_str(&(tenant_members.total - 5).to_string()); - message.push_str(" others"); - } - - return Err(error("Tenant has members", message.into())); - } - } - Type::Domain => { - if let Some(tenant_id) = tenant { - let name = principal.name.as_str(); - let tenant_members = self - .list_principals( - None, - tenant_id.into(), - &[ - Type::Individual, - Type::Group, - Type::Role, - Type::List, - Type::Resource, - Type::Other, - Type::Location, - ], - false, - 0, - 0, - ) - .await - .caused_by(trc::location!())?; - let domain_members = tenant_members - .items - .iter() - .filter(|v| { - v.name() - .rsplit_once('@') - .is_some_and(|(_, d)| d.eq_ignore_ascii_case(name)) - }) - .collect::>(); - let total_domain_members = domain_members.len(); - - if total_domain_members > 0 { - let mut message = - String::from("Domains must have no members to be deleted: Found: "); - - for (num, principal) in domain_members.iter().enumerate() { - if num > 0 { - message.push_str(", "); - } - message.push_str(principal.name()); - } - - if total_domain_members > 5 { - message.push_str(" and "); - message.push_str(&(total_domain_members - 5).to_string()); - message.push_str(" others"); - } - - return Err(error("Domain has members", message.into())); - } - } - } - - _ => {} - } - // SPDX-SnippetEnd - - // Revoke ACLs, obtain all changed principals - let mut changed_principals = ChangedPrincipals::default(); - - for member_id in self - .acl_revoke_all(principal_id) - .await - .caused_by(trc::location!())? - { - changed_principals.add_change( - member_id, - Type::Individual, - PrincipalField::EnabledPermissions, - ); - } - - // Delete principal - batch - .with_document(principal_id) - .clear(DirectoryClass::NameToId(principal.name.as_bytes().to_vec())) - .clear(DirectoryClass::Principal(principal_id)) - .clear(DirectoryClass::UsedQuota(principal_id)); - - for email in principal.data.iter() { - if let ArchivedPrincipalData::PrimaryEmail(email) - | ArchivedPrincipalData::EmailAlias(email) = email - { - batch.clear(DirectoryClass::EmailToId(email.as_bytes().to_vec())); - } - } - - build_search_index(&mut batch, principal_id, Some(principal), None); - - for member in self - .get_member_of(principal_id) - .await - .caused_by(trc::location!())? - { - // Update changed principals - changed_principals.add_member_change( - principal_id, - typ, - member.principal_id, - member.typ, - ); - - // Remove memberOf - batch.clear(DirectoryClass::MemberOf { - principal_id, - member_of: member.principal_id, - }); - batch.clear(DirectoryClass::Members { - principal_id: member.principal_id, - has_member: principal_id, - }); - } - - for member_id in self - .get_members(principal_id) - .await - .caused_by(trc::location!())? - { - // Update changed principals - if let Some(member_info) = self - .get_principal(member_id) - .await - .caused_by(trc::location!())? - { - changed_principals.add_member_change(member_id, member_info.typ, principal_id, typ); - } - - // Remove members - batch.clear(DirectoryClass::MemberOf { - principal_id: member_id, - member_of: principal_id, - }); - batch.clear(DirectoryClass::Members { - principal_id, - has_member: member_id, - }); - } - - // Delete push subscriptions - if matches!(typ, Type::Individual) { - batch.untag(field::PrincipalField::PushSubscriptions); - } - - self.write(batch.build_all()) - .await - .caused_by(trc::location!())?; - - changed_principals.add_deletion(principal_id, typ); - - Ok(changed_principals) - } - - async fn update_principal( - &self, - params: UpdatePrincipal<'_>, - ) -> trc::Result { - let principal_id = match params.query { - QueryBy::Name(name) => self - .get_principal_id(name) - .await - .caused_by(trc::location!())? - .ok_or_else(|| not_found(name.to_string()))?, - QueryBy::Id(principal_id) => principal_id, - QueryBy::Credentials(_) => unreachable!(), - }; - let changes = params.changes; - let tenant_id = params.tenant_id; - - // Fetch principal - let principal_ = self - .get_value::>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(principal_id), - ))) - .await - .caused_by(trc::location!())? - .ok_or_else(|| not_found(principal_id))?; - let prev_principal = principal_ - .to_unarchived::() - .caused_by(trc::location!())?; - let mut principal = prev_principal - .deserialize::() - .caused_by(trc::location!())?; - principal.id = principal_id; - let principal_type = principal.typ; - let validate_emails = principal_type != Type::OauthClient; - - // Keep track of changed principals - let mut changed_principals = ChangedPrincipals::default(); - - // Obtain members and memberOf - let mut member_of = self - .get_member_of(principal_id) - .await - .caused_by(trc::location!())?; - let mut members = self - .get_members(principal_id) - .await - .caused_by(trc::location!())?; - - // Prepare changes - let mut batch = BatchBuilder::new(); - let mut pinfo_name = - PrincipalInfo::new(principal_id, principal_type, principal.tenant()).serialize(); - let pinfo_email = PrincipalInfo::new(principal_id, principal_type, None).serialize(); - let update_principal = !changes.is_empty() - && !changes.iter().all(|c| { - matches!( - c.field, - PrincipalField::MemberOf - | PrincipalField::Members - | PrincipalField::Lists - | PrincipalField::Roles - ) - }); - - let mut used_quota: Option = None; - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - // Obtain used quota - #[cfg(feature = "enterprise")] - if tenant_id.is_none() - && changes - .iter() - .any(|c| matches!(c.field, PrincipalField::Tenant)) - { - let quota = self - .get_counter(DirectoryClass::UsedQuota(principal_id)) - .await - .caused_by(trc::location!())?; - if quota > 0 { - used_quota = Some(quota); - } - } - - // SPDX-SnippetEnd - - // Allowed principal types for Member fields - let allowed_member_types = match principal_type { - Type::Group => &[Type::Individual, Type::Group][..], - Type::Resource => &[Type::Resource][..], - Type::Location => &[ - Type::Location, - Type::Resource, - Type::Individual, - Type::Group, - Type::Other, - ][..], - Type::List => &[Type::Individual, Type::Group][..], - Type::Other - | Type::Domain - | Type::Tenant - | Type::Individual - | Type::ApiKey - | Type::OauthClient => &[][..], - Type::Role => &[Type::Role][..], - }; - let mut valid_domains = AHashSet::new(); - - // Process changes - for change in changes { - match (change.action, change.field, change.value) { - (PrincipalAction::Set, PrincipalField::Name, PrincipalValue::String(new_name)) => { - // Make sure new name is not taken - let new_name = new_name.to_lowercase(); - if principal.name() != new_name { - if tenant_id.is_some() - && !matches!(principal_type, Type::Tenant | Type::Domain) - { - if let Some(domain) = new_name.try_domain_part() - && self - .get_principal_info(domain) - .await - .caused_by(trc::location!())? - .filter(|v| { - v.typ == Type::Domain && v.has_tenant_access(tenant_id) - }) - .is_some() - { - valid_domains.insert(domain.to_string()); - } - - if valid_domains.is_empty() { - return Err(error( - "Invalid principal name", - "Principal name must include a valid domain assigned to the tenant".into(), - )); - } - } - - if self - .get_principal_id(&new_name) - .await - .caused_by(trc::location!())? - .is_some() - { - return Err(err_exists(PrincipalField::Name, new_name)); - } - - batch.clear(ValueClass::Directory(DirectoryClass::NameToId( - principal.name().as_bytes().to_vec(), - ))); - - batch.set( - ValueClass::Directory(DirectoryClass::NameToId( - new_name.as_bytes().to_vec(), - )), - pinfo_name.clone(), - ); - principal.name = new_name; - - // Name changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - } - } - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - ( - PrincipalAction::Set, - PrincipalField::Tenant, - PrincipalValue::String(tenant_name), - ) if tenant_id.is_none() => { - if !tenant_name.is_empty() { - let tenant_info = self - .get_principal_info(&tenant_name) - .await - .caused_by(trc::location!())? - .ok_or_else(|| not_found(tenant_name.clone()))?; - - if tenant_info.typ != Type::Tenant { - return Err(error( - "Not a tenant", - format!("Principal {tenant_name:?} is not a tenant").into(), - )); - } - - if principal.tenant() == Some(tenant_info.id) { - continue; - } - - // Update quota - if let Some(used_quota) = used_quota { - if let Some(old_tenant_id) = principal.tenant() { - batch.add(DirectoryClass::UsedQuota(old_tenant_id), -used_quota); - } - batch.add(DirectoryClass::UsedQuota(tenant_info.id), used_quota); - } - - // Tenant changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - - principal - .data - .retain(|v| !matches!(v, PrincipalData::Tenant(_))); - principal.data.push(PrincipalData::Tenant(tenant_info.id)); - pinfo_name = - PrincipalInfo::new(principal_id, principal_type, tenant_info.id.into()) - .serialize(); - } else if let Some(tenant_id) = principal.tenant() { - // Update quota - if let Some(used_quota) = used_quota { - batch.add(DirectoryClass::UsedQuota(tenant_id), -used_quota); - } - - // Tenant changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - - principal - .data - .retain(|v| !matches!(v, PrincipalData::Tenant(_))); - pinfo_name = - PrincipalInfo::new(principal_id, principal_type, None).serialize(); - } else { - continue; - } - - batch.set( - ValueClass::Directory(DirectoryClass::NameToId( - principal.name().as_bytes().to_vec(), - )), - pinfo_name.clone(), - ); - } - - // SPDX-SnippetEnd - ( - PrincipalAction::Set, - PrincipalField::Secrets, - value @ (PrincipalValue::StringList(_) | PrincipalValue::String(_)), - ) => { - // Password changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - principal.data.retain(|v| { - !matches!( - v, - PrincipalData::Password(_) - | PrincipalData::AppPassword(_) - | PrincipalData::OtpAuth(_) - ) - }); - let mut has_secret = false; - for secret in value.into_str_array() { - if secret.is_otp_secret() { - principal.data.push(PrincipalData::OtpAuth(secret)); - } else if secret.is_app_secret() { - principal.data.push(PrincipalData::AppPassword(secret)); - } else if !has_secret { - has_secret = true; - principal.data.push(PrincipalData::Password(secret)); - } - } - } - ( - PrincipalAction::AddItem, - PrincipalField::Secrets, - PrincipalValue::String(secret), - ) => { - if !principal.data.iter().any(|v| match v { - PrincipalData::Password(v) - | PrincipalData::AppPassword(v) - | PrincipalData::OtpAuth(v) => *v == secret, - _ => false, - }) { - if secret.is_app_secret() { - principal.data.push(PrincipalData::AppPassword(secret)); - } else if secret.is_otp_secret() { - principal - .data - .retain(|v| !matches!(v, PrincipalData::OtpAuth(_))); - principal.data.push(PrincipalData::OtpAuth(secret)); - } else { - principal - .data - .retain(|v| !matches!(v, PrincipalData::Password(_))); - principal.data.push(PrincipalData::Password(secret)); - } - - // Password changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - } - } - ( - PrincipalAction::RemoveItem, - PrincipalField::Secrets, - PrincipalValue::String(secret), - ) => { - // Password changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - - if secret.is_app_secret() || secret.is_otp_secret() { - principal.data.retain(|v| match v { - PrincipalData::AppPassword(v) | PrincipalData::OtpAuth(v) => { - *v != secret && !v.starts_with(secret.as_str()) - } - _ => true, - }); - } else if !secret.is_empty() { - principal.data.retain(|v| match v { - PrincipalData::Password(v) => *v != secret, - _ => true, - }); - } else { - principal.data.retain(|v| { - !matches!(v, PrincipalData::AppPassword(_) | PrincipalData::OtpAuth(_)) - }); - } - } - ( - PrincipalAction::Set, - PrincipalField::Description, - PrincipalValue::String(value), - ) => { - principal - .data - .retain(|v| !matches!(v, PrincipalData::Description(_))); - if !value.is_empty() { - principal.data.push(PrincipalData::Description(value)); - } - } - (PrincipalAction::Set, PrincipalField::Picture, PrincipalValue::String(value)) => { - principal - .data - .retain(|v| !matches!(v, PrincipalData::Picture(_))); - if !value.is_empty() { - principal.data.push(PrincipalData::Picture(value)); - } - } - (PrincipalAction::Set, PrincipalField::Locale, PrincipalValue::String(value)) => { - principal - .data - .retain(|v| !matches!(v, PrincipalData::Locale(_))); - if !value.is_empty() { - principal.data.push(PrincipalData::Locale(value)); - } - } - (PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) - if matches!( - principal_type, - Type::Individual | Type::Group | Type::Tenant - ) => - { - // Quota changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - principal - .data - .retain(|v| !matches!(v, PrincipalData::DiskQuota(_))); - principal.data.push(PrincipalData::DiskQuota(quota)); - } - (PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::String(quota)) - if matches!( - principal_type, - Type::Individual | Type::Group | Type::Tenant - ) && quota.is_empty() => - { - // Quota changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - principal - .data - .retain(|v| !matches!(v, PrincipalData::DiskQuota(_))); - } - ( - PrincipalAction::Set, - PrincipalField::Quota, - PrincipalValue::IntegerList(quotas), - ) if matches!(principal_type, Type::Tenant) - && quotas.len() <= (Type::MAX_ID + 2) => - { - let mut new_quota = None; - - principal.data.retain(|v| { - !matches!( - v, - PrincipalData::DiskQuota(_) | PrincipalData::DirectoryQuota { .. } - ) - }); - - for (idx, quota) in quotas.into_iter().enumerate() { - if quota != 0 { - if idx != 0 { - principal.data.push(PrincipalData::DirectoryQuota { - quota: quota as u32, - typ: Type::from_u8((idx - 1) as u8), - }); - } else { - new_quota = Some(quota); - } - } - } - - if let Some(new_quota) = new_quota { - principal.data.push(PrincipalData::DiskQuota(new_quota)); - } - } - - // Emails - ( - PrincipalAction::Set, - PrincipalField::Emails, - PrincipalValue::StringList(emails), - ) => { - // Validate unique emails - let emails = emails - .into_iter() - .map(|v| v.to_lowercase()) - .collect::>(); - for email in &emails { - if !principal.email_addresses().any(|v| v == email) { - if validate_emails { - self.validate_email(email, tenant_id, params.create_domains) - .await?; - } - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - )), - pinfo_email.clone(), - ); - } - } - - for email in principal.email_addresses() { - if !emails.iter().any(|v| v == email) { - batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - ))); - } - } - - // Emails changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - - principal.data.retain(|v| { - !matches!( - v, - PrincipalData::PrimaryEmail(_) | PrincipalData::EmailAlias(_) - ) - }); - for (idx, email) in emails.into_iter().enumerate() { - if idx == 0 { - principal.data.push(PrincipalData::PrimaryEmail(email)); - } else { - principal.data.push(PrincipalData::EmailAlias(email)); - } - } - } - ( - PrincipalAction::AddItem, - PrincipalField::Emails, - PrincipalValue::String(email), - ) => { - let email = email.to_lowercase(); - let mut emails_iter = principal.email_addresses().peekable(); - let has_emails = emails_iter.peek().is_some(); - let email_exists = emails_iter.any(|v| v == email); - drop(emails_iter); - if !email_exists { - if validate_emails { - self.validate_email(&email, tenant_id, params.create_domains) - .await?; - } - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - )), - pinfo_email.clone(), - ); - if has_emails { - principal.data.push(PrincipalData::EmailAlias(email)); - } else { - principal.data.push(PrincipalData::PrimaryEmail(email)); - } - - // Emails changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - } - } - ( - PrincipalAction::RemoveItem, - PrincipalField::Emails, - PrincipalValue::String(email), - ) => { - let email = email.to_lowercase(); - if principal.email_addresses().any(|v| v == email) { - let mut deleted_primary = false; - principal.data.retain(|v| match v { - PrincipalData::EmailAlias(v) => v != &email, - PrincipalData::PrimaryEmail(v) => { - if v == &email { - deleted_primary = true; - false - } else { - true - } - } - _ => true, - }); - batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), - ))); - - if deleted_primary { - for data in &mut principal.data { - if let PrincipalData::EmailAlias(email) = data { - *data = PrincipalData::PrimaryEmail(std::mem::take(email)); - break; - } - } - } - - // Emails changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - } - } - - // MemberOf - ( - PrincipalAction::Set, - PrincipalField::MemberOf | PrincipalField::Lists | PrincipalField::Roles, - PrincipalValue::StringList(members), - ) => { - let mut new_member_of = Vec::new(); - for member in members { - let member_info = match ( - self.get_principal_info(&member) - .await - .caused_by(trc::location!())? - .filter(|p| p.has_tenant_access(tenant_id)), - change.field.map_internal_roles(&member), - ) { - (_, Some(v)) => v, - (Some(v), _) => v, - _ => { - return Err(not_found(member.clone())); - } - }; - - validate_member_of(change.field, principal_type, member_info.typ, &member)?; - - if !member_of.iter().any(|v| v.principal_id == member_info.id) { - // Update changed principal ids - changed_principals.add_member_change( - principal_id, - principal_type, - member_info.id, - member_info.typ, - ); - - batch.set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: member_info.id, - }), - vec![member_info.typ as u8], - ); - batch.set( - ValueClass::Directory(DirectoryClass::Members { - principal_id: member_info.id, - has_member: principal_id, - }), - vec![], - ); - } - - new_member_of.push(MemberOf { - principal_id: member_info.id, - typ: member_info.typ, - }); - } - - for member in &member_of { - if !new_member_of - .iter() - .any(|v| v.principal_id == member.principal_id) - { - // Update changed principal ids - changed_principals.add_member_change( - principal_id, - principal_type, - member.principal_id, - member.typ, - ); - - batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: member.principal_id, - })); - batch.clear(ValueClass::Directory(DirectoryClass::Members { - principal_id: member.principal_id, - has_member: principal_id, - })); - } - } - - member_of = new_member_of; - } - ( - PrincipalAction::AddItem, - PrincipalField::MemberOf | PrincipalField::Lists | PrincipalField::Roles, - PrincipalValue::String(member), - ) => { - let member_info = match ( - self.get_principal_info(&member) - .await - .caused_by(trc::location!())? - .filter(|p| p.has_tenant_access(tenant_id)), - change.field.map_internal_roles(&member), - ) { - (_, Some(v)) => v, - (Some(v), _) => v, - _ => { - return Err(not_found(member.clone())); - } - }; - - if !member_of.iter().any(|v| v.principal_id == member_info.id) { - validate_member_of(change.field, principal_type, member_info.typ, &member)?; - - // Update changed principal ids - changed_principals.add_member_change( - principal_id, - principal_type, - member_info.id, - member_info.typ, - ); - - batch.set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: member_info.id, - }), - vec![member_info.typ as u8], - ); - - batch.set( - ValueClass::Directory(DirectoryClass::Members { - principal_id: member_info.id, - has_member: principal_id, - }), - vec![], - ); - - member_of.push(MemberOf { - principal_id: member_info.id, - typ: member_info.typ, - }); - } - } - ( - PrincipalAction::RemoveItem, - PrincipalField::MemberOf | PrincipalField::Lists | PrincipalField::Roles, - PrincipalValue::String(member), - ) => { - if let Some(member_info) = - self.get_principal_info(&member) - .await - .caused_by(trc::location!())? - .or_else(|| { - change.field.map_internal_role_name(&member).map(|id| { - PrincipalInfo { - id, - typ: Type::Role, - tenant: None, - } - }) - }) - { - for (pos, member) in member_of.iter().enumerate() { - if member.principal_id == member_info.id { - // Update changed principal ids - changed_principals.add_member_change( - principal_id, - principal_type, - member_info.id, - member_info.typ, - ); - - batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: member_info.id, - })); - - batch.clear(ValueClass::Directory(DirectoryClass::Members { - principal_id: member_info.id, - has_member: principal_id, - })); - - member_of.remove(pos); - break; - } - } - } - } - - ( - PrincipalAction::Set, - PrincipalField::Members, - PrincipalValue::StringList(members_), - ) => { - let mut new_members = Vec::new(); - - for member in members_ { - let member_info = self - .get_principal_info(&member) - .await - .caused_by(trc::location!())? - .filter(|p| p.has_tenant_access(tenant_id)) - .ok_or_else(|| not_found(member.clone()))?; - - if !allowed_member_types.contains(&member_info.typ) { - return Err(error( - "Invalid members value", - format!( - "Principal {member:?} is not one of {}.", - allowed_member_types - .iter() - .map(|v| v.description()) - .collect::>() - .join(", ") - ) - .into(), - )); - } - - if !members.contains(&member_info.id) { - // Update changed principal ids - changed_principals.add_member_change( - member_info.id, - member_info.typ, - principal_id, - principal_type, - ); - - batch.set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: member_info.id, - member_of: principal_id, - }), - vec![principal_type as u8], - ); - batch.set( - ValueClass::Directory(DirectoryClass::Members { - principal_id, - has_member: member_info.id, - }), - vec![], - ); - } - - new_members.push(member_info.id); - } - - for member_id in &members { - if !new_members.contains(member_id) { - // Update changed principal ids - if principal_type != Type::List - && let Some(member_info) = self - .get_principal(*member_id) - .await - .caused_by(trc::location!())? - { - changed_principals.add_member_change( - *member_id, - member_info.typ, - principal_id, - principal_type, - ); - } - - batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: *member_id, - member_of: principal_id, - })); - batch.clear(ValueClass::Directory(DirectoryClass::Members { - principal_id, - has_member: *member_id, - })); - } - } - - members = new_members; - } - ( - PrincipalAction::AddItem, - PrincipalField::Members, - PrincipalValue::String(member), - ) => { - let member_info = self - .get_principal_info(&member) - .await - .caused_by(trc::location!())? - .filter(|p| p.has_tenant_access(tenant_id)) - .ok_or_else(|| not_found(member.clone()))?; - - if !members.contains(&member_info.id) { - if !allowed_member_types.contains(&member_info.typ) { - return Err(error( - "Invalid members value", - format!( - "Principal {member:?} is not one of {}.", - allowed_member_types - .iter() - .map(|v| v.description()) - .collect::>() - .join(", ") - ) - .into(), - )); - } - - // Update changed principal ids - changed_principals.add_member_change( - member_info.id, - member_info.typ, - principal_id, - principal_type, - ); - - batch.set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: member_info.id, - member_of: principal_id, - }), - vec![principal_type as u8], - ); - batch.set( - ValueClass::Directory(DirectoryClass::Members { - principal_id, - has_member: member_info.id, - }), - vec![], - ); - members.push(member_info.id); - } - } - ( - PrincipalAction::RemoveItem, - PrincipalField::Members, - PrincipalValue::String(member), - ) => { - if let Some(member_info) = self - .get_principal_info(&member) - .await - .caused_by(trc::location!())? - { - for (pos, member_id) in members.iter().enumerate() { - if *member_id == member_info.id { - // Update changed principal ids - changed_principals.add_member_change( - member_info.id, - member_info.typ, - principal_id, - principal_type, - ); - - batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: member_info.id, - member_of: principal_id, - })); - batch.clear(ValueClass::Directory(DirectoryClass::Members { - principal_id, - has_member: member_info.id, - })); - members.remove(pos); - break; - } - } - } - } - - ( - PrincipalAction::Set, - PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions, - PrincipalValue::StringList(names), - ) => { - let is_disabled = change.field == PrincipalField::DisabledPermissions; - let mut permissions = AHashSet::with_capacity(names.len()); - for name in names { - let permission = Permission::from_name(&name).ok_or_else(|| { - error( - format!("Invalid {} value", change.field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })?; - - if !permissions.contains(&permission) { - if params - .allowed_permissions - .as_ref() - .is_none_or(|p| p.get(permission as usize)) - || is_disabled - { - permissions.insert(permission); - } else { - return Err(error( - "Invalid permission", - format!("Your account cannot grant the {name:?} permission") - .into(), - )); - } - } - } - - principal.remove_permissions(!is_disabled); - - if !permissions.is_empty() { - principal.add_permissions(permissions.into_iter().map(|permission| { - PermissionGrant { - permission, - grant: !is_disabled, - } - })); - } - - // Permissions changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - } - ( - PrincipalAction::AddItem, - PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions, - PrincipalValue::String(name), - ) => { - let permission = Permission::from_name(&name).ok_or_else(|| { - error( - format!("Invalid {} value", change.field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })?; - - if params - .allowed_permissions - .as_ref() - .is_none_or(|p| p.get(permission as usize)) - || change.field == PrincipalField::DisabledPermissions - { - principal.add_permission( - permission, - change.field == PrincipalField::EnabledPermissions, - ); - - // Permissions changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - } else { - return Err(error( - "Invalid permission", - format!("Your account cannot grant the {name:?} permission").into(), - )); - } - } - ( - PrincipalAction::RemoveItem, - PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions, - PrincipalValue::String(name), - ) => { - let permission = Permission::from_name(&name).ok_or_else(|| { - error( - format!("Invalid {} value", change.field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })?; - - principal.remove_permission( - permission, - change.field == PrincipalField::EnabledPermissions, - ); - - // Permissions changed, update changed principals - changed_principals.add_change(principal_id, principal_type, change.field); - } - ( - PrincipalAction::Set, - PrincipalField::ExternalMembers, - PrincipalValue::StringList(items), - ) => { - principal - .data - .retain(|v| !matches!(v, PrincipalData::ExternalMember(_))); - if !items.is_empty() { - principal.data.extend( - items - .into_iter() - .map(|item| { - sanitize_email(&item) - .map(PrincipalData::ExternalMember) - .ok_or_else(|| { - error( - "Invalid email address", - format!( - "Invalid value {:?} for {}", - item, - change.field.as_str() - ) - .into(), - ) - }) - }) - .collect::>>()?, - ); - } - } - (PrincipalAction::Set, PrincipalField::Urls, PrincipalValue::StringList(items)) => { - principal - .data - .retain(|v| !matches!(v, PrincipalData::Url(_))); - - if !items.is_empty() { - principal - .data - .extend(items.into_iter().map(PrincipalData::Url)); - } - } - ( - PrincipalAction::AddItem, - PrincipalField::Urls | PrincipalField::ExternalMembers, - PrincipalValue::String(mut item), - ) => { - if matches!(change.field, PrincipalField::ExternalMembers) { - item = sanitize_email(&item).ok_or_else(|| { - error( - "Invalid email address", - format!("Invalid value {:?} for {}", item, change.field.as_str()) - .into(), - ) - })? - } - - let mut found = false; - for data in &principal.data { - match (data, change.field) { - (PrincipalData::Url(url), PrincipalField::Urls) => { - if url == &item { - found = true; - break; - } - } - ( - PrincipalData::ExternalMember(email), - PrincipalField::ExternalMembers, - ) => { - if email == &item { - found = true; - break; - } - } - _ => {} - } - } - - if !found { - match change.field { - PrincipalField::Urls => principal.data.push(PrincipalData::Url(item)), - PrincipalField::ExternalMembers => { - principal.data.push(PrincipalData::ExternalMember(item)) - } - _ => {} - } - } - } - ( - PrincipalAction::RemoveItem, - PrincipalField::Urls, - PrincipalValue::String(item), - ) => { - principal.data.retain(|v| match v { - PrincipalData::Url(v) => v != &item, - _ => true, - }); - } - ( - PrincipalAction::RemoveItem, - PrincipalField::ExternalMembers, - PrincipalValue::String(item), - ) => { - principal.data.retain(|v| match v { - PrincipalData::ExternalMember(v) => v != &item, - _ => true, - }); - } - - (_, field, value) => { - return Err(error( - "Invalid parameter", - format!("Invalid value {:?} for {}", value, field.as_str()).into(), - )); - } - } - } - - // Validate object size - if principal.object_size() > 100_000 { - return Err(error( - "Invalid parameter", - "Principal object size exceeds 100kb safety limit.".into(), - )); - } - - if update_principal { - principal.sort(); - build_search_index( - &mut batch, - principal_id, - Some(prev_principal.inner), - Some(&principal), - ); - - batch - .assert_value( - ValueClass::Directory(DirectoryClass::Principal(principal_id)), - prev_principal, - ) - .set( - ValueClass::Directory(DirectoryClass::Principal(principal_id)), - Archiver::new(principal) - .serialize() - .caused_by(trc::location!())?, - ); - } - - self.write(batch.build_all()) - .await - .caused_by(trc::location!())?; - - Ok(changed_principals) - } - - async fn list_principals( - &self, - filter: Option<&str>, - tenant_id: Option, - types: &[Type], - fetch: bool, - page: usize, - limit: usize, - ) -> trc::Result> { - let filter = if let Some(filter) = filter.filter(|f| !f.trim().is_empty()) { - let mut matches = RoaringBitmap::new(); - - for token in WordTokenizer::new(filter, MAX_TOKEN_LENGTH) { - let word_bytes = token.word.as_bytes(); - let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Index { - word: word_bytes.to_vec(), - principal_id: 0, - })); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Index { - word: word_bytes.to_vec(), - principal_id: u32::MAX, - })); - - let mut word_matches = RoaringBitmap::new(); - self.iterate( - IterateParams::new(from_key, to_key).no_values(), - |key, _| { - let id_pos = key.len() - U32_LEN; - if key.get(1..id_pos).is_some_and(|v| v == word_bytes) { - word_matches.insert(key.deserialize_be_u32(id_pos)?); - Ok(true) - } else { - Ok(false) - } - }, - ) - .await - .caused_by(trc::location!())?; - - if matches.is_empty() { - matches = word_matches; - } else { - matches &= word_matches; - if matches.is_empty() { - break; - } - } - } - - if !matches.is_empty() { - Some(matches) - } else { - return Ok(PrincipalList { - total: 0, - items: vec![], - }); - } - } else { - None - }; - - let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![]))); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![ - u8::MAX; - 10 - ]))); - - let max_items = if limit > 0 { limit } else { usize::MAX }; - let mut offset = page.saturating_sub(1) * limit; - let mut result = PrincipalList { - items: Vec::new(), - total: 0, - }; - self.iterate( - IterateParams::new(from_key, to_key).ascending(), - |key, value| { - let pt = PrincipalInfo::deserialize(value).caused_by(trc::location!())?; - - if (types.is_empty() || types.contains(&pt.typ)) - && pt.has_tenant_access(tenant_id) - && filter.as_ref().is_none_or(|filter| filter.contains(pt.id)) - { - result.total += 1; - if offset == 0 { - if result.items.len() < max_items { - let mut principal = Principal::new(pt.id, pt.typ); - principal.name = - String::from_utf8_lossy(key.get(1..).unwrap_or_default()) - .into_owned(); - result.items.push(principal); - } - } else { - offset -= 1; - } - } - - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - - if fetch && !result.items.is_empty() { - let mut items = Vec::with_capacity(result.items.len()); - - for principal in result.items { - items.push( - self.query(QueryParams::id(principal.id).with_return_member_of(fetch)) - .await - .caused_by(trc::location!())? - .ok_or_else(|| not_found(principal.name().to_string()))?, - ); - } - result.items = items; - - Ok(result) - } else { - Ok(result) - } - } - - async fn count_principals( - &self, - filter: Option<&str>, - typ: Option, - tenant_id: Option, - ) -> trc::Result { - let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![]))); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![ - u8::MAX; - 10 - ]))); - - let mut count = 0; - self.iterate( - IterateParams::new(from_key, to_key).ascending(), - |key, value| { - let pt = PrincipalInfo::deserialize(value).caused_by(trc::location!())?; - let name = - std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default(); - - if typ.is_none_or(|t| pt.typ == t) - && pt.has_tenant_access(tenant_id) - && filter.is_none_or(|f| name.contains(f)) - { - count += 1; - } - - Ok(true) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| count) - } - - async fn principal_ids( - &self, - typ: Option, - tenant_id: Option, - ) -> trc::Result { - let mut results = RoaringBitmap::new(); - self.iterate( - IterateParams::new( - ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![0u8]))), - ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![ - u8::MAX; - 10 - ]))), - ), - |_, value| { - let pt = PrincipalInfo::deserialize(value).caused_by(trc::location!())?; - if typ.is_none_or(|t| pt.typ == t) && pt.has_tenant_access(tenant_id) { - results.insert(pt.id); - } - - Ok(true) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| results) - } - - async fn get_member_of(&self, principal_id: u32) -> trc::Result> { - let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: 0, - })); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id, - member_of: u32::MAX, - })); - let mut results = Vec::new(); - self.iterate(IterateParams::new(from_key, to_key), |key, value| { - results.push(MemberOf { - principal_id: key.deserialize_be_u32(key.len() - U32_LEN)?, - typ: value - .first() - .map(|v| Type::from_u8(*v)) - .unwrap_or(Type::Group), - }); - Ok(true) - }) - .await - .caused_by(trc::location!())?; - Ok(results) - } - - async fn get_members(&self, principal_id: u32) -> trc::Result> { - let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Members { - principal_id, - has_member: 0, - })); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Members { - principal_id, - has_member: u32::MAX, - })); - let mut results = Vec::new(); - self.iterate( - IterateParams::new(from_key, to_key).no_values(), - |key, _| { - results.push(key.deserialize_be_u32(key.len() - U32_LEN)?); - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - Ok(results) - } - - async fn map_principal( - &self, - principal: Principal, - fields: &[PrincipalField], - ) -> trc::Result { - let mut result = PrincipalSet::new(principal.id, principal.typ); - - let has_enabled = fields.is_empty() || fields.contains(&PrincipalField::EnabledPermissions); - let has_disabled = - fields.is_empty() || fields.contains(&PrincipalField::DisabledPermissions); - let mut directory_quotas = Vec::new(); - let mut quota = None; - let mut tenant_id = None; - - for data in principal.data { - match data { - PrincipalData::MemberOf(principal_id) - if fields.is_empty() || fields.contains(&PrincipalField::MemberOf) => - { - if let Some(name) = self - .get_principal_name(principal_id) - .await - .caused_by(trc::location!())? - { - result.append_str(PrincipalField::MemberOf, name); - } - } - PrincipalData::Role(principal_id) - if fields.is_empty() || fields.contains(&PrincipalField::Roles) => - { - match principal_id { - ROLE_ADMIN => { - result.append_str(PrincipalField::Roles, "admin"); - } - ROLE_TENANT_ADMIN => { - result.append_str(PrincipalField::Roles, "tenant-admin"); - } - ROLE_USER => { - result.append_str(PrincipalField::Roles, "user"); - } - principal_id => { - if let Some(name) = self - .get_principal_name(principal_id) - .await - .caused_by(trc::location!())? - { - result.append_str(PrincipalField::Roles, name); - } - } - } - } - PrincipalData::List(principal_id) - if fields.is_empty() || fields.contains(&PrincipalField::Lists) => - { - if let Some(name) = self - .get_principal_name(principal_id) - .await - .caused_by(trc::location!())? - { - result.append_str(PrincipalField::Lists, name); - } - } - PrincipalData::Permission { - permission_id, - grant, - } if has_enabled || has_disabled => { - if grant { - if has_enabled { - result.append_str( - PrincipalField::EnabledPermissions, - Permission::from_id(permission_id) - .map(|f| f.name()) - .unwrap_or("unknown"), - ); - } - } else if has_disabled { - result.append_str( - PrincipalField::DisabledPermissions, - Permission::from_id(permission_id) - .map(|f| f.name()) - .unwrap_or("unknown"), - ); - } - } - PrincipalData::DiskQuota(q) => { - quota = Some(q); - } - PrincipalData::Tenant(tid) => { - tenant_id = Some(tid); - } - PrincipalData::Description(description) => { - if fields.is_empty() || fields.contains(&PrincipalField::Description) { - result.set(PrincipalField::Description, description); - } - } - PrincipalData::Password(secret) - | PrincipalData::AppPassword(secret) - | PrincipalData::OtpAuth(secret) => { - if fields.is_empty() || fields.contains(&PrincipalField::Secrets) { - result.append_str(PrincipalField::Secrets, secret); - } - } - PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) => { - if fields.is_empty() || fields.contains(&PrincipalField::Emails) { - result.append_str(PrincipalField::Emails, email); - } - } - PrincipalData::Picture(picture) => { - if fields.is_empty() || fields.contains(&PrincipalField::Picture) { - result.set(PrincipalField::Picture, picture); - } - } - PrincipalData::Locale(locale) => { - if fields.is_empty() || fields.contains(&PrincipalField::Locale) { - result.set(PrincipalField::Locale, locale); - } - } - PrincipalData::ExternalMember(member) => { - if fields.is_empty() || fields.contains(&PrincipalField::ExternalMembers) { - result.append_str(PrincipalField::ExternalMembers, member); - } - } - PrincipalData::Url(url) => { - if fields.is_empty() || fields.contains(&PrincipalField::Urls) { - result.append_str(PrincipalField::Urls, url); - } - } - PrincipalData::DirectoryQuota { quota, typ } => { - directory_quotas.push((typ, quota)); - } - _ => (), - } - } - - // Obtain member names - if fields.is_empty() || fields.contains(&PrincipalField::Members) { - match principal.typ { - Type::Group | Type::List | Type::Role => { - for member_id in self.get_members(principal.id).await? { - if let Some(member_principal) = self - .query(QueryParams::id(member_id).with_return_member_of(false)) - .await? - { - result.append_str(PrincipalField::Members, member_principal.name); - } - } - } - Type::Domain => { - let from_key = - ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId(vec![]))); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId( - vec![u8::MAX; 10], - ))); - let domain_name = &principal.name; - let mut total: u64 = 0; - self.iterate( - IterateParams::new(from_key, to_key).no_values(), - |key, _| { - if std::str::from_utf8(key.get(1..).unwrap_or_default()) - .unwrap_or_default() - .rsplit_once('@') - .is_some_and(|(_, domain)| domain == domain_name) - { - total += 1; - } - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - result.set(PrincipalField::Members, total); - } - Type::Tenant => { - let from_key = - ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![]))); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId( - vec![u8::MAX; 10], - ))); - let mut total: u64 = 0; - - self.iterate(IterateParams::new(from_key, to_key), |_, value| { - let pinfo = - PrincipalInfo::deserialize(value).caused_by(trc::location!())?; - - if pinfo.typ == Type::Individual - && pinfo.has_tenant_access(Some(principal.id)) - { - total += 1; - } - Ok(true) - }) - .await - .caused_by(trc::location!())?; - - result.set(PrincipalField::Members, total); - } - _ => {} - } - } - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - // Map tenant name - #[cfg(feature = "enterprise")] - if let Some(tenant_id) = tenant_id - && (fields.is_empty() || fields.contains(&PrincipalField::Tenant)) - && let Some(name) = self - .get_principal_name(tenant_id) - .await - .caused_by(trc::location!())? - { - result.set(PrincipalField::Tenant, name); - } - - // SPDX-SnippetEnd - - // Map fields - if fields.is_empty() || fields.contains(&PrincipalField::Name) { - result.set(PrincipalField::Name, principal.name); - } - if fields.is_empty() || fields.contains(&PrincipalField::Quota) { - if !directory_quotas.is_empty() { - let mut quotas = vec![0u64; Type::MAX_ID + 2]; - if let Some(quota) = quota { - quotas[0] = quota; - } - for (typ, quota) in directory_quotas { - quotas[(typ as usize) + 1] = quota as u64; - } - - result.set(PrincipalField::Quota, quotas); - } else if let Some(quota) = quota { - result.set(PrincipalField::Quota, quota); - } - } - - // Obtain used quota - if matches!(principal.typ, Type::Individual | Type::Group | Type::Tenant) - && (fields.is_empty() || fields.contains(&PrincipalField::UsedQuota)) - { - let quota = self - .get_counter(DirectoryClass::UsedQuota(principal.id)) - .await - .caused_by(trc::location!())?; - if quota > 0 { - result.set(PrincipalField::UsedQuota, quota as u64); - } - } - - Ok(result) - } -} - -impl ValidateDirectory for Store { - async fn validate_email( - &self, - email: &str, - tenant_id: Option, - create_if_missing: bool, - ) -> trc::Result<()> { - if self.rcpt(email).await.caused_by(trc::location!())? != RcptType::Invalid { - Err(err_exists(PrincipalField::Emails, email.to_string())) - } else if let Some(domain) = email.try_domain_part() { - match self - .get_principal_info(domain) - .await - .caused_by(trc::location!())? - { - Some(v) if v.typ == Type::Domain && v.has_tenant_access(tenant_id) => Ok(()), - None if create_if_missing => self - .create_principal( - PrincipalSet::new(0, Type::Domain) - .with_field(PrincipalField::Name, domain) - .with_field(PrincipalField::Description, domain), - tenant_id, - None, - ) - .await - .caused_by(trc::location!()) - .map(|_| ()), - _ => Err(not_found(domain.to_string())), - } - } else { - Err(error("Invalid email", "Email address is invalid".into())) - } - } -} - -impl PrincipalField { - pub fn map_internal_role_name(&self, name: &str) -> Option { - match (self, name) { - (PrincipalField::Roles, "admin") => Some(ROLE_ADMIN), - (PrincipalField::Roles, "tenant-admin") => Some(ROLE_TENANT_ADMIN), - (PrincipalField::Roles, "user") => Some(ROLE_USER), - _ => None, - } - } - - pub fn map_internal_roles(&self, name: &str) -> Option { - self.map_internal_role_name(name) - .map(|role_id| PrincipalInfo::new(role_id, Type::Role, None)) - } -} - -impl<'x> UpdatePrincipal<'x> { - pub fn by_id(id: u32) -> Self { - Self { - query: QueryBy::Id(id), - changes: Vec::new(), - create_domains: false, - tenant_id: None, - allowed_permissions: None, - } - } - - pub fn by_name(name: &'x str) -> Self { - Self { - query: QueryBy::Name(name), - changes: Vec::new(), - create_domains: false, - tenant_id: None, - allowed_permissions: None, - } - } - - pub fn with_tenant(mut self, tenant_id: Option) -> Self { - self.tenant_id = tenant_id; - self - } - - pub fn with_updates(mut self, changes: Vec) -> Self { - self.changes = changes; - self - } - - pub fn with_allowed_permissions(mut self, permissions: &'x Permissions) -> Self { - self.allowed_permissions = permissions.into(); - self - } - - pub fn create_domains(mut self) -> Self { - self.create_domains = true; - self - } -} - -fn validate_member_of( - field: PrincipalField, - typ: Type, - member_type: Type, - member_name: &str, -) -> trc::Result<()> { - let expected_types = match (field, typ) { - (PrincipalField::MemberOf, Type::Individual) => &[Type::Group, Type::Individual][..], - (PrincipalField::MemberOf, Type::Group) => &[Type::Group][..], - (PrincipalField::Lists, Type::Individual | Type::Group) => &[Type::List][..], - (PrincipalField::Roles, Type::Individual | Type::Tenant | Type::Role) => &[Type::Role][..], - _ => &[][..], - }; - - if expected_types.is_empty() || !expected_types.contains(&member_type) { - Err(error( - format!("Invalid {} value", field.as_str()), - if !expected_types.is_empty() { - format!( - "Principal {member_name:?} is not a {}.", - expected_types - .iter() - .map(|t| t.description().to_string()) - .collect::>() - .join(", ") - ) - .into() - } else { - format!("Principal {member_name:?} cannot be added as a member.").into() - }, - )) - } else { - Ok(()) - } -} - -impl ChangedPrincipals { - pub fn new() -> Self { - Self::default() - } - - pub fn from_change(principal_id: u32, principal_type: Type, field: PrincipalField) -> Self { - let mut set = Self::default(); - set.add_change(principal_id, principal_type, field); - set - } - - pub fn add_change(&mut self, principal_id: u32, principal_type: Type, field: PrincipalField) { - if matches!( - (principal_type, field), - ( - Type::Individual | Type::Group, - PrincipalField::Name - | PrincipalField::Quota - | PrincipalField::Secrets - | PrincipalField::Emails - | PrincipalField::MemberOf - | PrincipalField::Members - | PrincipalField::Tenant - | PrincipalField::Roles - | PrincipalField::EnabledPermissions - | PrincipalField::DisabledPermissions, - ) | ( - Type::Tenant | Type::Role | Type::ApiKey | Type::OauthClient, - PrincipalField::MemberOf - | PrincipalField::Members - | PrincipalField::Secrets - | PrincipalField::Tenant - | PrincipalField::Roles - | PrincipalField::EnabledPermissions - | PrincipalField::DisabledPermissions, - ) - ) && principal_id < ROLE_USER - { - self.0 - .entry(principal_id) - .or_insert_with(|| ChangedPrincipal::new(principal_type)) - .update_member_change(matches!( - (field, principal_type), - ( - PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions, - Type::Role | Type::Tenant - ) - )) - .update_name_change(matches!(field, PrincipalField::Name)); - } - } - - pub fn add_member_change( - &mut self, - principal_id: u32, - principal_type: Type, - member_id: u32, - member_type: Type, - ) { - match (principal_type, member_type) { - (Type::Group | Type::Role, Type::Individual | Type::ApiKey | Type::OauthClient) => { - self.0 - .entry(member_id) - .or_insert_with(|| ChangedPrincipal::new(member_type)); - } - (Type::Individual | Type::ApiKey | Type::OauthClient, Type::Group | Type::Role) => { - self.0 - .entry(principal_id) - .or_insert_with(|| ChangedPrincipal::new(principal_type)); - } - ( - Type::Group | Type::Tenant | Type::Role, - Type::Individual | Type::Group | Type::Tenant | Type::Role, - ) => { - if principal_id < ROLE_USER { - self.0 - .entry(principal_id) - .or_insert_with(|| ChangedPrincipal::new(principal_type)) - .update_member_change(matches!(member_type, Type::Role)); - } - if member_id < ROLE_USER { - self.0 - .entry(member_id) - .or_insert_with(|| ChangedPrincipal::new(member_type)) - .update_member_change(matches!(principal_type, Type::Role)); - } - } - _ => {} - } - } - - pub fn add_deletion(&mut self, principal_id: u32, principal_type: Type) { - if matches!( - principal_type, - Type::Individual - | Type::Group - | Type::Tenant - | Type::Role - | Type::ApiKey - | Type::OauthClient - ) { - self.0 - .entry(principal_id) - .or_insert_with(|| ChangedPrincipal::new(principal_type)); - } - } - - pub fn contains(&self, principal_id: u32) -> bool { - self.0.contains_key(&principal_id) - } - - pub fn iter(&'_ self) -> std::collections::hash_map::Iter<'_, u32, ChangedPrincipal> { - self.0.iter() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl ChangedPrincipal { - pub fn new(typ: Type) -> Self { - Self { - typ, - member_change: false, - name_change: false, - } - } - - pub fn update_member_change(&mut self, member_change: bool) -> &mut Self { - self.member_change |= member_change; - self - } - - pub fn update_name_change(&mut self, name_change: bool) -> &mut Self { - self.name_change |= name_change; - self - } -} - -pub fn err_missing(field: impl Into) -> trc::Error { - trc::ManageEvent::MissingParameter.ctx(trc::Key::Key, field) -} - -pub fn err_exists(field: impl Into, value: impl Into) -> trc::Error { - trc::ManageEvent::AlreadyExists - .ctx(trc::Key::Key, field) - .ctx(trc::Key::Value, value) -} - -pub fn not_found(value: impl Into) -> trc::Error { - trc::ManageEvent::NotFound.ctx(trc::Key::Key, value) -} - -pub fn unsupported(details: impl Into) -> trc::Error { - trc::ManageEvent::NotSupported.ctx(trc::Key::Details, details) -} - -pub fn enterprise() -> trc::Error { - trc::ManageEvent::NotSupported.ctx(trc::Key::Details, "Enterprise feature") -} - -pub fn error(details: impl Into, reason: Option>) -> trc::Error { - trc::ManageEvent::Error - .ctx(trc::Key::Details, details) - .ctx_opt(trc::Key::Reason, reason) -} - -impl From for trc::Value { - fn from(value: PrincipalField) -> Self { - trc::Value::String(CompactString::const_new(value.as_str())) - } -} diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs deleted file mode 100644 index f6b068eb..00000000 --- a/crates/directory/src/backend/internal/mod.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -pub mod lookup; -pub mod manage; - -use crate::Type; -use ahash::AHashMap; - -use std::fmt::Display; -use store::{Deserialize, SerializeInfallible, U32_LEN, write::key::KeySerializer}; -use utils::codec::leb128::Leb128Iterator; - -pub struct PrincipalInfo { - pub id: u32, - pub typ: Type, - pub tenant: Option, -} - -impl PrincipalInfo { - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - pub fn has_tenant_access(&self, tenant_id: Option) -> bool { - tenant_id.is_none_or(|tenant_id| { - self.tenant.is_some_and(|t| tenant_id == t) - || (self.typ == Type::Tenant && self.id == tenant_id) - }) - } - // SPDX-SnippetEnd - - #[cfg(not(feature = "enterprise"))] - pub fn has_tenant_access(&self, _tenant_id: Option) -> bool { - true - } -} - -impl SerializeInfallible for PrincipalInfo { - fn serialize(&self) -> Vec { - if let Some(tenant) = self.tenant { - KeySerializer::new((U32_LEN * 2) + 1) - .write_leb128(self.id) - .write(self.typ as u8) - .write_leb128(tenant) - .finalize() - } else { - KeySerializer::new(U32_LEN + 1) - .write_leb128(self.id) - .write(self.typ as u8) - .finalize() - } - } -} - -impl Deserialize for PrincipalInfo { - fn deserialize(bytes_: &[u8]) -> trc::Result { - let mut bytes = bytes_.iter(); - Ok(PrincipalInfo { - id: bytes.next_leb128().ok_or_else(|| { - trc::StoreEvent::DataCorruption - .caused_by(trc::location!()) - .ctx(trc::Key::Value, bytes_) - })?, - typ: Type::from_u8(*bytes.next().ok_or_else(|| { - trc::StoreEvent::DataCorruption - .caused_by(trc::location!()) - .ctx(trc::Key::Value, bytes_) - })?), - tenant: bytes.next_leb128(), - }) - } -} - -impl PrincipalInfo { - pub fn new(principal_id: u32, typ: Type, tenant: Option) -> Self { - Self { - id: principal_id, - typ, - tenant, - } - } -} - -#[derive( - Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, -)] -#[serde(rename_all = "camelCase")] -pub enum PrincipalField { - Name, - Type, - Quota, - UsedQuota, - Description, - Secrets, - Emails, - MemberOf, - Members, - Tenant, - Roles, - Lists, - EnabledPermissions, - DisabledPermissions, - Picture, - Urls, - ExternalMembers, - Locale, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct PrincipalSet { - pub id: u32, - pub typ: Type, - pub fields: AHashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct PrincipalUpdate { - pub action: PrincipalAction, - pub field: PrincipalField, - pub value: PrincipalValue, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum PrincipalAction { - #[serde(rename = "set")] - Set, - #[serde(rename = "addItem")] - AddItem, - #[serde(rename = "removeItem")] - RemoveItem, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -#[serde(untagged)] -pub enum PrincipalValue { - String(String), - StringList(Vec), - Integer(u64), - IntegerList(Vec), -} - -impl PrincipalUpdate { - pub fn set(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate { - PrincipalUpdate { - action: PrincipalAction::Set, - field, - value, - } - } - - pub fn add_item(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate { - PrincipalUpdate { - action: PrincipalAction::AddItem, - field, - value, - } - } - - pub fn remove_item(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate { - PrincipalUpdate { - action: PrincipalAction::RemoveItem, - field, - value, - } - } -} - -impl Display for PrincipalField { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.as_str().fmt(f) - } -} - -impl PrincipalField { - pub fn id(&self) -> u8 { - match self { - PrincipalField::Name => 0, - PrincipalField::Type => 1, - PrincipalField::Quota => 2, - PrincipalField::Description => 3, - PrincipalField::Secrets => 4, - PrincipalField::Emails => 5, - PrincipalField::MemberOf => 6, - PrincipalField::Members => 7, - PrincipalField::Tenant => 8, - PrincipalField::Roles => 9, - PrincipalField::Lists => 10, - PrincipalField::EnabledPermissions => 11, - PrincipalField::DisabledPermissions => 12, - PrincipalField::UsedQuota => 13, - PrincipalField::Picture => 14, - PrincipalField::Urls => 15, - PrincipalField::ExternalMembers => 16, - PrincipalField::Locale => 17, - } - } - - pub fn from_id(id: u8) -> Option { - match id { - 0 => Some(PrincipalField::Name), - 1 => Some(PrincipalField::Type), - 2 => Some(PrincipalField::Quota), - 3 => Some(PrincipalField::Description), - 4 => Some(PrincipalField::Secrets), - 5 => Some(PrincipalField::Emails), - 6 => Some(PrincipalField::MemberOf), - 7 => Some(PrincipalField::Members), - 8 => Some(PrincipalField::Tenant), - 9 => Some(PrincipalField::Roles), - 10 => Some(PrincipalField::Lists), - 11 => Some(PrincipalField::EnabledPermissions), - 12 => Some(PrincipalField::DisabledPermissions), - 13 => Some(PrincipalField::UsedQuota), - 14 => Some(PrincipalField::Picture), - 15 => Some(PrincipalField::Urls), - 16 => Some(PrincipalField::ExternalMembers), - 17 => Some(PrincipalField::Locale), - _ => None, - } - } - - pub fn as_str(&self) -> &'static str { - match self { - PrincipalField::Name => "name", - PrincipalField::Type => "type", - PrincipalField::Quota => "quota", - PrincipalField::UsedQuota => "usedQuota", - PrincipalField::Description => "description", - PrincipalField::Secrets => "secrets", - PrincipalField::Emails => "emails", - PrincipalField::MemberOf => "memberOf", - PrincipalField::Members => "members", - PrincipalField::Tenant => "tenant", - PrincipalField::Roles => "roles", - PrincipalField::Lists => "lists", - PrincipalField::EnabledPermissions => "enabledPermissions", - PrincipalField::DisabledPermissions => "disabledPermissions", - PrincipalField::Picture => "picture", - PrincipalField::Urls => "urls", - PrincipalField::ExternalMembers => "externalMembers", - PrincipalField::Locale => "locale", - } - } - - pub fn try_parse(s: &str) -> Option { - match s { - "name" => Some(PrincipalField::Name), - "type" => Some(PrincipalField::Type), - "quota" => Some(PrincipalField::Quota), - "usedQuota" => Some(PrincipalField::UsedQuota), - "description" => Some(PrincipalField::Description), - "secrets" => Some(PrincipalField::Secrets), - "emails" => Some(PrincipalField::Emails), - "memberOf" => Some(PrincipalField::MemberOf), - "members" => Some(PrincipalField::Members), - "tenant" => Some(PrincipalField::Tenant), - "roles" => Some(PrincipalField::Roles), - "lists" => Some(PrincipalField::Lists), - "enabledPermissions" => Some(PrincipalField::EnabledPermissions), - "disabledPermissions" => Some(PrincipalField::DisabledPermissions), - "picture" => Some(PrincipalField::Picture), - "urls" => Some(PrincipalField::Urls), - "externalMembers" => Some(PrincipalField::ExternalMembers), - "locale" => Some(PrincipalField::Locale), - _ => None, - } - } -} - -pub trait SpecialSecrets { - fn is_otp_secret(&self) -> bool; - fn is_app_secret(&self) -> bool; -} - -impl SpecialSecrets for T -where - T: AsRef, -{ - fn is_otp_secret(&self) -> bool { - self.as_ref().starts_with("otpauth://") - } - - fn is_app_secret(&self) -> bool { - self.as_ref().starts_with("$app$") - } -} diff --git a/crates/directory/src/backend/ldap/config.rs b/crates/directory/src/backend/ldap/config.rs index ab9267d9..b219c4da 100644 --- a/crates/directory/src/backend/ldap/config.rs +++ b/crates/directory/src/backend/ldap/config.rs @@ -4,25 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; - +use super::{Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapFilterItem, LdapMappings}; +use crate::{Directory, backend::ldap::AuthBind}; +use deadpool::{Runtime, managed::Pool}; use ldap3::LdapConnSettings; -use store::Store; -use utils::config::{Config, utils::AsKey}; - -use crate::core::config::build_pool; - -use super::{ - AuthBind, Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapFilterItem, LdapMappings, -}; +use registry::schema::structs; impl LdapDirectory { - pub fn from_config(config: &mut Config, prefix: impl AsKey, data_store: Store) -> Option { - let prefix = prefix.as_key(); - let bind_dn = if let Some(dn) = config.value((&prefix, "bind.dn")) { + pub fn open(config: structs::LdapDirectory) -> Result { + let bind_dn = if let Some(dn) = config.bind_dn { Bind::new( - dn.to_string(), - config.value_require((&prefix, "bind.secret"))?.to_string(), + dn, + config.bind_secret.ok_or_else(|| { + "LDAP bind password is required when bind DN is set".to_string() + })?, ) .into() } else { @@ -30,180 +25,127 @@ impl LdapDirectory { }; let manager = LdapConnectionManager::new( - config.value_require((&prefix, "url"))?.to_string(), + config.url, LdapConnSettings::new() - .set_conn_timeout( - config - .property_or_default((&prefix, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)), - ) - .set_starttls( - config - .property_or_default((&prefix, "tls.enable"), "false") - .unwrap_or_default(), - ) - .set_no_tls_verify( - config - .property_or_default((&prefix, "tls.allow-invalid-certs"), "false") - .unwrap_or_default(), - ), + .set_conn_timeout(config.timeout.into_inner()) + .set_starttls(config.use_tls) + .set_no_tls_verify(config.allow_invalid_certs), bind_dn, ); let mut mappings = LdapMappings { - base_dn: config.value_require((&prefix, "base-dn"))?.to_string(), - filter_name: LdapFilter::from_config(config, (&prefix, "filter.name")), - filter_email: LdapFilter::from_config(config, (&prefix, "filter.email")), - attr_name: config - .values((&prefix, "attributes.name")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_groups: config - .values((&prefix, "attributes.groups")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_type: config - .values((&prefix, "attributes.class")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_description: config - .values((&prefix, "attributes.description")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_secret: config - .values((&prefix, "attributes.secret")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_secret_changed: config - .values((&prefix, "attributes.secret-changed")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_email_address: config - .values((&prefix, "attributes.email")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_quota: config - .values((&prefix, "attributes.quota")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attr_email_alias: config - .values((&prefix, "attributes.email-alias")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - attrs_principal: vec!["objectClass".to_lowercase()], + base_dn: config.base_dn, + filter_login: LdapFilter::new(&config.filter_login)?, + filter_mailbox: LdapFilter::new(&config.filter_mailbox)?, + attr_class: config.attr_class, + attr_groups: config.attr_groups, + attr_description: config.attr_description, + attr_secret: config.attr_secret, + attr_secret_changed: config.attr_secret_changed, + attr_email: config.attr_email, + attr_email_alias: config.attr_email_alias, + group_class: config.group_class, + attrs_principal: vec![], }; for attr in [ - &mappings.attr_name, - &mappings.attr_type, &mappings.attr_description, &mappings.attr_secret, &mappings.attr_secret_changed, - &mappings.attr_quota, &mappings.attr_groups, - &mappings.attr_email_address, &mappings.attr_email_alias, + &mappings.attr_email, + &mappings.attr_class, ] { mappings .attrs_principal .extend(attr.iter().filter(|a| !a.is_empty()).cloned()); } - let auth_bind = match config - .value((&prefix, "bind.auth.method")) - .unwrap_or("default") - { - "template" => AuthBind::Template { - template: LdapFilter::from_config(config, (&prefix, "bind.auth.template")), - can_search: config - .property_or_default::((&prefix, "bind.auth.search"), "true") - .unwrap_or(true), - }, - "lookup" => AuthBind::Lookup, - "default" => AuthBind::None, - unknown => { - config.new_parse_error( - (&prefix, "bind.auth.method"), - format!("Unknown LDAP bind method: {unknown}"), - ); - return None; + let auth_bind = match config.password_verification { + structs::LdapPasswordVerification::Local => AuthBind::None, + structs::LdapPasswordVerification::Bind(bind) => { + if let Some(template) = bind.bind_auth_template { + AuthBind::BindTemplate { + template: LdapFilter::new(&template)?, + can_search: bind.bind_auth_search, + } + } else { + AuthBind::Bind + } } }; - Some(LdapDirectory { + let pool = Pool::builder(manager) + .runtime(Runtime::Tokio1) + .max_size(config.pool_max_connections as usize) + .create_timeout(config.pool_timeout_create.into_inner().into()) + .wait_timeout(config.pool_timeout_wait.into_inner().into()) + .recycle_timeout(config.pool_timeout_recycle.into_inner().into()) + .build() + .map_err(|err| format!("Failed to build LDAP pool: {err}"))?; + + Ok(Directory::Ldap(LdapDirectory { mappings, - pool: build_pool(config, &prefix, manager) - .map_err(|e| { - config.new_parse_error(prefix, format!("Failed to build LDAP pool: {e:?}")) - }) - .ok()?, + pool, auth_bind, - data_store, - }) + })) } } impl LdapFilter { - fn from_config(config: &mut Config, key: impl AsKey) -> Self { - if let Some(value) = config.value(key.clone()) { - let mut filter = Vec::new(); - let mut token = String::new(); - let mut value = value.chars(); + fn new(value: &str) -> Result { + let mut filter = Vec::new(); + let mut token = String::new(); + let mut value = value.chars(); - while let Some(ch) = value.next() { - match ch { - '?' => { - // For backwards compatibility, we treat '?' as a placeholder for the full value. - if !token.is_empty() { - filter.push(LdapFilterItem::Static(token)); - token = String::new(); - } - filter.push(LdapFilterItem::Full); + while let Some(ch) = value.next() { + match ch { + '?' => { + // For backwards compatibility, we treat '?' as a placeholder for the full value. + if !token.is_empty() { + filter.push(LdapFilterItem::Static(token)); + token = String::new(); } - '{' => { - if !token.is_empty() { - filter.push(LdapFilterItem::Static(token)); - token = String::new(); - } - for ch in value.by_ref() { - if ch == '}' { - break; - } else { - token.push(ch); - } - } - match token.as_str() { - "user" | "username" | "email" => filter.push(LdapFilterItem::Full), - "local" => filter.push(LdapFilterItem::LocalPart), - "domain" => filter.push(LdapFilterItem::DomainPart), - _ => { - config.new_parse_error( - key, - format!("Unknown LDAP filter placeholder: {}", token), - ); - return Self::default(); - } - } - token.clear(); - } - _ => token.push(ch), + filter.push(LdapFilterItem::Full); } - } - - if !token.is_empty() { - filter.push(LdapFilterItem::Static(token)); - } - - if filter.len() >= 2 { - return LdapFilter { filter }; - } else { - config.new_parse_error( - key, - format!("Missing parameter placeholders in value {:?}", value), - ); + '{' => { + if !token.is_empty() { + filter.push(LdapFilterItem::Static(token)); + token = String::new(); + } + for ch in value.by_ref() { + if ch == '}' { + break; + } else { + token.push(ch); + } + } + match token.as_str() { + "user" | "username" | "email" => filter.push(LdapFilterItem::Full), + "local" => filter.push(LdapFilterItem::LocalPart), + "domain" => filter.push(LdapFilterItem::DomainPart), + _ => { + return Err(format!("Unknown LDAP filter placeholder: {}", token)); + } + } + token.clear(); + } + _ => token.push(ch), } } - Self::default() + if !token.is_empty() { + filter.push(LdapFilterItem::Static(token)); + } + + if filter.len() >= 2 { + Ok(LdapFilter { filter }) + } else { + Err(format!( + "Missing parameter placeholders in value {:?}", + value + )) + } } } diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index b2a5fa1e..3eea4d1e 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -4,223 +4,199 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{AuthBind, LdapDirectory, LdapMappings}; -use crate::{ - IntoError, Principal, PrincipalData, QueryBy, QueryParams, ROLE_ADMIN, ROLE_USER, Type, - backend::{ - RcptType, - internal::{ - SpecialSecrets, - lookup::DirectoryStore, - manage::{self, ManageDirectory, UpdatePrincipal}, - }, - }, -}; +use super::{LdapDirectory, LdapMappings}; +use crate::{Account, Credentials, Group, IntoError, Recipient, backend::ldap::AuthBind}; use ldap3::{Ldap, LdapConnAsync, ResultEntry, Scope, SearchEntry}; -use mail_send::Credentials; use store::xxhash_rust; -use trc::AddContext; +use utils::sanitize_email; impl LdapDirectory { - pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { + pub async fn authenticate(&self, credentials: &Credentials) -> trc::Result> { + let (username, secret) = match credentials { + Credentials::Basic { username, secret } => (username, secret), + Credentials::Bearer { token } => (token, token), + }; let mut conn = self.pool.get().await.map_err(|err| err.into_error())?; - let (mut external_principal, member_of, stored_principal) = match by.by { - QueryBy::Name(username) => { - let filter = self.mappings.filter_name.build(username); - if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { - if result.principal.name.is_empty() { - result.principal.name = username.into(); + + let mut account = match &self.auth_bind { + AuthBind::BindTemplate { + template, + can_search, + } => { + let (auth_bind_conn, mut ldap) = LdapConnAsync::with_settings( + self.pool.manager().settings.clone(), + &self.pool.manager().address, + ) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))?; + + ldap3::drive!(auth_bind_conn); + + let dn = template.build(username); + + if ldap + .simple_bind(&dn, secret) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))? + .success() + .is_err() + { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Secret rejected during auth bind using template", + Details = dn + ); + return Ok(None); + } + + let filter = self.mappings.filter_login.build(username); + let result = if *can_search { + self.find_object(&mut ldap, &filter).await + } else { + self.find_object(&mut conn, &filter).await + }; + + match result { + Ok(Some(mut result)) => { + if result.account.email.is_empty() { + result.account.email = username.into(); + } + result.account.is_authenticated = true; + result.account + } + Err(err) + if err.matches(trc::EventType::Store(trc::StoreEvent::LdapError)) + && err + .value(trc::Key::Code) + .and_then(|v| v.to_uint()) + .is_some_and(|rc| [49, 50].contains(&rc)) => + { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Error codes 49 or 50 returned by LDAP server", + Details = vec![dn, filter] + ); + return Ok(None); + } + Ok(None) => { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Auth bind successful but filter yielded no results", + Details = vec![dn, filter] + ); + + return Ok(None); + } + Err(err) => return Err(err), + } + } + AuthBind::Bind => { + let filter = self.mappings.filter_login.build(username); + if let Some(mut result) = self.find_object(&mut conn, &filter).await? { + // Perform bind auth using the found dn + let (auth_bind_conn, mut ldap) = LdapConnAsync::with_settings( + self.pool.manager().settings.clone(), + &self.pool.manager().address, + ) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))?; + + ldap3::drive!(auth_bind_conn); + + if ldap + .simple_bind(&result.dn, secret) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))? + .success() + .is_ok() + { + if result.account.email.is_empty() { + result.account.email = username.into(); + } + result.account.is_authenticated = true; + result.account + } else { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Secret rejected during auth bind using lookup filter", + Details = vec![result.dn, filter] + ); + return Ok(None); } - (result.principal, result.member_of, None) } else { trc::event!( Store(trc::StoreEvent::LdapWarning), - Reason = "Name filter yielded no results", + Reason = "Auth bind lookup filter yielded no results", Details = filter ); return Ok(None); } } - QueryBy::Id(uid) => { - if let Some(stored_principal_) = self - .data_store - .query(QueryParams::id(uid).with_return_member_of(by.return_member_of)) - .await? - { - if let Some(result) = self - .find_principal( - &mut conn, - &self.mappings.filter_name.build(stored_principal_.name()), - ) - .await? - { - (result.principal, result.member_of, Some(stored_principal_)) - } else { - return Ok(None); - } + AuthBind::None => { + let filter = self.mappings.filter_login.build(username); + if let Some(result) = self.find_object(&mut conn, &filter).await? { + result.account } else { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Authentication filter yielded no results", + Details = filter + ); return Ok(None); } } - QueryBy::Credentials(credentials) => { - let (username, secret) = match credentials { - Credentials::Plain { username, secret } => (username, secret), - Credentials::OAuthBearer { token } => (token, token), - Credentials::XOauth2 { username, secret } => (username, secret), - }; - - match &self.auth_bind { - AuthBind::Template { - template, - can_search, - } => { - let (auth_bind_conn, mut ldap) = LdapConnAsync::with_settings( - self.pool.manager().settings.clone(), - &self.pool.manager().address, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - - ldap3::drive!(auth_bind_conn); - - let dn = template.build(username); - - if ldap - .simple_bind(&dn, secret) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .success() - .is_err() - { - trc::event!( - Store(trc::StoreEvent::LdapWarning), - Reason = "Secret rejected during auth bind using template", - Details = dn - ); - return Ok(None); - } - - let filter = self.mappings.filter_name.build(username); - let result = if *can_search { - self.find_principal(&mut ldap, &filter).await - } else { - self.find_principal(&mut conn, &filter).await - }; - - match result { - Ok(Some(mut result)) => { - if result.principal.name.is_empty() { - result.principal.name = username.into(); - } - (result.principal, result.member_of, None) - } - Err(err) - if err - .matches(trc::EventType::Store(trc::StoreEvent::LdapError)) - && err - .value(trc::Key::Code) - .and_then(|v| v.to_uint()) - .is_some_and(|rc| [49, 50].contains(&rc)) => - { - trc::event!( - Store(trc::StoreEvent::LdapWarning), - Reason = "Error codes 49 or 50 returned by LDAP server", - Details = vec![dn, filter] - ); - return Ok(None); - } - Ok(None) => { - trc::event!( - Store(trc::StoreEvent::LdapWarning), - Reason = "Auth bind successful but filter yielded no results", - Details = vec![dn, filter] - ); - - return Ok(None); - } - Err(err) => return Err(err), - } - } - AuthBind::Lookup => { - let filter = self.mappings.filter_name.build(username); - if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { - // Perform bind auth using the found dn - let (auth_bind_conn, mut ldap) = LdapConnAsync::with_settings( - self.pool.manager().settings.clone(), - &self.pool.manager().address, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - - ldap3::drive!(auth_bind_conn); - - if ldap - .simple_bind(&result.dn, secret) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .success() - .is_ok() - { - if result.principal.name.is_empty() { - result.principal.name = username.into(); - } - (result.principal, result.member_of, None) - } else { - trc::event!( - Store(trc::StoreEvent::LdapWarning), - Reason = "Secret rejected during auth bind using lookup filter", - Details = vec![result.dn, filter] - ); - return Ok(None); - } - } else { - trc::event!( - Store(trc::StoreEvent::LdapWarning), - Reason = "Auth bind lookup filter yielded no results", - Details = filter - ); - return Ok(None); - } - } - AuthBind::None => { - let filter = self.mappings.filter_name.build(username); - if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { - if result.principal.verify_secret(secret, false, false).await? { - if result.principal.name.is_empty() { - result.principal.name = username.into(); - } - (result.principal, result.member_of, None) - } else { - trc::event!( - Store(trc::StoreEvent::LdapWarning), - Reason = "Password verification failed", - Details = vec![result.dn, filter] - ); - return Ok(None); - } - } else { - trc::event!( - Store(trc::StoreEvent::LdapWarning), - Reason = "Authentication filter yielded no results", - Details = filter - ); - return Ok(None); - } - } - } - } }; - // Query groups - if !member_of.is_empty() && by.return_member_of { - for mut name in member_of { - if name.contains('=') { + if !account.groups.is_empty() { + for name in std::mem::take(&mut account.groups) + .into_iter() + .filter(|name| name.contains('=')) + { + let (rs, _res) = conn + .search( + &name, + Scope::Base, + "objectClass=*", + &self.mappings.attr_email, + ) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))? + .success() + .map_err(|err| err.into_error().caused_by(trc::location!()))?; + for entry in rs { + 'outer: for (attr, value) in SearchEntry::construct(entry).attrs { + if self.mappings.attr_email.contains(&attr.to_lowercase()) + && let Some(email) = + value.first().map(|s| s.as_str()).and_then(sanitize_email) + { + account.groups.push(email); + break 'outer; + } + } + } + } + } + + Ok(Some(account)) + } + + pub async fn recipient(&self, address: &str) -> trc::Result { + let mut conn = self.pool.get().await.map_err(|err| err.into_error())?; + let filter = self.mappings.filter_mailbox.build(address); + if let Some(result) = self.find_object(&mut conn, &filter).await? { + let mut account = result.account; + + if !account.groups.is_empty() { + for name in std::mem::take(&mut account.groups) + .into_iter() + .filter(|name| name.contains('=')) + { let (rs, _res) = conn .search( &name, Scope::Base, "objectClass=*", - &self.mappings.attr_name, + &self.mappings.attr_email, ) .await .map_err(|err| err.into_error().caused_by(trc::location!()))? @@ -228,170 +204,39 @@ impl LdapDirectory { .map_err(|err| err.into_error().caused_by(trc::location!()))?; for entry in rs { 'outer: for (attr, value) in SearchEntry::construct(entry).attrs { - if self.mappings.attr_name.contains(&attr.to_lowercase()) - && let Some(group) = value.into_iter().next() - && !group.is_empty() + if self.mappings.attr_email.contains(&attr.to_lowercase()) + && let Some(email) = + value.first().map(|s| s.as_str()).and_then(sanitize_email) { - name = group; + account.groups.push(email); break 'outer; } } } } - - let account_id = self - .data_store - .get_or_create_principal_id(&name, Type::Group) - .await - .caused_by(trc::location!())?; - - external_principal - .data - .push(PrincipalData::MemberOf(account_id)); } - } - - // Obtain account ID if not available - let mut principal = if let Some(stored_principal) = stored_principal { - stored_principal - } else { - let id = self - .data_store - .get_or_create_principal_id(external_principal.name(), Type::Individual) - .await - .caused_by(trc::location!())?; - - self.data_store - .query(QueryParams::id(id).with_return_member_of(by.return_member_of)) - .await - .caused_by(trc::location!())? - .ok_or_else(|| manage::not_found(id).caused_by(trc::location!()))? - }; - - // Keep the internal store up to date with the LDAP server - let changes = principal.update_external(external_principal); - if !changes.is_empty() { - self.data_store - .update_principal( - UpdatePrincipal::by_id(principal.id) - .with_updates(changes) - .create_domains(), - ) - .await - .caused_by(trc::location!())?; - } - - Ok(Some(principal)) - } - - pub async fn email_to_id(&self, address: &str) -> trc::Result> { - let filter = self.mappings.filter_email.build(address.as_ref()); - let rs = self - .pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .search( - &self.mappings.base_dn, - Scope::Subtree, - &filter, - &self.mappings.attr_name, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .success() - .map(|(rs, _res)| rs) - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - - trc::event!( - Store(trc::StoreEvent::LdapQuery), - Details = filter, - Result = rs.iter().map(result_to_trace).collect::>() - ); - - for entry in rs { - for (attr, value) in SearchEntry::construct(entry).attrs { - if self.mappings.attr_name.contains(&attr.to_lowercase()) - && let Some(name) = value.into_iter().find(|name| !name.is_empty()) - { - return self - .data_store - .get_or_create_principal_id(&name, Type::Individual) - .await - .map(Some); - } + if result.is_group { + Ok(Recipient::Group(Group { + email: account.email, + email_aliases: account.email_aliases, + description: account.description, + })) + } else { + Ok(Recipient::Account(account)) } - } - - Ok(None) - } - - pub async fn rcpt(&self, address: &str) -> trc::Result { - let filter = self.mappings.filter_email.build(address.as_ref()); - let result = self - .pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .streaming_search( - &self.mappings.base_dn, - Scope::Subtree, - &filter, - &self.mappings.attr_email_address, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .next() - .await - .map(|entry| { - let result = if entry.is_some() { - RcptType::Mailbox - } else { - RcptType::Invalid - }; - - trc::event!( - Store(trc::StoreEvent::LdapQuery), - Details = filter, - Result = entry.as_ref().map(result_to_trace).unwrap_or_default() - ); - - result - }) - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - - if result != RcptType::Invalid { - Ok(result) } else { - self.data_store.rcpt(address).await.map(|result| { - if matches!(result, RcptType::List(_)) { - result - } else { - RcptType::Invalid - } - }) + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Mailbox filter yielded no results", + Details = filter + ); + Ok(Recipient::Invalid) } } - - pub async fn vrfy(&self, address: &str) -> trc::Result> { - self.data_store.vrfy(address).await - } - - pub async fn expn(&self, address: &str) -> trc::Result> { - self.data_store.expn(address).await - } - - pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - self.data_store.is_local_domain(domain).await - } } impl LdapDirectory { - async fn find_principal( - &self, - conn: &mut Ldap, - filter: &str, - ) -> trc::Result> { + async fn find_object(&self, conn: &mut Ldap, filter: &str) -> trc::Result> { conn.search( &self.mappings.base_dn, Scope::Subtree, @@ -408,10 +253,9 @@ impl LdapDirectory { Result = rs.first().map(result_to_trace).unwrap_or_default() ); - rs.into_iter().next().map(|entry| { - self.mappings - .entry_to_principal(SearchEntry::construct(entry)) - }) + rs.into_iter() + .next() + .map(|entry| self.mappings.map_entry(SearchEntry::construct(entry))) }) .map_err(|err| err.into_error().caused_by(trc::location!())) } @@ -419,129 +263,56 @@ impl LdapDirectory { struct LdapResult { dn: String, - principal: Principal, - member_of: Vec, + account: Account, + is_group: bool, } impl LdapMappings { - fn entry_to_principal(&self, entry: SearchEntry) -> LdapResult { - let mut principal = Principal::new(0, Type::Individual); - let mut role = ROLE_USER; - let mut member_of = vec![]; - let mut description = None; - let mut secret = None; - let mut otp_secret = None; - let mut email = None; - let mut email_aliases = Vec::new(); + fn map_entry(&self, entry: SearchEntry) -> LdapResult { + let mut account = Account::default(); + let mut is_group = false; for (attr, value) in entry.attrs { let attr = attr.to_lowercase(); - if self.attr_name.contains(&attr) { - if !self.attr_email_address.contains(&attr) { - principal.name = value.into_iter().next().unwrap_or_default(); - } else { - for (idx, item) in value.into_iter().enumerate() { - if email.is_none() { - email = Some(item.to_lowercase()); - } - - if idx == 0 { - principal.name = item; - } - } - } + if self.attr_email.contains(&attr) { + account.email = value + .into_iter() + .filter_map(|v| sanitize_email(&v)) + .next() + .unwrap_or_default(); } else if self.attr_secret.contains(&attr) { - for item in value { - if item.is_otp_secret() { - otp_secret = Some(item); - } else if item.is_app_secret() { - principal.data.push(PrincipalData::AppPassword(item)); - } else if secret.is_none() { - secret = Some(item); - } - } + account.secret = value.into_iter().next(); } else if self.attr_secret_changed.contains(&attr) { // Create a disabled AppPassword, used to indicate that the password has been changed // but cannot be used for authentication. - if secret.is_none() { - secret = value.into_iter().next().map(|item| { + if account.secret.is_none() { + account.secret = value.into_iter().next().map(|item| { format!("$app${}$", xxhash_rust::xxh3::xxh3_64(item.as_bytes())) }); } - } else if self.attr_email_address.contains(&attr) { - for item in value { - if email.is_some() { - email_aliases.push(item.to_lowercase()); - } else { - email = Some(item.to_lowercase()); - } - } } else if self.attr_email_alias.contains(&attr) { - for item in value { - email_aliases.push(item.to_lowercase()); + for item in value.into_iter().filter_map(|v| sanitize_email(&v)) { + account.email_aliases.push(item); } } else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) { - if (description.is_none() || idx == 0) + if (account.description.is_none() || idx == 0) && let Some(desc) = value.into_iter().next() { - description = Some(desc); + account.description = Some(desc); } } else if self.attr_groups.contains(&attr) { - member_of.extend(value); - } else if self.attr_quota.contains(&attr) { - if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse::() - && quota > 0 - { - principal.data.push(PrincipalData::DiskQuota(quota)); - } - } else if self.attr_type.contains(&attr) { + account.groups.extend(value); + } else if self.attr_class.contains(&attr) { for value in value { - match value.to_ascii_lowercase().as_str() { - "admin" | "administrator" | "root" | "superuser" => { - role = ROLE_ADMIN; - principal.typ = Type::Individual - } - "posixaccount" | "individual" | "person" | "inetorgperson" => { - principal.typ = Type::Individual - } - "posixgroup" | "groupofuniquenames" | "group" => { - principal.typ = Type::Group - } - _ => continue, - } - break; + is_group |= value.eq_ignore_ascii_case(&self.group_class); } } } - for alias in email_aliases { - if email.as_ref().is_none_or(|email| email != &alias) { - principal.data.push(PrincipalData::EmailAlias(alias)); - } - } - - if let Some(email) = email { - principal.data.push(PrincipalData::PrimaryEmail(email)); - } - - if let Some(secret) = secret { - principal.data.push(PrincipalData::Password(secret)); - } - - if let Some(otp_secret) = otp_secret { - principal.data.push(PrincipalData::OtpAuth(otp_secret)); - } - - if let Some(desc) = description { - principal.data.push(PrincipalData::Description(desc)); - } - - principal.data.push(PrincipalData::Role(role)); - LdapResult { dn: entry.dn, - principal, - member_of, + account, + is_group, } } } diff --git a/crates/directory/src/backend/ldap/mod.rs b/crates/directory/src/backend/ldap/mod.rs index 107922ed..74f2eacc 100644 --- a/crates/directory/src/backend/ldap/mod.rs +++ b/crates/directory/src/backend/ldap/mod.rs @@ -6,34 +6,40 @@ use deadpool::managed::Pool; use ldap3::{LdapConnSettings, ldap_escape}; -use store::Store; pub mod config; pub mod lookup; pub mod pool; +pub(crate) enum AuthBind { + Bind, + BindTemplate { + template: LdapFilter, + can_search: bool, + }, + None, +} + pub struct LdapDirectory { pool: Pool, mappings: LdapMappings, auth_bind: AuthBind, - pub(crate) data_store: Store, } #[derive(Debug, Default)] pub struct LdapMappings { base_dn: String, - filter_name: LdapFilter, - filter_email: LdapFilter, - attr_name: Vec, - attr_type: Vec, + filter_login: LdapFilter, + filter_mailbox: LdapFilter, + attr_class: Vec, attr_groups: Vec, attr_description: Vec, attr_secret: Vec, attr_secret_changed: Vec, - attr_email_address: Vec, + attr_email: Vec, attr_email_alias: Vec, - attr_quota: Vec, attrs_principal: Vec, + group_class: String, } #[derive(Debug, Default)] @@ -103,12 +109,3 @@ impl Bind { Self { dn, password } } } - -pub(crate) enum AuthBind { - Template { - template: LdapFilter, - can_search: bool, - }, - Lookup, - None, -} diff --git a/crates/directory/src/backend/memory/config.rs b/crates/directory/src/backend/memory/config.rs deleted file mode 100644 index 0e2252d3..00000000 --- a/crates/directory/src/backend/memory/config.rs +++ /dev/null @@ -1,158 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use store::Store; -use utils::config::{Config, utils::AsKey}; - -use crate::{ - Principal, PrincipalData, ROLE_ADMIN, ROLE_USER, Type, - backend::internal::manage::ManageDirectory, -}; - -use super::{EmailType, MemoryDirectory}; - -impl MemoryDirectory { - pub async fn from_config( - config: &mut Config, - prefix: impl AsKey, - data_store: Store, - ) -> Option { - let prefix = prefix.as_key(); - let mut directory = MemoryDirectory { - data_store, - principals: Default::default(), - emails_to_ids: Default::default(), - domains: Default::default(), - }; - - for lookup_id in config.sub_keys((prefix.as_str(), "principals"), ".name") { - let lookup_id = lookup_id.as_str(); - let name = config - .value_require((prefix.as_str(), "principals", lookup_id, "name"))? - .to_string(); - let (typ, is_superuser) = - match config.value((prefix.as_str(), "principals", lookup_id, "class")) { - Some("individual") => (Type::Individual, false), - Some("admin") => (Type::Individual, true), - Some("group") => (Type::Group, false), - _ => (Type::Individual, false), - }; - - // Obtain id - let id = directory - .data_store - .get_or_create_principal_id(&name, Type::Individual) - .await - .map_err(|err| { - config.new_build_error( - prefix.as_str(), - format!( - "Failed to obtain id for principal {} ({}): {:?}", - name, lookup_id, err - ), - ) - }) - .ok()?; - - // Create principal - let mut principal = Principal::new(id, typ); - principal.data.push(PrincipalData::Role(if is_superuser { - ROLE_ADMIN - } else { - ROLE_USER - })); - - // Obtain group ids - for group in config - .values((prefix.as_str(), "principals", lookup_id, "member-of")) - .map(|(_, s)| s.to_string()) - .collect::>() - { - principal.data.push(PrincipalData::MemberOf( - directory - .data_store - .get_or_create_principal_id(&group, Type::Group) - .await - .map_err(|err| { - config.new_build_error( - prefix.as_str(), - format!( - "Failed to obtain id for principal {} ({}): {:?}", - name, lookup_id, err - ), - ) - }) - .ok()?, - )); - } - - // Parse email addresses - for (pos, (_, email)) in config - .values((prefix.as_str(), "principals", lookup_id, "email")) - .enumerate() - { - directory - .emails_to_ids - .entry(email.to_string()) - .or_default() - .push(if pos > 0 { - EmailType::Alias(id) - } else { - EmailType::Primary(id) - }); - - if let Some((_, domain)) = email.rsplit_once('@') { - directory.domains.insert(domain.to_lowercase()); - } - - if pos == 0 { - principal - .data - .push(PrincipalData::PrimaryEmail(email.to_lowercase())); - } else { - principal - .data - .push(PrincipalData::EmailAlias(email.to_lowercase())); - } - } - - // Parse mailing lists - for (_, email) in - config.values((prefix.as_str(), "principals", lookup_id, "email-list")) - { - directory - .emails_to_ids - .entry(email.to_lowercase()) - .or_default() - .push(EmailType::List(id)); - if let Some((_, domain)) = email.rsplit_once('@') { - directory.domains.insert(domain.to_lowercase()); - } - } - - principal.name = name.as_str().into(); - for (_, secret) in config.values((prefix.as_str(), "principals", lookup_id, "secret")) { - principal.data.push(PrincipalData::Password(secret.into())); - } - if let Some(description) = - config.value((prefix.as_str(), "principals", lookup_id, "description")) - { - principal - .data - .push(PrincipalData::Description(description.into())); - } - if let Some(quota) = - config.property::((prefix.as_str(), "principals", lookup_id, "quota")) - { - principal.data.push(PrincipalData::DiskQuota(quota)); - } - - directory.principals.push(principal); - } - - Some(directory) - } -} diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs deleted file mode 100644 index 7f10fc36..00000000 --- a/crates/directory/src/backend/memory/lookup.rs +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::{EmailType, MemoryDirectory}; -use crate::{Principal, QueryBy, QueryParams, backend::RcptType}; - -use mail_send::Credentials; - -impl MemoryDirectory { - pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { - match by.by { - QueryBy::Name(name) => { - for principal in &self.principals { - if principal.name() == name { - return Ok(Some(principal.clone())); - } - } - } - QueryBy::Id(uid) => { - for principal in &self.principals { - if principal.id == uid { - return Ok(Some(principal.clone())); - } - } - } - QueryBy::Credentials(credentials) => { - let (username, secret) = match credentials { - Credentials::Plain { username, secret } => (username, secret), - Credentials::OAuthBearer { token } => (token, token), - Credentials::XOauth2 { username, secret } => (username, secret), - }; - - for principal in &self.principals { - if principal.name() == username { - return if principal.verify_secret(secret, false, false).await? { - Ok(Some(principal.clone())) - } else { - Ok(None) - }; - } - } - } - } - Ok(None) - } - - pub async fn email_to_id(&self, address: &str) -> trc::Result> { - Ok(self.emails_to_ids.get(address).and_then(|names| { - names - .iter() - .map(|t| match t { - EmailType::Primary(uid) | EmailType::Alias(uid) | EmailType::List(uid) => *uid, - }) - .next() - })) - } - - pub async fn rcpt(&self, address: &str) -> trc::Result { - Ok(self.emails_to_ids.contains_key(address).into()) - } - - pub async fn vrfy(&self, address: &str) -> trc::Result> { - let mut result = Vec::new(); - for (key, value) in &self.emails_to_ids { - if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) { - result.push(key.into()) - } - } - Ok(result) - } - - pub async fn expn(&self, address: &str) -> trc::Result> { - let mut result = Vec::new(); - for (key, value) in &self.emails_to_ids { - if key == address { - for item in value { - if let EmailType::List(uid) = item { - for principal in &self.principals { - if principal.id == *uid { - if let Some(addr) = principal.primary_email() { - result.push(addr.to_string()) - } - break; - } - } - } - } - } - } - Ok(result) - } - - pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - Ok(self.domains.contains(domain)) - } -} diff --git a/crates/directory/src/backend/memory/mod.rs b/crates/directory/src/backend/memory/mod.rs deleted file mode 100644 index f8bb1604..00000000 --- a/crates/directory/src/backend/memory/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use ahash::{AHashMap, AHashSet}; -use store::Store; - -use crate::Principal; - -pub mod config; -pub mod lookup; - -#[derive(Debug)] -pub struct MemoryDirectory { - principals: Vec, - emails_to_ids: AHashMap>, - pub(crate) data_store: Store, - domains: AHashSet, -} - -#[derive(Debug)] -enum EmailType { - Primary(u32), - Alias(u32), - List(u32), -} diff --git a/crates/directory/src/backend/mod.rs b/crates/directory/src/backend/mod.rs index 4b33986d..050b4c45 100644 --- a/crates/directory/src/backend/mod.rs +++ b/crates/directory/src/backend/mod.rs @@ -4,28 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -pub mod imap; -pub mod internal; pub mod ldap; -pub mod memory; pub mod oidc; -pub mod smtp; pub mod sql; - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub enum RcptType { - Mailbox, - List(Vec), - #[default] - Invalid, -} - -impl From for RcptType { - fn from(value: bool) -> Self { - if value { - RcptType::Mailbox - } else { - RcptType::Invalid - } - } -} diff --git a/crates/directory/src/backend/oidc/config.rs b/crates/directory/src/backend/oidc/config.rs index ce1473b3..ec472292 100644 --- a/crates/directory/src/backend/oidc/config.rs +++ b/crates/directory/src/backend/oidc/config.rs @@ -4,76 +4,44 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; - -use base64::{Engine, engine::general_purpose}; -use store::Store; -use utils::config::{Config, utils::AsKey}; - -use super::{Authentication, EndpointType, OpenIdConfig, OpenIdDirectory}; +use super::OpenIdDirectory; +use crate::Directory; +use registry::schema::structs; impl OpenIdDirectory { - pub fn from_config(config: &mut Config, prefix: impl AsKey, data_store: Store) -> Option { - let prefix = prefix.as_key(); - let endpoint_type = match config.value_require((&prefix, "endpoint.method"))? { - "introspect" => match config.value_require((&prefix, "auth.method"))? { - #[allow(clippy::to_string_in_format_args)] - "basic" => EndpointType::Introspect(Authentication::Header(format!( - "Basic {}", - general_purpose::STANDARD.encode( - format!( - "{}:{}", - config - .value_require((&prefix, "auth.username"))? - .to_string(), - config.value_require((&prefix, "auth.secret"))? - ) - .as_bytes() - ) - ))), - "token" => EndpointType::Introspect(Authentication::Header(format!( - "Bearer {}", - config.value_require((&prefix, "auth.token"))? - ))), - "user-token" => EndpointType::Introspect(Authentication::Bearer), - "none" => EndpointType::Introspect(Authentication::None), - _ => { - config.new_build_error( - (&prefix, "auth.method"), - "Invalid authentication method, must be 'header', 'bearer' or 'none'", - ); - return None; + pub fn open(config: structs::OidcDirectory) -> Result { + Ok(Directory::OpenId(match config { + structs::OidcDirectory::UserInfo(config) => OpenIdDirectory::UserInfo { + endpoint: config.endpoint, + timeout: config.timeout.into_inner(), + allow_invalid_certs: config.allow_invalid_certs, + claim_email: config.claim_email, + claim_name: config.claim_name, + }, + structs::OidcDirectory::Introspect(config) => { + let client = config.http_auth.build_http_client( + config.http_headers, + None, + config.timeout, + config.allow_invalid_certs, + )?; + OpenIdDirectory::Introspect { + client, + endpoint: config.endpoint, + claim_email: config.claim_email, + claim_name: config.claim_name, + require_aud: config.require_audience, + require_scopes: config.require_scopes, } - }, - "userinfo" => EndpointType::UserInfo, - _ => { - config.new_build_error( - (&prefix, "endpoint.method"), - "Invalid endpoint method, must be 'introspect' or 'userinfo'", - ); - return None; } - }; - - let email_field = config.value_require((&prefix, "fields.email"))?.to_string(); - - Some(OpenIdDirectory { - config: OpenIdConfig { - endpoint: config.value_require((&prefix, "endpoint.url"))?.to_string(), - endpoint_type, - endpoint_timeout: config - .property_or_default::((&prefix, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)), - username_field: config - .value((&prefix, "fields.username")) - .filter(|&v| v != email_field) - .map(|v| v.to_string()), - email_field, - full_name_field: config - .value((&prefix, "fields.full-name")) - .map(|v| v.to_string()), + structs::OidcDirectory::Jwt(config) => OpenIdDirectory::Jwt { + jwks_url: config.jwks_url, + jwks_cache: config.jwks_cache_duration.into_inner(), + claim_email: config.claim_email, + claim_name: config.claim_name, + require_aud: config.require_audience, + require_iss: config.require_issuer, }, - data_store, - }) + })) } } diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index 73e49e6d..ea0d6cd5 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -4,41 +4,62 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::OpenIdDirectory; +use crate::{Account, Credentials}; use ahash::HashMap; - -use mail_send::Credentials; -use reqwest::{StatusCode, header::AUTHORIZATION}; -use trc::{AddContext, AuthEvent}; - -use crate::{ - Principal, PrincipalData, QueryBy, QueryParams, ROLE_USER, Type, - backend::{ - RcptType, - internal::{ - lookup::DirectoryStore, - manage::{self, ManageDirectory, UpdatePrincipal}, - }, - oidc::{Authentication, EndpointType}, - }, -}; - -use super::{OpenIdConfig, OpenIdDirectory}; +use reqwest::{RequestBuilder, StatusCode}; +use trc::AuthEvent; +use utils::sanitize_email; type OpenIdResponse = HashMap; impl OpenIdDirectory { - pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { - match &by.by { - QueryBy::Credentials(Credentials::OAuthBearer { token }) => { - // Send request - #[cfg(feature = "test_mode")] - let client = reqwest::Client::builder().danger_accept_invalid_certs(true); + pub async fn authenticate(&self, credentials: &Credentials) -> trc::Result> { + let token = match credentials { + Credentials::Bearer { token } => token, + _ => { + return Err(AuthEvent::Error + .into_err() + .details("Unsupported credentials type for OIDC authentication")); + } + }; + let email; + let name; + let aud; + let iss; + let scopes; - #[cfg(not(feature = "test_mode"))] - let client = reqwest::Client::builder(); + let response = match self { + OpenIdDirectory::Introspect { + client, + endpoint, + claim_email, + claim_name, + require_aud, + require_scopes, + } => { + email = claim_email; + name = claim_name; + aud = require_aud; + scopes = require_scopes.as_slice(); + iss = &None; - let client = client - .timeout(self.config.endpoint_timeout) + send_request(client.post(endpoint).form(&[ + ("token", token.as_str()), + ("token_type_hint", "access_token"), + ])) + .await? + } + OpenIdDirectory::UserInfo { + endpoint, + timeout, + allow_invalid_certs, + claim_email, + claim_name, + } => { + let client = reqwest::Client::builder() + .danger_accept_invalid_certs(*allow_invalid_certs) + .timeout(*timeout) .build() .map_err(|err| { AuthEvent::Error @@ -46,175 +67,141 @@ impl OpenIdDirectory { .reason(err) .details("Failed to build client") })?; + email = claim_email; + name = claim_name; + aud = &None; + iss = &None; + scopes = &[]; + send_request(client.get(endpoint).bearer_auth(token)).await? + } + OpenIdDirectory::Jwt { + jwks_url, + jwks_cache, + claim_email, + claim_name, + require_aud, + require_iss, + } => { + email = claim_email; + name = claim_name; + aud = require_aud; + iss = require_iss; + scopes = &[]; + todo!() + } + }; - let client = match &self.config.endpoint_type { - EndpointType::UserInfo => client.get(&self.config.endpoint).bearer_auth(token), - EndpointType::Introspect(authentication) => { - let client = client.post(&self.config.endpoint).form(&[ - ("token", token.as_str()), - ("token_type_hint", "access_token"), - ]); - match authentication { - Authentication::Header(header) => client.header(AUTHORIZATION, header), - Authentication::Bearer => client.bearer_auth(token), - Authentication::None => client, + let mut account = Account::default(); + let mut aud_matched = aud.is_none(); + let mut iss_matched = iss.is_none(); + let mut scopes_unmatched = scopes.len(); + + for (field, value) in response { + let serde_json::Value::String(value) = value else { + continue; + }; + + if email == &field { + if let Some(sanitized_email) = sanitize_email(&value) { + account.email = sanitized_email; + } + } else if let Some(name_field) = name + && name_field == &field + { + account.description = Some(value); + } else if !aud_matched + && let Some(required_aud) = aud + && field == "aud" + { + if value == *required_aud { + aud_matched = true; + } else { + return Err(AuthEvent::Error + .into_err() + .details("Audience claim does not match")); + } + } else if !iss_matched + && let Some(required_iss) = iss + && field == "iss" + { + if value == *required_iss { + iss_matched = true; + } else { + return Err(AuthEvent::Error + .into_err() + .details("Issuer claim does not match")); + } + } else if scopes_unmatched > 0 && field == "scope" { + for scope in value.split_whitespace() { + if scopes.iter().any(|required_scope| required_scope == &scope) { + scopes_unmatched -= 1; + if scopes_unmatched == 0 { + break; } } - }; - - let response = client.send().await.map_err(|err| { - AuthEvent::Error - .into_err() - .reason(err) - .details("HTTP request failed") - })?; - - match response.status() { - StatusCode::OK => { - // Fetch response - let response = response.bytes().await.map_err(|err| { - AuthEvent::Error - .into_err() - .reason(err) - .details("Failed to read OIDC response") - })?; - - // Deserialize response - let external_principal = - serde_json::from_slice::(&response) - .map_err(|err| { - AuthEvent::Error - .into_err() - .reason(err) - .details("Failed to deserialize OIDC response") - })? - .build_principal(&self.config)?; - - // Fetch principal - let id = self - .data_store - .get_or_create_principal_id(external_principal.name(), Type::Individual) - .await - .caused_by(trc::location!())?; - let mut principal = self - .data_store - .query(QueryParams::id(id).with_return_member_of(by.return_member_of)) - .await - .caused_by(trc::location!())? - .ok_or_else(|| manage::not_found(id).caused_by(trc::location!()))?; - - // Keep the internal store up to date with the OIDC server - let changes = principal.update_external(external_principal); - if !changes.is_empty() { - self.data_store - .update_principal( - UpdatePrincipal::by_id(principal.id) - .with_updates(changes) - .create_domains(), - ) - .await - .caused_by(trc::location!())?; - } - - Ok(Some(principal)) - } - StatusCode::UNAUTHORIZED => Err(trc::AuthEvent::Failed - .into_err() - .code(401) - .details("Unauthorized")), - other => Err(trc::AuthEvent::Error - .into_err() - .code(other.as_u16()) - .ctx(trc::Key::Reason, response.text().await.unwrap_or_default()) - .details("Unexpected status code")), } } - _ => self.data_store.query(by.with_only_app_pass(true)).await, } - } - pub async fn email_to_id(&self, address: &str) -> trc::Result> { - self.data_store.email_to_id(address).await - } - - pub async fn rcpt(&self, address: &str) -> trc::Result { - self.data_store.rcpt(address).await - } - - pub async fn vrfy(&self, address: &str) -> trc::Result> { - self.data_store.vrfy(address).await - } - - pub async fn expn(&self, address: &str) -> trc::Result> { - self.data_store.expn(address).await - } - - pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - self.data_store.is_local_domain(domain).await - } -} - -trait BuildPrincipal { - fn build_principal(&mut self, config: &OpenIdConfig) -> trc::Result; - fn take_required_field(&mut self, field: &str) -> trc::Result; - fn take_field(&mut self, field: &str) -> Option; -} - -impl BuildPrincipal for OpenIdResponse { - fn build_principal(&mut self, config: &OpenIdConfig) -> trc::Result { - let email = self - .take_required_field(&config.email_field)? - .to_lowercase(); - let username = if let Some(username_field) = &config.username_field { - self.take_required_field(username_field)?.to_lowercase() + if !aud_matched { + Err(AuthEvent::Error + .into_err() + .details("Audience claim not found in OIDC response")) + } else if !iss_matched { + Err(AuthEvent::Error + .into_err() + .details("Issuer claim not found in OIDC response")) + } else if scopes_unmatched > 0 { + Err(AuthEvent::Error + .into_err() + .details("One or more required scopes not found in OIDC response")) + } else if !account.email.is_empty() { + account.is_authenticated = true; + Ok(Some(account)) } else { - email.clone() - }; - if !email.contains('@') && !email.contains('.') { - return Err(AuthEvent::Error + Err(trc::AuthEvent::Error .into_err() - .details("Email field is not valid") - .ctx(trc::Key::Key, email)); - } - let full_name = config - .full_name_field - .as_ref() - .and_then(|field| self.take_field(field)); - - // Build principal - let mut data = Vec::with_capacity(3); - data.push(PrincipalData::PrimaryEmail(email)); - if let Some(name) = full_name { - data.push(PrincipalData::Description(name)); - } - data.push(PrincipalData::Role(ROLE_USER)); - Ok(Principal { - id: u32::MAX, - typ: Type::Individual, - name: username, - data, - }) - } - - fn take_required_field(&mut self, field: &str) -> trc::Result { - match self.remove(field) { - Some(serde_json::Value::String(value)) if !value.is_empty() => Ok(value), - other => Err(trc::AuthEvent::Error - .into_err() - .details("Unexpected field type in OIDC response") - .ctx(trc::Key::Key, field.to_string()) - .ctx( - trc::Key::Value, - serde_json::to_string(&other.unwrap_or(serde_json::Value::Null)) - .unwrap_or_default(), - )), - } - } - - fn take_field(&mut self, field: &str) -> Option { - match self.remove(field) { - Some(serde_json::Value::String(value)) if !value.is_empty() => Some(value), - _ => None, + .details("Email claim not found in OIDC response")) } } } + +async fn send_request(request: RequestBuilder) -> trc::Result { + let response = request.send().await.map_err(|err| { + AuthEvent::Error + .into_err() + .reason(err) + .details("OIDC HTTP request failed") + })?; + + match response.status() { + StatusCode::OK => { + // Fetch response + let response = response.bytes().await.map_err(|err| { + AuthEvent::Error + .into_err() + .reason(err) + .details("Failed to read OIDC response") + })?; + + let todo = "deserialize directly into string, not serde_json::Value"; + + // Deserialize response + serde_json::from_slice::(&response).map_err(|err| { + AuthEvent::Error + .into_err() + .reason(err) + .details("Failed to deserialize OIDC response") + }) + } + StatusCode::UNAUTHORIZED => Err(trc::AuthEvent::Failed + .into_err() + .code(401) + .details("Unauthorized")), + other => Err(trc::AuthEvent::Error + .into_err() + .code(other.as_u16()) + .ctx(trc::Key::Reason, response.text().await.unwrap_or_default()) + .details("Unexpected status code")), + } +} diff --git a/crates/directory/src/backend/oidc/mod.rs b/crates/directory/src/backend/oidc/mod.rs index c563bddb..27531bfc 100644 --- a/crates/directory/src/backend/oidc/mod.rs +++ b/crates/directory/src/backend/oidc/mod.rs @@ -9,31 +9,28 @@ pub mod lookup; use std::time::Duration; -use store::Store; - -pub struct OpenIdDirectory { - config: OpenIdConfig, - pub(crate) data_store: Store, -} - -struct OpenIdConfig { - pub endpoint: String, - pub endpoint_type: EndpointType, - pub endpoint_timeout: Duration, - pub email_field: String, - pub username_field: Option, - pub full_name_field: Option, -} - -#[derive(Debug)] -pub enum EndpointType { - Introspect(Authentication), - UserInfo, -} - -#[derive(Debug)] -pub enum Authentication { - Header(String), - Bearer, - None, +pub enum OpenIdDirectory { + Introspect { + client: reqwest::Client, + endpoint: String, + claim_email: String, + claim_name: Option, + require_aud: Option, + require_scopes: Vec, + }, + UserInfo { + endpoint: String, + timeout: Duration, + allow_invalid_certs: bool, + claim_email: String, + claim_name: Option, + }, + Jwt { + jwks_url: String, + jwks_cache: Duration, + claim_email: String, + claim_name: Option, + require_aud: Option, + require_iss: Option, + }, } diff --git a/crates/directory/src/backend/smtp/config.rs b/crates/directory/src/backend/smtp/config.rs deleted file mode 100644 index ac22c709..00000000 --- a/crates/directory/src/backend/smtp/config.rs +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::time::Duration; - -use mail_send::{SmtpClientBuilder, smtp::tls::build_tls_connector}; -use utils::config::{Config, utils::AsKey}; - -use crate::core::config::build_pool; - -use super::{SmtpConnectionManager, SmtpDirectory}; - -impl SmtpDirectory { - pub fn from_config(config: &mut Config, prefix: impl AsKey, is_lmtp: bool) -> Option { - let prefix = prefix.as_key(); - let address = config.value_require((&prefix, "host"))?.to_string(); - let tls_implicit: bool = config - .property_or_default((&prefix, "tls.enable"), "false") - .unwrap_or_default(); - let port: u16 = config - .property_or_default((&prefix, "port"), if tls_implicit { "465" } else { "25" }) - .unwrap_or(if tls_implicit { 465 } else { 25 }); - - let manager = SmtpConnectionManager { - builder: SmtpClientBuilder { - addr: format!("{address}:{port}"), - timeout: config - .property_or_default((&prefix, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)), - tls_connector: build_tls_connector( - config - .property_or_default((&prefix, "tls.allow-invalid-certs"), "false") - .unwrap_or_default(), - ), - tls_hostname: address.to_string(), - tls_implicit, - is_lmtp, - credentials: None, - local_host: config - .value("server.hostname") - .unwrap_or("[127.0.0.1]") - .to_string(), - say_ehlo: false, - local_ip: None, - }, - max_rcpt: config - .property_or_default((&prefix, "limits.rcpt"), "10") - .unwrap_or(10), - max_auth_errors: config - .property_or_default((&prefix, "limits.auth-errors"), "3") - .unwrap_or(10), - }; - - Some(SmtpDirectory { - pool: build_pool(config, &prefix, manager) - .map_err(|e| { - config.new_parse_error( - prefix.as_str(), - format!("Failed to build SMTP pool: {e:?}"), - ) - }) - .ok()?, - domains: config - .values((&prefix, "lookup.domains")) - .map(|(_, v)| v.to_lowercase()) - .collect(), - }) - } -} diff --git a/crates/directory/src/backend/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs deleted file mode 100644 index 0dae263a..00000000 --- a/crates/directory/src/backend/smtp/lookup.rs +++ /dev/null @@ -1,133 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use mail_send::{Credentials, smtp::AssertReply}; -use smtp_proto::Severity; - -use crate::{IntoError, Principal, QueryBy, Type, backend::RcptType}; - -use super::{SmtpClient, SmtpDirectory}; - -impl SmtpDirectory { - pub async fn query(&self, query: QueryBy<'_>) -> trc::Result> { - if let QueryBy::Credentials(credentials) = query { - self.pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .authenticate(credentials) - .await - } else { - Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) - } - } - - pub async fn email_to_id(&self, _address: &str) -> trc::Result> { - Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) - } - - pub async fn rcpt(&self, address: &str) -> trc::Result { - let mut conn = self - .pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - if !conn.sent_mail_from { - conn.client - .cmd(b"MAIL FROM:<>\r\n") - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .assert_positive_completion() - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - conn.sent_mail_from = true; - } - let reply = conn - .client - .cmd(format!("RCPT TO:<{address}>\r\n").as_bytes()) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - match reply.severity() { - Severity::PositiveCompletion => { - conn.num_rcpts += 1; - if conn.num_rcpts >= conn.max_rcpt { - let _ = conn.client.rset().await; - conn.num_rcpts = 0; - conn.sent_mail_from = false; - } - Ok(RcptType::Mailbox) - } - Severity::PermanentNegativeCompletion => Ok(RcptType::Invalid), - _ => Err(trc::StoreEvent::UnexpectedError - .ctx(trc::Key::Code, reply.code()) - .ctx(trc::Key::Details, reply.message)), - } - } - - pub async fn vrfy(&self, address: &str) -> trc::Result> { - self.pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .expand(&format!("VRFY {address}\r\n")) - .await - } - - pub async fn expn(&self, address: &str) -> trc::Result> { - self.pool - .get() - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .expand(&format!("EXPN {address}\r\n")) - .await - } - - pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - Ok(self.domains.contains(domain)) - } -} - -impl SmtpClient { - async fn authenticate( - &mut self, - credentials: &Credentials, - ) -> trc::Result> { - match self - .client - .authenticate(credentials, &self.capabilities) - .await - { - Ok(_) => Ok(Some(Principal::new(u32::MAX, Type::Individual))), - Err(err) => match &err { - mail_send::Error::AuthenticationFailed(err) if err.code() == 535 => { - self.num_auth_failures += 1; - Ok(None) - } - _ => Err(err.into_error()), - }, - } - } - - async fn expand(&mut self, command: &str) -> trc::Result> { - let reply = self - .client - .cmd(command.as_bytes()) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - match reply.code() { - 250 | 251 => Ok(reply - .message() - .split('\n') - .map(|p| p.into()) - .collect::>()), - code @ (550 | 551 | 553 | 500 | 502) => { - Err(trc::StoreEvent::NotSupported.ctx(trc::Key::Code, code)) - } - code => Err(trc::StoreEvent::UnexpectedError - .ctx(trc::Key::Code, code) - .ctx(trc::Key::Details, reply.message)), - } - } -} diff --git a/crates/directory/src/backend/smtp/mod.rs b/crates/directory/src/backend/smtp/mod.rs deleted file mode 100644 index 5b6f15e3..00000000 --- a/crates/directory/src/backend/smtp/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -pub mod config; -pub mod lookup; -pub mod pool; - -use ahash::AHashSet; -use deadpool::managed::Pool; -use mail_send::SmtpClientBuilder; -use smtp_proto::EhloResponse; -use tokio::net::TcpStream; -use tokio_rustls::client::TlsStream; - -pub struct SmtpDirectory { - pool: Pool, - domains: AHashSet, -} - -pub struct SmtpConnectionManager { - builder: SmtpClientBuilder, - max_rcpt: usize, - max_auth_errors: usize, -} - -pub struct SmtpClient { - client: mail_send::SmtpClient>, - capabilities: EhloResponse, - max_rcpt: usize, - max_auth_errors: usize, - num_rcpts: usize, - num_auth_failures: usize, - sent_mail_from: bool, -} diff --git a/crates/directory/src/backend/smtp/pool.rs b/crates/directory/src/backend/smtp/pool.rs deleted file mode 100644 index e80ab5dd..00000000 --- a/crates/directory/src/backend/smtp/pool.rs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use async_trait::async_trait; -use deadpool::managed; -use mail_send::{Error, smtp::AssertReply}; - -use super::{SmtpClient, SmtpConnectionManager}; - -#[async_trait] -impl managed::Manager for SmtpConnectionManager { - type Type = SmtpClient; - type Error = Error; - - async fn create(&self) -> Result { - let mut client = self.builder.connect().await?; - let capabilities = client - .capabilities(&self.builder.local_host, self.builder.is_lmtp) - .await?; - - Ok(SmtpClient { - capabilities, - client, - max_auth_errors: self.max_auth_errors, - max_rcpt: self.max_rcpt, - num_rcpts: 0, - num_auth_failures: 0, - sent_mail_from: false, - }) - } - - async fn recycle( - &self, - conn: &mut SmtpClient, - _: &managed::Metrics, - ) -> managed::RecycleResult { - if conn.num_auth_failures < conn.max_auth_errors { - conn.client - .cmd(b"NOOP\r\n") - .await? - .assert_positive_completion() - .map(|_| ()) - .map_err(managed::RecycleError::Backend) - } else { - Err(managed::RecycleError::Message( - "No longer valid: Too many authentication failures".to_string(), - )) - } - } -} diff --git a/crates/directory/src/backend/sql/config.rs b/crates/directory/src/backend/sql/config.rs index 4c82282d..1f808790 100644 --- a/crates/directory/src/backend/sql/config.rs +++ b/crates/directory/src/backend/sql/config.rs @@ -4,70 +4,59 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use store::{Store, Stores}; -use utils::config::{Config, utils::AsKey}; - use super::{SqlDirectory, SqlMappings}; +use crate::Directory; +use registry::schema::structs; +use store::Store; +#[allow(unreachable_patterns)] impl SqlDirectory { - pub fn from_config( - config: &mut Config, - prefix: impl AsKey, - stores: &Stores, - data_store: Store, - ) -> Option { - let prefix = prefix.as_key(); - let store_id = config.value_require((&prefix, "store"))?.to_string(); - let sql_store = - if let Some(sql_store) = stores.stores.get(&store_id).filter(|store| store.is_sql()) { - sql_store.clone() - } else { - let err = format!("Directory references a non-existent store {store_id:?}"); - config.new_build_error((&prefix, "store"), err); - return None; - }; - - let mut mappings = SqlMappings { - column_description: config - .value((&prefix, "columns.description")) - .unwrap_or_default() - .to_string(), - column_secret: config - .value((&prefix, "columns.secret")) - .unwrap_or_default() - .to_string(), - column_email: config - .value((&prefix, "columns.email")) - .unwrap_or_default() - .to_string(), - column_quota: config - .value((&prefix, "columns.quota")) - .unwrap_or_default() - .to_string(), - column_type: config - .value((&prefix, "columns.class")) - .unwrap_or_default() - .to_string(), - ..Default::default() + pub async fn open( + config: structs::SqlDirectory, + data_store: &Store, + ) -> Result { + let sql_store = match config.store { + #[cfg(feature = "postgres")] + structs::SqlAuthStore::PostgreSql(store) => { + store::backend::postgres::PostgresStore::open(store).await? + } + #[cfg(feature = "mysql")] + structs::SqlAuthStore::MySql(store) => { + store::backend::mysql::MysqlStore::open(store).await? + } + #[cfg(feature = "sqlite")] + structs::SqlAuthStore::Sqlite(store) => { + store::backend::sqlite::SqliteStore::open(store)? + } + structs::SqlAuthStore::Default => { + if data_store.is_sql() { + data_store.clone() + } else { + return Err("The configured data store is not compatible with the SQL directory backend.".to_string()); + } + } + _ => { + return Err( + "Binary not compiled with support for the selected SQL directory backend." + .to_string(), + ); + } }; - for (query_id, query) in [ - ("name", &mut mappings.query_name), - ("members", &mut mappings.query_members), - ("emails", &mut mappings.query_emails), - ("recipients", &mut mappings.query_recipients), - ("secrets", &mut mappings.query_secrets), - ] { - *query = config - .value(("store", store_id.as_str(), "query", query_id)) - .unwrap_or_default() - .to_string(); - } + let mappings = SqlMappings { + query_login: config.query_login, + query_recipient: config.query_recipient, + query_member_of: config.query_member_of, + query_email_aliases: config.query_email_aliases, + column_email: config.column_email, + column_secret: config.column_secret, + column_type: config.column_class, + column_description: config.column_description, + }; - Some(SqlDirectory { + Ok(Directory::Sql(SqlDirectory { sql_store, mappings, - data_store, - }) + })) } } diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index 61925106..8b0647b9 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -5,351 +5,159 @@ */ use super::{SqlDirectory, SqlMappings}; -use crate::{ - Principal, PrincipalData, QueryBy, QueryParams, ROLE_ADMIN, ROLE_USER, Type, - backend::{ - RcptType, - internal::{ - SpecialSecrets, - lookup::DirectoryStore, - manage::{self, ManageDirectory, UpdatePrincipal}, - }, - }, -}; -use mail_send::Credentials; +use crate::{Account, Credentials, Recipient}; use store::{NamedRows, Rows, Value}; use trc::AddContext; +use utils::sanitize_email; impl SqlDirectory { - pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { - let (external_principal, stored_principal) = match by.by { - QueryBy::Name(username) => ( - self.mappings - .row_to_principal( - self.sql_store - .sql_query::( - &self.mappings.query_name, - vec![username.into()], - ) - .await - .caused_by(trc::location!())?, - ) - .caused_by(trc::location!())? - .map(|mut p| { - p.name = username.into(); - p - }), - None, - ), - QueryBy::Id(uid) => { - if let Some(principal) = self - .data_store - .query(QueryParams::id(uid).with_return_member_of(by.return_member_of)) - .await - .caused_by(trc::location!())? - { - ( - self.mappings - .row_to_principal( - self.sql_store - .sql_query::( - &self.mappings.query_name, - vec![principal.name().into()], - ) - .await - .caused_by(trc::location!())?, - ) - .caused_by(trc::location!())?, - Some(principal), - ) - } else { - return Ok(None); - } - } - QueryBy::Credentials(credentials) => { - let (username, secret) = match credentials { - Credentials::Plain { username, secret } => (username, secret), - Credentials::OAuthBearer { token } => (token, token), - Credentials::XOauth2 { username, secret } => (username, secret), - }; - - match self - .mappings - .row_to_principal( - self.sql_store - .sql_query::( - &self.mappings.query_name, - vec![username.into()], - ) - .await - .caused_by(trc::location!())?, - ) - .caused_by(trc::location!())? - { - Some(mut principal) => { - // Obtain secrets - if !self.mappings.query_secrets.is_empty() { - let secrets = self - .sql_store - .sql_query::( - &self.mappings.query_secrets, - vec![username.into()], - ) - .await - .caused_by(trc::location!())?; - - for row in secrets.rows { - for value in row.values { - if let Value::Text(secret) = value { - let secret = secret.into_owned(); - - if secret.is_otp_secret() { - if !principal.data.iter().any(|data| { - matches!(data, PrincipalData::OtpAuth(_)) - }) { - principal.data.push(PrincipalData::OtpAuth(secret)); - } - } else if secret.is_app_secret() { - principal.data.push(PrincipalData::AppPassword(secret)); - } else if !principal - .data - .iter() - .any(|data| matches!(data, PrincipalData::Password(_))) - { - principal.data.push(PrincipalData::Password(secret)); - } - } - } - } - } - - if principal - .verify_secret(secret, false, false) - .await - .caused_by(trc::location!())? - { - principal.name = username.into(); - (Some(principal), None) - } else { - (None, None) - } - } - - _ => (None, None), - } - } + pub async fn authenticate(&self, credentials: &Credentials) -> trc::Result> { + let username = match credentials { + Credentials::Basic { username, .. } => username, + Credentials::Bearer { token } => token, }; - let mut external_principal = if let Some(external_principal) = external_principal { - external_principal - } else { + let Recipient::Account(mut account) = self.mappings.row_to_account( + self.sql_store + .sql_query::(&self.mappings.query_login, vec![username.into()]) + .await + .caused_by(trc::location!())?, + ) else { return Ok(None); }; // Obtain members - if by.return_member_of && !self.mappings.query_members.is_empty() { + if let Some(query) = &self.mappings.query_member_of { for row in self .sql_store - .sql_query::( - &self.mappings.query_members, - vec![external_principal.name().into()], - ) + .sql_query::(query, vec![username.into()]) .await .caused_by(trc::location!())? .rows { - if let Some(Value::Text(account_name)) = row.values.first() { - let account_id = self - .data_store - .get_or_create_principal_id(account_name, Type::Group) - .await - .caused_by(trc::location!())?; - external_principal - .data - .push(PrincipalData::MemberOf(account_id)); + if let Some(Value::Text(address)) = row.values.first() + && let Some(email) = sanitize_email(address) + { + account.groups.push(email); } } } // Obtain emails - if !self.mappings.query_emails.is_empty() { - let mut rows = self - .sql_store - .sql_query::( - &self.mappings.query_emails, - vec![external_principal.name().into()], - ) - .await - .caused_by(trc::location!())? - .rows - .into_iter() - .flat_map(|v| v.values.into_iter().map(|v| v.into_lower_string())); - - if external_principal.primary_email().is_none() - && let Some(email) = rows.next() - { - external_principal - .data - .push(PrincipalData::PrimaryEmail(email)); - } - - external_principal - .data - .extend(rows.map(PrincipalData::EmailAlias)); - } - - // Obtain account ID if not available - let mut principal = if let Some(stored_principal) = stored_principal { - stored_principal - } else { - let id = self - .data_store - .get_or_create_principal_id(external_principal.name(), Type::Individual) - .await - .caused_by(trc::location!())?; - - self.data_store - .query(QueryParams::id(id).with_return_member_of(by.return_member_of)) - .await - .caused_by(trc::location!())? - .ok_or_else(|| manage::not_found(id).caused_by(trc::location!()))? - }; - - // Keep the internal store up to date with the SQL server - let changes = principal.update_external(external_principal); - if !changes.is_empty() { - self.data_store - .update_principal( - UpdatePrincipal::by_id(principal.id) - .with_updates(changes) - .create_domains(), - ) - .await - .caused_by(trc::location!())?; - } - - Ok(Some(principal)) - } - - pub async fn email_to_id(&self, address: &str) -> trc::Result> { - let names = self - .sql_store - .sql_query::(&self.mappings.query_recipients, vec![address.into()]) - .await - .caused_by(trc::location!())?; - - for row in names.rows { - if let Some(Value::Text(name)) = row.values.first() { - return self - .data_store - .get_or_create_principal_id(name, Type::Individual) + if let Some(query) = &self.mappings.query_email_aliases { + account.email_aliases.extend( + self.sql_store + .sql_query::(query, vec![username.into()]) .await - .caused_by(trc::location!()) - .map(Some); - } + .caused_by(trc::location!())? + .rows + .into_iter() + .flat_map(|v| { + v.values + .into_iter() + .filter_map(|v| sanitize_email(v.to_str().as_ref())) + }), + ); } - Ok(None) + Ok(Some(account)) } - pub async fn rcpt(&self, address: &str) -> trc::Result { - let result = self - .sql_store - .sql_query::( - &self.mappings.query_recipients, - vec![address.to_string().into()], - ) - .await?; + pub async fn recipient(&self, address: &str) -> trc::Result { + let recipient = self.mappings.row_to_account( + self.sql_store + .sql_query::(&self.mappings.query_recipient, vec![address.into()]) + .await + .caused_by(trc::location!())?, + ); - if result { - Ok(RcptType::Mailbox) - } else { - self.data_store.rcpt(address).await.map(|result| { - if matches!(result, RcptType::List(_)) { - result - } else { - RcptType::Invalid + match recipient { + Recipient::Account(mut account) => { + // Obtain members + if let Some(query) = &self.mappings.query_member_of { + for row in self + .sql_store + .sql_query::(query, vec![account.email.as_str().into()]) + .await + .caused_by(trc::location!())? + .rows + { + if let Some(Value::Text(address)) = row.values.first() + && let Some(email) = sanitize_email(address) + { + account.groups.push(email); + } + } } - }) + + // Obtain emails + if let Some(query) = &self.mappings.query_email_aliases { + account.email_aliases.extend( + self.sql_store + .sql_query::(query, vec![account.email.as_str().into()]) + .await + .caused_by(trc::location!())? + .rows + .into_iter() + .flat_map(|v| { + v.values + .into_iter() + .filter_map(|v| sanitize_email(v.to_str().as_ref())) + }), + ); + } + + Ok(Recipient::Account(account)) + } + Recipient::Group(group) => Ok(Recipient::Group(group)), + Recipient::Invalid => Ok(Recipient::Invalid), } } - - pub async fn vrfy(&self, address: &str) -> trc::Result> { - self.data_store.vrfy(address).await - } - - pub async fn expn(&self, address: &str) -> trc::Result> { - self.data_store.expn(address).await - } - - pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - self.data_store.is_local_domain(domain).await - } } impl SqlMappings { - pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result> { + pub fn row_to_account(&self, rows: NamedRows) -> Recipient { if rows.rows.is_empty() { - return Ok(None); + return Recipient::Invalid; } - let mut principal = Principal::new(u32::MAX, Type::Individual); - let mut role = ROLE_USER; - let mut has_primary_email = false; - let mut secret = None; + let mut account = Account::default(); + let mut is_group = false; if let Some(row) = rows.rows.into_iter().next() { for (name, value) in rows.names.into_iter().zip(row.values) { - if name.eq_ignore_ascii_case(&self.column_secret) { + if name.eq_ignore_ascii_case(&self.column_email) { + if let Value::Text(text) = value + && let Some(email) = sanitize_email(&text) + { + account.email = email; + } + } else if name.eq_ignore_ascii_case(&self.column_secret) { if let Value::Text(text) = value { - secret = Some(text.into_owned()); + account.secret = Some(text.into_owned()); } - } else if name.eq_ignore_ascii_case(&self.column_type) { - match value.to_str().as_ref() { - "individual" | "person" | "user" => { - principal.typ = Type::Individual; - } - "group" => principal.typ = Type::Group, - "admin" | "superuser" | "administrator" => { - principal.typ = Type::Individual; - role = ROLE_ADMIN; - } - _ => (), - } - } else if name.eq_ignore_ascii_case(&self.column_description) { - if let Value::Text(text) = value { - principal - .data - .push(PrincipalData::Description(text.as_ref().into())); - } - } else if name.eq_ignore_ascii_case(&self.column_email) { - if let Value::Text(text) = value { - if !has_primary_email { - has_primary_email = true; - principal - .data - .push(PrincipalData::PrimaryEmail(text.to_lowercase())); - } else { - principal - .data - .push(PrincipalData::EmailAlias(text.to_lowercase())); - } - } - } else if name.eq_ignore_ascii_case(&self.column_quota) - && let Value::Integer(quota) = value - && quota > 0 + } else if let Some(column_type) = &self.column_type + && name.eq_ignore_ascii_case(column_type) { - principal.data.push(PrincipalData::DiskQuota(quota as u64)); + is_group = value.to_str().eq_ignore_ascii_case("group"); + } else if let Some(column_description) = &self.column_description + && name.eq_ignore_ascii_case(column_description) + { + if let Value::Text(text) = value { + account.description = Some(text.into_owned()); + } } } } - if let Some(secret) = secret { - principal.data.push(PrincipalData::Password(secret)); + if !is_group { + Recipient::Account(account) + } else { + Recipient::Group(crate::Group { + email: account.email, + email_aliases: account.email_aliases, + description: account.description, + }) } - - principal.data.push(PrincipalData::Role(role)); - - Ok(Some(principal)) } } diff --git a/crates/directory/src/backend/sql/mod.rs b/crates/directory/src/backend/sql/mod.rs index 0615c070..816f9d22 100644 --- a/crates/directory/src/backend/sql/mod.rs +++ b/crates/directory/src/backend/sql/mod.rs @@ -12,19 +12,16 @@ pub mod lookup; pub struct SqlDirectory { sql_store: Store, mappings: SqlMappings, - pub(crate) data_store: Store, } #[derive(Debug, Default)] pub(crate) struct SqlMappings { - query_name: String, - query_members: String, - query_emails: String, - query_recipients: String, - query_secrets: String, - column_description: String, - column_secret: String, + query_login: String, + query_recipient: String, + query_member_of: Option, + query_email_aliases: Option, column_email: String, - column_quota: String, - column_type: String, + column_secret: String, + column_type: Option, + column_description: Option, } diff --git a/crates/directory/src/core/cache.rs b/crates/directory/src/core/cache.rs deleted file mode 100644 index 9b29b389..00000000 --- a/crates/directory/src/core/cache.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::time::Duration; - -use utils::{ - cache::CacheWithTtl, - config::{Config, utils::AsKey}, -}; - -use crate::backend::RcptType; - -pub struct CachedDirectory { - cached_domains: CacheWithTtl, - cached_rcpts: CacheWithTtl, - ttl_pos: Duration, - ttl_neg: Duration, -} - -impl CachedDirectory { - pub fn try_from_config(config: &mut Config, prefix: impl AsKey) -> Option { - let prefix = prefix.as_key(); - let cached_size = config - .property_or_default::>((&prefix, "cache.size"), "1048576") - .unwrap_or_default()?; - - Some(CachedDirectory { - cached_domains: CacheWithTtl::new(50, cached_size), - cached_rcpts: CacheWithTtl::new(100, cached_size), - ttl_pos: config - .property((&prefix, "cache.ttl.positive")) - .unwrap_or(Duration::from_secs(86400)), - ttl_neg: config - .property((&prefix, "cache.ttl.negative")) - .unwrap_or_else(|| Duration::from_secs(3600)), - }) - } - - pub fn get_rcpt(&self, address: &str) -> Option { - self.cached_rcpts.get(address).map(Into::into) - } - - pub fn set_rcpt(&self, address: &str, exists: &RcptType) { - let (exists, ttl) = match exists { - RcptType::Mailbox => (true, self.ttl_pos), - RcptType::Invalid => (false, self.ttl_neg), - RcptType::List(_) => return, - }; - - self.cached_rcpts.insert(address.to_string(), exists, ttl); - } - - pub fn get_domain(&self, domain: &str) -> Option { - self.cached_domains.get(domain) - } - - pub fn set_domain(&self, domain: &str, exists: bool) { - self.cached_domains.insert( - domain.to_string(), - exists, - if exists { self.ttl_pos } else { self.ttl_neg }, - ); - } -} diff --git a/crates/directory/src/core/config.rs b/crates/directory/src/core/config.rs index 5203ccda..68074ec7 100644 --- a/crates/directory/src/core/config.rs +++ b/crates/directory/src/core/config.rs @@ -4,145 +4,39 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use deadpool::{ - Runtime, - managed::{Manager, Pool}, -}; -use std::{sync::Arc, time::Duration}; -use store::{Store, Stores}; -use utils::config::Config; - -use ahash::AHashMap; - use crate::{ - Directories, Directory, DirectoryInner, - backend::{ - imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, oidc::OpenIdDirectory, - smtp::SmtpDirectory, sql::SqlDirectory, - }, + Directories, + backend::{ldap::LdapDirectory, oidc::OpenIdDirectory, sql::SqlDirectory}, }; - -use super::cache::CachedDirectory; +use ahash::AHashMap; +use registry::schema::structs; +use std::sync::Arc; +use store::registry::bootstrap::Bootstrap; impl Directories { - pub async fn parse( - config: &mut Config, - stores: &Stores, - data_store: Store, - is_enterprise: bool, - ) -> Self { + pub async fn build(bp: &mut Bootstrap) -> Self { let mut directories = AHashMap::new(); - for id in config.sub_keys("directory", ".type") { - // Parse directory - let id = id.as_str(); - #[cfg(feature = "test_mode")] - { - if config - .property_or_default::(("directory", id, "disable"), "false") - .unwrap_or(false) - { - continue; - } - } - let protocol = config - .value_require(("directory", id, "type")) - .unwrap() - .to_string(); - let prefix = ("directory", id); - let store = match protocol.as_str() { - "internal" => Some(DirectoryInner::Internal( - if let Some(store_id) = config.value_require(("directory", id, "store")) { - if let Some(data) = stores.stores.get(store_id) { - data.clone() - } else { - config.new_parse_error( - ("directory", id, "store"), - "Store does not exist", - ); - continue; - } - } else { - continue; - }, - )), - "ldap" => LdapDirectory::from_config(config, prefix, data_store.clone()) - .map(DirectoryInner::Ldap), - "sql" => SqlDirectory::from_config(config, prefix, stores, data_store.clone()) - .map(DirectoryInner::Sql), - "imap" => ImapDirectory::from_config(config, prefix).map(DirectoryInner::Imap), - "smtp" => { - SmtpDirectory::from_config(config, prefix, false).map(DirectoryInner::Smtp) - } - "lmtp" => { - SmtpDirectory::from_config(config, prefix, true).map(DirectoryInner::Smtp) - } - "memory" => MemoryDirectory::from_config(config, prefix, data_store.clone()) - .await - .map(DirectoryInner::Memory), - "oidc" => OpenIdDirectory::from_config(config, prefix, data_store.clone()) - .map(DirectoryInner::OpenId), - unknown => { - let err = format!("Unknown directory type: {unknown:?}"); - config.new_parse_error(("directory", id, "type"), err); - continue; + for directory in bp.list_infallible::().await { + let id = directory.id; + let result = match directory.object { + structs::Directory::Ldap(directory) => LdapDirectory::open(directory), + structs::Directory::Sql(directory) => { + SqlDirectory::open(directory, &bp.data_store).await } + structs::Directory::Oidc(directory) => OpenIdDirectory::open(directory), }; - // Build directory - if let Some(store) = store { - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - if store.is_enterprise_directory() && !is_enterprise { - let message = - format!("Directory {protocol:?} is an Enterprise Edition feature"); - config.new_parse_error(("directory", id, "type"), message); - continue; + match result { + Ok(directory) => { + directories.insert(id, Arc::new(directory)); + } + Err(err) => { + bp.build_error(id, err); } - // SPDX-SnippetEnd - - let directory = Arc::new(Directory { - store, - cache: CachedDirectory::try_from_config(config, ("directory", id)), - }); - - // Add directory - directories.insert(id.to_string(), directory); } } Directories { directories } } } - -pub(crate) fn build_pool( - config: &mut Config, - prefix: &str, - manager: M, -) -> Result, String> { - Pool::builder(manager) - .runtime(Runtime::Tokio1) - .max_size( - config - .property_or_default((prefix, "pool.max-connections"), "10") - .unwrap_or(10), - ) - .create_timeout( - config - .property_or_default::((prefix, "pool.timeout.create"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)) - .into(), - ) - .wait_timeout(config.property_or_default((prefix, "pool.timeout.wait"), "30s")) - .recycle_timeout(config.property_or_default((prefix, "pool.timeout.recycle"), "30s")) - .build() - .map_err(|err| { - format!( - "Failed to build pool for {prefix:?}: {err}", - prefix = prefix, - err = err - ) - }) -} diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs index f0dd92f7..05bfa769 100644 --- a/crates/directory/src/core/dispatch.rs +++ b/crates/directory/src/core/dispatch.rs @@ -4,135 +4,33 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{Account, Credentials, Directory, Recipient}; use trc::AddContext; -use crate::{ - Directory, DirectoryInner, Principal, QueryParams, - backend::{RcptType, internal::lookup::DirectoryStore}, -}; - impl Directory { - pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { - match &self.store { - DirectoryInner::Internal(store) => store.query(by).await, - DirectoryInner::Ldap(store) => store.query(by).await, - DirectoryInner::Sql(store) => store.query(by).await, - DirectoryInner::Imap(store) => store.query(by.by).await, - DirectoryInner::Smtp(store) => store.query(by.by).await, - DirectoryInner::Memory(store) => store.query(by).await, - DirectoryInner::OpenId(store) => store.query(by).await, + pub async fn authenticate(&self, credentials: &Credentials) -> trc::Result> { + match &self { + Directory::Ldap(store) => store.authenticate(credentials).await, + Directory::Sql(store) => store.authenticate(credentials).await, + Directory::OpenId(store) => store.authenticate(credentials).await, } .caused_by(trc::location!()) } - pub async fn email_to_id(&self, address: &str) -> trc::Result> { - match &self.store { - DirectoryInner::Internal(store) => store.email_to_id(address).await, - DirectoryInner::Ldap(store) => store.email_to_id(address).await, - DirectoryInner::Sql(store) => store.email_to_id(address).await, - DirectoryInner::Imap(store) => store.email_to_id(address).await, - DirectoryInner::Smtp(store) => store.email_to_id(address).await, - DirectoryInner::Memory(store) => store.email_to_id(address).await, - DirectoryInner::OpenId(store) => store.email_to_id(address).await, - } - .caused_by(trc::location!()) - } - - pub async fn is_local_domain(&self, domain: &str) -> trc::Result { - // Check cache - if let Some(cache) = &self.cache - && let Some(result) = cache.get_domain(domain) - { - return Ok(result); - } - - let result = match &self.store { - DirectoryInner::Internal(store) => store.is_local_domain(domain).await, - DirectoryInner::Ldap(store) => store.is_local_domain(domain).await, - DirectoryInner::Sql(store) => store.is_local_domain(domain).await, - DirectoryInner::Imap(store) => store.is_local_domain(domain).await, - DirectoryInner::Smtp(store) => store.is_local_domain(domain).await, - DirectoryInner::Memory(store) => store.is_local_domain(domain).await, - DirectoryInner::OpenId(store) => store.is_local_domain(domain).await, - } - .caused_by(trc::location!())?; - - // Update cache - if let Some(cache) = &self.cache { - cache.set_domain(domain, result); - } - - Ok(result) - } - - pub async fn rcpt(&self, email: &str) -> trc::Result { - // Check cache - if let Some(cache) = &self.cache - && let Some(result) = cache.get_rcpt(email) - { - return Ok(result); - } - - let result = match &self.store { - DirectoryInner::Internal(store) => store.rcpt(email).await, - DirectoryInner::Ldap(store) => store.rcpt(email).await, - DirectoryInner::Sql(store) => store.rcpt(email).await, - DirectoryInner::Imap(store) => store.rcpt(email).await, - DirectoryInner::Smtp(store) => store.rcpt(email).await, - DirectoryInner::Memory(store) => store.rcpt(email).await, - DirectoryInner::OpenId(store) => store.rcpt(email).await, - } - .caused_by(trc::location!())?; - - // Update cache - if let Some(cache) = &self.cache { - cache.set_rcpt(email, &result); - } - - Ok(result) - } - - pub async fn vrfy(&self, address: &str) -> trc::Result> { - match &self.store { - DirectoryInner::Internal(store) => store.vrfy(address).await, - DirectoryInner::Ldap(store) => store.vrfy(address).await, - DirectoryInner::Sql(store) => store.vrfy(address).await, - DirectoryInner::Imap(store) => store.vrfy(address).await, - DirectoryInner::Smtp(store) => store.vrfy(address).await, - DirectoryInner::Memory(store) => store.vrfy(address).await, - DirectoryInner::OpenId(store) => store.vrfy(address).await, - } - .caused_by(trc::location!()) - } - - pub async fn expn(&self, address: &str) -> trc::Result> { - match &self.store { - DirectoryInner::Internal(store) => store.expn(address).await, - DirectoryInner::Ldap(store) => store.expn(address).await, - DirectoryInner::Sql(store) => store.expn(address).await, - DirectoryInner::Imap(store) => store.expn(address).await, - DirectoryInner::Smtp(store) => store.expn(address).await, - DirectoryInner::Memory(store) => store.expn(address).await, - DirectoryInner::OpenId(store) => store.expn(address).await, + pub async fn recipient(&self, address: &str) -> trc::Result { + match &self { + Directory::Ldap(store) => store.recipient(address).await, + Directory::Sql(store) => store.recipient(address).await, + Directory::OpenId(_) => Ok(Recipient::Invalid), // OIDC directories do not support recipient lookups } .caused_by(trc::location!()) } pub fn has_bearer_token_support(&self) -> bool { - match &self.store { - DirectoryInner::Internal(_) - | DirectoryInner::Ldap(_) - | DirectoryInner::Sql(_) - | DirectoryInner::Imap(_) - | DirectoryInner::Smtp(_) - | DirectoryInner::Memory(_) => false, - DirectoryInner::OpenId(_) => true, - } + matches!(self, Directory::OpenId(_)) } -} -impl DirectoryInner { - pub fn is_enterprise_directory(&self) -> bool { - false + pub fn can_lookup_recipients(&self) -> bool { + !matches!(self, Directory::OpenId(_)) } } diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs index 905b003f..8e9fa4b3 100644 --- a/crates/directory/src/core/mod.rs +++ b/crates/directory/src/core/mod.rs @@ -4,354 +4,5 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::Permission; - -pub mod cache; pub mod config; pub mod dispatch; -pub mod principal; -pub mod secret; - -impl Permission { - pub fn description(&self) -> &'static str { - match self { - Permission::Impersonate => "Act on behalf of another user", - Permission::UnlimitedRequests => "Perform unlimited requests", - Permission::UnlimitedUploads => "Upload unlimited data", - Permission::DeleteSystemFolders_ => "", - Permission::MessageQueueList => "View message queue", - Permission::MessageQueueGet => "Retrieve specific messages from the queue", - Permission::MessageQueueUpdate => "Modify queued messages", - Permission::MessageQueueDelete => "Remove messages from the queue", - Permission::OutgoingReportList => "View outgoing DMARC and TLS reports", - Permission::OutgoingReportGet => "Retrieve specific outgoing DMARC and TLS reports", - Permission::OutgoingReportDelete => "Remove outgoing DMARC and TLS reports", - Permission::IncomingReportList => "View incoming DMARC, TLS and ARF reports", - Permission::IncomingReportGet => { - "Retrieve specific incoming DMARC, TLS and ARF reports" - } - Permission::IncomingReportDelete => "Remove incoming DMARC, TLS and ARF reports", - Permission::SettingsList => "View system settings", - Permission::SettingsUpdate => "Modify system settings", - Permission::SettingsDelete => "Remove system settings", - Permission::SettingsReload => "Refresh system settings", - Permission::IndividualList => "View list of user accounts", - Permission::IndividualGet => "Retrieve specific account information", - Permission::IndividualUpdate => "Modify user account information", - Permission::IndividualDelete => "Remove user accounts", - Permission::IndividualCreate => "Add new user accounts", - Permission::GroupList => "View list of user groups", - Permission::GroupGet => "Retrieve specific group information", - Permission::GroupUpdate => "Modify group information", - Permission::GroupDelete => "Remove user groups", - Permission::GroupCreate => "Add new user groups", - Permission::DomainList => "View list of email domains", - Permission::DomainGet => "Retrieve specific domain information", - Permission::DomainCreate => "Add new email domains", - Permission::DomainUpdate => "Modify domain information", - Permission::DomainDelete => "Remove email domains", - Permission::TenantList => "View list of tenants", - Permission::TenantGet => "Retrieve specific tenant information", - Permission::TenantCreate => "Add new tenants", - Permission::TenantUpdate => "Modify tenant information", - Permission::TenantDelete => "Remove tenants", - Permission::MailingListList => "View list of mailing lists", - Permission::MailingListGet => "Retrieve specific mailing list information", - Permission::MailingListCreate => "Create new mailing lists", - Permission::MailingListUpdate => "Modify mailing list information", - Permission::MailingListDelete => "Remove mailing lists", - Permission::RoleList => "View list of roles", - Permission::RoleGet => "Retrieve specific role information", - Permission::RoleCreate => "Create new roles", - Permission::RoleUpdate => "Modify role information", - Permission::RoleDelete => "Remove roles", - Permission::PrincipalList => "View list of principals", - Permission::PrincipalGet => "Retrieve specific principal information", - Permission::PrincipalCreate => "Create new principals", - Permission::PrincipalUpdate => "Modify principal information", - Permission::PrincipalDelete => "Remove principals", - Permission::BlobFetch => "Retrieve arbitrary blobs", - Permission::PurgeBlobStore => "Purge the blob storage", - Permission::PurgeDataStore => "Purge the data storage", - Permission::PurgeInMemoryStore => "Purge the in-memory storage", - Permission::PurgeAccount => "Purge user accounts", - Permission::FtsReindex => "Rebuild the full-text search index", - Permission::Undelete => "Restore deleted items", - Permission::DkimSignatureCreate => "Create DKIM signatures for email authentication", - Permission::DkimSignatureGet => "Retrieve DKIM signature information", - Permission::SpamFilterUpdate => "Modify spam filter settings", - Permission::WebadminUpdate => "Modify web admin interface settings", - Permission::LogsView => "Access system logs", - Permission::SpamFilterTrain => "Train the spam filter", - Permission::SpamFilterTest => "Test the spam filter", - Permission::Restart => "Restart the email server", - Permission::TracingList => "View stored traces", - Permission::TracingGet => "Retrieve specific trace information", - Permission::TracingLive => "Perform real-time tracing", - Permission::MetricsList => "View stored metrics", - Permission::MetricsLive => "View real-time metrics", - Permission::Authenticate => "Authenticate", - Permission::AuthenticateOauth => "Authenticate via OAuth", - Permission::EmailSend => "Send emails", - Permission::EmailReceive => "Receive emails", - Permission::ManageEncryption => "Manage encryption-at-rest settings", - Permission::ManagePasswords => "Manage account passwords", - Permission::JmapEmailGet => "Retrieve emails via JMAP", - Permission::JmapMailboxGet => "Retrieve mailboxes via JMAP", - Permission::JmapThreadGet => "Retrieve email threads via JMAP", - Permission::JmapIdentityGet => "Retrieve user identities via JMAP", - Permission::JmapEmailSubmissionGet => "Retrieve email submission info via JMAP", - Permission::JmapPushSubscriptionGet => "Retrieve push subscriptions via JMAP", - Permission::JmapSieveScriptGet => "Retrieve Sieve scripts via JMAP", - Permission::JmapVacationResponseGet => "Retrieve vacation responses via JMAP", - Permission::JmapPrincipalGet => "Retrieve principal information via JMAP", - Permission::JmapQuotaGet => "Retrieve quota information via JMAP", - Permission::JmapBlobGet => "Retrieve blobs via JMAP", - Permission::JmapEmailSet => "Modify emails via JMAP", - Permission::JmapMailboxSet => "Modify mailboxes via JMAP", - Permission::JmapIdentitySet => "Modify user identities via JMAP", - Permission::JmapEmailSubmissionSet => "Modify email submission settings via JMAP", - Permission::JmapPushSubscriptionSet => "Modify push subscriptions via JMAP", - Permission::JmapSieveScriptSet => "Modify Sieve scripts via JMAP", - Permission::JmapVacationResponseSet => "Modify vacation responses via JMAP", - Permission::JmapEmailChanges => "Track email changes via JMAP", - Permission::JmapMailboxChanges => "Track mailbox changes via JMAP", - Permission::JmapThreadChanges => "Track thread changes via JMAP", - Permission::JmapIdentityChanges => "Track identity changes via JMAP", - Permission::JmapEmailSubmissionChanges => "Track email submission changes via JMAP", - Permission::JmapQuotaChanges => "Track quota changes via JMAP", - Permission::JmapEmailCopy => "Copy emails via JMAP", - Permission::JmapBlobCopy => "Copy blobs via JMAP", - Permission::JmapEmailImport => "Import emails via JMAP", - Permission::JmapEmailParse => "Parse emails via JMAP", - Permission::JmapEmailQueryChanges => "Track email query changes via JMAP", - Permission::JmapMailboxQueryChanges => "Track mailbox query changes via JMAP", - Permission::JmapEmailSubmissionQueryChanges => { - "Track email submission query changes via JMAP" - } - Permission::JmapSieveScriptQueryChanges => "Track Sieve script query changes via JMAP", - Permission::JmapPrincipalQueryChanges => "Track principal query changes via JMAP", - Permission::JmapQuotaQueryChanges => "Track quota query changes via JMAP", - Permission::JmapEmailQuery => "Perform email queries via JMAP", - Permission::JmapMailboxQuery => "Perform mailbox queries via JMAP", - Permission::JmapEmailSubmissionQuery => "Perform email submission queries via JMAP", - Permission::JmapSieveScriptQuery => "Perform Sieve script queries via JMAP", - Permission::JmapPrincipalQuery => "Perform principal queries via JMAP", - Permission::JmapQuotaQuery => "Perform quota queries via JMAP", - Permission::JmapSearchSnippet => "Retrieve search snippets via JMAP", - Permission::JmapSieveScriptValidate => "Validate Sieve scripts via JMAP", - Permission::JmapBlobLookup => "Look up blobs via JMAP", - Permission::JmapBlobUpload => "Upload blobs via JMAP", - Permission::JmapEcho => "Perform JMAP echo requests", - Permission::ImapAuthenticate => "Authenticate via IMAP", - Permission::ImapAclGet => "Retrieve ACLs via IMAP", - Permission::ImapAclSet => "Set ACLs via IMAP", - Permission::ImapMyRights => "Retrieve own rights via IMAP", - Permission::ImapListRights => "List rights via IMAP", - Permission::ImapAppend => "Append messages via IMAP", - Permission::ImapCapability => "Retrieve server capabilities via IMAP", - Permission::ImapId => "Retrieve server ID via IMAP", - Permission::ImapCopy => "Copy messages via IMAP", - Permission::ImapMove => "Move messages via IMAP", - Permission::ImapCreate => "Create mailboxes via IMAP", - Permission::ImapDelete => "Delete mailboxes or messages via IMAP", - Permission::ImapEnable => "Enable IMAP extensions", - Permission::ImapExpunge => "Expunge deleted messages via IMAP", - Permission::ImapFetch => "Fetch messages or metadata via IMAP", - Permission::ImapIdle => "Use IMAP IDLE command", - Permission::ImapList => "List mailboxes via IMAP", - Permission::ImapLsub => "List subscribed mailboxes via IMAP", - Permission::ImapNamespace => "Retrieve namespaces via IMAP", - Permission::ImapRename => "Rename mailboxes via IMAP", - Permission::ImapSearch => "Search messages via IMAP", - Permission::ImapSort => "Sort messages via IMAP", - Permission::ImapSelect => "Select mailboxes via IMAP", - Permission::ImapExamine => "Examine mailboxes via IMAP", - Permission::ImapStatus => "Retrieve mailbox status via IMAP", - Permission::ImapStore => "Modify message flags via IMAP", - Permission::ImapSubscribe => "Subscribe to mailboxes via IMAP", - Permission::ImapThread => "Thread messages via IMAP", - Permission::Pop3Authenticate => "Authenticate via POP3", - Permission::Pop3List => "List messages via POP3", - Permission::Pop3Uidl => "Retrieve unique IDs via POP3", - Permission::Pop3Stat => "Retrieve mailbox statistics via POP3", - Permission::Pop3Retr => "Retrieve messages via POP3", - Permission::Pop3Dele => "Mark messages for deletion via POP3", - Permission::SieveAuthenticate => "Authenticate for Sieve script management", - Permission::SieveListScripts => "List Sieve scripts", - Permission::SieveSetActive => "Set active Sieve script", - Permission::SieveGetScript => "Retrieve Sieve scripts", - Permission::SievePutScript => "Upload Sieve scripts", - Permission::SieveDeleteScript => "Delete Sieve scripts", - Permission::SieveRenameScript => "Rename Sieve scripts", - Permission::SieveCheckScript => "Validate Sieve scripts", - Permission::SieveHaveSpace => "Check available space for Sieve scripts", - Permission::OauthClientRegistration => "Register OAuth clients", - Permission::OauthClientOverride => "Override OAuth client settings", - Permission::ApiKeyList => "View API keys", - Permission::ApiKeyGet => "Retrieve specific API keys", - Permission::ApiKeyCreate => "Create new API keys", - Permission::ApiKeyUpdate => "Modify API keys", - Permission::ApiKeyDelete => "Remove API keys", - Permission::OauthClientList => "View OAuth clients", - Permission::OauthClientGet => "Retrieve specific OAuth clients", - Permission::OauthClientCreate => "Create new OAuth clients", - Permission::OauthClientUpdate => "Modify OAuth clients", - Permission::OauthClientDelete => "Remove OAuth clients", - Permission::AiModelInteract => "Interact with AI models", - Permission::Troubleshoot => "Perform troubleshooting", - Permission::DavSyncCollection => "Synchronize collection changes with client", - Permission::DavPrincipalAcl => "Set principal properties for access control", - Permission::DavPrincipalMatch => "Match principals based on specified criteria", - Permission::DavPrincipalSearch => "Search for principals by property values", - Permission::DavPrincipalSearchPropSet => "Define property sets for principal searches", - Permission::DavExpandProperty => "Expand properties that reference other resources", - Permission::DavPrincipalList => "List available principals in the system", - Permission::DavFilePropFind => "Retrieve properties of file resources", - Permission::DavFilePropPatch => "Modify properties of file resources", - Permission::DavFileGet => "Download file resources", - Permission::DavFileMkCol => "Create new file collections or directories", - Permission::DavFileDelete => "Remove file resources", - Permission::DavFilePut => "Upload or modify file resources", - Permission::DavFileCopy => "Copy file resources to new locations", - Permission::DavFileMove => "Move file resources to new locations", - Permission::DavFileLock => "Lock file resources to prevent concurrent modifications", - Permission::DavFileAcl => "Manage access control lists for file resources", - Permission::DavCardPropFind => "Retrieve properties of address book entries", - Permission::DavCardPropPatch => "Modify properties of address book entries", - Permission::DavCardGet => "Download address book entries", - Permission::DavCardMkCol => "Create new address book collections", - Permission::DavCardDelete => "Remove address book entries or collections", - Permission::DavCardPut => "Upload or modify address book entries", - Permission::DavCardCopy => "Copy address book entries to new locations", - Permission::DavCardMove => "Move address book entries to new locations", - Permission::DavCardLock => { - "Lock address book entries to prevent concurrent modifications" - } - Permission::DavCardAcl => "Manage access control lists for address book entries", - Permission::DavCardQuery => "Search for address book entries matching criteria", - Permission::DavCardMultiGet => { - "Retrieve multiple address book entries in a single request" - } - Permission::DavCalPropFind => "Retrieve properties of calendar entries", - Permission::DavCalPropPatch => "Modify properties of calendar entries", - Permission::DavCalGet => "Download calendar entries", - Permission::DavCalMkCol => "Create new calendar collections", - Permission::DavCalDelete => "Remove calendar entries or collections", - Permission::DavCalPut => "Upload or modify calendar entries", - Permission::DavCalCopy => "Copy calendar entries to new locations", - Permission::DavCalMove => "Move calendar entries to new locations", - Permission::DavCalLock => "Lock calendar entries to prevent concurrent modifications", - Permission::DavCalAcl => "Manage access control lists for calendar entries", - Permission::DavCalQuery => "Search for calendar entries matching criteria", - Permission::DavCalMultiGet => "Retrieve multiple calendar entries in a single request", - Permission::DavCalFreeBusyQuery => "Query free/busy time information for scheduling", - Permission::CalendarAlarms => "Receive calendar alarms via e-mail", - Permission::CalendarSchedulingSend => "Send calendar scheduling requests via e-mail", - Permission::CalendarSchedulingReceive => { - "Receive calendar scheduling requests via e-mail" - } - Permission::JmapAddressBookGet => "Retrieve address books via JMAP", - Permission::JmapAddressBookSet => "Create or update address books via JMAP", - Permission::JmapAddressBookChanges => "Track address book changes via JMAP", - Permission::JmapContactCardGet => "Retrieve contact cards via JMAP", - Permission::JmapContactCardChanges => "Track contact card changes via JMAP", - Permission::JmapContactCardQuery => { - "Search for contact cards matching criteria via JMAP" - } - Permission::JmapContactCardQueryChanges => "Track contact card query changes via JMAP", - Permission::JmapContactCardSet => "Create or update contact cards via JMAP", - Permission::JmapContactCardCopy => "Copy contact cards to new locations via JMAP", - Permission::JmapContactCardParse => "Parse contact cards via JMAP", - Permission::JmapFileNodeGet => "Retrieve file nodes via JMAP", - Permission::JmapFileNodeSet => "Create or update file nodes via JMAP", - Permission::JmapFileNodeChanges => "Track file node changes via JMAP", - Permission::JmapFileNodeQuery => "Search for file nodes matching criteria via JMAP", - Permission::JmapFileNodeQueryChanges => "Track file node query changes via JMAP", - Permission::JmapPrincipalGetAvailability => { - "Retrieve availability information via JMAP" - } - Permission::JmapPrincipalChanges => "Track principal changes via JMAP", - Permission::JmapShareNotificationGet => "Retrieve share notifications via JMAP", - Permission::JmapShareNotificationSet => "Create or update share notifications via JMAP", - Permission::JmapShareNotificationChanges => "Track share notification changes via JMAP", - Permission::JmapShareNotificationQuery => { - "Search for share notifications matching criteria via JMAP" - } - Permission::JmapShareNotificationQueryChanges => { - "Track share notification query changes via JMAP" - } - Permission::JmapCalendarGet => "Retrieve calendars via JMAP", - Permission::JmapCalendarSet => "Create or update calendars via JMAP", - Permission::JmapCalendarChanges => "Track calendar changes via JMAP", - Permission::JmapCalendarEventGet => "Retrieve calendar events via JMAP", - Permission::JmapCalendarEventSet => "Create or update calendar events via JMAP", - Permission::JmapCalendarEventChanges => "Track calendar event changes via JMAP", - Permission::JmapCalendarEventQuery => { - "Search for calendar events matching criteria via JMAP" - } - Permission::JmapCalendarEventQueryChanges => { - "Track calendar event query changes via JMAP" - } - Permission::JmapCalendarEventCopy => "Copy calendar events to new locations via JMAP", - Permission::JmapCalendarEventParse => "Parse calendar events via JMAP", - Permission::JmapCalendarEventNotificationGet => { - "Retrieve calendar event notifications via JMAP" - } - Permission::JmapCalendarEventNotificationSet => { - "Create or update calendar event notifications via JMAP" - } - Permission::JmapCalendarEventNotificationChanges => { - "Track calendar event notification changes via JMAP" - } - Permission::JmapCalendarEventNotificationQuery => { - "Search for calendar event notifications matching criteria via JMAP" - } - Permission::JmapCalendarEventNotificationQueryChanges => { - "Track calendar event notification query changes via JMAP" - } - Permission::JmapParticipantIdentityGet => { - "Retrieve participant identity information via JMAP" - } - Permission::JmapParticipantIdentitySet => { - "Create or update participant identities via JMAP" - } - Permission::JmapParticipantIdentityChanges => { - "Track participant identity changes via JMAP" - } - } - } -} - -#[cfg(test)] -mod test { - use crate::Permission; - - #[test] - #[ignore] - #[allow(clippy::obfuscated_if_else)] - fn print_permissions() { - const CHECK: &str = ":white_check_mark:"; - - let mut permissions = Permission::all().collect::>(); - permissions.sort_by(|a, b| a.name().cmp(b.name())); - - for permission in permissions { - println!( - "|`{}`|{}|{}|{}|{}|", - permission.name(), - permission.description(), - CHECK, - permission - .is_tenant_admin_permission() - .then_some(CHECK) - .unwrap_or_default(), - permission - .is_user_permission() - .then_some(CHECK) - .unwrap_or_default() - ); - //println!("({:?},{:?}),", permission.name(), permission.description(),); - } - } -} diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs deleted file mode 100644 index dca7f9ec..00000000 --- a/crates/directory/src/core/principal.rs +++ /dev/null @@ -1,1738 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::{ - ArchivedPrincipal, ArchivedPrincipalData, FALLBACK_ADMIN_ID, Permission, PermissionGrant, - Principal, PrincipalData, ROLE_ADMIN, Type, - backend::internal::{PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue}, -}; -use ahash::AHashSet; -use nlp::tokenizers::word::WordTokenizer; -use serde::{ - Deserializer, Serializer, - de::{self, IgnoredAny, Visitor}, - ser::SerializeMap, -}; -use serde_json::Value; -use std::{cmp::Ordering, collections::hash_map::Entry, fmt, str::FromStr}; -use store::{ - U32_LEN, U64_LEN, - backend::MAX_TOKEN_LENGTH, - write::{BatchBuilder, DirectoryClass}, -}; - -impl Principal { - pub fn new(id: u32, typ: Type) -> Self { - Self { - id, - typ, - name: "".into(), - data: Default::default(), - } - } - - pub fn id(&self) -> u32 { - self.id - } - - pub fn typ(&self) -> Type { - self.typ - } - - pub fn name(&self) -> &str { - self.name.as_str() - } - - pub fn quota(&self) -> Option { - self.data.iter().find_map(|d| { - if let PrincipalData::DiskQuota(quota) = d { - if *quota > 0 { Some(*quota) } else { None } - } else { - None - } - }) - } - - pub fn directory_quota(&self, typ: &Type) -> Option { - self.data.iter().find_map(|d| { - if let PrincipalData::DirectoryQuota { quota, typ: qtyp } = d - && qtyp == typ - { - Some(*quota) - } else { - None - } - }) - } - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - #[cfg(feature = "enterprise")] - pub fn tenant(&self) -> Option { - self.data.iter().find_map(|item| { - if let PrincipalData::Tenant(tenant) = item { - Some(*tenant) - } else { - None - } - }) - } - // SPDX-SnippetEnd - - #[cfg(not(feature = "enterprise"))] - pub fn tenant(&self) -> Option { - None - } - - pub fn description(&self) -> Option<&str> { - self.data.iter().find_map(|item| { - if let PrincipalData::Description(description) = item { - if !description.is_empty() { - Some(description.as_str()) - } else { - None - } - } else { - None - } - }) - } - - pub fn secret(&self) -> Option<&str> { - if let Some(PrincipalData::Password(password)) = self.data.first() { - Some(password.as_str()) - } else if let Some(PrincipalData::Password(password)) = self.data.get(1) { - Some(password.as_str()) - } else { - None - } - } - - pub fn primary_email(&self) -> Option<&str> { - self.data.iter().find_map(|item| { - if let PrincipalData::PrimaryEmail(email) = item { - Some(email.as_str()) - } else { - None - } - }) - } - - pub fn email_addresses(&self) -> impl Iterator { - let mut found_email = false; - self.data - .iter() - .take_while(move |item| { - if matches!( - item, - PrincipalData::PrimaryEmail(_) | PrincipalData::EmailAlias(_) - ) { - found_email = true; - true - } else { - !found_email - } - }) - .filter_map(|item| { - if let PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) = item - { - Some(email.as_str()) - } else { - None - } - }) - } - - pub fn into_primary_email(self) -> Option { - self.data.into_iter().find_map(|item| { - if let PrincipalData::PrimaryEmail(email) = item { - Some(email) - } else { - None - } - }) - } - - pub fn into_email_addresses(self) -> impl Iterator { - self.data.into_iter().filter_map(|item| { - if let PrincipalData::PrimaryEmail(email) | PrincipalData::EmailAlias(email) = item { - Some(email) - } else { - None - } - }) - } - - pub fn member_of(&self) -> impl Iterator { - self.data.iter().filter_map(|item| { - if let PrincipalData::MemberOf(item) = item { - Some(*item) - } else { - None - } - }) - } - - pub fn roles(&self) -> impl Iterator { - self.data.iter().filter_map(|item| { - if let PrincipalData::Role(item) = item { - Some(*item) - } else { - None - } - }) - } - - pub fn permissions(&self) -> impl Iterator { - self.data.iter().filter_map(|item| { - if let PrincipalData::Permission { - permission_id, - grant, - } = item - { - Permission::from_id(*permission_id).map(|permission| PermissionGrant { - permission, - grant: *grant, - }) - } else { - None - } - }) - } - - pub fn urls(&self) -> impl Iterator { - self.data.iter().filter_map(|item| { - if let PrincipalData::Url(item) = item { - Some(item) - } else { - None - } - }) - } - - pub fn lists(&self) -> impl Iterator { - self.data.iter().filter_map(|item| { - if let PrincipalData::List(item) = item { - Some(item) - } else { - None - } - }) - } - - pub fn picture(&self) -> Option<&String> { - self.data.iter().find_map(|item| { - if let PrincipalData::Picture(picture) = item { - picture.into() - } else { - None - } - }) - } - - pub fn picture_mut(&mut self) -> Option<&mut String> { - self.data.iter_mut().find_map(|item| { - if let PrincipalData::Picture(picture) = item { - picture.into() - } else { - None - } - }) - } - - pub fn add_permission(&mut self, permission: Permission, grant: bool) { - let permission = permission.id(); - if let Some(permissions) = self.data.iter_mut().find_map(|item| { - if let PrincipalData::Permission { - permission_id, - grant, - } = item - { - if *permission_id == permission { - Some(grant) - } else { - None - } - } else { - None - } - }) { - *permissions = grant; - } else { - self.data.push(PrincipalData::Permission { - permission_id: permission, - grant, - }); - } - } - - pub fn add_permissions(&mut self, iter: impl Iterator) { - for grant in iter { - self.add_permission(grant.permission, grant.grant); - } - } - - pub fn remove_permission(&mut self, permission: Permission, grant: bool) { - let permission = permission.id(); - self.data.retain(|data| { - if let PrincipalData::Permission { - permission_id: p, - grant: g, - } = data - { - *p != permission || *g != grant - } else { - true - } - }); - } - - pub fn remove_permissions(&mut self, grant: bool) { - self.data.retain(|data| { - if let PrincipalData::Permission { grant: g, .. } = data { - *g != grant - } else { - true - } - }); - } - - pub fn update_external(&mut self, external: Principal) -> Vec { - let mut updates = Vec::new(); - let mut external_data = AHashSet::with_capacity(external.data.len()); - let mut has_role = false; - let mut has_member_of = false; - let mut has_quota = false; - let mut has_otp_auth = false; - let mut has_app_password = false; - - for item in external.data { - match item { - PrincipalData::DiskQuota(_) => { - has_quota = true; - external_data.insert(item); - } - PrincipalData::MemberOf(_) => { - has_member_of = true; - external_data.insert(item); - } - PrincipalData::Role(_) => { - has_role = true; - external_data.insert(item); - } - PrincipalData::OtpAuth(_) => { - has_otp_auth = true; - external_data.insert(item); - } - PrincipalData::AppPassword(_) => { - has_app_password = true; - external_data.insert(item); - } - PrincipalData::Password(_) - | PrincipalData::Description(_) - | PrincipalData::PrimaryEmail(_) - | PrincipalData::EmailAlias(_) => { - external_data.insert(item); - } - _ => {} - } - } - - let mut old_data = Vec::new(); - let data_len = self.data.len(); - - for item in std::mem::replace(&mut self.data, Vec::with_capacity(data_len)) { - match item { - PrincipalData::Password(_) - | PrincipalData::AppPassword(_) - | PrincipalData::OtpAuth(_) - | PrincipalData::Description(_) - | PrincipalData::PrimaryEmail(_) - | PrincipalData::EmailAlias(_) - | PrincipalData::DiskQuota(_) - | PrincipalData::MemberOf(_) - | PrincipalData::Role(_) => { - if external_data.remove(&item) - || match item { - PrincipalData::EmailAlias(_) => true, - PrincipalData::AppPassword(_) => !has_app_password, - PrincipalData::OtpAuth(_) => !has_otp_auth, - PrincipalData::Role(_) => !has_role, - PrincipalData::MemberOf(_) => !has_member_of, - PrincipalData::DiskQuota(_) => !has_quota, - _ => false, - } - { - self.data.push(item); - } else if matches!( - item, - PrincipalData::Password(_) - | PrincipalData::AppPassword(_) - | PrincipalData::OtpAuth(_) - | PrincipalData::PrimaryEmail(_) - | PrincipalData::EmailAlias(_) - ) { - old_data.push(item); - } - } - _ => { - self.data.push(item); - } - } - } - - // Add new data - let mut has_password = false; - let mut has_email = false; - for item in external_data { - match &item { - PrincipalData::Description(value) => { - updates.push(PrincipalUpdate::set( - PrincipalField::Description, - PrincipalValue::String(value.to_string()), - )); - } - PrincipalData::DiskQuota(value) => { - updates.push(PrincipalUpdate::set( - PrincipalField::Quota, - PrincipalValue::Integer(*value), - )); - } - PrincipalData::Password(value) - | PrincipalData::AppPassword(value) - | PrincipalData::OtpAuth(value) => { - let item = PrincipalUpdate::add_item( - PrincipalField::Secrets, - PrincipalValue::String(value.to_string()), - ); - if !has_password && !updates.is_empty() { - updates.insert(0, item); - } else { - updates.push(item); - } - has_password = true; - } - PrincipalData::PrimaryEmail(value) => { - let item = PrincipalUpdate::add_item( - PrincipalField::Emails, - PrincipalValue::String(value.to_string()), - ); - if !has_email && !updates.is_empty() { - updates.insert(0, item); - } else { - updates.push(item); - } - has_email = true; - } - PrincipalData::EmailAlias(value) => { - updates.push(PrincipalUpdate::add_item( - PrincipalField::Emails, - PrincipalValue::String(value.to_string()), - )); - } - _ => (), - } - self.data.push(item); - } - - // Remove old data - for item in old_data { - match item { - PrincipalData::Password(value) - | PrincipalData::AppPassword(value) - | PrincipalData::OtpAuth(value) => { - updates.push(PrincipalUpdate::remove_item( - PrincipalField::Secrets, - PrincipalValue::String(value), - )); - } - PrincipalData::PrimaryEmail(value) | PrincipalData::EmailAlias(value) => { - updates.push(PrincipalUpdate::remove_item( - PrincipalField::Emails, - PrincipalValue::String(value), - )); - } - _ => (), - } - } - - self.sort(); - - updates - } - - pub fn object_size(&self) -> usize { - self.name.len() - + self - .data - .iter() - .map(|item| item.object_size()) - .sum::() - } - - pub fn fallback_admin(fallback_pass: impl Into) -> Self { - Principal { - id: FALLBACK_ADMIN_ID, - typ: Type::Individual, - name: "Fallback Administrator".into(), - data: vec![ - PrincipalData::Role(ROLE_ADMIN), - PrincipalData::Password(fallback_pass.into()), - ], - } - } - - pub fn sort(&mut self) { - self.data.sort_unstable(); - } -} - -impl PrincipalData { - fn rank(&self) -> u8 { - match self { - PrincipalData::OtpAuth(_) => 0, - PrincipalData::Password(_) => 1, - PrincipalData::AppPassword(_) => 2, - PrincipalData::PrimaryEmail(_) => 3, - PrincipalData::EmailAlias(_) => 4, - _ => 5, - } - } - - fn rank_string(&self) -> Option<&str> { - match self { - PrincipalData::OtpAuth(s) - | PrincipalData::Password(s) - | PrincipalData::AppPassword(s) - | PrincipalData::PrimaryEmail(s) - | PrincipalData::EmailAlias(s) => Some(s), - _ => None, - } - } -} - -impl PartialOrd for PrincipalData { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PrincipalData { - fn cmp(&self, other: &Self) -> Ordering { - match self.rank().cmp(&other.rank()) { - Ordering::Equal => match (self.rank_string(), other.rank_string()) { - (Some(a), Some(b)) => a.cmp(b), - _ => Ordering::Equal, - }, - other => other, - } - } -} - -impl PrincipalData { - pub fn object_size(&self) -> usize { - match self { - PrincipalData::Password(v) - | PrincipalData::AppPassword(v) - | PrincipalData::OtpAuth(v) - | PrincipalData::Description(v) - | PrincipalData::PrimaryEmail(v) - | PrincipalData::EmailAlias(v) - | PrincipalData::Picture(v) - | PrincipalData::ExternalMember(v) - | PrincipalData::Url(v) - | PrincipalData::Locale(v) => v.len(), - PrincipalData::DiskQuota(_) => U64_LEN, - PrincipalData::Permission { .. } => U32_LEN + 1, - PrincipalData::DirectoryQuota { .. } | PrincipalData::ObjectQuota { .. } => U64_LEN + 1, - PrincipalData::Tenant(_) - | PrincipalData::MemberOf(_) - | PrincipalData::Role(_) - | PrincipalData::List(_) => U32_LEN, - } - } -} - -impl PrincipalSet { - pub fn new(id: u32, typ: Type) -> Self { - Self { - id, - typ, - ..Default::default() - } - } - - pub fn id(&self) -> u32 { - self.id - } - - pub fn typ(&self) -> Type { - self.typ - } - - pub fn name(&self) -> &str { - self.get_str(PrincipalField::Name).unwrap_or_default() - } - - pub fn has_name(&self) -> bool { - self.fields.contains_key(&PrincipalField::Name) - } - - pub fn quota(&self) -> u64 { - self.get_int(PrincipalField::Quota).unwrap_or_default() - } - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - pub fn tenant(&self) -> Option { - self.get_int(PrincipalField::Tenant).map(|v| v as u32) - } - // SPDX-SnippetEnd - - #[cfg(not(feature = "enterprise"))] - pub fn tenant(&self) -> Option { - None - } - - pub fn description(&self) -> Option<&str> { - self.get_str(PrincipalField::Description) - } - - pub fn get_str(&self, key: PrincipalField) -> Option<&str> { - self.fields.get(&key).and_then(|v| v.as_str()) - } - - pub fn get_int(&self, key: PrincipalField) -> Option { - self.fields.get(&key).and_then(|v| v.as_int()) - } - - pub fn get_str_array(&self, key: PrincipalField) -> Option<&[String]> { - self.fields.get(&key).and_then(|v| match v { - PrincipalValue::StringList(v) => Some(v.as_slice()), - PrincipalValue::String(v) => Some(std::slice::from_ref(v)), - PrincipalValue::Integer(_) | PrincipalValue::IntegerList(_) => None, - }) - } - - pub fn get_int_array(&self, key: PrincipalField) -> Option<&[u64]> { - self.fields.get(&key).and_then(|v| match v { - PrincipalValue::IntegerList(v) => Some(v.as_slice()), - PrincipalValue::Integer(v) => Some(std::slice::from_ref(v)), - PrincipalValue::String(_) | PrincipalValue::StringList(_) => None, - }) - } - - pub fn take(&mut self, key: PrincipalField) -> Option { - self.fields.remove(&key) - } - - pub fn take_str(&mut self, key: PrincipalField) -> Option { - self.take(key).and_then(|v| match v { - PrincipalValue::String(s) => Some(s), - PrincipalValue::StringList(l) => l.into_iter().next(), - PrincipalValue::Integer(i) => Some(i.to_string()), - PrincipalValue::IntegerList(l) => l.into_iter().next().map(|i| i.to_string()), - }) - } - - pub fn take_int(&mut self, key: PrincipalField) -> Option { - self.take(key).and_then(|v| match v { - PrincipalValue::Integer(i) => Some(i), - PrincipalValue::IntegerList(l) => l.into_iter().next(), - PrincipalValue::String(s) => s.parse().ok(), - PrincipalValue::StringList(l) => l.into_iter().next().and_then(|s| s.parse().ok()), - }) - } - - pub fn take_str_array(&mut self, key: PrincipalField) -> Option> { - self.take(key).map(|v| v.into_str_array()) - } - - pub fn take_int_array(&mut self, key: PrincipalField) -> Option> { - self.take(key).map(|v| v.into_int_array()) - } - - pub fn iter_str( - &self, - key: PrincipalField, - ) -> Box + Sync + Send + '_> { - self.fields - .get(&key) - .map(|v| v.iter_str()) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - pub fn iter_mut_str( - &mut self, - key: PrincipalField, - ) -> Box + Sync + Send + '_> { - self.fields - .get_mut(&key) - .map(|v| v.iter_mut_str()) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - pub fn iter_int( - &self, - key: PrincipalField, - ) -> Box + Sync + Send + '_> { - self.fields - .get(&key) - .map(|v| v.iter_int()) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - pub fn iter_mut_int( - &mut self, - key: PrincipalField, - ) -> Box + Sync + Send + '_> { - self.fields - .get_mut(&key) - .map(|v| v.iter_mut_int()) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - pub fn append_int(&mut self, key: PrincipalField, value: impl Into) -> &mut Self { - let value = value.into(); - match self.fields.entry(key) { - Entry::Occupied(v) => { - let v = v.into_mut(); - - match v { - PrincipalValue::IntegerList(v) => { - if !v.contains(&value) { - v.push(value); - } - } - PrincipalValue::Integer(i) => { - if value != *i { - *v = PrincipalValue::IntegerList(vec![*i, value]); - } - } - PrincipalValue::String(s) => { - *v = - PrincipalValue::IntegerList(vec![s.parse().unwrap_or_default(), value]); - } - PrincipalValue::StringList(l) => { - *v = PrincipalValue::IntegerList( - l.iter() - .map(|s| s.parse().unwrap_or_default()) - .chain(std::iter::once(value)) - .collect(), - ); - } - } - } - Entry::Vacant(v) => { - v.insert(PrincipalValue::IntegerList(vec![value])); - } - } - - self - } - - pub fn append_str(&mut self, key: PrincipalField, value: impl Into) -> &mut Self { - let value = value.into(); - match self.fields.entry(key) { - Entry::Occupied(v) => { - let v = v.into_mut(); - - match v { - PrincipalValue::StringList(v) => { - if !v.contains(&value) { - v.push(value); - } - } - PrincipalValue::String(s) => { - if s != &value { - *v = PrincipalValue::StringList(vec![std::mem::take(s), value]); - } - } - PrincipalValue::Integer(i) => { - *v = PrincipalValue::StringList(vec![i.to_string(), value]); - } - PrincipalValue::IntegerList(l) => { - *v = PrincipalValue::StringList( - l.iter() - .map(|i| i.to_string()) - .chain(std::iter::once(value)) - .collect(), - ); - } - } - } - Entry::Vacant(v) => { - v.insert(PrincipalValue::StringList(vec![value])); - } - } - self - } - - pub fn prepend_str(&mut self, key: PrincipalField, value: impl Into) -> &mut Self { - let value = value.into(); - match self.fields.entry(key) { - Entry::Occupied(v) => { - let v = v.into_mut(); - - match v { - PrincipalValue::StringList(v) => { - if !v.contains(&value) { - v.insert(0, value); - } - } - PrincipalValue::String(s) => { - if s != &value { - *v = PrincipalValue::StringList(vec![value, std::mem::take(s)]); - } - } - PrincipalValue::Integer(i) => { - *v = PrincipalValue::StringList(vec![value, i.to_string()]); - } - PrincipalValue::IntegerList(l) => { - *v = PrincipalValue::StringList( - std::iter::once(value) - .chain(l.iter().map(|i| i.to_string())) - .collect(), - ); - } - } - } - Entry::Vacant(v) => { - v.insert(PrincipalValue::StringList(vec![value])); - } - } - self - } - - pub fn set(&mut self, key: PrincipalField, value: impl Into) -> &mut Self { - self.fields.insert(key, value.into()); - self - } - - pub fn with_field(mut self, key: PrincipalField, value: impl Into) -> Self { - self.set(key, value); - self - } - - pub fn with_opt_field( - mut self, - key: PrincipalField, - value: Option>, - ) -> Self { - if let Some(value) = value { - self.set(key, value); - } - self - } - - pub fn has_field(&self, key: PrincipalField) -> bool { - self.fields.contains_key(&key) - } - - pub fn has_str_value(&self, key: PrincipalField, value: &str) -> bool { - self.fields.get(&key).is_some_and(|v| match v { - PrincipalValue::String(v) => v == value, - PrincipalValue::StringList(l) => l.iter().any(|v| v == value), - PrincipalValue::Integer(_) | PrincipalValue::IntegerList(_) => false, - }) - } - - pub fn has_int_value(&self, key: PrincipalField, value: u64) -> bool { - self.fields.get(&key).is_some_and(|v| match v { - PrincipalValue::Integer(v) => *v == value, - PrincipalValue::IntegerList(l) => l.contains(&value), - PrincipalValue::String(_) | PrincipalValue::StringList(_) => false, - }) - } - - pub fn find_str(&self, value: &str) -> bool { - self.fields.values().any(|v| v.find_str(value)) - } - - pub fn field_len(&self, key: PrincipalField) -> usize { - self.fields.get(&key).map_or(0, |v| match v { - PrincipalValue::String(_) => 1, - PrincipalValue::StringList(l) => l.len(), - PrincipalValue::Integer(_) => 1, - PrincipalValue::IntegerList(l) => l.len(), - }) - } - - pub fn remove(&mut self, key: PrincipalField) -> Option { - self.fields.remove(&key) - } - - pub fn retain_str(&mut self, key: PrincipalField, mut f: F) - where - F: FnMut(&String) -> bool, - { - if let Some(value) = self.fields.get_mut(&key) { - match value { - PrincipalValue::String(s) => { - if !f(s) { - self.fields.remove(&key); - } - } - PrincipalValue::StringList(l) => { - l.retain(f); - if l.is_empty() { - self.fields.remove(&key); - } - } - _ => {} - } - } - } - - pub fn retain_int(&mut self, key: PrincipalField, mut f: F) - where - F: FnMut(&u64) -> bool, - { - if let Some(value) = self.fields.get_mut(&key) { - match value { - PrincipalValue::Integer(i) => { - if !f(i) { - self.fields.remove(&key); - } - } - PrincipalValue::IntegerList(l) => { - l.retain(f); - if l.is_empty() { - self.fields.remove(&key); - } - } - _ => {} - } - } - } -} - -impl PrincipalValue { - pub fn as_str(&self) -> Option<&str> { - match self { - PrincipalValue::String(v) => Some(v.as_str()), - PrincipalValue::StringList(v) => v.first().map(|s| s.as_str()), - _ => None, - } - } - - pub fn as_int(&self) -> Option { - match self { - PrincipalValue::Integer(v) => Some(*v), - PrincipalValue::IntegerList(v) => v.first().copied(), - _ => None, - } - } - - pub fn iter_str(&self) -> Box + Sync + Send + '_> { - match self { - PrincipalValue::String(v) => Box::new(std::iter::once(v)), - PrincipalValue::StringList(v) => Box::new(v.iter()), - _ => Box::new(std::iter::empty()), - } - } - - pub fn iter_mut_str(&mut self) -> Box + Sync + Send + '_> { - match self { - PrincipalValue::String(v) => Box::new(std::iter::once(v)), - PrincipalValue::StringList(v) => Box::new(v.iter_mut()), - _ => Box::new(std::iter::empty()), - } - } - - pub fn iter_int(&self) -> Box + Sync + Send + '_> { - match self { - PrincipalValue::Integer(v) => Box::new(std::iter::once(*v)), - PrincipalValue::IntegerList(v) => Box::new(v.iter().copied()), - _ => Box::new(std::iter::empty()), - } - } - - pub fn iter_mut_int(&mut self) -> Box + Sync + Send + '_> { - match self { - PrincipalValue::Integer(v) => Box::new(std::iter::once(v)), - PrincipalValue::IntegerList(v) => Box::new(v.iter_mut()), - _ => Box::new(std::iter::empty()), - } - } - - pub fn into_array(self) -> Self { - match self { - PrincipalValue::String(v) => PrincipalValue::StringList(vec![v]), - PrincipalValue::Integer(v) => PrincipalValue::IntegerList(vec![v]), - v => v, - } - } - - pub fn into_str_array(self) -> Vec { - match self { - PrincipalValue::StringList(v) => v, - PrincipalValue::String(v) => vec![v], - PrincipalValue::Integer(v) => vec![v.to_string()], - PrincipalValue::IntegerList(v) => v.into_iter().map(|v| v.to_string()).collect(), - } - } - - pub fn into_int_array(self) -> Vec { - match self { - PrincipalValue::IntegerList(v) => v, - PrincipalValue::Integer(v) => vec![v], - PrincipalValue::String(v) => vec![v.parse().unwrap_or_default()], - PrincipalValue::StringList(v) => v - .into_iter() - .map(|v| v.parse().unwrap_or_default()) - .collect(), - } - } - - pub fn serialized_size(&self) -> usize { - match self { - PrincipalValue::String(s) => s.len() + 2, - PrincipalValue::StringList(s) => s.iter().map(|s| s.len() + 2).sum(), - PrincipalValue::Integer(_) => U64_LEN, - PrincipalValue::IntegerList(l) => l.len() * U64_LEN, - } - } - - pub fn find_str(&self, value: &str) -> bool { - match self { - PrincipalValue::String(s) => s.to_lowercase().contains(value), - PrincipalValue::StringList(l) => l.iter().any(|s| s.to_lowercase().contains(value)), - _ => false, - } - } -} - -impl From for PrincipalValue { - fn from(v: u64) -> Self { - Self::Integer(v) - } -} - -impl From for PrincipalValue { - fn from(v: String) -> Self { - Self::String(v) - } -} - -impl From<&str> for PrincipalValue { - fn from(v: &str) -> Self { - Self::String(v.into()) - } -} - -impl From> for PrincipalValue { - fn from(v: Vec) -> Self { - Self::StringList(v) - } -} - -impl From> for PrincipalValue { - fn from(v: Vec) -> Self { - Self::IntegerList(v) - } -} - -impl From for PrincipalValue { - fn from(v: u32) -> Self { - Self::Integer(v as u64) - } -} - -impl From> for PrincipalValue { - fn from(v: Vec) -> Self { - Self::IntegerList(v.into_iter().map(|v| v as u64).collect()) - } -} - -pub(crate) fn build_search_index( - batch: &mut BatchBuilder, - principal_id: u32, - current: Option<&ArchivedPrincipal>, - new: Option<&Principal>, -) { - let mut current_words = AHashSet::new(); - let mut new_words = AHashSet::new(); - - if let Some(current) = current { - for word in [Some(current.name.as_str())] - .into_iter() - .chain(current.data.iter().map(|s| match s { - ArchivedPrincipalData::Description(v) - | ArchivedPrincipalData::PrimaryEmail(v) - | ArchivedPrincipalData::EmailAlias(v) => Some(v.as_str()), - _ => None, - })) - .flatten() - { - current_words.extend(WordTokenizer::new(word, MAX_TOKEN_LENGTH).map(|t| t.word)); - } - } - - if let Some(new) = new { - for word in [Some(new.name.as_str())] - .into_iter() - .chain(new.data.iter().map(|s| match s { - PrincipalData::Description(v) - | PrincipalData::PrimaryEmail(v) - | PrincipalData::EmailAlias(v) => Some(v.as_str()), - _ => None, - })) - .flatten() - { - new_words.extend(WordTokenizer::new(word, MAX_TOKEN_LENGTH).map(|t| t.word)); - } - } - - for word in new_words.difference(¤t_words) { - batch.set( - DirectoryClass::Index { - word: word.as_bytes().to_vec(), - principal_id, - }, - vec![], - ); - } - - for word in current_words.difference(&new_words) { - batch.clear(DirectoryClass::Index { - word: word.as_bytes().to_vec(), - principal_id, - }); - } -} - -impl Type { - pub fn as_str(&self) -> &'static str { - match self { - Self::Individual => "individual", - Self::Group => "group", - Self::Resource => "resource", - Self::Location => "location", - Self::Other => "other", - Self::List => "list", - Self::Tenant => "tenant", - Self::Role => "role", - Self::Domain => "domain", - Self::ApiKey => "apiKey", - Self::OauthClient => "oauthClient", - } - } - - pub fn description(&self) -> &'static str { - match self { - Self::Individual => "Individual", - Self::Group => "Group", - Self::Resource => "Resource", - Self::Location => "Location", - Self::Tenant => "Tenant", - Self::List => "List", - Self::Other => "Other", - Self::Role => "Role", - Self::Domain => "Domain", - Self::ApiKey => "API Key", - Self::OauthClient => "OAuth Client", - } - } - - pub fn parse(value: &str) -> Option { - match value { - "individual" => Some(Type::Individual), - "group" => Some(Type::Group), - "resource" => Some(Type::Resource), - "location" => Some(Type::Location), - "list" => Some(Type::List), - "tenant" => Some(Type::Tenant), - "superuser" => Some(Type::Individual), // legacy - "role" => Some(Type::Role), - "domain" => Some(Type::Domain), - "apiKey" => Some(Type::ApiKey), - "oauthClient" => Some(Type::OauthClient), - _ => None, - } - } - - pub const MAX_ID: usize = 11; - - pub fn from_u8(value: u8) -> Self { - match value { - 0 => Type::Individual, - 1 => Type::Group, - 2 => Type::Resource, - 3 => Type::Location, - 4 => Type::Other, // legacy - 5 => Type::List, - 6 => Type::Other, - 7 => Type::Domain, - 8 => Type::Tenant, - 9 => Type::Role, - 10 => Type::ApiKey, - 11 => Type::OauthClient, - _ => Type::Other, - } - } -} - -impl FromStr for Type { - type Err = (); - - fn from_str(s: &str) -> Result { - Type::parse(s).ok_or(()) - } -} - -impl serde::Serialize for PrincipalSet { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(None)?; - - map.serialize_entry("id", &self.id)?; - map.serialize_entry("type", &self.typ.as_str())?; - - for (key, value) in &self.fields { - match value { - PrincipalValue::String(v) => map.serialize_entry(key.as_str(), v)?, - PrincipalValue::StringList(v) => map.serialize_entry(key.as_str(), v)?, - PrincipalValue::Integer(v) => map.serialize_entry(key.as_str(), v)?, - PrincipalValue::IntegerList(v) => map.serialize_entry(key.as_str(), v)?, - }; - } - - map.end() - } -} - -const MAX_STRING_LEN: usize = 512; - -impl<'de> serde::Deserialize<'de> for PrincipalValue { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct PrincipalValueVisitor; - - impl<'de> Visitor<'de> for PrincipalValueVisitor { - type Value = PrincipalValue; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an optional values or a sequence of values") - } - - fn visit_none(self) -> Result - where - E: de::Error, - { - Ok(PrincipalValue::String("".into())) - } - - fn visit_some(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(self) - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - Ok(PrincipalValue::Integer(value)) - } - - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - if value.len() <= MAX_STRING_LEN { - Ok(PrincipalValue::String(value)) - } else { - Err(serde::de::Error::custom("string too long")) - } - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - if value.len() <= MAX_STRING_LEN { - Ok(PrincipalValue::String(value.into())) - } else { - Err(serde::de::Error::custom("string too long")) - } - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: de::SeqAccess<'de>, - { - let mut vec_u64 = Vec::new(); - let mut vec_string = Vec::new(); - - while let Some(value) = seq.next_element::()? { - match value { - StringOrU64::String(s) => { - if s.len() <= MAX_STRING_LEN { - vec_string.push(s); - } else { - return Err(serde::de::Error::custom("string too long")); - } - } - StringOrU64::U64(u) => vec_u64.push(u), - } - } - - match (vec_u64.is_empty(), vec_string.is_empty()) { - (true, false) => Ok(PrincipalValue::StringList(vec_string)), - (false, true) => Ok(PrincipalValue::IntegerList(vec_u64)), - (true, true) => Ok(PrincipalValue::StringList(vec_string)), - _ => Err(serde::de::Error::custom("invalid principal value")), - } - } - } - - deserializer.deserialize_any(PrincipalValueVisitor) - } -} - -impl<'de> serde::Deserialize<'de> for PrincipalSet { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct PrincipalVisitor; - - // Deserialize the principal - impl<'de> Visitor<'de> for PrincipalVisitor { - type Value = PrincipalSet; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a valid principal") - } - - fn visit_map(self, mut map: A) -> Result - where - A: de::MapAccess<'de>, - { - let mut principal = PrincipalSet::default(); - - while let Some(key) = map.next_key::<&str>()? { - if key == "id" { - // Ignored - map.next_value::()?; - continue; - } - - let key = PrincipalField::try_parse(key).ok_or_else(|| { - serde::de::Error::custom(format!("invalid principal field: {}", key)) - })?; - - let value = match key { - PrincipalField::Name => { - PrincipalValue::String(map.next_value::().and_then(|v| { - if v.len() <= MAX_STRING_LEN { - Ok(v) - } else { - Err(serde::de::Error::custom("string too long")) - } - })?) - } - PrincipalField::Description - | PrincipalField::Tenant - | PrincipalField::Picture - | PrincipalField::Locale => { - if let Some(v) = map.next_value::>()? { - if v.len() <= MAX_STRING_LEN { - PrincipalValue::String(v) - } else { - return Err(serde::de::Error::custom("string too long")); - } - } else { - continue; - } - } - PrincipalField::Type => { - principal.typ = Type::parse(map.next_value()?).ok_or_else(|| { - serde::de::Error::custom("invalid principal type") - })?; - continue; - } - PrincipalField::Quota => map.next_value::()?, - PrincipalField::Secrets - | PrincipalField::Emails - | PrincipalField::MemberOf - | PrincipalField::Members - | PrincipalField::Roles - | PrincipalField::Lists - | PrincipalField::EnabledPermissions - | PrincipalField::DisabledPermissions - | PrincipalField::Urls - | PrincipalField::ExternalMembers => match map.next_value::()? { - Value::String(v) => { - if v.len() <= MAX_STRING_LEN { - PrincipalValue::StringList(vec![v]) - } else { - return Err(serde::de::Error::custom("string too long")); - } - } - Value::Array(v) => { - if !v.is_empty() { - PrincipalValue::StringList( - v.into_iter() - .filter_map(|item| { - if let Value::String(s) = item { - if s.len() <= MAX_STRING_LEN { - Some(s) - } else { - None - } - } else { - None - } - }) - .collect(), - ) - } else { - continue; - } - } - _ => continue, - }, - PrincipalField::UsedQuota => { - // consume and ignore - map.next_value::()?; - continue; - } - }; - - principal.fields.insert(key, value); - } - - Ok(principal) - } - } - - deserializer.deserialize_map(PrincipalVisitor) - } -} - -#[derive(Debug)] -enum StringOrU64 { - String(String), - U64(u64), -} - -impl<'de> serde::Deserialize<'de> for StringOrU64 { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct StringOrU64Visitor; - - impl Visitor<'_> for StringOrU64Visitor { - type Value = StringOrU64; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string or u64") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - if value.len() <= MAX_STRING_LEN { - Ok(StringOrU64::String(value.to_string())) - } else { - Err(serde::de::Error::custom("string too long")) - } - } - - fn visit_string(self, v: String) -> Result - where - E: de::Error, - { - if v.len() <= MAX_STRING_LEN { - Ok(StringOrU64::String(v)) - } else { - Err(serde::de::Error::custom("string too long")) - } - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - Ok(StringOrU64::U64(value)) - } - } - - deserializer.deserialize_any(StringOrU64Visitor) - } -} - -impl Permission { - pub fn all() -> impl Iterator { - (0..Permission::COUNT as u32).filter_map(Permission::from_id) - } - - pub const fn is_user_permission(&self) -> bool { - matches!( - self, - Permission::Authenticate - | Permission::AuthenticateOauth - | Permission::EmailSend - | Permission::EmailReceive - | Permission::ManageEncryption - | Permission::ManagePasswords - | Permission::JmapEmailGet - | Permission::JmapMailboxGet - | Permission::JmapThreadGet - | Permission::JmapIdentityGet - | Permission::JmapEmailSubmissionGet - | Permission::JmapPushSubscriptionGet - | Permission::JmapSieveScriptGet - | Permission::JmapVacationResponseGet - | Permission::JmapQuotaGet - | Permission::JmapBlobGet - | Permission::JmapEmailSet - | Permission::JmapMailboxSet - | Permission::JmapIdentitySet - | Permission::JmapEmailSubmissionSet - | Permission::JmapPushSubscriptionSet - | Permission::JmapSieveScriptSet - | Permission::JmapVacationResponseSet - | Permission::JmapEmailChanges - | Permission::JmapMailboxChanges - | Permission::JmapThreadChanges - | Permission::JmapIdentityChanges - | Permission::JmapEmailSubmissionChanges - | Permission::JmapQuotaChanges - | Permission::JmapEmailCopy - | Permission::JmapBlobCopy - | Permission::JmapEmailImport - | Permission::JmapEmailParse - | Permission::JmapEmailQueryChanges - | Permission::JmapMailboxQueryChanges - | Permission::JmapEmailSubmissionQueryChanges - | Permission::JmapSieveScriptQueryChanges - | Permission::JmapQuotaQueryChanges - | Permission::JmapEmailQuery - | Permission::JmapMailboxQuery - | Permission::JmapEmailSubmissionQuery - | Permission::JmapSieveScriptQuery - | Permission::JmapQuotaQuery - | Permission::JmapSearchSnippet - | Permission::JmapSieveScriptValidate - | Permission::JmapBlobLookup - | Permission::JmapBlobUpload - | Permission::JmapEcho - | Permission::ImapAuthenticate - | Permission::ImapAclGet - | Permission::ImapAclSet - | Permission::ImapMyRights - | Permission::ImapListRights - | Permission::ImapAppend - | Permission::ImapCapability - | Permission::ImapId - | Permission::ImapCopy - | Permission::ImapMove - | Permission::ImapCreate - | Permission::ImapDelete - | Permission::ImapEnable - | Permission::ImapExpunge - | Permission::ImapFetch - | Permission::ImapIdle - | Permission::ImapList - | Permission::ImapLsub - | Permission::ImapNamespace - | Permission::ImapRename - | Permission::ImapSearch - | Permission::ImapSort - | Permission::ImapSelect - | Permission::ImapExamine - | Permission::ImapStatus - | Permission::ImapStore - | Permission::ImapSubscribe - | Permission::ImapThread - | Permission::Pop3Authenticate - | Permission::Pop3List - | Permission::Pop3Uidl - | Permission::Pop3Stat - | Permission::Pop3Retr - | Permission::Pop3Dele - | Permission::SieveAuthenticate - | Permission::SieveListScripts - | Permission::SieveSetActive - | Permission::SieveGetScript - | Permission::SievePutScript - | Permission::SieveDeleteScript - | Permission::SieveRenameScript - | Permission::SieveCheckScript - | Permission::SieveHaveSpace - | Permission::DavSyncCollection - | Permission::DavExpandProperty - | Permission::DavPrincipalAcl - | Permission::DavPrincipalList - | Permission::DavPrincipalSearch - | Permission::DavPrincipalMatch - | Permission::DavPrincipalSearchPropSet - | Permission::DavFilePropFind - | Permission::DavFilePropPatch - | Permission::DavFileGet - | Permission::DavFileMkCol - | Permission::DavFileDelete - | Permission::DavFilePut - | Permission::DavFileCopy - | Permission::DavFileMove - | Permission::DavFileLock - | Permission::DavFileAcl - | Permission::DavCardPropFind - | Permission::DavCardPropPatch - | Permission::DavCardGet - | Permission::DavCardMkCol - | Permission::DavCardDelete - | Permission::DavCardPut - | Permission::DavCardCopy - | Permission::DavCardMove - | Permission::DavCardLock - | Permission::DavCardAcl - | Permission::DavCardQuery - | Permission::DavCardMultiGet - | Permission::DavCalPropFind - | Permission::DavCalPropPatch - | Permission::DavCalGet - | Permission::DavCalMkCol - | Permission::DavCalDelete - | Permission::DavCalPut - | Permission::DavCalCopy - | Permission::DavCalMove - | Permission::DavCalLock - | Permission::DavCalAcl - | Permission::DavCalQuery - | Permission::DavCalMultiGet - | Permission::DavCalFreeBusyQuery - | Permission::CalendarAlarms - | Permission::CalendarSchedulingSend - | Permission::CalendarSchedulingReceive - | Permission::JmapAddressBookGet - | Permission::JmapAddressBookSet - | Permission::JmapAddressBookChanges - | Permission::JmapContactCardGet - | Permission::JmapContactCardChanges - | Permission::JmapContactCardQuery - | Permission::JmapContactCardQueryChanges - | Permission::JmapContactCardSet - | Permission::JmapContactCardCopy - | Permission::JmapContactCardParse - | Permission::JmapFileNodeGet - | Permission::JmapFileNodeSet - | Permission::JmapFileNodeChanges - | Permission::JmapFileNodeQuery - | Permission::JmapFileNodeQueryChanges - | Permission::JmapPrincipalGetAvailability - | Permission::JmapPrincipalChanges - | Permission::JmapPrincipalQuery - | Permission::JmapPrincipalGet - | Permission::JmapPrincipalQueryChanges - | Permission::JmapShareNotificationGet - | Permission::JmapShareNotificationSet - | Permission::JmapShareNotificationChanges - | Permission::JmapShareNotificationQuery - | Permission::JmapShareNotificationQueryChanges - | Permission::JmapCalendarGet - | Permission::JmapCalendarSet - | Permission::JmapCalendarChanges - | Permission::JmapCalendarEventGet - | Permission::JmapCalendarEventSet - | Permission::JmapCalendarEventChanges - | Permission::JmapCalendarEventQuery - | Permission::JmapCalendarEventQueryChanges - | Permission::JmapCalendarEventCopy - | Permission::JmapCalendarEventParse - | Permission::JmapCalendarEventNotificationGet - | Permission::JmapCalendarEventNotificationSet - | Permission::JmapCalendarEventNotificationChanges - | Permission::JmapCalendarEventNotificationQuery - | Permission::JmapCalendarEventNotificationQueryChanges - | Permission::JmapParticipantIdentityGet - | Permission::JmapParticipantIdentitySet - | Permission::JmapParticipantIdentityChanges - ) - } - - #[cfg(not(feature = "enterprise"))] - pub const fn is_tenant_admin_permission(&self) -> bool { - false - } - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - #[cfg(feature = "enterprise")] - pub const fn is_tenant_admin_permission(&self) -> bool { - matches!( - self, - Permission::MessageQueueList - | Permission::MessageQueueGet - | Permission::MessageQueueUpdate - | Permission::MessageQueueDelete - | Permission::OutgoingReportList - | Permission::OutgoingReportGet - | Permission::OutgoingReportDelete - | Permission::IncomingReportList - | Permission::IncomingReportGet - | Permission::IncomingReportDelete - | Permission::IndividualList - | Permission::IndividualGet - | Permission::IndividualUpdate - | Permission::IndividualDelete - | Permission::IndividualCreate - | Permission::GroupList - | Permission::GroupGet - | Permission::GroupUpdate - | Permission::GroupDelete - | Permission::GroupCreate - | Permission::DomainList - | Permission::DomainGet - | Permission::DomainCreate - | Permission::DomainUpdate - | Permission::DomainDelete - | Permission::MailingListList - | Permission::MailingListGet - | Permission::MailingListCreate - | Permission::MailingListUpdate - | Permission::MailingListDelete - | Permission::RoleList - | Permission::RoleGet - | Permission::RoleCreate - | Permission::RoleUpdate - | Permission::RoleDelete - | Permission::PrincipalList - | Permission::PrincipalGet - | Permission::PrincipalCreate - | Permission::PrincipalUpdate - | Permission::PrincipalDelete - | Permission::Undelete - | Permission::DkimSignatureCreate - | Permission::DkimSignatureGet - | Permission::ApiKeyList - | Permission::ApiKeyGet - | Permission::ApiKeyCreate - | Permission::ApiKeyUpdate - | Permission::ApiKeyDelete - | Permission::SpamFilterTrain - | Permission::SpamFilterTest - ) || self.is_user_permission() - } - - // SPDX-SnippetEnd -} diff --git a/crates/directory/src/core/secret.rs b/crates/directory/src/core/secret.rs deleted file mode 100644 index 3bf2c337..00000000 --- a/crates/directory/src/core/secret.rs +++ /dev/null @@ -1,263 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::Principal; -use crate::PrincipalData; -use argon2::Argon2; -use compact_str::ToCompactString; -use mail_builder::encoders::base64::base64_encode; -use mail_parser::decoders::base64::base64_decode; -use password_hash::PasswordHash; -use pbkdf2::Pbkdf2; -use pwhash::{bcrypt, bsdi_crypt, md5_crypt, sha1_crypt, sha256_crypt, sha512_crypt, unix_crypt}; -use scrypt::Scrypt; -use sha1::Digest; -use sha1::Sha1; -use sha2::Sha256; -use sha2::Sha512; -use tokio::sync::oneshot; -use totp_rs::TOTP; - -impl Principal { - pub async fn verify_secret( - &self, - code: &str, - only_app_pass: bool, - is_ordered: bool, - ) -> trc::Result { - let mut seen_password = false; - let mut password = None; - let mut otp_auth = None; - - for item in &self.data { - match item { - PrincipalData::OtpAuth(secret) => { - if !only_app_pass { - otp_auth = Some(secret); - } - seen_password = true; - } - PrincipalData::Password(secret) => { - if !only_app_pass { - password = Some(secret); - } - seen_password = true; - } - PrincipalData::AppPassword(secret) => { - // App passwords do not require TOTP - if let Some((_, app_secret)) = - secret.strip_prefix("$app$").and_then(|s| s.split_once('$')) - && verify_secret_hash(app_secret, code).await? - { - return Ok(true); - } - - seen_password = true; - } - _ => { - if seen_password && is_ordered { - // Password-related secrets are expected to be at the beginning of the list - break; - } - } - } - } - - // Validate TOTP - match (otp_auth, password) { - (Some(otp_auth), Some(password)) => { - if let Some((code, totp_token)) = code.rsplit_once('$').filter(|(c, t)| { - !c.is_empty() - && (6..=8).contains(&t.len()) - && t.as_bytes().iter().all(|b| b.is_ascii_digit()) - }) { - let result = verify_secret_hash(password, code).await? - && TOTP::from_url(otp_auth) - .map_err(|err| { - trc::AuthEvent::Error - .reason(err) - .details(otp_auth.to_compact_string()) - })? - .check_current(totp_token) - .unwrap_or(false); - Ok(result) - } else if verify_secret_hash(password, code).await? { - // Only let the client know if the TOTP code is missing - // if the password is correct - - Err(trc::AuthEvent::MissingTotp.into_err()) - } else { - Ok(false) - } - } - (None, Some(password)) => verify_secret_hash(password, code).await, - _ => Ok(false), - } - } -} - -async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> trc::Result { - if hashed_secret.starts_with("$argon2") - || hashed_secret.starts_with("$pbkdf2") - || hashed_secret.starts_with("$scrypt") - { - let (tx, rx) = oneshot::channel(); - let secret = secret.to_string(); - let hashed_secret = hashed_secret.to_string(); - - tokio::task::spawn_blocking(move || match PasswordHash::new(&hashed_secret) { - Ok(hash) => { - tx.send(Ok(hash - .verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], &secret) - .is_ok())) - .ok(); - } - Err(err) => { - tx.send(Err(trc::AuthEvent::Error - .reason(err) - .details(hashed_secret))) - .ok(); - } - }); - - match rx.await { - Ok(result) => result, - Err(err) => Err(trc::EventType::Server(trc::ServerEvent::ThreadError) - .caused_by(trc::location!()) - .reason(err)), - } - } else if hashed_secret.starts_with("$2") { - // Blowfish crypt - Ok(bcrypt::verify(secret, hashed_secret)) - } else if hashed_secret.starts_with("$6$") { - // SHA-512 crypt - Ok(sha512_crypt::verify(secret, hashed_secret)) - } else if hashed_secret.starts_with("$5$") { - // SHA-256 crypt - Ok(sha256_crypt::verify(secret, hashed_secret)) - } else if hashed_secret.starts_with("$sha1") { - // SHA-1 crypt - Ok(sha1_crypt::verify(secret, hashed_secret)) - } else if hashed_secret.starts_with("$1") { - // MD5 based hash - Ok(md5_crypt::verify(secret, hashed_secret)) - } else { - Err(trc::AuthEvent::Error - .into_err() - .details(hashed_secret.to_string())) - } -} - -pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> trc::Result { - if hashed_secret.starts_with('$') { - verify_hash_prefix(hashed_secret, secret).await - } else if hashed_secret.starts_with('_') { - // Enhanced DES-based hash - Ok(bsdi_crypt::verify(secret, hashed_secret)) - } else if let Some(hashed_secret) = hashed_secret.strip_prefix('{') { - if let Some((algo, hashed_secret)) = hashed_secret.split_once('}') { - match algo { - "ARGON2" | "ARGON2I" | "ARGON2ID" | "PBKDF2" => { - verify_hash_prefix(hashed_secret, secret).await - } - "SHA" => { - // SHA-1 - let mut hasher = Sha1::new(); - hasher.update(secret.as_bytes()); - Ok( - String::from_utf8( - base64_encode(&hasher.finalize()[..]).unwrap_or_default(), - ) - .unwrap() - == hashed_secret, - ) - } - "SSHA" => { - // Salted SHA-1 - let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default(); - let hash = decoded.get(..20).unwrap_or_default(); - let salt = decoded.get(20..).unwrap_or_default(); - let mut hasher = Sha1::new(); - hasher.update(secret.as_bytes()); - hasher.update(salt); - Ok(&hasher.finalize()[..] == hash) - } - "SHA256" => { - // Verify hash - let mut hasher = Sha256::new(); - hasher.update(secret.as_bytes()); - Ok( - String::from_utf8( - base64_encode(&hasher.finalize()[..]).unwrap_or_default(), - ) - .unwrap() - == hashed_secret, - ) - } - "SSHA256" => { - // Salted SHA-256 - let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default(); - let hash = decoded.get(..32).unwrap_or_default(); - let salt = decoded.get(32..).unwrap_or_default(); - let mut hasher = Sha256::new(); - hasher.update(secret.as_bytes()); - hasher.update(salt); - Ok(&hasher.finalize()[..] == hash) - } - "SHA512" => { - // SHA-512 - let mut hasher = Sha512::new(); - hasher.update(secret.as_bytes()); - Ok( - String::from_utf8( - base64_encode(&hasher.finalize()[..]).unwrap_or_default(), - ) - .unwrap() - == hashed_secret, - ) - } - "SSHA512" => { - // Salted SHA-512 - let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default(); - let hash = decoded.get(..64).unwrap_or_default(); - let salt = decoded.get(64..).unwrap_or_default(); - let mut hasher = Sha512::new(); - hasher.update(secret.as_bytes()); - hasher.update(salt); - Ok(&hasher.finalize()[..] == hash) - } - "MD5" => { - // MD5 - let digest = md5::compute(secret.as_bytes()); - Ok( - String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap() - == hashed_secret, - ) - } - "CRYPT" | "crypt" => { - if hashed_secret.starts_with('$') { - verify_hash_prefix(hashed_secret, secret).await - } else { - // Unix crypt - Ok(unix_crypt::verify(secret, hashed_secret)) - } - } - "PLAIN" | "plain" | "CLEAR" | "clear" => Ok(hashed_secret == secret), - _ => Err(trc::AuthEvent::Error - .ctx(trc::Key::Reason, "Unsupported algorithm") - .details(hashed_secret.to_string())), - } - } else { - Err(trc::AuthEvent::Error - .into_err() - .details(hashed_secret.to_string())) - } - } else if !hashed_secret.is_empty() { - Ok(hashed_secret == secret) - } else { - Ok(false) - } -} diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 9dc75d19..05fb5e12 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -6,463 +6,55 @@ #![warn(clippy::large_futures)] +use crate::backend::oidc::OpenIdDirectory; use ahash::AHashMap; -use backend::{ - imap::{ImapDirectory, ImapError}, - ldap::LdapDirectory, - memory::MemoryDirectory, - smtp::SmtpDirectory, - sql::SqlDirectory, -}; -use core::cache::CachedDirectory; +use backend::{ldap::LdapDirectory, sql::SqlDirectory}; use deadpool::managed::PoolError; use ldap3::LdapError; -use mail_send::Credentials; -use proc_macros::EnumMethods; +use registry::types::id::Id; use std::{fmt::Debug, sync::Arc}; -use store::Store; -use trc::ipc::bitset::Bitset; -use types::collection::Collection; pub mod backend; pub mod core; -pub struct Directory { - pub store: DirectoryInner, - pub cache: Option, +pub enum Credentials { + Basic { username: String, secret: String }, + Bearer { token: String }, } -pub const FALLBACK_ADMIN_ID: u32 = u32::MAX; - -#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] -pub struct Principal { - pub id: u32, - pub typ: Type, - pub name: String, - pub data: Vec, -} - -#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq, Hash)] -pub enum PrincipalData { - Password(String), - - // Permissions and memberships - Tenant(u32), - MemberOf(u32), - Role(u32), - List(u32), - Permission { permission_id: u32, grant: bool }, - - // Quotas - DiskQuota(u64), - DirectoryQuota { quota: u32, typ: Type }, - ObjectQuota { quota: u32, typ: Collection }, - - // Profile data - Description(String), - PrimaryEmail(String), - EmailAlias(String), - Picture(String), - ExternalMember(String), - Url(String), - Locale(String), - - // Secrets - AppPassword(String), - OtpAuth(String), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PermissionGrant { - pub permission: Permission, - pub grant: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MemberOf { - pub principal_id: u32, - pub typ: Type, -} - -#[derive( - rkyv::Archive, - rkyv::Deserialize, - rkyv::Serialize, - Debug, - Default, - Clone, - Copy, - PartialEq, - Eq, - serde::Serialize, - serde::Deserialize, - Hash, -)] -#[serde(rename_all = "camelCase")] -pub enum Type { - #[default] - Individual = 0, - Group = 1, - Resource = 2, - Location = 3, - List = 5, - Other = 6, - Domain = 7, - Tenant = 8, - Role = 9, - ApiKey = 10, - OauthClient = 11, -} - -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods, -)] -#[serde(rename_all = "kebab-case")] -pub enum Permission { - // WARNING: add new ids at the end (TODO: use static ids) - - // Admin - Impersonate, - UnlimitedRequests, - UnlimitedUploads, - DeleteSystemFolders_, - MessageQueueList, - MessageQueueGet, - MessageQueueUpdate, - MessageQueueDelete, - OutgoingReportList, - OutgoingReportGet, - OutgoingReportDelete, - IncomingReportList, - IncomingReportGet, - IncomingReportDelete, - SettingsList, - SettingsUpdate, - SettingsDelete, - SettingsReload, - IndividualList, - IndividualGet, - IndividualUpdate, - IndividualDelete, - IndividualCreate, - GroupList, - GroupGet, - GroupUpdate, - GroupDelete, - GroupCreate, - DomainList, - DomainGet, - DomainCreate, - DomainUpdate, - DomainDelete, - TenantList, - TenantGet, - TenantCreate, - TenantUpdate, - TenantDelete, - MailingListList, - MailingListGet, - MailingListCreate, - MailingListUpdate, - MailingListDelete, - RoleList, - RoleGet, - RoleCreate, - RoleUpdate, - RoleDelete, - PrincipalList, - PrincipalGet, - PrincipalCreate, - PrincipalUpdate, - PrincipalDelete, - BlobFetch, - PurgeBlobStore, - PurgeDataStore, - PurgeInMemoryStore, - PurgeAccount, - FtsReindex, - Undelete, - DkimSignatureCreate, - DkimSignatureGet, - SpamFilterUpdate, - WebadminUpdate, - LogsView, - SpamFilterTrain, - Restart, - TracingList, - TracingGet, - TracingLive, - MetricsList, - MetricsLive, - - // Generic - Authenticate, - AuthenticateOauth, - EmailSend, - EmailReceive, - - // Account Management - ManageEncryption, - ManagePasswords, - - // JMAP - JmapEmailGet, - JmapMailboxGet, - JmapThreadGet, - JmapIdentityGet, - JmapEmailSubmissionGet, - JmapPushSubscriptionGet, - JmapSieveScriptGet, - JmapVacationResponseGet, - JmapPrincipalGet, - JmapQuotaGet, - JmapBlobGet, - JmapEmailSet, - JmapMailboxSet, - JmapIdentitySet, - JmapEmailSubmissionSet, - JmapPushSubscriptionSet, - JmapSieveScriptSet, - JmapVacationResponseSet, - JmapEmailChanges, - JmapMailboxChanges, - JmapThreadChanges, - JmapIdentityChanges, - JmapEmailSubmissionChanges, - JmapQuotaChanges, - JmapEmailCopy, - JmapBlobCopy, - JmapEmailImport, - JmapEmailParse, - JmapEmailQueryChanges, - JmapMailboxQueryChanges, - JmapEmailSubmissionQueryChanges, - JmapSieveScriptQueryChanges, - JmapPrincipalQueryChanges, - JmapQuotaQueryChanges, - JmapEmailQuery, - JmapMailboxQuery, - JmapEmailSubmissionQuery, - JmapSieveScriptQuery, - JmapPrincipalQuery, - JmapQuotaQuery, - JmapSearchSnippet, - JmapSieveScriptValidate, - JmapBlobLookup, - JmapBlobUpload, - JmapEcho, - - // IMAP - ImapAuthenticate, - ImapAclGet, - ImapAclSet, - ImapMyRights, - ImapListRights, - ImapAppend, - ImapCapability, - ImapId, - ImapCopy, - ImapMove, - ImapCreate, - ImapDelete, - ImapEnable, - ImapExpunge, - ImapFetch, - ImapIdle, - ImapList, - ImapLsub, - ImapNamespace, - ImapRename, - ImapSearch, - ImapSort, - ImapSelect, - ImapExamine, - ImapStatus, - ImapStore, - ImapSubscribe, - ImapThread, - - // POP3 - Pop3Authenticate, - Pop3List, - Pop3Uidl, - Pop3Stat, - Pop3Retr, - Pop3Dele, - - // ManageSieve - SieveAuthenticate, - SieveListScripts, - SieveSetActive, - SieveGetScript, - SievePutScript, - SieveDeleteScript, - SieveRenameScript, - SieveCheckScript, - SieveHaveSpace, - - // API keys - ApiKeyList, - ApiKeyGet, - ApiKeyCreate, - ApiKeyUpdate, - ApiKeyDelete, - - // OAuth clients - OauthClientList, - OauthClientGet, - OauthClientCreate, - OauthClientUpdate, - OauthClientDelete, - - // OAuth client registration - OauthClientRegistration, - OauthClientOverride, - - AiModelInteract, - Troubleshoot, - SpamFilterTest, - - // WebDAV permissions - DavSyncCollection, - DavExpandProperty, - - DavPrincipalAcl, - DavPrincipalList, - DavPrincipalMatch, - DavPrincipalSearch, - DavPrincipalSearchPropSet, - - DavFilePropFind, - DavFilePropPatch, - DavFileGet, - DavFileMkCol, - DavFileDelete, - DavFilePut, - DavFileCopy, - DavFileMove, - DavFileLock, - DavFileAcl, - - DavCardPropFind, - DavCardPropPatch, - DavCardGet, - DavCardMkCol, - DavCardDelete, - DavCardPut, - DavCardCopy, - DavCardMove, - DavCardLock, - DavCardAcl, - DavCardQuery, - DavCardMultiGet, - - DavCalPropFind, - DavCalPropPatch, - DavCalGet, - DavCalMkCol, - DavCalDelete, - DavCalPut, - DavCalCopy, - DavCalMove, - DavCalLock, - DavCalAcl, - DavCalQuery, - DavCalMultiGet, - DavCalFreeBusyQuery, - - CalendarAlarms, - CalendarSchedulingSend, - CalendarSchedulingReceive, - - JmapAddressBookGet, - JmapAddressBookSet, - JmapAddressBookChanges, - - JmapContactCardGet, - JmapContactCardChanges, - JmapContactCardQuery, - JmapContactCardQueryChanges, - JmapContactCardSet, - JmapContactCardCopy, - JmapContactCardParse, - - JmapFileNodeGet, - JmapFileNodeSet, - JmapFileNodeChanges, - JmapFileNodeQuery, - JmapFileNodeQueryChanges, - - JmapPrincipalGetAvailability, - JmapPrincipalChanges, - - JmapShareNotificationGet, - JmapShareNotificationSet, - JmapShareNotificationChanges, - JmapShareNotificationQuery, - JmapShareNotificationQueryChanges, - - JmapCalendarGet, - JmapCalendarSet, - JmapCalendarChanges, - - JmapCalendarEventGet, - JmapCalendarEventSet, - JmapCalendarEventChanges, - JmapCalendarEventQuery, - JmapCalendarEventQueryChanges, - JmapCalendarEventCopy, - JmapCalendarEventParse, - - JmapCalendarEventNotificationGet, - JmapCalendarEventNotificationSet, - JmapCalendarEventNotificationChanges, - JmapCalendarEventNotificationQuery, - JmapCalendarEventNotificationQueryChanges, - - JmapParticipantIdentityGet, - JmapParticipantIdentitySet, - JmapParticipantIdentityChanges, - // TODO: Reuse _ suffixes for new permissions - // WARNING: add new ids at the end (TODO: use static ids) -} - -pub const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::()); -pub type Permissions = Bitset; - -pub const ROLE_ADMIN: u32 = u32::MAX; -pub const ROLE_TENANT_ADMIN: u32 = u32::MAX - 1; -pub const ROLE_USER: u32 = u32::MAX - 2; - -pub enum DirectoryInner { - Internal(Store), +pub enum Directory { Ldap(LdapDirectory), Sql(SqlDirectory), - OpenId(backend::oidc::OpenIdDirectory), - Imap(ImapDirectory), - Smtp(SmtpDirectory), - Memory(MemoryDirectory), + OpenId(OpenIdDirectory), } -pub enum QueryBy<'x> { - Name(&'x str), - Id(u32), - Credentials(&'x Credentials), +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Recipient { + Account(Account), + Group(Group), + Invalid, } -pub struct QueryParams<'x> { - pub by: QueryBy<'x>, - pub return_member_of: bool, - pub only_app_pass: bool, +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Account { + pub email: String, + pub email_aliases: Vec, + pub secret: Option, + pub is_authenticated: bool, + pub groups: Vec, + pub description: Option, } -impl Default for Directory { - fn default() -> Self { - Self { - store: DirectoryInner::Internal(Store::None), - cache: None, - } - } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Group { + pub email: String, + pub email_aliases: Vec, + pub description: Option, +} + +#[derive(Default, Clone, Debug)] +pub struct Directories { + pub directories: AHashMap>, } impl Debug for Directory { @@ -471,11 +63,6 @@ impl Debug for Directory { } } -#[derive(Default, Clone, Debug)] -pub struct Directories { - pub directories: AHashMap>, -} - trait IntoError { fn into_error(self) -> trc::Error; } @@ -492,42 +79,6 @@ impl IntoError for PoolError { } } -impl IntoError for PoolError { - fn into_error(self) -> trc::Error { - match self { - PoolError::Backend(error) => error.into_error(), - PoolError::Timeout(_) => trc::StoreEvent::PoolError - .into_err() - .details("Connection timed out"), - err => trc::StoreEvent::PoolError.reason(err), - } - } -} - -impl IntoError for PoolError { - fn into_error(self) -> trc::Error { - match self { - PoolError::Backend(error) => error.into_error(), - PoolError::Timeout(_) => trc::StoreEvent::PoolError - .into_err() - .details("Connection timed out"), - err => trc::StoreEvent::PoolError.reason(err), - } - } -} - -impl IntoError for ImapError { - fn into_error(self) -> trc::Error { - trc::ImapEvent::Error.into_err().reason(self) - } -} - -impl IntoError for mail_send::Error { - fn into_error(self) -> trc::Error { - trc::SmtpEvent::Error.into_err().reason(self) - } -} - impl IntoError for LdapError { fn into_error(self) -> trc::Error { if let LdapError::LdapResult { result } = &self { @@ -539,65 +90,3 @@ impl IntoError for LdapError { } } } - -impl From<&ArchivedType> for Type { - fn from(archived: &ArchivedType) -> Self { - match archived { - ArchivedType::Individual => Type::Individual, - ArchivedType::Group => Type::Group, - ArchivedType::Resource => Type::Resource, - ArchivedType::Location => Type::Location, - ArchivedType::List => Type::List, - ArchivedType::Other => Type::Other, - ArchivedType::Domain => Type::Domain, - ArchivedType::Tenant => Type::Tenant, - ArchivedType::Role => Type::Role, - ArchivedType::ApiKey => Type::ApiKey, - ArchivedType::OauthClient => Type::OauthClient, - } - } -} - -impl<'x> QueryParams<'x> { - pub fn name(name: &'x str) -> Self { - QueryParams { - by: QueryBy::Name(name), - return_member_of: false, - only_app_pass: false, - } - } - - pub fn credentials(credentials: &'x Credentials) -> Self { - QueryParams { - by: QueryBy::Credentials(credentials), - return_member_of: false, - only_app_pass: false, - } - } - - pub fn id(id: u32) -> Self { - QueryParams { - by: QueryBy::Id(id), - return_member_of: false, - only_app_pass: false, - } - } - - pub fn by(by: QueryBy<'x>) -> Self { - QueryParams { - by, - return_member_of: false, - only_app_pass: false, - } - } - - pub fn with_return_member_of(mut self, return_member_of: bool) -> Self { - self.return_member_of = return_member_of; - self - } - - pub fn with_only_app_pass(mut self, only_app_pass: bool) -> Self { - self.only_app_pass = only_app_pass; - self - } -} diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index c0b7c7c3..0ed1ed50 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -42,10 +42,10 @@ jemallocator = "0.5.0" [features] #default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "azure", "nats", "enterprise"] default = ["rocks", "enterprise"] -sqlite = ["store/sqlite"] +sqlite = ["store/sqlite", "directory/sqlite"] foundationdb = ["store/foundation", "common/foundation"] -postgres = ["store/postgres"] -mysql = ["store/mysql"] +postgres = ["store/postgres", "directory/postgres"] +mysql = ["store/mysql", "directory/mysql"] rocks = ["store/rocks"] s3 = ["store/s3"] redis = ["store/redis", "coordinator/redis"] diff --git a/crates/store/src/backend/mysql/main.rs b/crates/store/src/backend/mysql/main.rs index 99d16864..b5c63eea 100644 --- a/crates/store/src/backend/mysql/main.rs +++ b/crates/store/src/backend/mysql/main.rs @@ -30,7 +30,7 @@ impl MysqlStore { .client_found_rows(true) .tcp_port(config.port as u16); - if config.enable_tls { + if config.use_tls { opts = opts.ssl_opts(Some( SslOpts::default() .with_danger_accept_invalid_certs(config.allow_invalid_certs) diff --git a/crates/store/src/backend/postgres/main.rs b/crates/store/src/backend/postgres/main.rs index fca6dbd8..be055408 100644 --- a/crates/store/src/backend/postgres/main.rs +++ b/crates/store/src/backend/postgres/main.rs @@ -47,7 +47,7 @@ impl PostgresStore { cfg.port = (replica.port as u16).into(); cfg.options = replica.options; replicas.push(Store::PostgreSQL(Arc::new(PostgresStore { - conn_pool: if config.enable_tls { + conn_pool: if config.use_tls { cfg.create_pool( Some(Runtime::Tokio1), MakeRustlsConnect::new(rustls_client_config(config.allow_invalid_certs)), @@ -60,7 +60,7 @@ impl PostgresStore { } let primary = Store::PostgreSQL(Arc::new(PostgresStore { - conn_pool: if config.enable_tls { + conn_pool: if config.use_tls { cfg.create_pool( Some(Runtime::Tokio1), MakeRustlsConnect::new(rustls_client_config(config.allow_invalid_certs)), diff --git a/crates/store/src/bootstrap/blob.rs b/crates/store/src/bootstrap/blob.rs index fd4d1bae..9a68c788 100644 --- a/crates/store/src/bootstrap/blob.rs +++ b/crates/store/src/bootstrap/blob.rs @@ -11,7 +11,7 @@ use registry::schema::{prelude::Object, structs}; impl BlobStore { pub async fn build(bp: &mut Bootstrap) -> Option { let result = match bp.setting_infallible::().await { - structs::BlobStore::Default => return None, + structs::BlobStore::Default => return Some(BlobStore::Store(bp.data_store.clone())), #[cfg(feature = "foundation")] structs::BlobStore::FoundationDb(foundation_db_store) => { crate::backend::foundationdb::FdbStore::open(foundation_db_store) diff --git a/crates/store/src/bootstrap/memory.rs b/crates/store/src/bootstrap/memory.rs index 98e04890..e7c6c4d4 100644 --- a/crates/store/src/bootstrap/memory.rs +++ b/crates/store/src/bootstrap/memory.rs @@ -11,7 +11,9 @@ use registry::schema::{prelude::Object, structs}; impl InMemoryStore { pub async fn build(bp: &mut Bootstrap) -> Option { let result = match bp.setting_infallible::().await { - structs::InMemoryStore::Default => return None, + structs::InMemoryStore::Default => { + return Some(InMemoryStore::Store(bp.data_store.clone())); + } #[cfg(feature = "redis")] structs::InMemoryStore::Redis(redis_store) => { crate::backend::redis::RedisStore::open_single(redis_store).await diff --git a/crates/store/src/bootstrap/search.rs b/crates/store/src/bootstrap/search.rs index c4345429..8733f3e4 100644 --- a/crates/store/src/bootstrap/search.rs +++ b/crates/store/src/bootstrap/search.rs @@ -15,7 +15,9 @@ use registry::schema::{prelude::Object, structs}; impl SearchStore { pub async fn build(bp: &mut Bootstrap) -> Option { let result = match bp.setting_infallible::().await { - structs::SearchStore::Default => return None, + structs::SearchStore::Default => { + return Some(SearchStore::Store(bp.data_store.clone())); + } structs::SearchStore::ElasticSearch(elastic_search_store) => { ElasticSearchStore::open(elastic_search_store).await } diff --git a/crates/store/src/registry/bootstrap.rs b/crates/store/src/registry/bootstrap.rs index e0d421ee..139cb7f4 100644 --- a/crates/store/src/registry/bootstrap.rs +++ b/crates/store/src/registry/bootstrap.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{RegistryStore, registry::RegistryObject}; +use crate::{RegistryStore, Store, registry::RegistryObject}; use registry::{ schema::{ prelude::Property, @@ -19,6 +19,7 @@ use registry::{ pub struct Bootstrap { pub registry: RegistryStore, + pub data_store: Store, pub errors: Vec, pub warnings: Vec, pub has_fatal_errors: bool, @@ -35,6 +36,7 @@ impl Bootstrap { has_fatal_errors: false, node: Node::default(), local: LocalSettings::default(), + data_store: Store::None, } } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 0bca1d0d..6e824d9b 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -7,10 +7,10 @@ edition = "2024" default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "nats", "azure", "foundationdb"] #default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "foundationdb"] #default = ["rocks"] -sqlite = ["store/sqlite"] +sqlite = ["store/sqlite", "directory/sqlite"] foundationdb = ["store/foundation", "common/foundation"] -postgres = ["store/postgres"] -mysql = ["store/mysql"] +postgres = ["store/postgres", "directory/postgres"] +mysql = ["store/mysql", "directory/mysql"] rocks = ["store/rocks"] s3 = ["store/s3"] redis = ["store/redis", "coordinator/redis"]