diff options
128 files changed, 2404 insertions, 895 deletions
@@ -1667,6 +1667,7 @@ dependencies = [ "parking_lot", "password-hash", "pbkdf2", + "proc_macros", "pwhash", "regex", "rustls 0.23.12", @@ -1963,7 +1964,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] @@ -4505,6 +4506,7 @@ name = "pop3" version = "0.9.4" dependencies = [ "common", + "directory", "imap", "jmap", "jmap_proto", @@ -4660,6 +4662,15 @@ dependencies = [ ] [[package]] +name = "proc_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] name = "prometheus" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/cli/src/modules/domain.rs b/crates/cli/src/modules/domain.rs index fa859b82..6ba2b69b 100644 --- a/crates/cli/src/modules/domain.rs +++ b/crates/cli/src/modules/domain.rs @@ -6,7 +6,7 @@ use std::borrow::Cow; -use prettytable::{Attr, Cell, Row, Table, format}; +use prettytable::{format, Attr, Cell, Row, Table}; use reqwest::Method; use serde_json::Value; diff --git a/crates/common/src/config/jmap/settings.rs b/crates/common/src/config/jmap/settings.rs index 97899ebf..700a5464 100644 --- a/crates/common/src/config/jmap/settings.rs +++ b/crates/common/src/config/jmap/settings.rs @@ -83,8 +83,6 @@ pub struct JmapConfig { pub encrypt: bool, pub encrypt_append: bool, - pub principal_allow_lookups: bool, - pub capabilities: BaseCapabilities, pub session_purge_frequency: SimpleCron, pub account_purge_frequency: SimpleCron, @@ -371,9 +369,6 @@ impl JmapConfig { push_max_total: config .property_or_default("jmap.push.max-total", "100") .unwrap_or(100), - principal_allow_lookups: config - .property("jmap.principal.allow-lookups") - .unwrap_or(true), encrypt: config .property_or_default("storage.encryption.enable", "true") .unwrap_or(true), diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index efe2f250..7ff3b897 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -19,7 +19,7 @@ use config::{ storage::Storage, telemetry::Metrics, }; -use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy, Type}; +use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy}; use expr::if_block::IfBlock; use listener::{ blocked::{AllowedIps, BlockedIps}, @@ -227,7 +227,7 @@ impl Core { credentials: &Credentials<String>, remote_ip: IpAddr, return_member_of: bool, - ) -> trc::Result<Principal<u32>> { + ) -> trc::Result<Principal> { // First try to authenticate the user against the default directory let result = match directory .query(QueryBy::Credentials(credentials), return_member_of) @@ -237,9 +237,9 @@ impl Core { trc::event!( Auth(trc::AuthEvent::Success), AccountName = credentials.login().to_string(), - AccountId = principal.id, + AccountId = principal.id(), SpanId = session_id, - Type = principal.typ.as_str(), + Type = principal.typ().as_str(), ); return Ok(principal); @@ -268,7 +268,6 @@ impl Core { Auth(trc::AuthEvent::Success), AccountName = username.clone(), SpanId = session_id, - Type = Type::Superuser.as_str(), ); return Ok(Principal::fallback_admin(fallback_pass)); @@ -289,8 +288,8 @@ impl Core { Auth(trc::AuthEvent::Success), AccountName = username.to_string(), SpanId = session_id, - AccountId = principal.id, - Type = principal.typ.as_str(), + AccountId = principal.id(), + Type = principal.typ().as_str(), ); return Ok(principal); diff --git a/crates/common/src/listener/acme/jose.rs b/crates/common/src/listener/acme/jose.rs index 40626dd5..0b8ee99f 100644 --- a/crates/common/src/listener/acme/jose.rs +++ b/crates/common/src/listener/acme/jose.rs @@ -23,7 +23,11 @@ pub(crate) fn sign( let combined = format!("{}.{}", &protected, &payload); let signature = key .sign(&SystemRandom::new(), combined.as_bytes()) - .map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).caused_by(trc::location!()).reason(err))?; + .map_err(|err| { + trc::EventType::Acme(trc::AcmeEvent::Error) + .caused_by(trc::location!()) + .reason(err) + })?; let signature = URL_SAFE_NO_PAD.encode(signature.as_ref()); let body = Body { protected, @@ -31,7 +35,8 @@ pub(crate) fn sign( signature, }; - serde_json::to_string(&body).map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err)) + serde_json::to_string(&body) + .map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err)) } pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> trc::Result<String> { diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index 30cf4c7f..ac23f369 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -6,6 +6,7 @@ resolver = "2" [dependencies] utils = { path = "../utils" } +proc_macros = { path = "../utils/proc-macros" } store = { path = "../store" } trc = { path = "../trc" } jmap_proto = { path = "../jmap-proto" } diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs index acfc1ed9..49fd6fbf 100644 --- a/crates/directory/src/backend/imap/lookup.rs +++ b/crates/directory/src/backend/imap/lookup.rs @@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy}; use super::{ImapDirectory, ImapError}; impl ImapDirectory { - pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> { + pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> { if let QueryBy::Credentials(credentials) = query { let mut client = self .pool diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index cbe1d353..3d86b831 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -12,7 +12,7 @@ use store::{ use crate::{Principal, QueryBy, Type}; -use super::{manage::ManageDirectory, PrincipalIdType}; +use super::{manage::ManageDirectory, PrincipalField, PrincipalIdType}; #[allow(async_fn_in_trait)] pub trait DirectoryStore: Sync + Send { @@ -20,7 +20,7 @@ pub trait DirectoryStore: Sync + Send { &self, by: QueryBy<'_>, return_member_of: bool, - ) -> trc::Result<Option<Principal<u32>>>; + ) -> trc::Result<Option<Principal>>; async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>>; async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>; @@ -34,7 +34,7 @@ impl DirectoryStore for Store { &self, by: QueryBy<'_>, return_member_of: bool, - ) -> trc::Result<Option<Principal<u32>>> { + ) -> trc::Result<Option<Principal>> { let (account_id, secret) = match by { QueryBy::Name(name) => (self.get_account_id(name).await?, None), QueryBy::Id(account_id) => (account_id.into(), None), @@ -53,7 +53,7 @@ impl DirectoryStore for Store { if let Some(account_id) = account_id { match ( - self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory( + self.get_value::<Principal>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), ))) .await?, @@ -61,13 +61,19 @@ impl DirectoryStore for Store { ) { (Some(mut principal), Some(secret)) if principal.verify_secret(secret).await? => { if return_member_of { - principal.member_of = self.get_member_of(principal.id).await?; + principal.set( + PrincipalField::MemberOf, + self.get_member_of(principal.id).await?, + ); } Ok(Some(principal)) } (Some(mut principal), None) => { if return_member_of { - principal.member_of = self.get_member_of(principal.id).await?; + principal.set( + PrincipalField::MemberOf, + self.get_member_of(principal.id).await?, + ); } Ok(Some(principal)) @@ -143,11 +149,11 @@ impl DirectoryStore for Store { let mut results = Vec::new(); for account_id in self.email_to_ids(address).await? { if let Some(email) = self - .get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory( + .get_value::<Principal>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), ))) .await? - .and_then(|p| p.emails.into_iter().next()) + .and_then(|mut p| p.take_str(PrincipalField::Emails)) { results.push(email); } diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 710245b5..0838b391 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -28,11 +28,7 @@ pub trait ManageDirectory: Sized { async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>>; async fn get_member_of(&self, account_id: u32) -> trc::Result<Vec<u32>>; async fn get_members(&self, account_id: u32) -> trc::Result<Vec<u32>>; - async fn create_account( - &self, - principal: Principal<String>, - members: Vec<String>, - ) -> trc::Result<u32>; + async fn create_account(&self, principal: Principal) -> trc::Result<u32>; async fn update_account( &self, by: QueryBy<'_>, @@ -44,12 +40,12 @@ pub trait ManageDirectory: Sized { filter: Option<&str>, typ: Option<Type>, ) -> trc::Result<Vec<String>>; - async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>>; + async fn map_group_ids(&self, principal: Principal) -> trc::Result<Principal>; async fn map_principal( &self, - principal: Principal<String>, + principal: Principal, create_if_missing: bool, - ) -> trc::Result<Principal<u32>>; + ) -> trc::Result<Principal>; async fn map_group_names( &self, members: Vec<String>, @@ -62,11 +58,11 @@ pub trait ManageDirectory: Sized { impl ManageDirectory for Store { async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>> { - self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory( + self.get_value::<Principal>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), ))) .await - .map(|v| if let Some(v) = v { Some(v.name) } else { None }) + .map(|v| v.and_then(|mut v| v.take_str(PrincipalField::Name))) .caused_by(trc::location!()) } @@ -108,9 +104,9 @@ impl ManageDirectory for Store { ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Dynamic(0))), Principal { typ: Type::Individual, - name: name.to_string(), ..Default::default() - }, + } + .with_field(PrincipalField::Name, name.to_string()), ); match self @@ -133,39 +129,42 @@ impl ManageDirectory for Store { } } - async fn create_account( - &self, - principal: Principal<String>, - members: Vec<String>, - ) -> trc::Result<u32> { + async fn create_account(&self, mut principal: Principal) -> trc::Result<u32> { // Make sure the principal has a name - if principal.name.is_empty() { + let name = principal.name().to_lowercase(); + if name.is_empty() { return Err(err_missing(PrincipalField::Name)); } // Map group names - let mut principal = self - .map_principal(principal, false) + let members = self + .map_group_names( + principal + .take(PrincipalField::Members) + .map(|v| v.into_str_array()) + .unwrap_or_default(), + false, + ) .await .caused_by(trc::location!())?; - let members = self - .map_group_names(members, false) + let mut principal = self + .map_principal(principal, false) .await .caused_by(trc::location!())?; // Make sure new name is not taken - principal.name = principal.name.to_lowercase(); if self - .get_account_id(&principal.name) + .get_account_id(&name) .await .caused_by(trc::location!())? .is_some() { - return Err(err_exists(PrincipalField::Name, principal.name)); + return Err(err_exists(PrincipalField::Name, name)); } + principal.set(PrincipalField::Name, name); // Make sure the e-mail is not taken and validate domain - for email in principal.emails.iter_mut() { + for email in principal.iter_mut_str(PrincipalField::Emails) { *email = email.to_lowercase(); if self.rcpt(email).await.caused_by(trc::location!())? { return Err(err_exists(PrincipalField::Emails, email.to_string())); @@ -183,14 +182,14 @@ impl ManageDirectory for Store { // Write principal let mut batch = BatchBuilder::new(); - let ptype = DynamicPrincipalIdType(principal.typ.into_base_type()); + let ptype = DynamicPrincipalIdType(principal.typ); batch .with_account_id(u32::MAX) .with_collection(Collection::Principal) .create_document() .assert_value( ValueClass::Directory(DirectoryClass::NameToId( - principal.name.clone().into_bytes(), + principal.name().to_string().into_bytes(), )), (), ) @@ -199,30 +198,40 @@ impl ManageDirectory for Store { principal.clone(), ) .set( - ValueClass::Directory(DirectoryClass::NameToId(principal.name.into_bytes())), + ValueClass::Directory(DirectoryClass::NameToId( + principal + .take_str(PrincipalField::Name) + .unwrap() + .into_bytes(), + )), ptype, ); // Write email to id mapping - for email in principal.emails { - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())), - ptype, - ); + if let Some(emails) = principal + .take(PrincipalField::Emails) + .map(|v| v.into_str_array()) + { + for email in emails { + batch.set( + ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())), + ptype, + ); + } } // Write membership - for member_of in principal.member_of { + for member_of in principal.iter_int(PrincipalField::MemberOf) { batch.set( ValueClass::Directory(DirectoryClass::MemberOf { principal_id: MaybeDynamicId::Dynamic(0), - member_of: MaybeDynamicId::Static(member_of), + member_of: MaybeDynamicId::Static(member_of as u32), }), vec![], ); batch.set( ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(member_of), + principal_id: MaybeDynamicId::Static(member_of as u32), has_member: MaybeDynamicId::Dynamic(0), }), vec![], @@ -261,8 +270,8 @@ impl ManageDirectory for Store { QueryBy::Credentials(_) => unreachable!(), }; - let principal = self - .get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory( + let mut principal = self + .get_value::<Principal>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), ))) .await @@ -288,14 +297,21 @@ impl ManageDirectory for Store { let mut batch = BatchBuilder::new(); batch .with_account_id(account_id) - .clear(DirectoryClass::NameToId(principal.name.into_bytes())) + .clear(DirectoryClass::NameToId( + principal + .take_str(PrincipalField::Name) + .unwrap_or_default() + .into_bytes(), + )) .clear(DirectoryClass::Principal(MaybeDynamicId::Static( account_id, ))) .clear(DirectoryClass::UsedQuota(account_id)); - for email in principal.emails { - batch.clear(DirectoryClass::EmailToId(email.into_bytes())); + if let Some(emails) = principal.take_str_array(PrincipalField::Emails) { + for email in emails { + batch.clear(DirectoryClass::EmailToId(email.into_bytes())); + } } for member_id in self @@ -352,7 +368,7 @@ impl ManageDirectory for Store { // Fetch principal let mut principal = self - .get_value::<HashedValue<Principal<u32>>>(ValueKey::from(ValueClass::Directory( + .get_value::<HashedValue<Principal>>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), ))) .await @@ -371,8 +387,7 @@ impl ManageDirectory for Store { // Apply changes let mut batch = BatchBuilder::new(); - let ptype = - PrincipalIdType::new(account_id, principal.inner.typ.into_base_type()).serialize(); + let ptype = PrincipalIdType::new(account_id, principal.inner.typ).serialize(); let update_principal = !changes.is_empty() && !changes .iter() @@ -391,7 +406,7 @@ impl ManageDirectory for Store { (PrincipalAction::Set, PrincipalField::Name, PrincipalValue::String(new_name)) => { // Make sure new name is not taken let new_name = new_name.to_lowercase(); - if principal.inner.name != new_name { + if principal.inner.name() != new_name { if self .get_account_id(&new_name) .await @@ -402,10 +417,10 @@ impl ManageDirectory for Store { } batch.clear(ValueClass::Directory(DirectoryClass::NameToId( - principal.inner.name.as_bytes().to_vec(), + principal.inner.name().as_bytes().to_vec(), ))); - principal.inner.name.clone_from(&new_name); + principal.inner.set(PrincipalField::Name, new_name.clone()); batch.set( ValueClass::Directory(DirectoryClass::NameToId(new_name.into_bytes())), @@ -413,35 +428,27 @@ impl ManageDirectory for Store { ); } } - (PrincipalAction::Set, PrincipalField::Type, PrincipalValue::String(new_type)) => { - if let Some(new_type) = Type::parse(&new_type) { - if matches!(principal.inner.typ, Type::Individual | Type::Superuser) - && matches!(new_type, Type::Individual | Type::Superuser) - { - principal.inner.typ = new_type; - continue; - } - } - return Err(trc::ManageEvent::NotSupported.caused_by(trc::location!())); - } ( PrincipalAction::Set, PrincipalField::Secrets, - PrincipalValue::StringList(secrets), + value @ (PrincipalValue::StringList(_) | PrincipalValue::String(_)), ) => { - principal.inner.secrets = secrets; + principal.inner.set(PrincipalField::Secrets, value); } ( PrincipalAction::AddItem, PrincipalField::Secrets, PrincipalValue::String(secret), ) => { - if !principal.inner.secrets.contains(&secret) { - if secret.is_otp_auth() && !principal.inner.secrets.is_empty() { + if !principal + .inner + .has_str_value(PrincipalField::Secrets, &secret) + { + if secret.is_otp_auth() { // Add OTP Auth URLs to the beginning of the list - principal.inner.secrets.insert(0, secret); + principal.inner.prepend_str(PrincipalField::Secrets, secret); } else { - principal.inner.secrets.push(secret); + principal.inner.append_str(PrincipalField::Secrets, secret); } } } @@ -451,14 +458,17 @@ impl ManageDirectory for Store { PrincipalValue::String(secret), ) => { if secret.is_app_password() || secret.is_otp_auth() { + principal.inner.retain_str(PrincipalField::Secrets, |v| { + *v != secret && !v.starts_with(&secret) + }); + } else if !secret.is_empty() { principal .inner - .secrets - .retain(|v| *v != secret && !v.starts_with(&secret)); - } else if !secret.is_empty() { - principal.inner.secrets.retain(|v| *v != secret); + .retain_str(PrincipalField::Secrets, |v| *v != secret); } else { - principal.inner.secrets.retain(|v| !v.is_password()); + principal + .inner + .retain_str(PrincipalField::Secrets, |v| !v.is_password()); } } ( @@ -467,13 +477,15 @@ impl ManageDirectory for Store { PrincipalValue::String(description), ) => { if !description.is_empty() { - principal.inner.description = Some(description); + principal + .inner + .set(PrincipalField::Description, description); } else { - principal.inner.description = None; + principal.inner.remove(PrincipalField::Description); } } (PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) => { - principal.inner.quota = quota; + principal.inner.set(PrincipalField::Quota, quota); } // Emails @@ -488,7 +500,7 @@ impl ManageDirectory for Store { .map(|v| v.to_lowercase()) .collect::<Vec<_>>(); for email in &emails { - if !principal.inner.emails.contains(email) { + if !principal.inner.has_str_value(PrincipalField::Emails, email) { if self.rcpt(email).await.caused_by(trc::location!())? { return Err(err_exists(PrincipalField::Emails, email.to_string())); } @@ -510,7 +522,7 @@ impl ManageDirectory for Store { } } - for email in &principal.inner.emails { + for email in principal.inner.iter_str(PrincipalField::Emails) { if !emails.contains(email) { batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( email.as_bytes().to_vec(), @@ -518,7 +530,7 @@ impl ManageDirectory for Store { } } - principal.inner.emails = emails; + principal.inner.set(PrincipalField::Emails, emails); } ( PrincipalAction::AddItem, @@ -526,7 +538,10 @@ impl ManageDirectory for Store { PrincipalValue::String(email), ) => { let email = email.to_lowercase(); - if !principal.inner.emails.contains(&email) { + if !principal + .inner + .has_str_value(PrincipalField::Emails, &email) + { if self.rcpt(&email).await.caused_by(trc::location!())? { return Err(err_exists(PrincipalField::Emails, email)); } @@ -545,7 +560,7 @@ impl ManageDirectory for Store { )), ptype.clone(), ); - principal.inner.emails.push(email); + principal.inner.append_str(PrincipalField::Emails, email); } } ( @@ -554,11 +569,16 @@ impl ManageDirectory for Store { PrincipalValue::String(email), ) => { let email = email.to_lowercase(); - if let Some(pos) = principal.inner.emails.iter().position(|v| *v == email) { + if principal + .inner + .has_str_value(PrincipalField::Emails, &email) + { + principal + .inner + .retain_str(PrincipalField::Emails, |v| *v != email); batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( - email.as_bytes().to_vec(), + email.into_bytes(), ))); - principal.inner.emails.remove(pos); } } @@ -806,49 +826,37 @@ impl ManageDirectory for Store { self.write(batch.build()).await.map(|_| ()) } - async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>> { - let mut mapped = Principal { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - secrets: principal.secrets, - emails: principal.emails, - member_of: Vec::with_capacity(principal.member_of.len()), - description: principal.description, - }; - - for account_id in principal.member_of { - if let Some(name) = self - .get_account_name(account_id) - .await - .caused_by(trc::location!())? - { - mapped.member_of.push(name); + async fn map_group_ids(&self, mut principal: Principal) -> trc::Result<Principal> { + if let Some(member_of) = principal.take_int_array(PrincipalField::MemberOf) { + for account_id in member_of { + if let Some(name) = self + .get_account_name(account_id as u32) + .await + .caused_by(trc::location!())? + { + principal.append_str(PrincipalField::MemberOf, name); + } } } - Ok(mapped) + Ok(principal) } async fn map_principal( &self, - principal: Principal<String>, + mut principal: Principal, create_if_missing: bool, - ) -> trc::Result<Principal<u32>> { - Ok(Principal { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - secrets: principal.secrets, - emails: principal.emails, - member_of: self - .map_group_names(principal.member_of, create_if_missing) - .await - .caused_by(trc::location!())?, - description: principal.description, - }) + ) -> trc::Result<Principal> { + if let Some(member_of) = principal.take_str_array(PrincipalField::MemberOf) { + principal.set( + PrincipalField::MemberOf, + self.map_group_names(member_of, create_if_missing) + .await + .caused_by(trc::location!())?, + ); + } + + Ok(principal) } async fn map_group_names( @@ -914,21 +922,20 @@ impl ManageDirectory for Store { for (account_id, account_name) in results { let principal = self - .get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory( + .get_value::<Principal>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), ))) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(account_id.to_string()))?; if filters.iter().all(|f| { - principal.name.to_lowercase().contains(f) + principal.name().to_lowercase().contains(f) || principal - .description + .description() .as_ref() .map_or(false, |d| d.to_lowercase().contains(f)) || principal - .emails - .iter() + .iter_str(PrincipalField::Emails) .any(|email| email.to_lowercase().contains(f)) }) { filtered.push(account_name); @@ -1010,7 +1017,7 @@ impl ManageDirectory for Store { } } -impl SerializeWithId for Principal<u32> { +impl SerializeWithId for Principal { fn serialize_with_id(&self, ids: &AssignedIds) -> trc::Result<Vec<u8>> { let mut principal = self.clone(); principal.id = ids.last_document_id().caused_by(trc::location!())?; @@ -1018,8 +1025,8 @@ impl SerializeWithId for Principal<u32> { } } -impl From<Principal<u32>> for MaybeDynamicValue { - fn from(principal: Principal<u32>) -> Self { +impl From<Principal> for MaybeDynamicValue { + fn from(principal: Principal) -> Self { MaybeDynamicValue::Dynamic(Box::new(principal)) } } @@ -1040,21 +1047,6 @@ impl From<DynamicPrincipalIdType> for MaybeDynamicValue { } } -impl From<Principal<String>> for Principal<u32> { - fn from(principal: Principal<String>) -> Self { - Principal { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - secrets: principal.secrets, - emails: principal.emails, - member_of: Vec::with_capacity(0), - description: principal.description, - } - } -} - pub fn err_missing(field: impl Into<trc::Value>) -> trc::Error { trc::ManageEvent::MissingParameter.ctx(trc::Key::Key, field) } diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index f0421612..b8ac95df 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -9,45 +9,70 @@ pub mod manage; use std::{fmt::Display, slice::Iter, str::FromStr}; +use ahash::AHashMap; use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN}; use utils::codec::leb128::Leb128Iterator; use crate::{Principal, Type}; +const INT_MARKER: u8 = 1 << 7; + pub(super) struct PrincipalIdType { pub account_id: u32, pub typ: Type, } -impl Serialize for Principal<u32> { +impl Serialize for Principal { fn serialize(self) -> Vec<u8> { (&self).serialize() } } -impl Serialize for &Principal<u32> { +impl Serialize for &Principal { fn serialize(self) -> Vec<u8> { let mut serializer = KeySerializer::new( - U32_LEN * 3 + U32_LEN * 2 + 2 - + self.name.len() - + self.emails.iter().map(|s| s.len()).sum::<usize>() - + self.secrets.iter().map(|s| s.len()).sum::<usize>() - + self.description.as_ref().map(|s| s.len()).unwrap_or(0), + + self + .fields + .values() + .map(|v| v.serialized_size() + 1) + .sum::<usize>(), ) - .write(1u8) + .write(2u8) .write_leb128(self.id) .write(self.typ as u8) - .write_leb128(self.quota) - .write_leb128(self.name.len()) - .write(self.name.as_bytes()) - .write_leb128(self.description.as_ref().map_or(0, |s| s.len())) - .write(self.description.as_deref().unwrap_or_default().as_bytes()); - - for list in [&self.secrets, &self.emails] { - serializer = serializer.write_leb128(list.len()); - for value in list { - serializer = serializer.write_leb128(value.len()).write(value.as_bytes()); + .write_leb128(self.fields.len()); + + for (k, v) in &self.fields { + let id = k.id(); + + match v { + PrincipalValue::String(v) => { + serializer = serializer + .write(id) + .write_leb128(1usize) + .write_leb128(v.len()) + .write(v.as_bytes()); + } + PrincipalValue::StringList(l) => { + serializer = serializer.write(id).write_leb128(l.len()); + for v in l { + serializer = serializer.write_leb128(v.len()).write(v.as_bytes()); + } + } + PrincipalValue::Integer(v) => { + serializer = serializer + .write(id | INT_MARKER) + .write_leb128(1usize) + .write_leb128(*v); + } + PrincipalValue::IntegerList(l) => { + serializer = serializer.write(id | INT_MARKER).write_leb128(l.len()); + for v in l { + serializer = serializer.write_leb128(*v); + } + } } } @@ -55,7 +80,7 @@ impl Serialize for &Principal<u32> { } } -impl Deserialize for Principal<u32> { +impl Deserialize for Principal { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { deserialize(bytes).ok_or_else(|| { trc::StoreEvent::DataCorruption @@ -98,32 +123,89 @@ impl PrincipalIdType { } } -fn deserialize(bytes: &[u8]) -> Option<Principal<u32>> { +fn deserialize(bytes: &[u8]) -> Option<Principal> { let mut bytes = bytes.iter(); - if bytes.next()? != &1 { - return None; - } - Principal { - id: bytes.next_leb128()?, - typ: Type::from_u8(*bytes.next()?), - quota: bytes.next_leb128()?, - name: deserialize_string(&mut bytes)?, - description: deserialize_string(&mut bytes).map(|v| { - if !v.is_empty() { - Some(v) + let version = *bytes.next()?; + let id = bytes.next_leb128()?; + let type_id = *bytes.next()?; + let typ = Type::from_u8(type_id); + + match version { + 1 => { + // Version 1 (legacy) + let mut principal = Principal { + id, + typ, + ..Default::default() + }; + + principal.set(PrincipalField::Quota, bytes.next_leb128::<u64>()?); + principal.set(PrincipalField::Name, deserialize_string(&mut bytes)?); + if let Some(description) = deserialize_string(&mut bytes).filter(|s| !s.is_empty()) { + principal.set(PrincipalField::Description, description); + } + for key in [PrincipalField::Secrets, PrincipalField::Emails] { + for _ in 0..bytes.next_leb128::<usize>()? { + principal.append_str(key, deserialize_string(&mut bytes)?); + } + } + + if type_id != 4 { + principal } else { - None + principal.into_superuser() + } + .into() + } + 2 => { + // Version 2 + let num_fields = bytes.next_leb128::<usize>()?; + + let mut principal = Principal { + id, + typ, + fields: AHashMap::with_capacity(num_fields), + }; + + for _ in 0..num_fields { + let id = *bytes.next()?; + let num_values = bytes.next_leb128::<usize>()?; + + if (id & INT_MARKER) == 0 { + let field = PrincipalField::from_id(id)?; + if num_values == 1 { + principal.set(field, deserialize_string(&mut bytes)?); + } else { + let mut values = Vec::with_capacity(num_values); + for _ in 0..num_values { + values.push(deserialize_string(&mut bytes)?); + } + principal.set(field, values); + } + } else { + let field = PrincipalField::from_id(id & !INT_MARKER)?; + if num_values == 1 { + principal.set(field, bytes.next_leb128::<u64>()?); + } else { + let mut values = Vec::with_capacity(num_values); + for _ in 0..num_values { + values.push(bytes.next_leb128::<u64>()?); + } + principal.set(field, values); + } + } } - })?, - secrets: deserialize_string_list(&mut bytes)?, - emails: deserialize_string_list(&mut bytes)?, - member_of: Vec::new(), + + principal.into() + } + _ => None, } - .into() } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] pub enum PrincipalField { #[serde(rename = "name")] Name, @@ -166,6 +248,7 @@ pub enum PrincipalValue { String(String), StringList(Vec<String>), Integer(u64), + IntegerList(Vec<u64>), } impl PrincipalUpdate { @@ -201,6 +284,33 @@ impl Display for PrincipalField { } 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, + } + } + + pub fn from_id(id: u8) -> Option<Self> { + 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), + _ => None, + } + } + pub fn as_str(&self) -> &'static str { match self { PrincipalField::Name => "name", @@ -224,24 +334,16 @@ fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option<String> { String::from_utf8(string).ok() } -fn deserialize_string_list(bytes: &mut Iter<'_, u8>) -> Option<Vec<String>> { - let len = bytes.next_leb128()?; - let mut list = Vec::with_capacity(len); - for _ in 0..len { - list.push(deserialize_string(bytes)?); - } - Some(list) -} - impl Type { pub fn parse(value: &str) -> Option<Self> { match value { "individual" => Some(Type::Individual), - "superuser" => Some(Type::Superuser), "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 _ => None, } } @@ -252,18 +354,12 @@ impl Type { 1 => Type::Group, 2 => Type::Resource, 3 => Type::Location, - 4 => Type::Superuser, + 4 => Type::Individual, // legacy 5 => Type::List, + 7 => Type::Tenant, _ => Type::Other, } } - - pub fn into_base_type(self) -> Self { - match self { - Type::Superuser => Type::Individual, - any => any, - } - } } impl FromStr for Type { @@ -275,7 +371,6 @@ impl FromStr for Type { } pub trait SpecialSecrets { - fn is_disabled(&self) -> bool; fn is_otp_auth(&self) -> bool; fn is_app_password(&self) -> bool; fn is_password(&self) -> bool; @@ -285,10 +380,6 @@ impl<T> SpecialSecrets for T where T: AsRef<str>, { - fn is_disabled(&self) -> bool { - self.as_ref() == "$disabled$" - } - fn is_otp_auth(&self) -> bool { self.as_ref().starts_with("otpauth://") } @@ -298,6 +389,6 @@ where } fn is_password(&self) -> bool { - !self.is_disabled() && !self.is_otp_auth() && !self.is_app_password() + !self.is_otp_auth() && !self.is_app_password() } } diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index 63baa697..1576de22 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -7,7 +7,10 @@ use ldap3::{Ldap, LdapConnAsync, Scope, SearchEntry}; use mail_send::Credentials; -use crate::{backend::internal::manage::ManageDirectory, IntoError, Principal, QueryBy, Type}; +use crate::{ + backend::internal::{manage::ManageDirectory, PrincipalField}, + IntoError, Principal, QueryBy, Type, +}; use super::{LdapDirectory, LdapMappings}; @@ -16,7 +19,7 @@ impl LdapDirectory { &self, by: QueryBy<'_>, return_member_of: bool, - ) -> trc::Result<Option<Principal<u32>>> { + ) -> trc::Result<Option<Principal>> { let mut conn = self.pool.get().await.map_err(|err| err.into_error())?; let mut account_id = None; let account_name; @@ -125,11 +128,11 @@ impl LdapDirectory { .get_or_create_account_id(&account_name) .await?; } - principal.name = account_name; + principal.append_str(PrincipalField::Name, account_name); // Obtain groups - if return_member_of && !principal.member_of.is_empty() { - for member_of in principal.member_of.iter_mut() { + if return_member_of && principal.has_field(PrincipalField::MemberOf) { + for member_of in principal.iter_mut_str(PrincipalField::MemberOf) { if member_of.contains('=') { let (rs, _res) = conn .search( @@ -163,8 +166,8 @@ impl LdapDirectory { .await .map(Some) } else { - principal.member_of.clear(); - Ok(Some(principal.into())) + principal.remove(PrincipalField::MemberOf); + Ok(Some(principal)) } } @@ -370,7 +373,7 @@ impl LdapDirectory { &self, conn: &mut Ldap, filter: &str, - ) -> trc::Result<Option<Principal<String>>> { + ) -> trc::Result<Option<Principal>> { conn.search( &self.mappings.base_dn, Scope::Subtree, @@ -400,39 +403,47 @@ impl LdapDirectory { } impl LdapMappings { - fn entry_to_principal(&self, entry: SearchEntry) -> Principal<String> { + fn entry_to_principal(&self, entry: SearchEntry) -> Principal { let mut principal = Principal::default(); for (attr, value) in entry.attrs { if self.attr_name.contains(&attr) { - principal.name = value.into_iter().next().unwrap_or_default(); + principal.set( + PrincipalField::Name, + value.into_iter().next().unwrap_or_default(), + ); } else if self.attr_secret.contains(&attr) { - principal.secrets.extend(value); + for item in value { + principal.append_str(PrincipalField::Secrets, item); + } } else if self.attr_email_address.contains(&attr) { - for value in value { - if principal.emails.is_empty() { - principal.emails.push(value); - } else { - principal.emails.insert(0, value); - } + for item in value { + principal.prepend_str(PrincipalField::Emails, item); } } else if self.attr_email_alias.contains(&attr) { - principal.emails.extend(value); + for item in value { + principal.append_str(PrincipalField::Emails, item); + } } else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) { - if principal.description.is_none() || idx == 0 { - principal.description = value.into_iter().next(); + if !principal.has_field(PrincipalField::Description) || idx == 0 { + principal.set( + PrincipalField::Description, + value.into_iter().next().unwrap_or_default(), + ); } } else if self.attr_groups.contains(&attr) { - principal.member_of.extend(value); + for item in value { + principal.append_str(PrincipalField::MemberOf, item); + } } else if self.attr_quota.contains(&attr) { - if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() { - principal.quota = quota; + if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse::<u64>() { + principal.set(PrincipalField::Quota, quota); } } else if self.attr_type.contains(&attr) { for value in value { match value.to_ascii_lowercase().as_str() { "admin" | "administrator" | "root" | "superuser" => { - principal.typ = Type::Superuser + principal = principal.into_superuser(); } "posixaccount" | "individual" | "person" | "inetorgperson" => { principal.typ = Type::Individual diff --git a/crates/directory/src/backend/memory/config.rs b/crates/directory/src/backend/memory/config.rs index d3df6d86..2ed6bf08 100644 --- a/crates/directory/src/backend/memory/config.rs +++ b/crates/directory/src/backend/memory/config.rs @@ -7,7 +7,10 @@ use store::Store; use utils::config::{utils::AsKey, Config}; -use crate::{backend::internal::manage::ManageDirectory, Principal, Type}; +use crate::{ + backend::internal::{manage::ManageDirectory, PrincipalField}, + Principal, Type, +}; use super::{EmailType, MemoryDirectory}; @@ -34,12 +37,13 @@ impl MemoryDirectory { let name = config .value_require((prefix.as_str(), "principals", lookup_id, "name"))? .to_string(); - let typ = match config.value((prefix.as_str(), "principals", lookup_id, "class")) { - Some("individual") => Type::Individual, - Some("admin") => Type::Superuser, - Some("group") => Type::Group, - _ => Type::Individual, - }; + 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 @@ -57,14 +61,30 @@ impl MemoryDirectory { }) .ok()?; + // Create principal + let mut principal = if is_superuser { + Principal { + id, + typ, + ..Default::default() + } + .into_superuser() + } else { + Principal { + id, + typ, + ..Default::default() + } + }; + // Obtain group ids - let mut member_of = Vec::new(); for group in config .values((prefix.as_str(), "principals", lookup_id, "member-of")) .map(|(_, s)| s.to_string()) .collect::<Vec<_>>() { - member_of.push( + principal.append_int( + PrincipalField::MemberOf, directory .data_store .get_or_create_account_id(&group) @@ -83,7 +103,6 @@ impl MemoryDirectory { } // Parse email addresses - let mut emails = Vec::new(); for (pos, (_, email)) in config .values((prefix.as_str(), "principals", lookup_id, "email")) .enumerate() @@ -102,7 +121,7 @@ impl MemoryDirectory { directory.domains.insert(domain.to_lowercase()); } - emails.push(email.to_lowercase()); + principal.append_str(PrincipalField::Emails, email.to_lowercase()); } // Parse mailing lists @@ -119,23 +138,20 @@ impl MemoryDirectory { } } - directory.principals.push(Principal { - name: name.clone(), - secrets: config - .values((prefix.as_str(), "principals", lookup_id, "secret")) - .map(|(_, v)| v.to_string()) - .collect(), - typ, - description: config - .value((prefix.as_str(), "principals", lookup_id, "description")) - .map(|v| v.to_string()), - quota: config - .property((prefix.as_str(), "principals", lookup_id, "quota")) - .unwrap_or(0), - member_of, - id, - emails, - }); + principal.set(PrincipalField::Name, name.clone()); + for (_, secret) in config.values((prefix.as_str(), "principals", lookup_id, "secret")) { + principal.append_str(PrincipalField::Secrets, secret.to_string()); + } + if let Some(description) = + config.value((prefix.as_str(), "principals", lookup_id, "description")) + { + principal.set(PrincipalField::Description, description.to_string()); + } + if let Some(quota) = + config.property::<u64>((prefix.as_str(), "principals", lookup_id, "quota")) + { + principal.set(PrincipalField::Quota, quota); + } } Some(directory) diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs index 979f70b1..60270097 100644 --- a/crates/directory/src/backend/memory/lookup.rs +++ b/crates/directory/src/backend/memory/lookup.rs @@ -6,16 +6,16 @@ use mail_send::Credentials; -use crate::{Principal, QueryBy}; +use crate::{backend::internal::PrincipalField, Principal, QueryBy}; use super::{EmailType, MemoryDirectory}; impl MemoryDirectory { - pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> { + pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal>> { match by { QueryBy::Name(name) => { for principal in &self.principals { - if principal.name == name { + if principal.name() == name { return Ok(Some(principal.clone())); } } @@ -35,7 +35,7 @@ impl MemoryDirectory { }; for principal in &self.principals { - if &principal.name == username { + if principal.name() == username { return if principal.verify_secret(secret).await? { Ok(Some(principal.clone())) } else { @@ -87,8 +87,10 @@ impl MemoryDirectory { if let EmailType::List(uid) = item { for principal in &self.principals { if principal.id == *uid { - if let Some(addr) = principal.emails.first() { - result.push(addr.clone()) + if let Some(addr) = + principal.iter_str(PrincipalField::Emails).next() + { + result.push(addr.to_string()) } break; } diff --git a/crates/directory/src/backend/memory/mod.rs b/crates/directory/src/backend/memory/mod.rs index fde9160b..d1971e1f 100644 --- a/crates/directory/src/backend/memory/mod.rs +++ b/crates/directory/src/backend/memory/mod.rs @@ -14,7 +14,7 @@ pub mod lookup; #[derive(Debug)] pub struct MemoryDirectory { - principals: Vec<Principal<u32>>, + principals: Vec<Principal>, emails_to_ids: AHashMap<String, Vec<EmailType>>, pub(crate) data_store: Store, domains: AHashSet<String>, diff --git a/crates/directory/src/backend/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs index 8bf684ba..62285426 100644 --- a/crates/directory/src/backend/smtp/lookup.rs +++ b/crates/directory/src/backend/smtp/lookup.rs @@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy}; use super::{SmtpClient, SmtpDirectory}; impl SmtpDirectory { - pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> { + pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> { if let QueryBy::Credentials(credentials) = query { self.pool .get() @@ -93,7 +93,7 @@ impl SmtpClient { async fn authenticate( &mut self, credentials: &Credentials<String>, - ) -> trc::Result<Option<Principal<u32>>> { + ) -> trc::Result<Option<Principal>> { match self .client .authenticate(credentials, &self.capabilities) diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index 1638af3f..e86b12e4 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -8,7 +8,10 @@ use mail_send::Credentials; use store::{NamedRows, Rows, Value}; use trc::AddContext; -use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; +use crate::{ + backend::internal::{manage::ManageDirectory, PrincipalField, PrincipalValue}, + Principal, QueryBy, Type, +}; use super::{SqlDirectory, SqlMappings}; @@ -17,7 +20,7 @@ impl SqlDirectory { &self, by: QueryBy<'_>, return_member_of: bool, - ) -> trc::Result<Option<Principal<u32>>> { + ) -> trc::Result<Option<Principal>> { let mut account_id = None; let account_name; let mut secret = None; @@ -99,22 +102,20 @@ impl SqlDirectory { .await .caused_by(trc::location!())?; } - principal.name = account_name; + principal.set(PrincipalField::Name, account_name); // Obtain members if return_member_of && !self.mappings.query_members.is_empty() { for row in self .store - .query::<Rows>( - &self.mappings.query_members, - vec![principal.name.clone().into()], - ) + .query::<Rows>(&self.mappings.query_members, vec![principal.name().into()]) .await .caused_by(trc::location!())? .rows { if let Some(Value::Text(account_id)) = row.values.first() { - principal.member_of.push( + principal.append_int( + PrincipalField::MemberOf, self.data_store .get_or_create_account_id(account_id) .await @@ -126,15 +127,16 @@ impl SqlDirectory { // Obtain emails if !self.mappings.query_emails.is_empty() { - principal.emails = self - .store - .query::<Rows>( - &self.mappings.query_emails, - vec![principal.name.clone().into()], - ) - .await - .caused_by(trc::location!())? - .into(); + principal.set( + PrincipalField::Emails, + PrincipalValue::StringList( + self.store + .query::<Rows>(&self.mappings.query_emails, vec![principal.name().into()]) + .await + .caused_by(trc::location!())? + .into(), + ), + ); } Ok(Some(principal)) @@ -204,7 +206,7 @@ impl SqlDirectory { } impl SqlMappings { - pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal<u32>> { + pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal> { let mut principal = Principal::default(); if let Some(row) = rows.rows.into_iter().next() { @@ -215,22 +217,25 @@ impl SqlMappings { .any(|c| name.eq_ignore_ascii_case(c)) { if let Value::Text(secret) = value { - principal.secrets.push(secret.into_owned()); + principal.append_str(PrincipalField::Secrets, secret.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::Superuser, + "admin" | "superuser" | "administrator" => { + principal.typ = Type::Individual; + principal = principal.into_superuser(); + } _ => (), } } else if name.eq_ignore_ascii_case(&self.column_description) { if let Value::Text(text) = value { - principal.description = text.into_owned().into(); + principal.set(PrincipalField::Description, text.into_owned()); } } else if name.eq_ignore_ascii_case(&self.column_quota) { if let Value::Integer(quota) = value { - principal.quota = quota as u64; + principal.set(PrincipalField::Quota, quota as u64); } } } diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs index f907b32f..77108e3e 100644 --- a/crates/directory/src/core/dispatch.rs +++ b/crates/directory/src/core/dispatch.rs @@ -15,7 +15,7 @@ impl Directory { &self, by: QueryBy<'_>, return_member_of: bool, - ) -> trc::Result<Option<Principal<u32>>> { + ) -> trc::Result<Option<Principal>> { match &self.store { DirectoryInner::Internal(store) => store.query(by, return_member_of).await, DirectoryInner::Ldap(store) => store.query(by, return_member_of).await, diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs index ae094f1e..c9ee3326 100644 --- a/crates/directory/src/core/mod.rs +++ b/crates/directory/src/core/mod.rs @@ -7,4 +7,5 @@ pub mod cache; pub mod config; pub mod dispatch; +pub mod principal; pub mod secret; diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs new file mode 100644 index 00000000..bdec5d7a --- /dev/null +++ b/crates/directory/src/core/principal.rs @@ -0,0 +1,490 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::collections::hash_map::Entry; + +use store::U64_LEN; + +use crate::{ + backend::internal::{PrincipalField, PrincipalValue}, + Principal, Type, +}; + +impl Principal { + 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() + } + + 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<u64> { + self.fields.get(&key).and_then(|v| v.as_int()) + } + + pub fn take(&mut self, key: PrincipalField) -> Option<PrincipalValue> { + self.fields.remove(&key) + } + + pub fn take_str(&mut self, key: PrincipalField) -> Option<String> { + 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_str_array(&mut self, key: PrincipalField) -> Option<Vec<String>> { + self.take(key).map(|v| v.into_str_array()) + } + + pub fn take_int_array(&mut self, key: PrincipalField) -> Option<Vec<u64>> { + self.take(key).map(|v| v.into_int_array()) + } + + pub fn iter_str( + &self, + key: PrincipalField, + ) -> Box<dyn Iterator<Item = &String> + 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<dyn Iterator<Item = &mut String> + 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<dyn Iterator<Item = u64> + 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<dyn Iterator<Item = &mut u64> + 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<u64>) -> &mut Self { + let value = value.into(); + match self.fields.entry(key) { + Entry::Occupied(v) => { + let v = v.into_mut(); + + match v { + PrincipalValue::IntegerList(v) => { + v.push(value); + } + PrincipalValue::Integer(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<String>) -> &mut Self { + let value = value.into(); + match self.fields.entry(key) { + Entry::Occupied(v) => { + let v = v.into_mut(); + + match v { + PrincipalValue::StringList(v) => { + v.push(value); + } + PrincipalValue::String(s) => { + *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<String>) -> &mut Self { + let value = value.into(); + match self.fields.entry(key) { + Entry::Occupied(v) => { + let v = v.into_mut(); + + match v { + PrincipalValue::StringList(v) => { + v.insert(0, value); + } + PrincipalValue::String(s) => { + *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<PrincipalValue>) -> &mut Self { + self.fields.insert(key, value.into()); + self + } + + pub fn with_field(mut self, key: PrincipalField, value: impl Into<PrincipalValue>) -> Self { + self.set(key, value); + self + } + + pub fn with_opt_field( + mut self, + key: PrincipalField, + value: Option<impl Into<PrincipalValue>>, + ) -> 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).map_or(false, |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).map_or(false, |v| match v { + PrincipalValue::Integer(v) => *v == value, + PrincipalValue::IntegerList(l) => l.iter().any(|v| *v == value), + PrincipalValue::String(_) | PrincipalValue::StringList(_) => false, + }) + } + + 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<PrincipalValue> { + self.fields.remove(&key) + } + + pub fn retain_str<F>(&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<F>(&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); + } + } + _ => {} + } + } + } + + pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self { + Principal { + id: u32::MAX, + typ: Type::Individual, + ..Default::default() + } + .with_field(PrincipalField::Name, "Fallback Administrator") + .with_field( + PrincipalField::Secrets, + PrincipalValue::String(fallback_pass.into()), + ) + .into_superuser() + } + + pub fn into_superuser(mut self) -> Self { + let todo = "add role"; + self + } +} + +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<u64> { + match self { + PrincipalValue::Integer(v) => Some(*v), + PrincipalValue::IntegerList(v) => v.first().copied(), + _ => None, + } + } + + pub fn iter_str(&self) -> Box<dyn Iterator<Item = &String> + 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<dyn Iterator<Item = &mut String> + 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<dyn Iterator<Item = u64> + 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<dyn Iterator<Item = &mut u64> + 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<String> { + 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<u64> { + 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, + } + } +} + +impl From<u64> for PrincipalValue { + fn from(v: u64) -> Self { + Self::Integer(v) + } +} + +impl From<String> for PrincipalValue { + fn from(v: String) -> Self { + Self::String(v) + } +} + +impl From<&str> for PrincipalValue { + fn from(v: &str) -> Self { + Self::String(v.to_string()) + } +} + +impl From<Vec<String>> for PrincipalValue { + fn from(v: Vec<String>) -> Self { + Self::StringList(v) + } +} + +impl From<Vec<u64>> for PrincipalValue { + fn from(v: Vec<u64>) -> Self { + Self::IntegerList(v) + } +} + +impl From<u32> for PrincipalValue { + fn from(v: u32) -> Self { + Self::Integer(v as u64) + } +} + +impl From<Vec<u32>> for PrincipalValue { + fn from(v: Vec<u32>) -> Self { + Self::IntegerList(v.into_iter().map(|v| v as u64).collect()) + } +} + +impl Type { + pub fn to_jmap(&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", + } + } + + pub fn as_str(&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", + } + } +} diff --git a/crates/directory/src/core/secret.rs b/crates/directory/src/core/secret.rs index 8a07c10a..cbe27d6a 100644 --- a/crates/directory/src/core/secret.rs +++ b/crates/directory/src/core/secret.rs @@ -18,10 +18,11 @@ use sha2::Sha512; use tokio::sync::oneshot; use totp_rs::TOTP; +use crate::backend::internal::PrincipalField; use crate::backend::internal::SpecialSecrets; use crate::Principal; -impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> { +impl Principal { pub async fn verify_secret(&self, mut code: &str) -> trc::Result<bool> { let mut totp_token = None; let mut is_totp_token_missing = false; @@ -30,12 +31,10 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> { let mut is_authenticated = false; let mut is_app_authenticated = false; - for secret in &self.secrets { - if secret.is_disabled() { - // Account is disabled, no need to check further + let todo = "validate authenticate permission"; - return Ok(false); - } else if secret.is_otp_auth() { + for secret in self.iter_str(PrincipalField::Secrets) { + if secret.is_otp_auth() { if !is_totp_verified && !is_totp_token_missing { is_totp_required = true; @@ -99,7 +98,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> { } else { if is_totp_verified { // TOTP URL appeared after password hash in secrets list - for secret in &self.secrets { + for secret in self.iter_str(PrincipalField::Secrets) { if secret.is_password() && verify_secret_hash(secret, code).await? { return Ok(true); } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index bb6e1e99..07d320d3 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -10,6 +10,7 @@ use std::{fmt::Debug, sync::Arc}; use ahash::AHashMap; use backend::{ imap::{ImapDirectory, ImapError}, + internal::{PrincipalField, PrincipalValue}, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory, @@ -18,6 +19,7 @@ use backend::{ use deadpool::managed::PoolError; use ldap3::LdapError; use mail_send::Credentials; +use proc_macros::EnumMethods; use store::Store; pub mod backend; @@ -28,24 +30,12 @@ pub struct Directory { pub cache: Option<CachedDirectory>, } -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct Principal<T> { - #[serde(default, skip)] - pub id: u32, - #[serde(rename = "type")] - pub typ: Type, - #[serde(default)] - pub quota: u64, - pub name: String, - #[serde(default)] - pub secrets: Vec<String>, - #[serde(default)] - pub emails: Vec<String>, - #[serde(default)] - #[serde(rename = "memberOf")] - pub member_of: Vec<T>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option<String>, +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Principal { + pub(crate) id: u32, + pub(crate) typ: Type, + + pub(crate) fields: AHashMap<PrincipalField, PrincipalValue>, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -59,14 +49,178 @@ pub enum Type { Resource = 2, #[serde(rename = "location")] Location = 3, - #[serde(rename = "superuser")] - Superuser = 4, #[serde(rename = "list")] List = 5, #[serde(rename = "other")] Other = 6, + #[serde(rename = "tenant")] + Tenant = 7, } +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods, +)] +#[serde(rename_all = "camelCase")] +pub enum Permission { + // Admin + Impersonate, + UnlimitedRequests, + UnlimitedUploads, + DeleteSystemFolders, + MessageQueueList, + MessageQueueGet, + MessageQueueUpdate, + MessageQueueDelete, + OutgoingReportList, + OutgoingReportGet, + OutgoingReportDelete, + IncomingReportList, + IncomingReportGet, + IncomingReportDelete, + SettingsList, + SettingsUpdate, + SettingsDelete, + SettingsReload, + PrincipalList, + PrincipalGet, + PrincipalUpdate, + PrincipalDelete, + PrincipalCreate, + DomainList, + DomainGet, + DomainCreate, + DomainUpdate, + DomainDelete, + BlobFetch, + PurgeBlobStore, + PurgeDataStore, + PurgeLookupStore, + PurgeAccount, + Undelete, + DkimSignatureCreate, + DkimSignatureGet, + UpdateSpamFilter, + UpdateWebadmin, + LogsView, + SieveRun, + Restart, + TracingList, + TracingGet, + TracingLive, + MetricsList, + MetricsLive, + + // Generic + Authenticate, + AuthenticateOauth, + + // 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, + + // SMTP + SmtpAuthenticate, + + // POP3 + Pop3Authenticate, + Pop3List, + Pop3Uidl, + Pop3Stat, + Pop3Retr, + Pop3Dele, + + // ManageSieve + SieveAuthenticate, + SieveListScripts, + SieveSetActive, + SieveGetScript, + SievePutScript, + SieveDeleteScript, + SieveRenameScript, + SieveCheckScript, + SieveHaveSpace, +} + +pub const PERMISSION_BITMAP_SIZE: usize = + (Permission::COUNT + std::mem::size_of::<usize>() - 1) / std::mem::size_of::<usize>(); + pub enum DirectoryInner { Internal(Store), Ldap(LdapDirectory), @@ -82,20 +236,6 @@ pub enum QueryBy<'x> { Credentials(&'x Credentials<String>), } -impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> { - pub fn name(&self) -> &str { - &self.name - } - - pub fn has_name(&self) -> bool { - !self.name.is_empty() - } - - pub fn description(&self) -> Option<&str> { - self.description.as_deref() - } -} - impl Default for Directory { fn default() -> Self { Self { @@ -111,57 +251,11 @@ impl Debug for Directory { } } -impl Type { - pub fn to_jmap(&self) -> &'static str { - match self { - Self::Individual | Self::Superuser => "individual", - Self::Group => "group", - Self::Resource => "resource", - Self::Location => "location", - Self::Other => "other", - Self::List => "list", - } - } - - pub fn as_str(&self) -> &'static str { - match self { - Self::Individual => "Individual", - Self::Group => "Group", - Self::Resource => "Resource", - Self::Location => "Location", - Self::Superuser => "Superuser", - Self::List => "List", - Self::Other => "Other", - } - } -} - #[derive(Default, Clone, Debug)] pub struct Directories { pub directories: AHashMap<String, Arc<Directory>>, } -impl Principal<u32> { - pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self { - Principal { - id: u32::MAX, - typ: Type::Superuser, - quota: 0, - name: "Fallback Administrator".to_string(), - secrets: vec![fallback_pass.into()], - ..Default::default() - } - } -} - -impl<T: Ord> Principal<T> { - pub fn into_sorted(mut self) -> Self { - self.member_of.sort_unstable(); - self.emails.sort_unstable(); - self - } -} - trait IntoError { fn into_error(self) -> trc::Error; } diff --git a/crates/imap-proto/src/protocol/mod.rs b/crates/imap-proto/src/protocol/mod.rs index ec8caa5f..5aacec43 100644 --- a/crates/imap-proto/src/protocol/mod.rs +++ b/crates/imap-proto/src/protocol/mod.rs @@ -501,9 +501,12 @@ impl SerializeResponse for trc::Error { Some(ResponseCode::NonExistent.as_str()) } trc::EventType::Store(_) => Some(ResponseCode::ContactAdmin.as_str()), - trc::EventType::Limit(trc::LimitEvent::Quota) => Some(ResponseCode::OverQuota.as_str()), + trc::EventType::Limit(trc::LimitEvent::Quota) => { + Some(ResponseCode::OverQuota.as_str()) + } trc::EventType::Limit(_) => Some(ResponseCode::Limit.as_str()), trc::EventType::Auth(_) => Some(ResponseCode::AuthenticationFailed.as_str()), + trc::EventType::Security(_) => Some(ResponseCode::AuthorizationFailed.as_str()), _ => None, }) { diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 519323d5..abed8c8a 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -8,7 +8,7 @@ use common::{ config::jmap::settings::SpecialUse, listener::{limiter::InFlight, SessionStream}, }; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use imap_proto::protocol::list::Attribute; use jmap::{ auth::{acl::EffectiveAcl, AccessToken}, @@ -28,7 +28,7 @@ use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, Sessio impl<T: SessionStream> SessionData<T> { pub async fn new( session: &Session<T>, - access_token: &AccessToken, + access_token: Arc<AccessToken>, in_flight: Option<InFlight>, ) -> trc::Result<Self> { let mut session = SessionData { @@ -39,12 +39,14 @@ impl<T: SessionStream> SessionData<T> { session_id: session.session_id, mailboxes: Mutex::new(vec![]), state: access_token.state().into(), + access_token, in_flight, }; + let access_token = session.access_token.clone(); // Fetch mailboxes for the main account let mut mailboxes = vec![session - .fetch_account_mailboxes(session.account_id, None, access_token) + .fetch_account_mailboxes(session.account_id, None, &access_token) .await .caused_by(trc::location!())?]; @@ -65,11 +67,11 @@ impl<T: SessionStream> SessionData<T> { .query(QueryBy::Id(account_id), false) .await .unwrap_or_default() - .map(|p| p.name) + .and_then(|mut p| p.take_str(PrincipalField::Name)) .unwrap_or_else(|| Id::from(account_id).to_string()) ) .into(), - access_token, + &access_token, ) .await .caused_by(trc::location!())?, @@ -389,8 +391,8 @@ impl<T: SessionStream> SessionData<T> { .directory .query(QueryBy::Id(account_id), false) .await - .unwrap_or_default() - .map(|p| p.name) + .caused_by(trc::location!())? + .and_then(|mut p| p.take_str(PrincipalField::Name)) .unwrap_or_else(|| Id::from(account_id).to_string()) ); added_accounts.push( @@ -495,7 +497,7 @@ impl<T: SessionStream> SessionData<T> { .query(QueryBy::Id(account_id), false) .await .caused_by(trc::location!())? - .map(|p| p.name) + .and_then(|mut p| p.take_str(PrincipalField::Name)) .unwrap_or_else(|| Id::from(account_id).to_string()) ) .into() diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs index 92f812ae..d6834906 100644 --- a/crates/imap/src/core/mod.rs +++ b/crates/imap/src/core/mod.rs @@ -82,6 +82,7 @@ pub struct Session<T: SessionStream> { pub struct SessionData<T: SessionStream> { pub account_id: u32, + pub access_token: Arc<AccessToken>, pub jmap: JMAP, pub imap: Arc<Inner>, pub session_id: u64, @@ -239,6 +240,7 @@ impl<T: SessionStream> SessionData<T> { stream_tx: new_stream, state: self.state, in_flight: self.in_flight, + access_token: self.access_token, } } } diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index 92d39afa..499da72b 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -7,7 +7,7 @@ use std::{sync::Arc, time::Instant}; use common::listener::SessionStream; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, Permission, QueryBy}; use imap_proto::{ protocol::acl::{ Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights, @@ -36,13 +36,16 @@ use trc::AddContext; use utils::map::bitmap::Bitmap; use crate::{ - core::{MailboxId, Session, SessionData}, + core::{MailboxId, Session, SessionData, State}, op::ImapContext, spawn_op, }; impl<T: SessionStream> Session<T> { pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapAuthenticate)?; + let op_start = Instant::now(); let arguments = request.parse_acl(self.version)?; let is_rev2 = self.version.is_rev2(); @@ -69,7 +72,7 @@ impl<T: SessionStream> Session<T> { .query(QueryBy::Id(item.account_id), false) .await .imap_ctx(&arguments.tag, trc::location!())? - .map(|p| p.name) + .and_then(|mut p| p.take_str(PrincipalField::Name)) { let mut rights = Vec::new(); @@ -142,6 +145,9 @@ impl<T: SessionStream> Session<T> { } pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapMyRights)?; + let op_start = Instant::now(); let arguments = request.parse_acl(self.version)?; let data = self.state.session_data(); @@ -224,6 +230,9 @@ impl<T: SessionStream> Session<T> { } pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapAclSet)?; + let op_start = Instant::now(); let command = request.command; let arguments = request.parse_acl(self.version)?; @@ -252,7 +261,7 @@ impl<T: SessionStream> Session<T> { .id(arguments.tag.to_string()) .caused_by(trc::location!()) })? - .id; + .id(); // Prepare changes let mut changes = Object::with_capacity(1); @@ -381,6 +390,9 @@ impl<T: SessionStream> Session<T> { } pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapListRights)?; + let op_start = Instant::now(); let arguments = request.parse_acl(self.version)?; @@ -415,6 +427,15 @@ impl<T: SessionStream> Session<T> { ) .await } + + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + match &self.state { + State::Authenticated { data } | State::Selected { data, .. } => { + data.access_token.assert_has_permission(permission) + } + State::NotAuthenticated { .. } => Ok(()), + } + } } impl<T: SessionStream> SessionData<T> { diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index 70777d82..a056d842 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -6,6 +6,7 @@ use std::{sync::Arc, time::Instant}; +use directory::Permission; use imap_proto::{ protocol::{append::Arguments, select::HighestModSeq}, receiver::Request, @@ -25,6 +26,9 @@ use super::{ImapContext, ToModSeq}; impl<T: SessionStream> Session<T> { pub async fn handle_append(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapAppend)?; + let op_start = Instant::now(); let arguments = request.parse_append(self.version)?; let (data, selected_mailbox) = self.state.session_mailbox_state(); diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index a68254af..92ba8916 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -5,6 +5,7 @@ */ use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{authenticate::Mechanism, capability::Capability}, receiver::{self, Request}, @@ -121,6 +122,9 @@ impl<T: SessionStream> Session<T> { } }; + // Validate access + access_token.assert_has_permission(Permission::ImapAuthenticate)?; + // Cache access token let access_token = Arc::new(access_token); self.jmap.cache_access_token(access_token.clone()); @@ -128,7 +132,7 @@ impl<T: SessionStream> Session<T> { // Create session self.state = State::Authenticated { data: Arc::new( - SessionData::new(self, &access_token, in_flight) + SessionData::new(self, access_token, in_flight) .await .map_err(|err| err.id(tag.clone()))?, ), diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index e4814079..fc6c163c 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -8,6 +8,7 @@ use std::time::Instant; use crate::core::Session; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{ capability::{Capability, Response}, @@ -19,6 +20,9 @@ use imap_proto::{ impl<T: SessionStream> Session<T> { pub async fn handle_capability(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapCapability)?; + let op_start = Instant::now(); trc::event!( Imap(trc::ImapEvent::Capabilities), @@ -45,6 +49,9 @@ impl<T: SessionStream> Session<T> { } pub async fn handle_id(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapId)?; + let op_start = Instant::now(); trc::event!( Imap(trc::ImapEvent::Id), diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs index 13d45c04..84043f62 100644 --- a/crates/imap/src/op/copy_move.rs +++ b/crates/imap/src/op/copy_move.rs @@ -6,6 +6,7 @@ use std::{sync::Arc, time::Instant}; +use directory::Permission; use imap_proto::{ protocol::copy_move::Arguments, receiver::Request, Command, ResponseCode, ResponseType, StatusResponse, @@ -38,6 +39,13 @@ impl<T: SessionStream> Session<T> { is_move: bool, is_uid: bool, ) -> trc::Result<()> { + // Validate access + self.assert_has_permission(if is_move { + Permission::ImapMove + } else { + Permission::ImapCopy + })?; + let op_start = Instant::now(); let arguments = request.parse_copy_move(self.version)?; let (data, src_mailbox) = self.state.mailbox_state(); diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs index 9320b89e..edd10ea0 100644 --- a/crates/imap/src/op/create.rs +++ b/crates/imap/src/op/create.rs @@ -12,6 +12,7 @@ use crate::{ spawn_op, }; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{create::Arguments, list::Attribute}, receiver::Request, @@ -30,6 +31,9 @@ use trc::AddContext; impl<T: SessionStream> Session<T> { pub async fn handle_create(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapCreate)?; + let data = self.state.session_data(); let version = self.version; diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs index ceb7a46f..d8d028a8 100644 --- a/crates/imap/src/op/delete.rs +++ b/crates/imap/src/op/delete.rs @@ -11,6 +11,7 @@ use crate::{ spawn_op, }; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse, }; @@ -21,6 +22,9 @@ use super::ImapContext; impl<T: SessionStream> Session<T> { pub async fn handle_delete(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapDelete)?; + let data = self.state.session_data(); let version = self.version; diff --git a/crates/imap/src/op/enable.rs b/crates/imap/src/op/enable.rs index 0ee178e4..e5dde55c 100644 --- a/crates/imap/src/op/enable.rs +++ b/crates/imap/src/op/enable.rs @@ -8,6 +8,7 @@ use std::time::Instant; use crate::core::Session; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{capability::Capability, enable, ImapResponse, ProtocolVersion}, receiver::Request, @@ -16,6 +17,9 @@ use imap_proto::{ impl<T: SessionStream> Session<T> { pub async fn handle_enable(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapEnable)?; + let op_start = Instant::now(); let arguments = request.parse_enable()?; diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs index 3576143b..36c99003 100644 --- a/crates/imap/src/op/expunge.rs +++ b/crates/imap/src/op/expunge.rs @@ -7,6 +7,7 @@ use std::{sync::Arc, time::Instant}; use ahash::AHashMap; +use directory::Permission; use imap_proto::{ parser::parse_sequence_set, receiver::{Request, Token}, @@ -34,6 +35,9 @@ impl<T: SessionStream> Session<T> { request: Request<Command>, is_uid: bool, ) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapExpunge)?; + let op_start = Instant::now(); let (data, mailbox) = self.state.select_data(); diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs index 1b9d29b2..182b8303 100644 --- a/crates/imap/src/op/fetch.rs +++ b/crates/imap/src/op/fetch.rs @@ -12,6 +12,7 @@ use crate::{ }; use ahash::AHashMap; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ parser::PushUnique, protocol::{ @@ -44,6 +45,9 @@ impl<T: SessionStream> Session<T> { request: Request<Command>, is_uid: bool, ) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapFetch)?; + let op_start = Instant::now(); let arguments = request.parse_fetch()?; diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs index 4699b1d4..be97d88f 100644 --- a/crates/imap/src/op/idle.rs +++ b/crates/imap/src/op/idle.rs @@ -7,6 +7,7 @@ use std::{sync::Arc, time::Instant}; use ahash::AHashSet; +use directory::Permission; use imap_proto::{ protocol::{ fetch, @@ -32,6 +33,9 @@ use crate::{ impl<T: SessionStream> Session<T> { pub async fn handle_idle(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapIdle)?; + let op_start = Instant::now(); let (data, mailbox, types) = match &self.state { State::Authenticated { data, .. } => { diff --git a/crates/imap/src/op/list.rs b/crates/imap/src/op/list.rs index 6d7e670b..dce26b75 100644 --- a/crates/imap/src/op/list.rs +++ b/crates/imap/src/op/list.rs @@ -11,6 +11,7 @@ use crate::{ spawn_op, }; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{ list::{ @@ -30,8 +31,14 @@ impl<T: SessionStream> Session<T> { let command = request.command; let is_lsub = command == Command::Lsub; let arguments = if !is_lsub { + // Validate access + self.assert_has_permission(Permission::ImapList)?; + request.parse_list(self.version) } else { + // Validate access + self.assert_has_permission(Permission::ImapLsub)?; + request.parse_lsub() }?; diff --git a/crates/imap/src/op/namespace.rs b/crates/imap/src/op/namespace.rs index 3935955e..0f9ed1ac 100644 --- a/crates/imap/src/op/namespace.rs +++ b/crates/imap/src/op/namespace.rs @@ -6,6 +6,7 @@ use crate::core::Session; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{namespace::Response, ImapResponse}, receiver::Request, @@ -14,6 +15,9 @@ use imap_proto::{ impl<T: SessionStream> Session<T> { pub async fn handle_namespace(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapNamespace)?; + trc::event!( Imap(trc::ImapEvent::Namespace), SpanId = self.session_id, diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs index a19f6a9b..cc8b27fb 100644 --- a/crates/imap/src/op/rename.rs +++ b/crates/imap/src/op/rename.rs @@ -11,6 +11,7 @@ use crate::{ spawn_op, }; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse, }; @@ -29,6 +30,9 @@ use super::ImapContext; impl<T: SessionStream> Session<T> { pub async fn handle_rename(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapRename)?; + let op_start = Instant::now(); let arguments = request.parse_rename(self.version)?; let data = self.state.session_data(); diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index 3b5e699a..86329275 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -7,6 +7,7 @@ use std::{sync::Arc, time::Instant}; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{ search::{self, Arguments, Filter, Response, ResultOption}, @@ -43,8 +44,14 @@ impl<T: SessionStream> Session<T> { ) -> trc::Result<()> { let op_start = Instant::now(); let mut arguments = if !is_sort { + // Validate access + self.assert_has_permission(Permission::ImapSearch)?; + request.parse_search(self.version) } else { + // Validate access + self.assert_has_permission(Permission::ImapSort)?; + request.parse_sort() }?; diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs index 43417a0c..a36125ea 100644 --- a/crates/imap/src/op/select.rs +++ b/crates/imap/src/op/select.rs @@ -6,6 +6,7 @@ use std::{sync::Arc, time::Instant}; +use directory::Permission; use imap_proto::{ protocol::{ fetch, @@ -26,6 +27,13 @@ use super::{ImapContext, ToModSeq}; impl<T: SessionStream> Session<T> { pub async fn handle_select(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(if request.command == Command::Select { + Permission::ImapSelect + } else { + Permission::ImapExamine + })?; + let op_start = Instant::now(); let is_select = request.command == Command::Select; let command = request.command; diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs index 00f197dd..ce8c6029 100644 --- a/crates/imap/src/op/status.rs +++ b/crates/imap/src/op/status.rs @@ -12,6 +12,7 @@ use crate::{ spawn_op, }; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ parser::PushUnique, protocol::status::{Status, StatusItem, StatusItemType}, @@ -34,6 +35,9 @@ use super::ToModSeq; impl<T: SessionStream> Session<T> { pub async fn handle_status(&mut self, request: Request<Command>) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapStatus)?; + let op_start = Instant::now(); let arguments = request.parse_status(self.version)?; let version = self.version; diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs index 24b5b932..3a799841 100644 --- a/crates/imap/src/op/store.rs +++ b/crates/imap/src/op/store.rs @@ -12,6 +12,7 @@ use crate::{ }; use ahash::AHashSet; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{ fetch::{DataItem, FetchItem}, @@ -39,6 +40,9 @@ impl<T: SessionStream> Session<T> { request: Request<Command>, is_uid: bool, ) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapStore)?; + let op_start = Instant::now(); let arguments = request.parse_store()?; let (data, mailbox) = self.state.select_data(); diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs index 9eb648ab..995ed430 100644 --- a/crates/imap/src/op/subscribe.rs +++ b/crates/imap/src/op/subscribe.rs @@ -11,6 +11,7 @@ use crate::{ spawn_op, }; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse}; use jmap::mailbox::set::{MailboxSubscribe, SCHEMA}; use jmap_proto::{ @@ -30,6 +31,9 @@ impl<T: SessionStream> Session<T> { request: Request<Command>, is_subscribe: bool, ) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapSubscribe)?; + let op_start = Instant::now(); let arguments = request.parse_subscribe(self.version)?; let data = self.state.session_data(); diff --git a/crates/imap/src/op/thread.rs b/crates/imap/src/op/thread.rs index 5c9748c7..a8425652 100644 --- a/crates/imap/src/op/thread.rs +++ b/crates/imap/src/op/thread.rs @@ -12,6 +12,7 @@ use crate::{ }; use ahash::AHashMap; use common::listener::SessionStream; +use directory::Permission; use imap_proto::{ protocol::{ thread::{Arguments, Response}, @@ -28,6 +29,9 @@ impl<T: SessionStream> Session<T> { request: Request<Command>, is_uid: bool, ) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapThread)?; + let op_start = Instant::now(); let command = request.command; let mut arguments = request.parse_thread()?; diff --git a/crates/jmap/src/api/autoconfig.rs b/crates/jmap/src/api/autoconfig.rs index c8c67ef9..11d29547 100644 --- a/crates/jmap/src/api/autoconfig.rs +++ b/crates/jmap/src/api/autoconfig.rs @@ -7,7 +7,7 @@ use std::fmt::Write; use common::manager::webadmin::Resource; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use quick_xml::events::Event; use quick_xml::Reader; use utils::url_params::UrlParams; @@ -187,14 +187,14 @@ impl JMAP { .await .unwrap_or_default() { - if let Ok(Some(principal)) = self + if let Ok(Some(mut principal)) = self .core .storage .directory .query(QueryBy::Id(id), false) .await { - account_name = principal.name; + account_name = principal.take_str(PrincipalField::Name).unwrap_or_default(); break; } } diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 4968863e..120777db 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -12,6 +12,7 @@ use common::{ manager::webadmin::Resource, Core, }; +use directory::Permission; use http_body_util::{BodyExt, Full}; use hyper::{ body::{self, Bytes}, @@ -29,7 +30,7 @@ use jmap_proto::{ }; use crate::{ - auth::{authenticate::HttpHeaders, oauth::OAuthMetadata}, + auth::{authenticate::HttpHeaders, oauth::OAuthMetadata, AccessToken}, blob::{DownloadResponse, UploadResponse}, services::state, JmapInstance, JMAP, @@ -81,7 +82,7 @@ impl JMAP { let request = fetch_body( &mut req, - if !access_token.is_super_user() { + if !access_token.has_permission(Permission::UnlimitedUploads) { self.core.jmap.upload_max_size } else { 0 @@ -142,7 +143,7 @@ impl JMAP { { return match fetch_body( &mut req, - if !access_token.is_super_user() { + if !access_token.has_permission(Permission::UnlimitedUploads) { self.core.jmap.upload_max_size } else { 0 @@ -302,27 +303,29 @@ impl JMAP { if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) && self.core.is_enterprise_edition() { - if let Some((live_path, token)) = req + if let Some((live_path, grant_type, token)) = req .uri() .path() .strip_prefix("/api/telemetry/") .and_then(|p| { p.strip_prefix("traces/live/") - .map(|t| ("traces", t)) + .map(|t| ("traces", "live_tracing", t)) .or_else(|| { p.strip_prefix("metrics/live/") - .map(|t| ("metrics", t)) + .map(|t| ("metrics", "live_metrics", t)) }) }) { let (account_id, _, _) = - self.validate_access_token("live_telemetry", token).await?; + self.validate_access_token(grant_type, token).await?; return self .handle_telemetry_api_request( &req, vec!["", live_path, "live"], - account_id, + &AccessToken::from_id(account_id) + .with_permission(Permission::MetricsLive) + .with_permission(Permission::TracingLive), ) .await; } @@ -893,7 +896,13 @@ impl ToRequestError for trc::Error { trc::AuthEvent::TooManyAttempts => RequestError::too_many_auth_attempts(), _ => RequestError::unauthorized(), }, - trc::EventType::Security(_) => RequestError::too_many_auth_attempts(), + trc::EventType::Security(cause) => match cause { + trc::SecurityEvent::AuthenticationBan + | trc::SecurityEvent::BruteForceBan + | trc::SecurityEvent::LoiterBan + | trc::SecurityEvent::IpBlocked => RequestError::too_many_auth_attempts(), + trc::SecurityEvent::Unauthorized => RequestError::forbidden(), + }, trc::EventType::Resource(cause) => match cause { trc::ResourceEvent::NotFound => RequestError::not_found(), trc::ResourceEvent::BadParameters => RequestError::blank( diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs index 00589441..06ddaa59 100644 --- a/crates/jmap/src/api/management/dkim.rs +++ b/crates/jmap/src/api/management/dkim.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use common::config::smtp::auth::simple_pem_parse; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use hyper::Method; use mail_auth::{ common::crypto::{Ed25519Key, RsaKey, Sha256}, @@ -23,6 +23,7 @@ use store::write::now; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -48,10 +49,21 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match *req.method() { - Method::GET => self.handle_get_public_key(path).await, - Method::POST => self.handle_create_signature(body).await, + Method::GET => { + // Validate the access token + access_token.assert_has_permission(Permission::DkimSignatureGet)?; + + self.handle_get_public_key(path).await + } + Method::POST => { + // Validate the access token + access_token.assert_has_permission(Permission::DkimSignatureCreate)?; + + self.handle_create_signature(body).await + } _ => Err(trc::ResourceEvent::NotFound.into_err()), } } diff --git a/crates/jmap/src/api/management/domain.rs b/crates/jmap/src/api/management/domain.rs index e029e4c6..eba1312b 100644 --- a/crates/jmap/src/api/management/domain.rs +++ b/crates/jmap/src/api/management/domain.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::backend::internal::manage::{self, ManageDirectory}; +use directory::{ + backend::internal::manage::{self, ManageDirectory}, + Permission, +}; use hyper::Method; use serde::{Deserialize, Serialize}; @@ -19,6 +22,7 @@ use crate::{ management::dkim::{obtain_dkim_public_key, Algorithm}, HttpRequest, HttpResponse, JsonResponse, }, + auth::AccessToken, JMAP, }; @@ -37,9 +41,13 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1), req.method()) { (None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainList)?; + // List domains let params = UrlParams::new(req.uri().query()); let filter = params.get("filter"); @@ -66,6 +74,9 @@ impl JMAP { .into_http_response()) } (Some(domain), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainGet)?; + // Obtain DNS records let domain = decode_path_element(domain); Ok(JsonResponse::new(json!({ @@ -74,6 +85,9 @@ impl JMAP { .into_http_response()) } (Some(domain), &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainCreate)?; + // Create domain let domain = decode_path_element(domain); self.core @@ -103,6 +117,9 @@ impl JMAP { .into_http_response()) } (Some(domain), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainDelete)?; + // Delete domain let domain = decode_path_element(domain); self.core diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs index 6186509c..80ea5548 100644 --- a/crates/jmap/src/api/management/enterprise/telemetry.rs +++ b/crates/jmap/src/api/management/enterprise/telemetry.rs @@ -17,7 +17,7 @@ use common::telemetry::{ metrics::store::{Metric, MetricsStore}, tracers::store::{TracingQuery, TracingStore}, }; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use http_body_util::{combinators::BoxBody, StreamBody}; use hyper::{ body::{Bytes, Frame}, @@ -38,6 +38,7 @@ use crate::{ http::ToHttpResponse, management::Timestamp, HttpRequest, HttpResponse, HttpResponseBody, JsonResponse, }, + auth::AccessToken, JMAP, }; @@ -46,9 +47,10 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, - account_id: u32, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { let params = UrlParams::new(req.uri().query()); + let account_id = access_token.primary_id(); match ( path.get(1).copied().unwrap_or_default(), @@ -56,6 +58,9 @@ impl JMAP { req.method(), ) { ("traces", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingList)?; + let page: usize = params.parse("page").unwrap_or(0); let limit: usize = params.parse("limit").unwrap_or(0); let mut tracing_query = Vec::new(); @@ -162,6 +167,9 @@ impl JMAP { } } ("traces", Some("live"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingLive)?; + let mut key_filters = AHashMap::new(); let mut filter = None; @@ -290,6 +298,9 @@ impl JMAP { }) } ("trace", id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingGet)?; + let store = &self .core .enterprise @@ -327,15 +338,32 @@ impl JMAP { .into_http_response()) } } - ("live", Some("token"), &Method::GET) => { + ("live", Some("tracing-token"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingLive)?; + // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ - "data": self.issue_custom_token(account_id, "live_telemetry", "web", 60).await?, + "data": self.issue_custom_token(account_id, "live_tracing", "web", 60).await?, + })) + .into_http_response()) + } + ("live", Some("metrics-token"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MetricsLive)?; + + // Issue a live telemetry token valid for 60 seconds + + Ok(JsonResponse::new(json!({ + "data": self.issue_custom_token(account_id, "live_metrics", "web", 60).await?, })) .into_http_response()) } ("metrics", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MetricsList)?; + let before = params .parse::<Timestamp>("before") .map(|t| t.into_inner()) @@ -395,6 +423,9 @@ impl JMAP { .into_http_response()) } ("metrics", Some("live"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MetricsLive)?; + let interval = Duration::from_secs( params .parse::<u64>("interval") diff --git a/crates/jmap/src/api/management/log.rs b/crates/jmap/src/api/management/log.rs index 1083a009..0bebbf48 100644 --- a/crates/jmap/src/api/management/log.rs +++ b/crates/jmap/src/api/management/log.rs @@ -5,7 +5,7 @@ use std::{ }; use chrono::DateTime; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use rev_lines::RevLines; use serde::Serialize; use serde_json::json; @@ -14,6 +14,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -27,7 +28,14 @@ struct LogEntry { } impl JMAP { - pub async fn handle_view_logs(&self, req: &HttpRequest) -> trc::Result<HttpResponse> { + pub async fn handle_view_logs( + &self, + req: &HttpRequest, + access_token: &AccessToken, + ) -> trc::Result<HttpResponse> { + // Validate the access token + access_token.assert_has_permission(Permission::LogsView)?; + let path = self .core .metrics diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs index 1715363b..9972a86f 100644 --- a/crates/jmap/src/api/management/mod.rs +++ b/crates/jmap/src/api/management/mod.rs @@ -19,7 +19,7 @@ pub mod stores; use std::{borrow::Cow, str::FromStr, sync::Arc}; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use hyper::Method; use mail_parser::DateTime; use serde::Serialize; @@ -50,31 +50,68 @@ impl JMAP { session: &HttpSessionData, ) -> trc::Result<HttpResponse> { let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>(); - let is_superuser = access_token.is_super_user(); match path.first().copied().unwrap_or_default() { - "queue" if is_superuser => self.handle_manage_queue(req, path).await, - "settings" if is_superuser => self.handle_manage_settings(req, path, body).await, - "reports" if is_superuser => self.handle_manage_reports(req, path).await, - "principal" if is_superuser => self.handle_manage_principal(req, path, body).await, - "domain" if is_superuser => self.handle_manage_domain(req, path).await, - "store" if is_superuser => self.handle_manage_store(req, path, body, session).await, - "reload" if is_superuser => self.handle_manage_reload(req, path).await, - "dkim" if is_superuser => self.handle_manage_dkim(req, path, body).await, - "update" if is_superuser => self.handle_manage_update(req, path).await, - "logs" if is_superuser && req.method() == Method::GET => { - self.handle_view_logs(req).await + "queue" => self.handle_manage_queue(req, path, &access_token).await, + "settings" => { + self.handle_manage_settings(req, path, body, &access_token) + .await } - "sieve" if is_superuser => self.handle_run_sieve(req, path, body).await, - "restart" if is_superuser && req.method() == Method::GET => { + "reports" => self.handle_manage_reports(req, path, &access_token).await, + "principal" => { + self.handle_manage_principal(req, path, body, &access_token) + .await + } + "domain" => self.handle_manage_domain(req, path, &access_token).await, + "store" => { + self.handle_manage_store(req, path, body, session, &access_token) + .await + } + "reload" => self.handle_manage_reload(req, path, &access_token).await, + "dkim" => { + self.handle_manage_dkim(req, path, body, &access_token) + .await + } + "update" => self.handle_manage_update(req, path, &access_token).await, + "logs" if req.method() == Method::GET => { + self.handle_view_logs(req, &access_token).await + } + "sieve" => self.handle_run_sieve(req, path, body, &access_token).await, + "restart" if req.method() == Method::GET => { + // Validate the access token + access_token.assert_has_permission(Permission::Restart)?; + Err(manage::unsupported("Restart is not yet supported")) } - "oauth" => self.handle_oauth_api_request(access_token, body).await, + "oauth" => { + // Validate the access token + access_token.assert_has_permission(Permission::AuthenticateOauth)?; + + self.handle_oauth_api_request(access_token, body).await + } "account" => match (path.get(1).copied().unwrap_or_default(), req.method()) { - ("crypto", &Method::POST) => self.handle_crypto_post(access_token, body).await, - ("crypto", &Method::GET) => self.handle_crypto_get(access_token).await, - ("auth", &Method::GET) => self.handle_account_auth_get(access_token).await, + ("crypto", &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManageEncryption)?; + + self.handle_crypto_post(access_token, body).await + } + ("crypto", &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManageEncryption)?; + + self.handle_crypto_get(access_token).await + } + ("auth", &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManagePasswords)?; + + self.handle_account_auth_get(access_token).await + } ("auth", &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManagePasswords)?; + self.handle_account_auth_post(req, access_token, body).await } _ => Err(trc::ResourceEvent::NotFound.into_err()), @@ -83,7 +120,7 @@ impl JMAP { // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - "telemetry" if is_superuser => { + "telemetry" => { // WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED // Any attempt to modify, bypass, or disable this license validation mechanism // constitutes a severe violation of the Stalwart Enterprise License Agreement. @@ -94,7 +131,7 @@ impl JMAP { // for copyright infringement, breach of contract, and fraud. if self.core.is_enterprise_edition() { - self.handle_telemetry_api_request(req, path, access_token.primary_id()) + self.handle_telemetry_api_request(req, path, &access_token) .await } else { Err(manage::enterprise()) diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs index e88dea9d..e47d56d4 100644 --- a/crates/jmap/src/api/management/principal.rs +++ b/crates/jmap/src/api/management/principal.rs @@ -12,7 +12,7 @@ use directory::{ manage::{self, ManageDirectory}, PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets, }, - DirectoryInner, Principal, QueryBy, Type, + DirectoryInner, Permission, Principal, QueryBy, Type, }; use hyper::{header, Method}; @@ -28,7 +28,7 @@ use crate::{ use super::decode_path_element; #[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct PrincipalResponse { +pub struct PrincipalPayload { #[serde(default)] pub id: u32, #[serde(rename = "type")] @@ -68,8 +68,6 @@ pub enum AccountAuthRequest { pub struct AccountAuthResponse { #[serde(rename = "otpEnabled")] pub otp_auth: bool, - #[serde(rename = "isAdministrator")] - pub is_admin: bool, #[serde(rename = "appPasswords")] pub app_passwords: Vec<String>, } @@ -80,43 +78,47 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1), req.method()) { (None, &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::PrincipalCreate)?; + // Make sure the current directory supports updates self.assert_supported_directory()?; // Create principal - let principal = serde_json::from_slice::<PrincipalResponse>( - body.as_deref().unwrap_or_default(), - ) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err) - })?; + let principal = + serde_json::from_slice::<PrincipalPayload>(body.as_deref().unwrap_or_default()) + .map_err(|err| { + trc::EventType::Resource(trc::ResourceEvent::BadParameters) + .from_json_error(err) + })?; + + let principal = Principal::new(principal.id, principal.typ) + .with_field(PrincipalField::Name, principal.name) + .with_field(PrincipalField::Secrets, principal.secrets) + .with_field(PrincipalField::Quota, principal.quota) + .with_field(PrincipalField::Emails, principal.emails) + .with_field(PrincipalField::MemberOf, principal.member_of) + .with_field(PrincipalField::Members, principal.members) + .with_opt_field(PrincipalField::Description, principal.description); Ok(JsonResponse::new(json!({ "data": self .core .storage .data - .create_account( - Principal { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - secrets: principal.secrets, - emails: principal.emails, - member_of: principal.member_of, - description: principal.description, - }, - principal.members, - ) + .create_account(principal) .await?, })) .into_http_response()) } (None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PrincipalList)?; + // List principal ids let params = UrlParams::new(req.uri().query()); let filter = params.get("filter"); @@ -144,6 +146,20 @@ impl JMAP { .into_http_response()) } (Some(name), method) => { + // Validate the access token + match *method { + Method::GET => { + access_token.assert_has_permission(Permission::PrincipalGet)?; + } + Method::DELETE => { + access_token.assert_has_permission(Permission::PrincipalDelete)?; + } + Method::PATCH => { + access_token.assert_has_permission(Permission::PrincipalUpdate)?; + } + _ => {} + } + // Fetch, update or delete principal let name = decode_path_element(name); let account_id = self @@ -166,19 +182,23 @@ impl JMAP { let principal = self.core.storage.data.map_group_ids(principal).await?; // Obtain quota usage - let mut principal = PrincipalResponse::from(principal); + let mut principal = PrincipalPayload::from(principal); principal.used_quota = self.get_used_quota(account_id).await? as u64; // Obtain member names for member_id in self.core.storage.data.get_members(account_id).await? { - if let Some(member_principal) = self + if let Some(mut member_principal) = self .core .storage .data .query(QueryBy::Id(member_id), false) .await? { - principal.members.push(member_principal.name); + principal.members.push( + member_principal + .take_str(PrincipalField::Name) + .unwrap_or_default(), + ); } } @@ -257,7 +277,6 @@ impl JMAP { ) -> trc::Result<HttpResponse> { let mut response = AccountAuthResponse { otp_auth: false, - is_admin: access_token.is_super_user(), app_passwords: Vec::new(), }; @@ -270,7 +289,7 @@ impl JMAP { .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - for secret in principal.secrets { + for secret in principal.iter_str(PrincipalField::Secrets) { if secret.is_otp_auth() { response.otp_auth = true; } else if let Some((app_name, _)) = @@ -327,7 +346,7 @@ impl JMAP { } // Handle Fallback admin password changes - if access_token.is_super_user() && access_token.primary_id() == u32::MAX { + if access_token.primary_id() == u32::MAX { match requests.into_iter().next().unwrap() { AccountAuthRequest::SetPassword { password } => { self.core @@ -420,24 +439,31 @@ impl JMAP { Err(manage::unsupported(format!( concat!( "{} directory cannot be managed. ", - "Only internal directories support inserts and update operations." + "Only internal directories support inserts ", + "and update operations." ), class ))) } } -impl From<Principal<String>> for PrincipalResponse { - fn from(principal: Principal<String>) -> Self { - PrincipalResponse { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - emails: principal.emails, - member_of: principal.member_of, - description: principal.description, - secrets: principal.secrets, +impl From<Principal> for PrincipalPayload { + fn from(mut principal: Principal) -> Self { + PrincipalPayload { + id: principal.id(), + typ: principal.typ(), + quota: principal.quota(), + name: principal.take_str(PrincipalField::Name).unwrap_or_default(), + emails: principal + .take_str_array(PrincipalField::Emails) + .unwrap_or_default(), + member_of: principal + .take_str_array(PrincipalField::MemberOf) + .unwrap_or_default(), + description: principal.take_str(PrincipalField::Description), + secrets: principal + .take_str_array(PrincipalField::Secrets) + .unwrap_or_default(), used_quota: 0, members: Vec::new(), } diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs index 2723827e..46fa0616 100644 --- a/crates/jmap/src/api/management/queue.rs +++ b/crates/jmap/src/api/management/queue.rs @@ -5,6 +5,7 @@ */ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use directory::Permission; use hyper::Method; use mail_auth::{ dmarc::URI, @@ -23,6 +24,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -105,6 +107,7 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { let params = UrlParams::new(req.uri().query()); @@ -114,6 +117,9 @@ impl JMAP { req.method(), ) { ("messages", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueList)?; + let text = params.get("text"); let from = params.get("from"); let to = params.get("to"); @@ -217,6 +223,9 @@ impl JMAP { .into_http_response()) } ("messages", Some(queue_id), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueGet)?; + if let Some(message) = self .smtp .read_message(queue_id.parse().unwrap_or_default()) @@ -231,6 +240,9 @@ impl JMAP { } } ("messages", Some(queue_id), &Method::PATCH) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueUpdate)?; + let time = params .parse::<FutureTimestamp>("at") .map(|t| t.into_inner()) @@ -278,6 +290,9 @@ impl JMAP { } } ("messages", Some(queue_id), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueDelete)?; + if let Some(mut message) = self .smtp .read_message(queue_id.parse().unwrap_or_default()) @@ -358,6 +373,9 @@ impl JMAP { } } ("reports", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::OutgoingReportList)?; + let domain = params.get("domain").map(|d| d.to_lowercase()); let type_ = params.get("type").and_then(|t| match t { "dmarc" => 0u8.into(), @@ -436,6 +454,9 @@ impl JMAP { .into_http_response()) } ("reports", Some(report_id), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::OutgoingReportGet)?; + let mut result = None; if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { match report_id { @@ -473,6 +494,9 @@ impl JMAP { } } ("reports", Some(report_id), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::OutgoingReportDelete)?; + if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { match report_id { QueueClass::DmarcReportHeader(event) => { diff --git a/crates/jmap/src/api/management/reload.rs b/crates/jmap/src/api/management/reload.rs index c8653fb1..c8410fc2 100644 --- a/crates/jmap/src/api/management/reload.rs +++ b/crates/jmap/src/api/management/reload.rs @@ -4,12 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use directory::Permission; use hyper::Method; use serde_json::json; use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, services::housekeeper::Event, JMAP, }; @@ -19,7 +21,11 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsReload)?; + match (path.get(1).copied(), req.method()) { (Some("lookup"), &Method::GET) => { let result = self.core.reload_lookups().await?; @@ -92,18 +98,27 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1).copied(), req.method()) { - (Some("spam-filter"), &Method::GET) => Ok(JsonResponse::new(json!({ - "data": self - .core - .storage - .config - .update_config_resource("spam-filter") - .await?, - })) - .into_http_response()), + (Some("spam-filter"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::UpdateSpamFilter)?; + + Ok(JsonResponse::new(json!({ + "data": self + .core + .storage + .config + .update_config_resource("spam-filter") + .await?, + })) + .into_http_response()) + } (Some("webadmin"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::UpdateWebadmin)?; + self.inner.webadmin.update_and_unpack(&self.core).await?; Ok(JsonResponse::new(json!({ diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs index d8bd0318..a53db1f9 100644 --- a/crates/jmap/src/api/management/report.rs +++ b/crates/jmap/src/api/management/report.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use directory::Permission; use hyper::Method; use mail_auth::report::{ tlsrpt::{FailureDetails, Policy, TlsReport}, @@ -19,6 +20,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -35,6 +37,7 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match ( path.get(1).copied().unwrap_or_default(), @@ -42,6 +45,9 @@ impl JMAP { req.method(), ) { (class @ ("dmarc" | "tls" | "arf"), None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::IncomingReportList)?; + let params = UrlParams::new(req.uri().query()); let filter = params.get("text"); let page: usize = params.parse::<usize>("page").unwrap_or_default(); @@ -154,6 +160,9 @@ impl JMAP { .into_http_response()) } (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::IncomingReportGet)?; + if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { match &report_id { ReportClass::Tls { .. } => match self @@ -207,6 +216,9 @@ impl JMAP { } } (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::IncomingReportDelete)?; + if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { let mut batch = BatchBuilder::new(); batch.clear(ValueClass::Report(report_id)); diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs index db0b6047..1d46799a 100644 --- a/crates/jmap/src/api/management/settings.rs +++ b/crates/jmap/src/api/management/settings.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use directory::Permission; use hyper::Method; use serde_json::json; use store::ahash::AHashMap; @@ -11,6 +12,7 @@ use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams}; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -38,9 +40,13 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1).copied(), req.method()) { (Some("group"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsList)?; + // List settings let params = UrlParams::new(req.uri().query()); let prefix = params @@ -168,6 +174,9 @@ impl JMAP { } } (Some("list"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsList)?; + // List settings let params = UrlParams::new(req.uri().query()); let prefix = params @@ -200,6 +209,9 @@ impl JMAP { .into_http_response()) } (Some("keys"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsList)?; + // Obtain keys let params = UrlParams::new(req.uri().query()); let keys = params @@ -232,6 +244,9 @@ impl JMAP { .into_http_response()) } (Some(prefix), &Method::DELETE) if !prefix.is_empty() => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsDelete)?; + let prefix = decode_path_element(prefix); self.core.storage.config.clear(prefix.as_ref()).await?; @@ -242,6 +257,9 @@ impl JMAP { .into_http_response()) } (None, &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsUpdate)?; + let changes = serde_json::from_slice::<Vec<UpdateSettings>>( body.as_deref().unwrap_or_default(), ) diff --git a/crates/jmap/src/api/management/sieve.rs b/crates/jmap/src/api/management/sieve.rs index 1305eafa..8e56ed11 100644 --- a/crates/jmap/src/api/management/sieve.rs +++ b/crates/jmap/src/api/management/sieve.rs @@ -7,6 +7,7 @@ use std::time::SystemTime; use common::{scripts::ScriptModification, IntoString}; +use directory::Permission; use hyper::Method; use serde_json::json; use sieve::{runtime::Variable, Envelope}; @@ -15,6 +16,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -41,7 +43,11 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { + // Validate the access token + access_token.assert_has_permission(Permission::SieveRun)?; + let (script, script_id) = match ( path.get(1).and_then(|name| { self.core diff --git a/crates/jmap/src/api/management/stores.rs b/crates/jmap/src/api/management/stores.rs index c8066441..038f7286 100644 --- a/crates/jmap/src/api/management/stores.rs +++ b/crates/jmap/src/api/management/stores.rs @@ -6,7 +6,10 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use common::manager::webadmin::Resource; -use directory::backend::internal::manage::{self, ManageDirectory}; +use directory::{ + backend::internal::manage::{self, ManageDirectory}, + Permission, +}; use hyper::Method; use serde_json::json; use utils::url_params::UrlParams; @@ -16,6 +19,7 @@ use crate::{ http::{HttpSessionData, ToHttpResponse}, HttpRequest, HttpResponse, JsonResponse, }, + auth::AccessToken, services::housekeeper::{Event, PurgeType}, JMAP, }; @@ -29,6 +33,7 @@ impl JMAP { path: Vec<&str>, body: Option<Vec<u8>>, session: &HttpSessionData, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match ( path.get(1).copied(), @@ -37,6 +42,9 @@ impl JMAP { req.method(), ) { (Some("blobs"), Some(blob_hash), _, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::BlobFetch)?; + let blob_hash = URL_SAFE_NO_PAD .decode(decode_path_element(blob_hash).as_bytes()) .map_err(|err| { @@ -69,6 +77,9 @@ impl JMAP { .into_http_response()) } (Some("purge"), Some("blob"), _, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeBlobStore)?; + self.housekeeper_request(Event::Purge(PurgeType::Blobs { store: self.core.storage.data.clone(), blob_store: self.core.storage.blob.clone(), @@ -76,6 +87,9 @@ impl JMAP { .await } (Some("purge"), Some("data"), id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeDataStore)?; + let store = if let Some(id) = id { if let Some(store) = self.core.storage.stores.get(id) { store.clone() @@ -90,6 +104,9 @@ impl JMAP { .await } (Some("purge"), Some("lookup"), id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeLookupStore)?; + let store = if let Some(id) = id { if let Some(store) = self.core.storage.lookups.get(id) { store.clone() @@ -104,6 +121,9 @@ impl JMAP { .await } (Some("purge"), Some("account"), id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeAccount)?; + let account_id = if let Some(id) = id { self.core .storage @@ -133,6 +153,9 @@ impl JMAP { // violators to the fullest extent of the law, including but not limited to claims // for copyright infringement, breach of contract, and fraud. + // Validate the access token + access_token.assert_has_permission(Permission::Undelete)?; + if self.core.is_enterprise_edition() { self.handle_undelete_api_request(req, path, body, session) .await diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index f41e0b76..59f2d81a 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -135,6 +135,11 @@ impl JMAP { session: &HttpSessionData, ) -> trc::Result<ResponseMethod> { let op_start = Instant::now(); + + // Check permissions + access_token.assert_has_jmap_permission(&method)?; + + // Handle method let response = match method { RequestMethod::Get(mut req) => match req.take_arguments() { get::RequestArguments::Email(arguments) => { @@ -177,15 +182,7 @@ impl JMAP { self.vacation_response_get(req).await?.into() } - get::RequestArguments::Principal => { - if self.core.jmap.principal_allow_lookups || access_token.is_super_user() { - self.principal_get(req).await?.into() - } else { - return Err(trc::JmapEvent::Forbidden - .into_err() - .details("Principal lookups are disabled".to_string())); - } - } + get::RequestArguments::Principal => self.principal_get(req).await?.into(), get::RequestArguments::Quota => { access_token.assert_is_member(req.account_id)?; @@ -225,13 +222,7 @@ impl JMAP { self.sieve_script_query(req).await?.into() } query::RequestArguments::Principal => { - if self.core.jmap.principal_allow_lookups || access_token.is_super_user() { - self.principal_query(req, session).await?.into() - } else { - return Err(trc::JmapEvent::Forbidden - .into_err() - .details("Principal lookups are disabled".to_string())); - } + self.principal_query(req, session).await?.into() } query::RequestArguments::Quota => { access_token.assert_is_member(req.account_id)?; diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 15c342d2..3a426e9b 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -6,7 +6,7 @@ use std::sync::Arc; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ request::capability::{Capability, Session}, types::{acl::Acl, collection::Collection, id::Id}, @@ -52,7 +52,7 @@ impl JMAP { .query(QueryBy::Id(*id), false) .await .caused_by(trc::location!())? - .map(|p| p.name) + .and_then(|mut p| p.take_str(PrincipalField::Name)) .unwrap_or_else(|| Id::from(*id).to_string()), is_personal, is_readonly, diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 490f675d..d9ec2d2b 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ error::set::SetError, object::Object, @@ -67,17 +67,10 @@ impl JMAP { } if !collections.is_empty() { - if let Some((_, sharing)) = access_token + access_token .access_to - .iter_mut() - .find(|(account_id, _)| *account_id == acl_item.to_account_id) - { - sharing.union(&collections); - } else { - access_token - .access_to - .push((acl_item.to_account_id, collections)); - } + .get_mut_or_insert_with(acl_item.to_account_id, Bitmap::new) + .union(&collections); } } } @@ -322,7 +315,7 @@ impl JMAP { { let mut acl_obj = Object::with_capacity(value.len() / 2); for item in value { - if let Some(principal) = self + if let Some(mut principal) = self .core .storage .directory @@ -331,7 +324,7 @@ impl JMAP { .unwrap_or_default() { acl_obj.append( - Property::_T(principal.name), + Property::_T(principal.take_str(PrincipalField::Name).unwrap_or_default()), item.grants .map(|acl_item| Value::Text(acl_item.to_string())) .collect::<Vec<_>>(), @@ -402,7 +395,7 @@ impl JMAP { { Ok(Some(principal)) => { acls.push(AclGrant { - account_id: principal.id, + account_id: principal.id(), grants: Bitmap::from(*grants), }); } @@ -443,7 +436,7 @@ impl JMAP { { Ok(Some(principal)) => Ok(( AclGrant { - account_id: principal.id, + account_id: principal.id(), grants: Bitmap::from(*grants), }, acl_patch.get(2).map(|v| v.as_bool().unwrap_or(false)), diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs index 6830f82a..3e5c9f66 100644 --- a/crates/jmap/src/auth/mod.rs +++ b/crates/jmap/src/auth/mod.rs @@ -14,10 +14,14 @@ use aes_gcm_siv::{ AeadInPlace, Aes256GcmSiv, KeyInit, Nonce, }; -use directory::{Principal, Type}; -use jmap_proto::types::{collection::Collection, id::Id}; +use directory::{backend::internal::PrincipalField, Permission, Principal, PERMISSION_BITMAP_SIZE}; +use jmap_proto::{ + request::RequestMethod, + types::{collection::Collection, id::Id}, +}; use store::blake3; -use utils::map::bitmap::Bitmap; +use trc::ipc::bitset::Bitset; +use utils::map::{bitmap::Bitmap, vec_map::VecMap}; pub mod acl; pub mod authenticate; @@ -28,30 +32,45 @@ pub mod rate_limit; pub struct AccessToken { pub primary_id: u32, pub member_of: Vec<u32>, - pub access_to: Vec<(u32, Bitmap<Collection>)>, + pub access_to: VecMap<u32, Bitmap<Collection>>, pub name: String, pub description: Option<String>, pub quota: u64, - pub is_superuser: bool, + pub permissions: Bitset<PERMISSION_BITMAP_SIZE>, } impl AccessToken { - pub fn new(principal: Principal<u32>) -> Self { + pub fn new(mut principal: Principal) -> Self { Self { - primary_id: principal.id, - member_of: principal.member_of, - access_to: Vec::new(), - name: principal.name, - description: principal.description, - quota: principal.quota, - is_superuser: principal.typ == Type::Superuser, + primary_id: principal.id(), + member_of: principal + .iter_int(PrincipalField::MemberOf) + .map(|v| v as u32) + .collect(), + access_to: VecMap::new(), + name: principal.take_str(PrincipalField::Name).unwrap_or_default(), + description: principal.take_str(PrincipalField::Description), + quota: principal.quota(), + permissions: Default::default(), } } - pub fn with_access_to(self, access_to: Vec<(u32, Bitmap<Collection>)>) -> Self { + pub fn from_id(primary_id: u32) -> Self { + Self { + primary_id, + ..Default::default() + } + } + + pub fn with_access_to(self, access_to: VecMap<u32, Bitmap<Collection>>) -> Self { Self { access_to, ..self } } + pub fn with_permission(mut self, permission: Permission) -> Self { + self.permissions.set(permission.id()); + self + } + pub fn state(&self) -> u32 { // Hash state let mut s = DefaultHasher::new(); @@ -71,15 +90,44 @@ impl AccessToken { } pub fn is_member(&self, account_id: u32) -> bool { - self.primary_id == account_id || self.member_of.contains(&account_id) || self.is_superuser + self.primary_id == account_id + || self.member_of.contains(&account_id) + || self.has_permission(Permission::Impersonate) } pub fn is_primary_id(&self, account_id: u32) -> bool { self.primary_id == account_id } - pub fn is_super_user(&self) -> bool { - self.is_superuser + #[inline(always)] + pub fn has_permission(&self, permission: Permission) -> bool { + self.permissions.get(permission.id()) + } + + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + if self.has_permission(permission) { + Ok(()) + } else { + Err(trc::SecurityEvent::Unauthorized + .into_err() + .details(permission.name())) + } + } + + pub fn permissions(&self) -> Vec<Permission> { + let mut permissions = Vec::new(); + for (block_num, bytes) in self.permissions.inner().iter().enumerate() { + let mut bytes = *bytes; + + while bytes != 0 { + let item = std::mem::size_of::<usize>() - 1 - bytes.leading_zeros() as usize; + bytes ^= 1 << item; + permissions.push( + Permission::from_id((block_num * std::mem::size_of::<usize>()) + item).unwrap(), + ); + } + } + permissions } pub fn is_shared(&self, account_id: u32) -> bool { @@ -131,6 +179,127 @@ impl AccessToken { .details(format!("You are not an owner of account {}", account_id))) } } + + pub fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()> { + let permission = match request { + RequestMethod::Get(m) => match &m.arguments { + jmap_proto::method::get::RequestArguments::Email(_) => Permission::JmapEmailGet, + jmap_proto::method::get::RequestArguments::Mailbox => Permission::JmapMailboxGet, + jmap_proto::method::get::RequestArguments::Thread => Permission::JmapThreadGet, + jmap_proto::method::get::RequestArguments::Identity => Permission::JmapIdentityGet, + jmap_proto::method::get::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionGet + } + jmap_proto::method::get::RequestArguments::PushSubscription => { + Permission::JmapPushSubscriptionGet + } + jmap_proto::method::get::RequestArguments::SieveScript => { + Permission::JmapSieveScriptGet + } + jmap_proto::method::get::RequestArguments::VacationResponse => { + Permission::JmapVacationResponseGet + } + jmap_proto::method::get::RequestArguments::Principal => { + Permission::JmapPrincipalGet + } + jmap_proto::method::get::RequestArguments::Quota => Permission::JmapQuotaGet, + jmap_proto::method::get::RequestArguments::Blob(_) => Permission::JmapBlobGet, + }, + RequestMethod::Set(m) => match &m.arguments { + jmap_proto::method::set::RequestArguments::Email => Permission::JmapEmailSet, + jmap_proto::method::set::RequestArguments::Mailbox(_) => Permission::JmapMailboxSet, + jmap_proto::method::set::RequestArguments::Identity => Permission::JmapIdentitySet, + jmap_proto::method::set::RequestArguments::EmailSubmission(_) => { + Permission::JmapEmailSubmissionSet + } + jmap_proto::method::set::RequestArguments::PushSubscription => { + Permission::JmapPushSubscriptionSet + } + jmap_proto::method::set::RequestArguments::SieveScript(_) => { + Permission::JmapSieveScriptSet + } + jmap_proto::method::set::RequestArguments::VacationResponse => { + Permission::JmapVacationResponseSet + } + }, + RequestMethod::Changes(m) => match m.arguments { + jmap_proto::method::changes::RequestArguments::Email => { + Permission::JmapEmailChanges + } + jmap_proto::method::changes::RequestArguments::Mailbox => { + Permission::JmapMailboxChanges + } + jmap_proto::method::changes::RequestArguments::Thread => { + Permission::JmapThreadChanges + } + jmap_proto::method::changes::RequestArguments::Identity => { + Permission::JmapIdentityChanges + } + jmap_proto::method::changes::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionChanges + } + jmap_proto::method::changes::RequestArguments::Quota => { + Permission::JmapQuotaChanges + } + }, + RequestMethod::Copy(m) => match m.arguments { + jmap_proto::method::copy::RequestArguments::Email => Permission::JmapEmailCopy, + }, + RequestMethod::CopyBlob(_) => Permission::JmapBlobCopy, + RequestMethod::ImportEmail(_) => Permission::JmapEmailImport, + RequestMethod::ParseEmail(_) => Permission::JmapEmailParse, + RequestMethod::QueryChanges(m) => match m.arguments { + jmap_proto::method::query::RequestArguments::Email(_) => { + Permission::JmapEmailQueryChanges + } + jmap_proto::method::query::RequestArguments::Mailbox(_) => { + Permission::JmapMailboxQueryChanges + } + jmap_proto::method::query::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionQueryChanges + } + jmap_proto::method::query::RequestArguments::SieveScript => { + Permission::JmapSieveScriptQueryChanges + } + jmap_proto::method::query::RequestArguments::Principal => { + Permission::JmapPrincipalQueryChanges + } + jmap_proto::method::query::RequestArguments::Quota => { + Permission::JmapQuotaQueryChanges + } + }, + RequestMethod::Query(m) => match m.arguments { + jmap_proto::method::query::RequestArguments::Email(_) => Permission::JmapEmailQuery, + jmap_proto::method::query::RequestArguments::Mailbox(_) => { + Permission::JmapMailboxQuery + } + jmap_proto::method::query::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionQuery + } + jmap_proto::method::query::RequestArguments::SieveScript => { + Permission::JmapSieveScriptQuery + } + jmap_proto::method::query::RequestArguments::Principal => { + Permission::JmapPrincipalQuery + } + jmap_proto::method::query::RequestArguments::Quota => Permission::JmapQuotaQuery, + }, + RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, + RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, + RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup, + RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload, + RequestMethod::Echo(_) => Permission::JmapEcho, + RequestMethod::Error(_) => return Ok(()), + }; + + if self.has_permission(permission) { + Ok(()) + } else { + Err(trc::JmapEvent::Forbidden + .into_err() + .details("You are not authorized to perform this action")) + } + } } pub struct SymmetricEncrypt { diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs index 38cf87e6..b25c13cf 100644 --- a/crates/jmap/src/auth/oauth/auth.rs +++ b/crates/jmap/src/auth/oauth/auth.rs @@ -91,8 +91,8 @@ impl JMAP { json!({ "data": { "code": client_code, - "is_admin": access_token.is_super_user(), - "is_enterprise": is_enterprise, + "permissions": access_token.permissions(), + "isEnterprise": is_enterprise, }, }) } diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 1817399c..590a9c4d 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -6,7 +6,7 @@ use std::time::SystemTime; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use hyper::StatusCode; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; @@ -187,7 +187,8 @@ impl JMAP { .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? - .secrets + .take_str_array(PrincipalField::Secrets) + .unwrap_or_default() .into_iter() .next() .ok_or("Failed to obtain password hash") diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index b5ac3491..97548d20 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -7,6 +7,7 @@ use std::{net::IpAddr, sync::Arc}; use common::listener::limiter::{ConcurrencyLimiter, InFlight}; +use directory::Permission; use trc::AddContext; use crate::JMAP; @@ -61,12 +62,12 @@ impl JMAP { if is_rate_allowed { if let Some(in_flight_request) = limiter.concurrent_requests.is_allowed() { Ok(in_flight_request) - } else if access_token.is_super_user() { + } else if access_token.has_permission(Permission::UnlimitedRequests) { Ok(InFlight::default()) } else { Err(trc::LimitEvent::ConcurrentRequest.into_err()) } - } else if access_token.is_super_user() { + } else if access_token.has_permission(Permission::UnlimitedRequests) { Ok(InFlight::default()) } else { Err(trc::LimitEvent::TooManyRequests.into_err()) @@ -97,7 +98,7 @@ impl JMAP { .is_allowed() { Ok(in_flight_request) - } else if access_token.is_super_user() { + } else if access_token.has_permission(Permission::UnlimitedRequests) { Ok(InFlight::default()) } else { Err(trc::LimitEvent::ConcurrentUpload.into_err()) diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index 7167f948..867b6cb7 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use directory::Permission; use jmap_proto::{ error::set::SetError, method::upload::{ @@ -149,7 +150,7 @@ impl JMAP { && used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size) || (self.core.jmap.upload_tmp_quota_amount > 0 && used.count + 1 > self.core.jmap.upload_tmp_quota_amount)) - && !access_token.is_super_user() + && !access_token.has_permission(Permission::UnlimitedUploads) { response.not_created.append( create_id, @@ -209,7 +210,7 @@ impl JMAP { && used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size) || (self.core.jmap.upload_tmp_quota_amount > 0 && used.count + 1 > self.core.jmap.upload_tmp_quota_amount)) - && !access_token.is_super_user() + && !access_token.has_permission(Permission::UnlimitedUploads) { let err = Err(trc::LimitEvent::BlobQuota .into_err() diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 360219f1..f02d8e24 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -125,7 +125,8 @@ impl JMAP { .await .caused_by(trc::location!())? .unwrap_or_default(); - if principal.emails.is_empty() { + let num_emails = principal.field_len(PrincipalField::Emails); + if num_emails == 0 { return Ok(identity_ids); } @@ -136,14 +137,14 @@ impl JMAP { // Create identities let name = principal - .description - .unwrap_or(principal.name) + .description() + .unwrap_or(principal.name()) .trim() .to_string(); - let has_many = principal.emails.len() > 1; - for (idx, email) in principal.emails.into_iter().enumerate() { + let has_many = num_emails > 1; + for (idx, email) in principal.iter_str(PrincipalField::Emails).enumerate() { let document_id = idx as u32; - let email = sanitize_email(&email).unwrap_or_default(); + let email = sanitize_email(email).unwrap_or_default(); if email.is_empty() { continue; } diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index e840f90f..3ab94e51 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ error::set::SetError, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -61,11 +61,9 @@ impl JMAP { .storage .directory .query(QueryBy::Id(account_id), false) - .await + .await? .unwrap_or_default() - .unwrap_or_default() - .emails - .contains(email) + .has_str_value(PrincipalField::Emails, email) { response.not_created.append( id, diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index c99ff030..5eb6beb6 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -329,7 +329,7 @@ impl JMAP { .query(QueryBy::Id(account_id), false) .await .add_context(|err| err.caused_by(trc::location!()).account_id(account_id))? - .map(|p| p.quota as i64) + .map(|p| p.quota() as i64) .unwrap_or_default() }) } diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 3a765296..eea8fb13 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -5,6 +5,7 @@ */ use common::config::jmap::settings::SpecialUse; +use directory::Permission; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{SetRequest, SetResponse}, @@ -295,7 +296,9 @@ impl JMAP { ) -> trc::Result<Result<bool, SetError>> { // Internal folders cannot be deleted #[cfg(feature = "test_mode")] - if [INBOX_ID, TRASH_ID].contains(&document_id) && !access_token.is_super_user() { + if [INBOX_ID, TRASH_ID].contains(&document_id) + && !access_token.has_permission(Permission::DeleteSystemFolders) + { return Ok(Err(SetError::forbidden().with_description( "You are not allowed to delete Inbox, Junk or Trash folders.", ))); diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index b2ab9104..b59abd77 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -67,16 +67,15 @@ impl JMAP { for property in &properties { let value = match property { Property::Id => Value::Id(id), - Property::Type => Value::Text(principal.typ.to_jmap().to_string()), - Property::Name => Value::Text(principal.name.clone()), + Property::Type => Value::Text(principal.typ().to_jmap().to_string()), + Property::Name => Value::Text(principal.name().to_string()), Property::Description => principal - .description - .clone() - .map(Value::Text) + .description() + .map(|v| Value::Text(v.to_string())) .unwrap_or(Value::Null), Property::Email => principal - .emails - .first() + .iter_str(PrincipalField::Emails) + .next() .map(|email| Value::Text(email.clone())) .unwrap_or(Value::Null), _ => Value::Null, diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index ed53c4aa..0ed616ea 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -37,9 +37,9 @@ impl JMAP { .query(QueryBy::Name(name.as_str()), false) .await? { - if is_set || result_set.results.contains(principal.id) { + if is_set || result_set.results.contains(principal.id()) { result_set.results = - RoaringBitmap::from_sorted_iter([principal.id]).unwrap(); + RoaringBitmap::from_sorted_iter([principal.id()]).unwrap(); } else { result_set.results = RoaringBitmap::new(); } diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index ca2548b9..af20adb5 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -104,7 +104,7 @@ impl JMAP { .query(QueryBy::Id(*uid), false) .await { - Ok(Some(p)) => p.quota as i64, + Ok(Some(p)) => p.quota() as i64, Ok(None) => 0, Err(err) => { trc::error!(err diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index 08ccb3a5..a107bf2f 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -7,7 +7,7 @@ use std::borrow::Cow; use common::listener::stream::NullIo; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::MessageParser; use sieve::{Envelope, Event, Input, Mailbox, Recipient}; @@ -72,9 +72,15 @@ impl JMAP { .query(QueryBy::Id(account_id), false) .await { - Ok(Some(p)) => { + Ok(Some(mut p)) => { instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); - (p.quota as i64, p.emails.into_iter().next()) + ( + p.quota() as i64, + p.take_str_array(PrincipalField::Emails) + .unwrap_or_default() + .into_iter() + .next(), + ) } Ok(None) => (0, None), Err(err) => { diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs index 80e7b298..13198e70 100644 --- a/crates/managesieve/src/op/authenticate.rs +++ b/crates/managesieve/src/op/authenticate.rs @@ -5,6 +5,7 @@ */ use common::listener::{limiter::ConcurrencyLimiter, SessionStream}; +use directory::Permission; use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain}; use imap_proto::{ protocol::authenticate::Mechanism, @@ -116,6 +117,9 @@ impl<T: SessionStream> Session<T> { } }; + // Validate access + access_token.assert_has_permission(Permission::SieveAuthenticate)?; + // Cache access token let access_token = Arc::new(access_token); self.jmap.cache_access_token(access_token.clone()); diff --git a/crates/managesieve/src/op/checkscript.rs b/crates/managesieve/src/op/checkscript.rs index c014ea0b..58f79217 100644 --- a/crates/managesieve/src/op/checkscript.rs +++ b/crates/managesieve/src/op/checkscript.rs @@ -6,13 +6,17 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use imap_proto::receiver::Request; -use tokio::io::{AsyncRead, AsyncWrite}; use crate::core::{Command, Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_checkscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SieveCheckScript)?; + let op_start = Instant::now(); if request.tokens.is_empty() { diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs index 092ffa8b..bd7219a9 100644 --- a/crates/managesieve/src/op/deletescript.rs +++ b/crates/managesieve/src/op/deletescript.rs @@ -6,16 +6,20 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use imap_proto::receiver::Request; use jmap_proto::types::collection::Collection; use store::write::log::ChangeLogBuilder; -use tokio::io::{AsyncRead, AsyncWrite}; use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_deletescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SieveDeleteScript)?; + let op_start = Instant::now(); let name = request diff --git a/crates/managesieve/src/op/getscript.rs b/crates/managesieve/src/op/getscript.rs index 77406bea..e21f368d 100644 --- a/crates/managesieve/src/op/getscript.rs +++ b/crates/managesieve/src/op/getscript.rs @@ -6,19 +6,23 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use imap_proto::receiver::Request; use jmap::sieve::set::ObjectBlobId; use jmap_proto::{ object::Object, types::{collection::Collection, property::Property, value::Value}, }; -use tokio::io::{AsyncRead, AsyncWrite}; use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_getscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SieveGetScript)?; + let op_start = Instant::now(); let name = request .tokens diff --git a/crates/managesieve/src/op/havespace.rs b/crates/managesieve/src/op/havespace.rs index 05eeb59e..056c6208 100644 --- a/crates/managesieve/src/op/havespace.rs +++ b/crates/managesieve/src/op/havespace.rs @@ -6,14 +6,18 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use imap_proto::receiver::Request; -use tokio::io::{AsyncRead, AsyncWrite}; use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_havespace(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SieveHaveSpace)?; + let op_start = Instant::now(); let mut tokens = request.tokens.into_iter(); let name = tokens diff --git a/crates/managesieve/src/op/listscripts.rs b/crates/managesieve/src/op/listscripts.rs index 5d3b004e..5659d293 100644 --- a/crates/managesieve/src/op/listscripts.rs +++ b/crates/managesieve/src/op/listscripts.rs @@ -6,17 +6,21 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use jmap_proto::{ object::Object, types::{collection::Collection, property::Property, value::Value}, }; -use tokio::io::{AsyncRead, AsyncWrite}; use trc::AddContext; use crate::core::{Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_listscripts(&mut self) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SieveListScripts)?; + let op_start = Instant::now(); let account_id = self.state.access_token().primary_id(); let document_ids = self diff --git a/crates/managesieve/src/op/mod.rs b/crates/managesieve/src/op/mod.rs index bd92df4d..e49dc19e 100644 --- a/crates/managesieve/src/op/mod.rs +++ b/crates/managesieve/src/op/mod.rs @@ -5,8 +5,9 @@ */ use common::listener::SessionStream; +use directory::Permission; -use crate::core::{Session, StatusResponse}; +use crate::core::{Session, State, StatusResponse}; pub mod authenticate; pub mod capability; @@ -31,4 +32,13 @@ impl<T: SessionStream> Session<T> { Ok(StatusResponse::ok("Begin TLS negotiation now").into_bytes()) } + + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + match &self.state { + State::Authenticated { access_token, .. } => { + access_token.assert_has_permission(permission) + } + State::NotAuthenticated { .. } => Ok(()), + } + } } diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs index b6a7c05b..24ab413a 100644 --- a/crates/managesieve/src/op/putscript.rs +++ b/crates/managesieve/src/op/putscript.rs @@ -6,6 +6,8 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use imap_proto::receiver::Request; use jmap::sieve::set::{ObjectBlobId, SCHEMA}; use jmap_proto::{ @@ -18,13 +20,15 @@ use store::{ write::{assert::HashedValue, log::LogInsert, BatchBuilder, BlobOp, DirectoryClass}, BlobClass, }; -use tokio::io::{AsyncRead, AsyncWrite}; use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_putscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SievePutScript)?; + let op_start = Instant::now(); let mut tokens = request.tokens.into_iter(); let name = tokens diff --git a/crates/managesieve/src/op/renamescript.rs b/crates/managesieve/src/op/renamescript.rs index b0c76d25..6bd84345 100644 --- a/crates/managesieve/src/op/renamescript.rs +++ b/crates/managesieve/src/op/renamescript.rs @@ -6,6 +6,8 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use imap_proto::receiver::Request; use jmap::sieve::set::SCHEMA; use jmap_proto::{ @@ -13,13 +15,15 @@ use jmap_proto::{ types::{collection::Collection, property::Property, value::Value}, }; use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder}; -use tokio::io::{AsyncRead, AsyncWrite}; use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_renamescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SieveRenameScript)?; + let op_start = Instant::now(); let mut tokens = request.tokens.into_iter(); let name = tokens diff --git a/crates/managesieve/src/op/setactive.rs b/crates/managesieve/src/op/setactive.rs index 616b004d..fa88b725 100644 --- a/crates/managesieve/src/op/setactive.rs +++ b/crates/managesieve/src/op/setactive.rs @@ -6,16 +6,20 @@ use std::time::Instant; +use common::listener::SessionStream; +use directory::Permission; use imap_proto::receiver::Request; use jmap_proto::types::collection::Collection; use store::write::log::ChangeLogBuilder; -use tokio::io::{AsyncRead, AsyncWrite}; use trc::AddContext; use crate::core::{Command, Session, StatusResponse}; -impl<T: AsyncRead + AsyncWrite> Session<T> { +impl<T: SessionStream> Session<T> { pub async fn handle_setactive(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { + // Validate access + self.assert_has_permission(Permission::SieveSetActive)?; + let op_start = Instant::now(); let name = request .tokens diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml index d24e18c8..36bdfa3d 100644 --- a/crates/pop3/Cargo.toml +++ b/crates/pop3/Cargo.toml @@ -7,6 +7,7 @@ resolver = "2" [dependencies] store = { path = "../store" } common = { path = "../common" } +directory = { path = "../directory" } jmap = { path = "../jmap" } imap = { path = "../imap" } utils = { path = "../utils" } diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs index e70b1c7d..5cea1276 100644 --- a/crates/pop3/src/client.rs +++ b/crates/pop3/src/client.rs @@ -9,7 +9,7 @@ use mail_send::Credentials; use trc::AddContext; use crate::{ - protocol::{request::Error, response::Response, Command, Mechanism}, + protocol::{request::Error, Command, Mechanism}, Session, State, }; @@ -117,54 +117,11 @@ impl<T: SessionStream> Session<T> { self.write_ok("NOOP").await.map(|_| SessionResult::Continue) } Command::Rset => self.handle_rset().await.map(|_| SessionResult::Continue), - Command::Capa => { - let mechanisms = - if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { - vec![Mechanism::Plain, Mechanism::OAuthBearer] - } else { - vec![Mechanism::OAuthBearer] - }; - - trc::event!( - Pop3(trc::Pop3Event::Capabilities), - SpanId = self.session_id, - Tls = self.stream.is_tls(), - Strict = !self.jmap.core.imap.allow_plain_auth, - Elapsed = trc::Value::Duration(0) - ); - - self.write_bytes( - Response::Capability::<u32> { - mechanisms, - stls: !self.stream.is_tls(), - } - .serialize(), - ) - .await - .map(|_| SessionResult::Continue) - } + Command::Capa => self.handle_capa().await.map(|_| SessionResult::Continue), Command::Stls => { - trc::event!( - Pop3(trc::Pop3Event::StartTls), - SpanId = self.session_id, - Elapsed = trc::Value::Duration(0) - ); - - self.write_ok("Begin TLS negotiation now") - .await - .map(|_| SessionResult::UpgradeTls) - } - Command::Utf8 => { - trc::event!( - Pop3(trc::Pop3Event::Utf8), - SpanId = self.session_id, - Elapsed = trc::Value::Duration(0) - ); - - self.write_ok("UTF8 enabled") - .await - .map(|_| SessionResult::Continue) + self.handle_stls().await.map(|_| SessionResult::UpgradeTls) } + Command::Utf8 => self.handle_utf8().await.map(|_| SessionResult::Continue), Command::Auth { mechanism, params } => self .handle_sasl(mechanism, params) .await diff --git a/crates/pop3/src/lib.rs b/crates/pop3/src/lib.rs index 6c0177d9..63b2b3d0 100644 --- a/crates/pop3/src/lib.rs +++ b/crates/pop3/src/lib.rs @@ -8,7 +8,7 @@ use std::{net::IpAddr, sync::Arc}; use common::listener::{limiter::InFlight, ServerInstance, SessionStream}; use imap::core::{ImapInstance, Inner}; -use jmap::JMAP; +use jmap::{auth::AccessToken, JMAP}; use mailbox::Mailbox; use protocol::request::Parser; @@ -51,6 +51,7 @@ pub enum State { Authenticated { mailbox: Mailbox, in_flight: Option<InFlight>, + access_token: Arc<AccessToken>, }, } @@ -68,4 +69,11 @@ impl State { _ => unreachable!(), } } + + pub fn access_token(&self) -> &Arc<AccessToken> { + match self { + State::Authenticated { access_token, .. } => access_token, + _ => unreachable!(), + } + } } diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs index c3662c29..39d129a0 100644 --- a/crates/pop3/src/op/authenticate.rs +++ b/crates/pop3/src/op/authenticate.rs @@ -5,6 +5,7 @@ */ use common::listener::{limiter::ConcurrencyLimiter, SessionStream}; +use directory::Permission; use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain}; use jmap::auth::rate_limit::ConcurrencyLimiters; use mail_parser::decoders::base64::base64_decode; @@ -112,6 +113,9 @@ impl<T: SessionStream> Session<T> { } }; + // Validate access + access_token.assert_has_permission(Permission::Pop3Authenticate)?; + // Cache access token let access_token = Arc::new(access_token); self.jmap.cache_access_token(access_token.clone()); @@ -120,7 +124,11 @@ impl<T: SessionStream> Session<T> { let mailbox = self.fetch_mailbox(access_token.primary_id()).await?; // Create session - self.state = State::Authenticated { in_flight, mailbox }; + self.state = State::Authenticated { + in_flight, + mailbox, + access_token, + }; self.write_ok("Authentication successful").await } diff --git a/crates/pop3/src/op/delete.rs b/crates/pop3/src/op/delete.rs index 86fbcd1c..4e7799b2 100644 --- a/crates/pop3/src/op/delete.rs +++ b/crates/pop3/src/op/delete.rs @@ -7,6 +7,7 @@ use std::time::Instant; use common::listener::SessionStream; +use directory::Permission; use jmap_proto::types::{state::StateChange, type_state::DataType}; use store::roaring::RoaringBitmap; use trc::AddContext; @@ -15,6 +16,11 @@ use crate::{protocol::response::Response, Session, State}; impl<T: SessionStream> Session<T> { pub async fn handle_dele(&mut self, msgs: Vec<u32>) -> trc::Result<()> { + // Validate access + self.state + .access_token() + .assert_has_permission(Permission::Pop3Dele)?; + let op_start = Instant::now(); let mailbox = self.state.mailbox_mut(); let mut response = Vec::new(); diff --git a/crates/pop3/src/op/fetch.rs b/crates/pop3/src/op/fetch.rs index 3d395e1c..be322723 100644 --- a/crates/pop3/src/op/fetch.rs +++ b/crates/pop3/src/op/fetch.rs @@ -7,6 +7,7 @@ use std::time::Instant; use common::listener::SessionStream; +use directory::Permission; use jmap::email::metadata::MessageMetadata; use jmap_proto::types::{collection::Collection, property::Property}; use store::write::Bincode; @@ -16,6 +17,11 @@ use crate::{protocol::response::Response, Session}; impl<T: SessionStream> Session<T> { pub async fn handle_fetch(&mut self, msg: u32, lines: Option<u32>) -> trc::Result<()> { + // Validate access + self.state + .access_token() + .assert_has_permission(Permission::Pop3Retr)?; + let op_start = Instant::now(); let mailbox = self.state.mailbox(); if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) { diff --git a/crates/pop3/src/op/list.rs b/crates/pop3/src/op/list.rs index a3519e37..0c004ce0 100644 --- a/crates/pop3/src/op/list.rs +++ b/crates/pop3/src/op/list.rs @@ -7,11 +7,17 @@ use std::time::Instant; use common::listener::SessionStream; +use directory::Permission; use crate::{protocol::response::Response, Session}; impl<T: SessionStream> Session<T> { pub async fn handle_list(&mut self, msg: Option<u32>) -> trc::Result<()> { + // Validate access + self.state + .access_token() + .assert_has_permission(Permission::Pop3List)?; + let op_start = Instant::now(); let mailbox = self.state.mailbox(); if let Some(msg) = msg { @@ -48,6 +54,11 @@ impl<T: SessionStream> Session<T> { } pub async fn handle_uidl(&mut self, msg: Option<u32>) -> trc::Result<()> { + // Validate access + self.state + .access_token() + .assert_has_permission(Permission::Pop3Uidl)?; + let op_start = Instant::now(); let mailbox = self.state.mailbox(); if let Some(msg) = msg { @@ -92,6 +103,11 @@ impl<T: SessionStream> Session<T> { } pub async fn handle_stat(&mut self) -> trc::Result<()> { + // Validate access + self.state + .access_token() + .assert_has_permission(Permission::Pop3Stat)?; + let op_start = Instant::now(); let mailbox = self.state.mailbox(); diff --git a/crates/pop3/src/op/mod.rs b/crates/pop3/src/op/mod.rs index bb5f22d7..0488caa8 100644 --- a/crates/pop3/src/op/mod.rs +++ b/crates/pop3/src/op/mod.rs @@ -4,7 +4,61 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::listener::SessionStream; + +use crate::{ + protocol::{response::Response, Mechanism}, + Session, +}; + pub mod authenticate; pub mod delete; pub mod fetch; pub mod list; + +impl<T: SessionStream> Session<T> { + pub async fn handle_capa(&mut self) -> trc::Result<()> { + let mechanisms = if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { + vec![Mechanism::Plain, Mechanism::OAuthBearer] + } else { + vec![Mechanism::OAuthBearer] + }; + + trc::event!( + Pop3(trc::Pop3Event::Capabilities), + SpanId = self.session_id, + Tls = self.stream.is_tls(), + Strict = !self.jmap.core.imap.allow_plain_auth, + Elapsed = trc::Value::Duration(0) + ); + + self.write_bytes( + Response::Capability::<u32> { + mechanisms, + stls: !self.stream.is_tls(), + } + .serialize(), + ) + .await + } + + pub async fn handle_stls(&mut self) -> trc::Result<()> { + trc::event!( + Pop3(trc::Pop3Event::StartTls), + SpanId = self.session_id, + Elapsed = trc::Value::Duration(0) + ); + + self.write_ok("Begin TLS negotiation now").await + } + + pub async fn handle_utf8(&mut self) -> trc::Result<()> { + trc::event!( + Pop3(trc::Pop3Event::Utf8), + SpanId = self.session_id, + Elapsed = trc::Value::Duration(0) + ); + + self.write_ok("UTF8 enabled").await + } +} diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index 4e8131cd..d7ff8c24 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -5,6 +5,7 @@ */ use common::listener::SessionStream; +use directory::backend::internal::PrincipalField; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use smtp_proto::{IntoString, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; @@ -177,10 +178,10 @@ impl<T: SessionStream> Session<T> { .await { Ok(principal) => { + let todo = "check smtp auth permissions"; self.data.authenticated_as = authenticated_as.to_lowercase(); self.data.authenticated_emails = principal - .emails - .into_iter() + .iter_str(PrincipalField::Emails) .map(|e| e.trim().to_lowercase()) .collect(); self.eval_post_auth_params().await; diff --git a/crates/store/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs index 1642b629..8f7dd69a 100644 --- a/crates/store/src/backend/rocksdb/write.rs +++ b/crates/store/src/backend/rocksdb/write.rs @@ -308,7 +308,9 @@ impl<'x> RocksDBTransaction<'x> { if !matches { txn.rollback()?; - return Err(CommitError::Internal(trc::StoreEvent::AssertValueFailed.into())); + return Err(CommitError::Internal( + trc::StoreEvent::AssertValueFailed.into(), + )); } } } diff --git a/crates/trc/event-macro/Cargo.toml b/crates/trc/event-macro/Cargo.toml index 52ea6407..46fe7980 100644 --- a/crates/trc/event-macro/Cargo.toml +++ b/crates/trc/event-macro/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" proc-macro = true [dependencies] -syn = { version = "1.0", features = ["full"] } +syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs index c0ecb311..549a92c0 100644 --- a/crates/trc/src/event/description.rs +++ b/crates/trc/src/event/description.rs @@ -1784,6 +1784,7 @@ impl SecurityEvent { SecurityEvent::BruteForceBan => "Banned due to brute force attack", SecurityEvent::LoiterBan => "Banned due to loitering", SecurityEvent::IpBlocked => "Blocked IP address", + SecurityEvent::Unauthorized => "Unauthorized access", } } @@ -1797,6 +1798,7 @@ impl SecurityEvent { } SecurityEvent::LoiterBan => "IP address was banned due to multiple loitering events", SecurityEvent::IpBlocked => "Rejected connection from blocked IP address", + SecurityEvent::Unauthorized => "Account does not have permission to access resource", } } } diff --git a/crates/trc/src/event/mod.rs b/crates/trc/src/event/mod.rs index 761a49e3..6be92077 100644 --- a/crates/trc/src/event/mod.rs +++ b/crates/trc/src/event/mod.rs @@ -261,6 +261,7 @@ impl EventType { EventType::Auth(cause) => cause.message(), EventType::Config(_) => "Configuration error", EventType::Resource(cause) => cause.message(), + EventType::Security(_) => "Insufficient permissions", _ => "Internal server error", } } diff --git a/crates/trc/src/ipc/bitset.rs b/crates/trc/src/ipc/bitset.rs index e7ccbc22..e4d6b0f1 100644 --- a/crates/trc/src/ipc/bitset.rs +++ b/crates/trc/src/ipc/bitset.rs @@ -51,6 +51,10 @@ impl<const N: usize> Bitset<N> { } true } + + pub fn inner(&self) -> &[usize; N] { + &self.0 + } } impl<const N: usize> Default for Bitset<N> { diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index c3e914ae..6b40b68b 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -202,6 +202,7 @@ pub enum SecurityEvent { BruteForceBan, LoiterBan, IpBlocked, + Unauthorized, } #[event_type] diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs index 68f346b1..07102ffa 100644 --- a/crates/trc/src/serializers/binary.rs +++ b/crates/trc/src/serializers/binary.rs @@ -858,6 +858,7 @@ impl EventType { EventType::Security(SecurityEvent::BruteForceBan) => 549, EventType::Security(SecurityEvent::LoiterBan) => 550, EventType::Smtp(SmtpEvent::MailFromNotAllowed) => 551, + EventType::Security(SecurityEvent::Unauthorized) => 552, } } @@ -1455,6 +1456,7 @@ impl EventType { 549 => Some(EventType::Security(SecurityEvent::BruteForceBan)), 550 => Some(EventType::Security(SecurityEvent::LoiterBan)), 551 => Some(EventType::Smtp(SmtpEvent::MailFromNotAllowed)), + 552 => Some(EventType::Security(SecurityEvent::Unauthorized)), _ => None, } } diff --git a/crates/utils/proc-macros/Cargo.toml b/crates/utils/proc-macros/Cargo.toml new file mode 100644 index 00000000..7f1060ea --- /dev/null +++ b/crates/utils/proc-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "proc_macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" diff --git a/crates/utils/proc-macros/src/lib.rs b/crates/utils/proc-macros/src/lib.rs new file mode 100644 index 00000000..77fbf969 --- /dev/null +++ b/crates/utils/proc-macros/src/lib.rs @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput}; + +#[proc_macro_derive(EnumMethods)] +pub fn enum_id(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let variants = match input.data { + Data::Enum(ref data) => &data.variants, + _ => panic!("EnumMethods only works on enums"), + }; + + let variant_count = variants.len(); + let variant_names: Vec<_> = variants.iter().map(|v| &v.ident).collect(); + let variant_ids: Vec<usize> = (0..variant_count).collect(); + let snake_case_names: Vec<String> = variant_names + .iter() + .map(|name| to_snake_case(&name.to_string())) + .collect(); + + let expanded = quote! { + impl #name { + pub const COUNT: usize = #variant_count; + + pub const fn id(&self) -> usize { + match self { + #(#name::#variant_names => #variant_ids,)* + } + } + + pub fn from_id(id: usize) -> Option<Self> { + match id { + #(#variant_ids => Some(#name::#variant_names),)* + _ => None, + } + } + + pub fn name(&self) -> &'static str { + match self { + #(#name::#variant_names => #snake_case_names,)* + } + } + + pub fn from_name(name: &str) -> Option<Self> { + match name { + #(#snake_case_names => Some(#name::#variant_names),)* + _ => None, + } + } + } + }; + + TokenStream::from(expanded) +} + +fn to_snake_case(s: &str) -> String { + let mut result = String::new(); + for (i, ch) in s.char_indices() { + if ch.is_uppercase() { + if i > 0 { + result.push('-'); + } + result.push(ch.to_ascii_lowercase()); + } else { + result.push(ch); + } + } + result +} diff --git a/crates/utils/src/map/vec_map.rs b/crates/utils/src/map/vec_map.rs index d39087c9..32d8803b 100644 --- a/crates/utils/src/map/vec_map.rs +++ b/crates/utils/src/map/vec_map.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Borrow, fmt}; +use std::{borrow::Borrow, cmp::Ordering, fmt, hash::Hash}; use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize}; @@ -14,50 +14,51 @@ use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct VecMap<K: Eq + PartialEq, V> { - pub k: Vec<K>, - pub v: Vec<V>, + inner: Vec<KeyValue<K, V>>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyValue<K: Eq + PartialEq, V> { + key: K, + value: V, } impl<K: Eq + PartialEq, V> Default for VecMap<K, V> { fn default() -> Self { - VecMap { - k: Vec::new(), - v: Vec::new(), - } + VecMap { inner: Vec::new() } } } impl<K: Eq + PartialEq, V> VecMap<K, V> { pub fn new() -> Self { - Self { - k: Vec::new(), - v: Vec::new(), - } + Self::default() } pub fn with_capacity(capacity: usize) -> Self { Self { - k: Vec::with_capacity(capacity), - v: Vec::with_capacity(capacity), + inner: Vec::with_capacity(capacity), } } #[inline(always)] pub fn set(&mut self, key: K, value: V) -> bool { - if let Some(pos) = self.k.iter().position(|k| *k == key) { - self.v[pos] = value; + if let Some(kv) = self.inner.iter_mut().find(|kv| kv.key == key) { + kv.value = value; false } else { - self.k.push(key); - self.v.push(value); + self.inner.push(KeyValue { key, value }); true } } #[inline(always)] pub fn append(&mut self, key: K, value: V) { - self.k.push(key); - self.v.push(value); + self.inner.push(KeyValue { key, value }); + } + + #[inline(always)] + pub fn insert(&mut self, idx: usize, key: K, value: V) { + self.inner.insert(idx, KeyValue { key, value }); } #[inline(always)] @@ -65,104 +66,158 @@ impl<K: Eq + PartialEq, V> VecMap<K, V> { where K: Borrow<Q> + PartialEq<Q>, { - self.k.iter().position(|k| k == key).map(|pos| &self.v[pos]) + self.inner.iter().find_map(|kv| { + if &kv.key == key { + Some(&kv.value) + } else { + None + } + }) } #[inline(always)] pub fn get_mut(&mut self, key: &K) -> Option<&mut V> { - self.k - .iter_mut() - .position(|k| k == key) - .map(|pos| &mut self.v[pos]) + self.inner.iter_mut().find_map(|kv| { + if &kv.key == key { + Some(&mut kv.value) + } else { + None + } + }) } #[inline(always)] pub fn contains_key(&self, key: &K) -> bool { - self.k.contains(key) + self.inner.iter().any(|kv| kv.key == *key) } #[inline(always)] pub fn remove(&mut self, key: &K) -> Option<V> { - self.k.iter().position(|k| k == key).map(|pos| { - self.k.swap_remove(pos); - self.v.swap_remove(pos) - }) + self.inner + .iter() + .position(|kv| kv.key == *key) + .map(|pos| self.inner.remove(pos).value) + } + + #[inline(always)] + pub fn remove_all(&mut self, key: &K) { + self.inner.retain(|kv| kv.key != *key); } #[inline(always)] pub fn remove_entry(&mut self, key: &K) -> Option<(K, V)> { - self.k - .iter() - .position(|k| k == key) - .map(|pos| (self.k.swap_remove(pos), self.v.swap_remove(pos))) + self.inner.iter().position(|k| &k.key == key).map(|pos| { + let kv = self.inner.remove(pos); + (kv.key, kv.value) + }) } #[inline(always)] pub fn swap_remove(&mut self, index: usize) -> V { - self.k.swap_remove(index); - self.v.swap_remove(index) + self.inner.swap_remove(index).value } #[inline(always)] pub fn is_empty(&self) -> bool { - self.k.is_empty() + self.inner.is_empty() } #[inline(always)] pub fn len(&self) -> usize { - self.k.len() + self.inner.len() } #[inline(always)] pub fn clear(&mut self) { - self.k.clear(); - self.v.clear(); + self.inner.clear(); } #[inline(always)] pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> { - self.k.iter().zip(self.v.iter()) + self.inner.iter().map(|kv| (&kv.key, &kv.value)) + } + + #[inline(always)] + pub fn iter_by_key<'x, 'y: 'x>(&'x self, key: &'y K) -> impl Iterator<Item = &'x V> + 'x { + self.inner.iter().filter_map(move |kv| { + if &kv.key == key { + Some(&kv.value) + } else { + None + } + }) } #[inline(always)] pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut K, &mut V)> { - self.k.iter_mut().zip(self.v.iter_mut()) + self.inner.iter_mut().map(|kv| (&mut kv.key, &mut kv.value)) + } + + #[inline(always)] + pub fn iter_mut_by_key<'x, 'y: 'x>( + &'x mut self, + key: &'y K, + ) -> impl Iterator<Item = &'x mut V> + 'x { + self.inner.iter_mut().filter_map(move |kv| { + if &kv.key == key { + Some(&mut kv.value) + } else { + None + } + }) } #[inline(always)] pub fn keys(&self) -> impl Iterator<Item = &K> { - self.k.iter() + self.inner.iter().map(|kv| &kv.key) } #[inline(always)] pub fn values(&self) -> impl Iterator<Item = &V> { - self.v.iter() + self.inner.iter().map(|kv| &kv.value) } #[inline(always)] pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> { - self.v.iter_mut() + self.inner.iter_mut().map(|kv| &mut kv.value) } pub fn get_mut_or_insert_with(&mut self, key: K, fnc: impl FnOnce() -> V) -> &mut V { - if let Some(pos) = self.k.iter().position(|k| k == &key) { - &mut self.v[pos] + if let Some(pos) = self.inner.iter().position(|kv| kv.key == key) { + &mut self.inner[pos].value } else { - self.k.push(key); - self.v.push(fnc()); - self.v.last_mut().unwrap() + self.inner.push(KeyValue { key, value: fnc() }); + &mut self.inner.last_mut().unwrap().value } } + + pub fn with_key_value(mut self, key: K, value: V) -> Self { + self.append(key, value); + self + } + + pub fn sort_unstable(&mut self) + where + K: Ord, + V: Ord, + { + self.inner.sort_unstable_by(|a, b| match a.key.cmp(&b.key) { + Ordering::Equal => a.value.cmp(&b.value), + cmp => cmp, + }); + } } impl<K: Eq + PartialEq, V: Default> VecMap<K, V> { pub fn get_mut_or_insert(&mut self, key: K) -> &mut V { - if let Some(pos) = self.k.iter().position(|k| k == &key) { - &mut self.v[pos] + if let Some(pos) = self.inner.iter().position(|kv| kv.key == key) { + &mut self.inner[pos].value } else { - self.k.push(key); - self.v.push(V::default()); - self.v.last_mut().unwrap() + self.inner.push(KeyValue { + key, + value: V::default(), + }); + &mut self.inner.last_mut().unwrap().value } } } @@ -170,20 +225,34 @@ impl<K: Eq + PartialEq, V: Default> VecMap<K, V> { impl<K: Eq + PartialEq, V> IntoIterator for VecMap<K, V> { type Item = (K, V); - type IntoIter = std::iter::Zip<std::vec::IntoIter<K>, std::vec::IntoIter<V>>; + type IntoIter = + std::iter::Map<std::vec::IntoIter<KeyValue<K, V>>, fn(KeyValue<K, V>) -> (K, V)>; fn into_iter(self) -> Self::IntoIter { - self.k.into_iter().zip(self.v) + self.inner.into_iter().map(|kv| (kv.key, kv.value)) } } impl<'x, K: Eq + PartialEq, V> IntoIterator for &'x VecMap<K, V> { type Item = (&'x K, &'x V); - type IntoIter = std::iter::Zip<std::slice::Iter<'x, K>, std::slice::Iter<'x, V>>; + type IntoIter = std::iter::Map< + std::slice::Iter<'x, KeyValue<K, V>>, + fn(&'x KeyValue<K, V>) -> (&'x K, &'x V), + >; fn into_iter(self) -> Self::IntoIter { - self.k.iter().zip(self.v.iter()) + self.inner.iter().map(|kv| (&kv.key, &kv.value)) + } +} + +impl<K, V> Hash for VecMap<K, V> +where + K: Eq + PartialEq + Hash, + V: Hash, +{ + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.inner.hash(state); } } diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index 81b2f215..dc7acf3a 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -21,7 +21,7 @@ use store::{ BitmapKey, ValueKey, }; -use crate::directory::DirectoryTest; +use crate::directory::{DirectoryTest, IntoTestPrincipal, TestPrincipal}; #[tokio::test] async fn internal_directory() { @@ -33,20 +33,20 @@ async fn internal_directory() { // A principal without name should fail assert_eq!( - store.create_account(Principal::default(), vec![]).await, + store.create_account(Principal::default()).await, Err(manage::err_missing(PrincipalField::Name)) ); // Basic account creation let john_id = store .create_account( - Principal { + TestPrincipal { name: "john".to_string(), description: Some("John Doe".to_string()), secrets: vec!["secret".to_string(), "secret2".to_string()], ..Default::default() - }, - vec![], + } + .into(), ) .await .unwrap(); @@ -55,11 +55,11 @@ async fn internal_directory() { assert_eq!( store .create_account( - Principal { + TestPrincipal { name: "john".to_string(), ..Default::default() - }, - vec![] + } + .into(), ) .await, Err(manage::err_exists(PrincipalField::Name, "john".to_string())) @@ -69,12 +69,12 @@ async fn internal_directory() { assert_eq!( store .create_account( - Principal { + TestPrincipal { name: "jane".to_string(), emails: vec!["jane@example.org".to_string()], ..Default::default() - }, - vec![] + } + .into(), ) .await, Err(manage::not_found("example.org".to_string())) @@ -121,15 +121,15 @@ async fn internal_directory() { // Create an account with an email address let jane_id = store .create_account( - Principal { + TestPrincipal { name: "jane".to_string(), description: Some("Jane Doe".to_string()), secrets: vec!["my_secret".to_string(), "my_secret2".to_string()], emails: vec!["jane@example.org".to_string()], quota: 123, ..Default::default() - }, - vec![], + } + .into(), ) .await .unwrap(); @@ -151,8 +151,9 @@ async fn internal_directory() { true ) .await - .unwrap(), - Some(Principal { + .unwrap() + .map(|p| p.into_test()), + Some(TestPrincipal { id: jane_id, name: "jane".to_string(), description: Some("Jane Doe".to_string()), @@ -180,13 +181,13 @@ async fn internal_directory() { assert_eq!( store .create_account( - Principal { + TestPrincipal { name: "janeth".to_string(), description: Some("Janeth Doe".to_string()), emails: vec!["jane@example.org".to_string()], ..Default::default() - }, - vec![] + } + .into() ) .await, Err(manage::err_exists( @@ -198,13 +199,13 @@ async fn internal_directory() { // Create a mailing list let list_id = store .create_account( - Principal { + TestPrincipal { name: "list".to_string(), typ: Type::List, emails: vec!["list@example.org".to_string()], ..Default::default() - }, - vec![], + } + .into(), ) .await .unwrap(); @@ -235,8 +236,9 @@ async fn internal_directory() { .query(QueryBy::Name("list"), true) .await .unwrap() - .unwrap(), - Principal { + .unwrap() + .into_test(), + TestPrincipal { name: "list".to_string(), id: list_id, typ: Type::List, @@ -260,25 +262,25 @@ async fn internal_directory() { // Create groups store .create_account( - Principal { + TestPrincipal { name: "sales".to_string(), description: Some("Sales Team".to_string()), typ: Type::Group, ..Default::default() - }, - vec![], + } + .into(), ) .await .unwrap(); store .create_account( - Principal { + TestPrincipal { name: "support".to_string(), description: Some("Support Team".to_string()), typ: Type::Group, ..Default::default() - }, - vec![], + } + .into(), ) .await .unwrap(); @@ -313,8 +315,9 @@ async fn internal_directory() { ) .await .unwrap() + .into_test() .into_sorted(), - Principal { + TestPrincipal { id: john_id, name: "john".to_string(), description: Some("John Doe".to_string()), @@ -367,8 +370,9 @@ async fn internal_directory() { ) .await .unwrap() + .into_test() .into_sorted(), - Principal { + TestPrincipal { id: john_id, name: "john".to_string(), description: Some("John Doe".to_string()), @@ -398,10 +402,6 @@ async fn internal_directory() { PrincipalValue::StringList(vec!["12345".to_string()]) ), PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(1024)), - PrincipalUpdate::set( - PrincipalField::Type, - PrincipalValue::String("superuser".to_string()) - ), PrincipalUpdate::remove_item( PrincipalField::Emails, PrincipalValue::String("john@example.org".to_string()), @@ -426,15 +426,16 @@ async fn internal_directory() { ) .await .unwrap() + .into_test() .into_sorted(), - Principal { + TestPrincipal { id: john_id, name: "john.doe".to_string(), description: Some("Johnny Doe".to_string()), secrets: vec!["12345".to_string()], emails: vec!["john.doe@example.org".to_string()], quota: 1024, - typ: Type::Superuser, + typ: Type::Individual, member_of: vec!["list".to_string(), "sales".to_string()], } ); diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 2435cf88..41245794 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -6,10 +6,10 @@ use std::fmt::Debug; -use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; +use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type}; use mail_send::Credentials; -use crate::directory::{map_account_ids, DirectoryTest}; +use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal}; #[tokio::test] async fn ldap_directory() { @@ -40,14 +40,19 @@ async fn ldap_directory() { .await .unwrap() .unwrap() + .into_test() .into_sorted(), - Principal { + TestPrincipal { id: base_store.get_account_id("john").await.unwrap().unwrap(), name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], typ: Type::Individual, - member_of: map_account_ids(base_store, vec!["sales"]).await, + member_of: map_account_ids(base_store, vec!["sales"]) + .await + .into_iter() + .map(|v| v.to_string()) + .collect(), emails: vec![ "john@example.org".to_string(), "john.doe@example.org".to_string() @@ -68,8 +73,9 @@ async fn ldap_directory() { .await .unwrap() .unwrap() + .into_test() .into_sorted(), - Principal { + TestPrincipal { id: base_store.get_account_id("bill").await.unwrap().unwrap(), name: "bill".to_string(), description: "Bill Foobar".to_string().into(), @@ -102,14 +108,19 @@ async fn ldap_directory() { .await .unwrap() .unwrap() + .into_test() .into_sorted(), - Principal { + TestPrincipal { id: base_store.get_account_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, secrets: vec!["abcde".to_string()], - member_of: map_account_ids(base_store, vec!["sales", "support"]).await, + member_of: map_account_ids(base_store, vec!["sales", "support"]) + .await + .into_iter() + .map(|v| v.to_string()) + .collect(), emails: vec!["jane@example.org".to_string(),], ..Default::default() } @@ -122,8 +133,9 @@ async fn ldap_directory() { .query(QueryBy::Name("sales"), true) .await .unwrap() - .unwrap(), - Principal { + .unwrap() + .into_test(), + TestPrincipal { id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), description: "sales".to_string().into(), diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index d97f8730..732e2e90 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -11,7 +11,10 @@ pub mod smtp; pub mod sql; use common::{config::smtp::session::AddressMapping, Core}; -use directory::{backend::internal::manage::ManageDirectory, Directories}; +use directory::{ + backend::internal::{manage::ManageDirectory, PrincipalField}, + Directories, Principal, Type, +}; use mail_send::Credentials; use rustls::ServerConfig; use rustls_pemfile::{certs, pkcs8_private_keys}; @@ -254,6 +257,18 @@ pub struct DirectoryTest { pub core: Core, } +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TestPrincipal { + pub id: u32, + pub typ: Type, + pub quota: u64, + pub name: String, + pub secrets: Vec<String>, + pub emails: Vec<String>, + pub member_of: Vec<String>, + pub description: Option<String>, +} + impl DirectoryTest { pub async fn new(id_store: Option<&str>) -> DirectoryTest { let temp_dir = TempDir::new("directory_tests", true); @@ -408,6 +423,61 @@ pub fn dummy_tls_acceptor() -> Arc<TlsAcceptor> { ))) } +trait IntoTestPrincipal { + fn into_test(self) -> TestPrincipal; +} + +impl IntoTestPrincipal for Principal { + fn into_test(self) -> TestPrincipal { + TestPrincipal::from(self) + } +} + +impl TestPrincipal { + pub fn into_sorted(mut self) -> Self { + self.member_of.sort_unstable(); + self.emails.sort_unstable(); + self + } +} + +impl From<Principal> for TestPrincipal { + fn from(mut value: Principal) -> Self { + Self { + id: value.id(), + typ: value.typ(), + quota: value.quota(), + name: value.take_str(PrincipalField::Name).unwrap_or_default(), + secrets: value + .take_str_array(PrincipalField::Secrets) + .unwrap_or_default(), + emails: value + .take_str_array(PrincipalField::Emails) + .unwrap_or_default(), + member_of: value + .take_str_array(PrincipalField::MemberOf) + .unwrap_or_default(), + /*member_of: value + .iter_int(PrincipalField::MemberOf) + .map(|v| v as u32) + .collect(),*/ + description: value.take_str(PrincipalField::Description), + } + } +} + +impl From<TestPrincipal> for Principal { + fn from(value: TestPrincipal) -> Self { + Principal::new(value.id, value.typ) + .with_field(PrincipalField::Name, value.name) + .with_field(PrincipalField::Quota, value.quota) + .with_field(PrincipalField::Secrets, value.secrets) + .with_field(PrincipalField::Emails, value.emails) + .with_field(PrincipalField::MemberOf, value.member_of) + .with_opt_field(PrincipalField::Description, value.description) + } +} + #[derive(Clone, PartialEq, Eq, Hash)] pub enum Item { IsAccount(String), @@ -500,92 +570,6 @@ impl core::fmt::Debug for Item { } } -/* - -// DEPRECATED - TODO: Remove -#[tokio::test(flavor = "multi_thread")] -#[ignore] -async fn lookup_local() { - const LOOKUP_CONFIG: &str = r#" - [store."local/regex"] - type = "memory" - format = "regex" - values = ["^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", - "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"] - - [store."local/glob"] - type = "memory" - format = "glob" - values = ["*@example.org", "test@*", "localhost", "*+*@*.domain.net"] - - [store."local/list"] - type = "memory" - format = "list" - values = ["abc", "xyz", "123"] - - [store."local/suffix"] - type = "memory" - format = "glob" - comment = "//" - values = ["https://publicsuffix.org/list/public_suffix_list.dat", "fallback+file://%PATH%/public_suffix_list.dat.gz"] - "#; - - /*tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(tracing::Level::TRACE) - .finish(), - ) - .unwrap();*/ - - let mut config = utils::config::Config::new( - &LOOKUP_CONFIG.replace( - "%PATH%", - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .to_path_buf() - .join("resources") - .join("config") - .join("lists") - .to_str() - .unwrap(), - ), - ) - .unwrap(); - - let lookups = Stores::parse_all(&mut config).await.lookup_stores; - - for (lookup, item, expect) in [ - ("glob", "user@example.org", true), - ("glob", "test@otherdomain.org", true), - ("glob", "localhost", true), - ("glob", "john+doe@doefamily.domain.net", true), - ("glob", "john@domain.net", false), - ("glob", "example.org", false), - ("list", "abc", true), - ("list", "xyz", true), - ("list", "zzz", false), - ("regex", "user@domain.com", true), - ("regex", "127.0.0.1", true), - ("regex", "hello", false), - ("suffix", "co.uk", true), - ("suffix", "coco", false), - ] { - assert_eq!( - lookups - .get(&format!("local/{lookup}")) - .unwrap() - .key_get::<String>(item.as_bytes().to_vec()) - .await - .unwrap() - .is_some(), - expect, - "failed for {lookup}, item {item}" - ); - } -} -*/ - #[tokio::test] async fn address_mappings() { const MAPPINGS: &str = r#" diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index 95fa8eca..5fd22d71 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -4,11 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; +use directory::{backend::internal::manage::ManageDirectory, QueryBy, Type}; use mail_send::Credentials; use store::{LookupStore, Store}; -use crate::directory::{map_account_ids, DirectoryTest}; +use crate::directory::{map_account_ids, DirectoryTest, IntoTestPrincipal, TestPrincipal}; use super::DirectoryStore; @@ -110,14 +110,19 @@ async fn sql_directory() { ) .await .unwrap() - .unwrap(), - Principal { + .unwrap() + .into_test(), + TestPrincipal { id: base_store.get_account_id("john").await.unwrap().unwrap(), name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], typ: Type::Individual, - member_of: map_account_ids(base_store, vec!["sales"]).await, + member_of: map_account_ids(base_store, vec!["sales"]) + .await + .into_iter() + .map(|v| v.to_string()) + .collect(), emails: vec![ "john@example.org".to_string(), "jdoe@example.org".to_string(), @@ -137,8 +142,9 @@ async fn sql_directory() { ) .await .unwrap() - .unwrap(), - Principal { + .unwrap() + .into_test(), + TestPrincipal { id: base_store.get_account_id("bill").await.unwrap().unwrap(), name: "bill".to_string(), description: "Bill Foobar".to_string().into(), @@ -169,14 +175,19 @@ async fn sql_directory() { .query(QueryBy::Name("jane"), true) .await .unwrap() - .unwrap(), - Principal { + .unwrap() + .into_test(), + TestPrincipal { id: base_store.get_account_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, secrets: vec!["abcde".to_string()], - member_of: map_account_ids(base_store, vec!["sales", "support"]).await, + member_of: map_account_ids(base_store, vec!["sales", "support"]) + .await + .into_iter() + .map(|v| v.to_string()) + .collect(), emails: vec!["jane@example.org".to_string(),], ..Default::default() } @@ -188,8 +199,9 @@ async fn sql_directory() { .query(QueryBy::Name("sales"), true) .await .unwrap() - .unwrap(), - Principal { + .unwrap() + .into_test(), + TestPrincipal { id: base_store.get_account_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), description: "Sales Team".to_string().into(), diff --git a/tests/src/smtp/inbound/basic.rs b/tests/src/smtp/inbound/basic.rs index 55058a5a..1db73a77 100644 --- a/tests/src/smtp/inbound/basic.rs +++ b/tests/src/smtp/inbound/basic.rs @@ -14,8 +14,8 @@ use crate::smtp::{ #[tokio::test] async fn basic_commands() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); let mut session = Session::test(build_smtp(Core::default(), Inner::default())); diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index fdd43bdc..bd57d035 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -92,8 +92,8 @@ verify = [{if = "sender_domain = 'test.net'", then = 'relaxed'}, #[tokio::test] async fn dmarc() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); let mut inner = Inner::default(); let tmp_dir = TempDir::new("smtp_dmarc_test", true); diff --git a/tests/src/smtp/inbound/ehlo.rs b/tests/src/smtp/inbound/ehlo.rs index 8c438878..48445ddd 100644 --- a/tests/src/smtp/inbound/ehlo.rs +++ b/tests/src/smtp/inbound/ehlo.rs @@ -38,8 +38,8 @@ ehlo = [{if = "remote_ip = '10.0.0.2'", then = 'strict'}, #[tokio::test] async fn ehlo() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); let mut config = Config::new(CONFIG).unwrap(); let core = Core::parse(&mut config, Default::default(), Default::default()).await; diff --git a/tests/src/smtp/inbound/limits.rs b/tests/src/smtp/inbound/limits.rs index 3946aebc..8f92d0eb 100644 --- a/tests/src/smtp/inbound/limits.rs +++ b/tests/src/smtp/inbound/limits.rs @@ -29,8 +29,8 @@ duration = [{if = "remote_ip = '10.0.0.3'", then = '500ms'}, #[tokio::test] async fn limits() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); let mut config = Config::new(CONFIG).unwrap(); let core = Core::parse(&mut config, Default::default(), Default::default()).await; diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs index ec1c7399..3ea7fe3d 100644 --- a/tests/src/smtp/inbound/rewrite.rs +++ b/tests/src/smtp/inbound/rewrite.rs @@ -69,7 +69,6 @@ async fn address_rewrite() { // Enable logging crate::enable_logging(); - // Prepare config let mut config = Config::new(CONFIG).unwrap(); let core = Core::parse(&mut config, Default::default(), Default::default()).await; diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs index 9aa3a35a..17bf5821 100644 --- a/tests/src/smtp/lookup/utils.rs +++ b/tests/src/smtp/lookup/utils.rs @@ -49,8 +49,8 @@ ip-strategy = "ipv6_then_ipv4" #[tokio::test] async fn lookup_ip() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); let ipv6 = [ "a:b::1".parse().unwrap(), diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index 28bfb8bb..cfc4d12a 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -69,7 +69,6 @@ async fn manage_queue() { // Enable logging crate::enable_logging(); - // Start remote test server let mut remote = TestServer::new("smtp_manage_queue_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index 86cfc87d..76052870 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -57,7 +57,6 @@ async fn manage_reports() { // Enable logging crate::enable_logging(); - // Start reporting service let local = TestServer::new("smtp_manage_reports", CONFIG, true).await; let _rx = local.start(&[ServerProtocol::Http]).await; diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs index e7889825..42e4a717 100644 --- a/tests/src/smtp/outbound/dane.rs +++ b/tests/src/smtp/outbound/dane.rs @@ -85,7 +85,6 @@ async fn dane_verify() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("smtp_dane_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs index d660f7f8..57bbb1f2 100644 --- a/tests/src/smtp/outbound/extensions.rs +++ b/tests/src/smtp/outbound/extensions.rs @@ -53,7 +53,6 @@ async fn extensions() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("smtp_ext_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/outbound/fallback_relay.rs b/tests/src/smtp/outbound/fallback_relay.rs index 0e736078..76173bda 100644 --- a/tests/src/smtp/outbound/fallback_relay.rs +++ b/tests/src/smtp/outbound/fallback_relay.rs @@ -54,7 +54,6 @@ async fn fallback_relay() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("smtp_fallback_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs index 7d4bfd2c..23e4625c 100644 --- a/tests/src/smtp/outbound/ip_lookup.rs +++ b/tests/src/smtp/outbound/ip_lookup.rs @@ -33,7 +33,6 @@ async fn ip_lookup_strategy() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("smtp_iplookup_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/outbound/lmtp.rs b/tests/src/smtp/outbound/lmtp.rs index 98baed03..f27c94fc 100644 --- a/tests/src/smtp/outbound/lmtp.rs +++ b/tests/src/smtp/outbound/lmtp.rs @@ -65,7 +65,6 @@ async fn lmtp_delivery() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("lmtp_delivery_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Lmtp]).await; diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs index a6658a54..bae82bd7 100644 --- a/tests/src/smtp/outbound/mta_sts.rs +++ b/tests/src/smtp/outbound/mta_sts.rs @@ -62,7 +62,6 @@ async fn mta_sts_verify() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("smtp_mta_sts_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs index 58e4fcd1..f24ffb1f 100644 --- a/tests/src/smtp/outbound/smtp.rs +++ b/tests/src/smtp/outbound/smtp.rs @@ -73,7 +73,6 @@ async fn smtp_delivery() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("smtp_delivery_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/outbound/throttle.rs b/tests/src/smtp/outbound/throttle.rs index 436673b5..2ea8223a 100644 --- a/tests/src/smtp/outbound/throttle.rs +++ b/tests/src/smtp/outbound/throttle.rs @@ -69,7 +69,6 @@ async fn throttle_outbound() { // Enable logging crate::enable_logging(); - // Build test message let mut test_message = new_message(0); test_message.return_path_domain = "foobar.org".to_string(); diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs index ae40d850..32c712ce 100644 --- a/tests/src/smtp/outbound/tls.rs +++ b/tests/src/smtp/outbound/tls.rs @@ -46,7 +46,6 @@ async fn starttls_optional() { // Enable logging crate::enable_logging(); - // Start test server let mut remote = TestServer::new("smtp_starttls_remote", REMOTE, true).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; diff --git a/tests/src/smtp/queue/dsn.rs b/tests/src/smtp/queue/dsn.rs index 22e6d485..9397edee 100644 --- a/tests/src/smtp/queue/dsn.rs +++ b/tests/src/smtp/queue/dsn.rs @@ -34,8 +34,8 @@ sign = "['rsa']" #[tokio::test] async fn generate_dsn() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push("resources"); diff --git a/tests/src/smtp/queue/manager.rs b/tests/src/smtp/queue/manager.rs index 4c142970..58b749c2 100644 --- a/tests/src/smtp/queue/manager.rs +++ b/tests/src/smtp/queue/manager.rs @@ -23,8 +23,8 @@ relay = true #[tokio::test] async fn queue_due() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); let local = TestServer::new("smtp_queue_due_test", CONFIG, true).await; let core = local.build_smtp(); diff --git a/tests/src/smtp/queue/retry.rs b/tests/src/smtp/queue/retry.rs index ab495013..aaaaea60 100644 --- a/tests/src/smtp/queue/retry.rs +++ b/tests/src/smtp/queue/retry.rs @@ -38,7 +38,6 @@ async fn queue_retry() { // Enable logging crate::enable_logging(); - // Create temp dir for queue let mut local = TestServer::new("smtp_queue_retry_test", CONFIG, true).await; diff --git a/tests/src/smtp/reporting/analyze.rs b/tests/src/smtp/reporting/analyze.rs index bfba3396..9a28ae1d 100644 --- a/tests/src/smtp/reporting/analyze.rs +++ b/tests/src/smtp/reporting/analyze.rs @@ -28,8 +28,8 @@ store = "1s" #[tokio::test(flavor = "multi_thread")] async fn report_analyze() { - // Enable logging - crate::enable_logging(); + // Enable logging + crate::enable_logging(); // Create temp dir for queue let mut local = TestServer::new("smtp_analyze_report_test", CONFIG, true).await; |