diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index af669a71..2cf9a8ed 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -50,12 +50,17 @@ impl Server { UserRoles::User => { self.core.network.security.default_role_ids_user.as_slice() } - UserRoles::TenantAdmin => self - .core - .network - .security - .default_role_ids_tenant - .as_slice(), + UserRoles::Admin => { + if tenant_id.is_none() { + self.core.network.security.default_role_ids_admin.as_slice() + } else { + self.core + .network + .security + .default_role_ids_tenant + .as_slice() + } + } UserRoles::Custom(custom_roles) => custom_roles.role_ids.as_slice(), }, tenant_id, @@ -363,14 +368,22 @@ impl Server { } impl AccessToken { - pub fn new(inner: Arc) -> Self { + pub fn new(inner: Arc, remote_ip: IpAddr) -> trc::Result { + AccessToken { + scope_idx: 0, + inner, + } + .assert_is_valid(remote_ip) + } + + pub fn new_maybe_invalid(inner: Arc) -> Self { AccessToken { scope_idx: 0, inner, } } - pub fn scoped( + pub fn new_scoped( inner: Arc, credential_id: u32, remote_ip: IpAddr, @@ -396,7 +409,7 @@ impl AccessToken { remote_ip: IpAddr, ) -> trc::Result { if let Some(credential_id) = credential_id { - Self::scoped(inner, credential_id, remote_ip) + Self::new_scoped(inner, credential_id, remote_ip) } else { Ok(AccessToken { scope_idx: 0, @@ -475,30 +488,92 @@ impl AccessToken { } pub fn assert_is_valid(self, remote_ip: IpAddr) -> trc::Result { - if let Some(scope) = self - .inner - .scopes - .get(self.scope_idx) - .filter(|scope| scope.expires_at > now()) - { - if scope.allowed_ips.is_empty() + if let Some(scope) = self.inner.scopes.get(self.scope_idx) { + let has_expired = scope.expires_at <= now(); + let is_valid_ip = scope.allowed_ips.is_empty() || scope .allowed_ips .iter() - .any(|ip_mask| ip_mask.matches(&remote_ip)) - { - Ok(self) + .any(|ip_mask| ip_mask.matches(&remote_ip)); + + let mut access_token = self; + if has_expired { + if access_token.scope_idx > 0 { + return Err(trc::AuthEvent::CredentialExpired + .into_err() + .ctx(trc::Key::AccountId, access_token.inner.account_id) + .reason("Credential expired.")); + } else { + trc::event!( + Auth(trc::AuthEvent::CredentialExpired), + AccountId = access_token.inner.account_id, + Reason = "Main credential expired, downgrading permissions.", + ); + } + + // Downgrade permissions to allow password change + let mut scopes = Vec::with_capacity(access_token.inner.scopes.len()); + for (idx, scope) in access_token.inner.scopes.iter().enumerate() { + if idx == 0 { + let mut permissions = Permissions::new(); + + for permission in [ + Permission::Authenticate, + Permission::AuthenticateWithAlias, + Permission::SysCredentialGet, + Permission::SysCredentialQuery, + Permission::SysCredentialUpdate, + Permission::EmailReceive, + ] { + if scope.permissions.get(permission as usize) { + permissions.set(permission as usize); + } + } + + scopes.push(AccessScope { + permissions, + credential_id: scope.credential_id, + expires_at: u64::MAX, + allowed_ips: scope.allowed_ips.clone(), + }); + } else { + scopes.push(scope.clone()); + } + } + let old_inner = &access_token.inner; + let inner = AccessTokenInner { + scopes: scopes.into_boxed_slice(), + account_id: old_inner.account_id, + tenant_id: old_inner.tenant_id, + member_of: old_inner.member_of.clone(), + access_to: old_inner.access_to.clone(), + concurrent_http_requests: old_inner.concurrent_http_requests.clone(), + concurrent_imap_requests: old_inner.concurrent_imap_requests.clone(), + concurrent_uploads: old_inner.concurrent_uploads.clone(), + revision_account: old_inner.revision_account, + revision: old_inner.revision, + obj_size: old_inner.obj_size, + }; + + access_token = AccessToken { + scope_idx: access_token.scope_idx, + inner: Arc::new(inner), + }; + } + + if is_valid_ip { + Ok(access_token) } else { - Err(trc::SecurityEvent::Unauthorized + Err(trc::SecurityEvent::IpUnauthorized .into_err() - .ctx(trc::Key::AccountId, self.inner.account_id) + .ctx(trc::Key::AccountId, access_token.inner.account_id) .reason("IP address not allowed.")) } } else { Err(trc::SecurityEvent::Unauthorized .into_err() .ctx(trc::Key::AccountId, self.inner.account_id) - .reason("Credential expired.")) + .reason("Credential not valid.")) } } @@ -669,8 +744,8 @@ impl AccessToken { } } - pub fn from_id(account_id: u32) -> Self { - AccessToken::new(Arc::new(AccessTokenInner::from_id(account_id))) + pub fn from_id_maybe_invalid(account_id: u32) -> Self { + AccessToken::new_maybe_invalid(Arc::new(AccessTokenInner::from_id(account_id))) } } @@ -742,7 +817,7 @@ fn hash_account(account: &Account) -> u64 { UserRoles::User => { 0u8.hash(&mut s); } - UserRoles::TenantAdmin => { + UserRoles::Admin => { 1u8.hash(&mut s); } UserRoles::Custom(custom_roles) => { diff --git a/crates/common/src/auth/authentication.rs b/crates/common/src/auth/authentication.rs index bba8c9be..a872dde6 100644 --- a/crates/common/src/auth/authentication.rs +++ b/crates/common/src/auth/authentication.rs @@ -44,8 +44,22 @@ impl Server { { Ok(token) => Ok(token), Err(err) => { - if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) - && self.has_auth_fail2ban() + // Random delay to mitigate user enumeration attacks + #[cfg(not(feature = "test_mode"))] + { + use store::rand::{self, Rng}; + + tokio::time::sleep(std::time::Duration::from_millis( + rand::rng().random_range(50..500), + )) + .await; + } + + if matches!( + err.as_ref(), + trc::EventType::Auth(trc::AuthEvent::Failed) + | trc::EventType::Security(trc::SecurityEvent::IpUnauthorized) + ) && self.has_auth_fail2ban() && self .is_auth_fail2banned(req.remote_ip, req.username()) .await? @@ -88,7 +102,9 @@ impl Server { Details = fallback_user.to_string(), ); - self.access_token(account_id).await.map(AccessToken::new) + self.access_token(account_id) + .await + .and_then(|token| AccessToken::new(token, req.remote_ip)) } else { Err(trc::AuthEvent::Failed .into_err() @@ -149,7 +165,8 @@ impl Server { let directory_account = directory.authenticate(&req.credentials).await?; is_alias_login = directory_account.email != auth_as_address; - self.build_directory_token(directory_account).await + self.build_directory_token(directory_account, req.remote_ip) + .await } else if let Some(account_id) = self.account_id_from_parts(auth_as_local, domain.id).await? { @@ -177,22 +194,10 @@ impl Server { .await? { SecretVerificationResult::Valid => { - if credential - .expires_at - .as_ref() - .is_none_or(|exp| exp.timestamp() > now() as i64) - { - is_alias_login = account.name != auth_as_address; - self.access_token(account_id).await.map(AccessToken::new) - } else { - Err(trc::AuthEvent::Failed - .into_err() - .ctx(trc::Key::AccountName, account.name.to_string()) - .ctx(trc::Key::AccountId, account_id) - .ctx(trc::Key::Id, credential.credential_id.id()) - .ctx(trc::Key::SpanId, req.session_id) - .reason("Password credential has expired")) - } + is_alias_login = account.name != auth_as_local; + self.access_token(account_id) + .await + .and_then(|token| AccessToken::new(token, req.remote_ip)) } SecretVerificationResult::Invalid => Err(trc::AuthEvent::Failed .into_err() @@ -250,7 +255,9 @@ impl Server { Details = master_address.to_string(), ); - self.access_token(account_id).await.map(AccessToken::new) + self.access_token(account_id) + .await + .map(AccessToken::new_maybe_invalid) } else { Err(trc::AuthEvent::Failed .into_err() @@ -299,7 +306,7 @@ impl Server { { match directory.authenticate(&req.credentials).await { Ok(result) => { - return self.build_directory_token(result).await; + return self.build_directory_token(result, req.remote_ip).await; } Err(err) => { if !err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) { @@ -315,7 +322,7 @@ impl Server { .await?; self.access_token(token_info.account_id) .await - .map(AccessToken::new) + .and_then(|token| AccessToken::new(token, req.remote_ip)) } } } @@ -359,7 +366,7 @@ impl Server { .as_ref() .is_some_and(|exp| exp.timestamp() < now() as i64) { - return Err(trc::AuthEvent::Failed + return Err(trc::AuthEvent::CredentialExpired .into_err() .ctx(trc::Key::AccountName, account.name) .ctx(trc::Key::AccountId, account_id) @@ -391,7 +398,7 @@ impl Server { .access_token_from_account(account_id, structs::Account::User(account)) .await?; - AccessToken::scoped(token, credential_id, remote_ip) + AccessToken::new_scoped(token, credential_id, remote_ip) .add_context(|ctx| ctx.span_id(span_id)) } else { Err(trc::AuthEvent::Failed @@ -441,11 +448,15 @@ impl Server { } } - async fn build_directory_token(&self, account: directory::Account) -> trc::Result { + async fn build_directory_token( + &self, + account: directory::Account, + remote_ip: IpAddr, + ) -> trc::Result { let account = self.synchronize_account(account).await?; self.access_token_from_account(account.id, account.account) .await - .map(AccessToken::new) + .and_then(|token| AccessToken::new(token, remote_ip)) } pub async fn get_directory_for_domain( diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index fe54b081..af120e3f 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -125,7 +125,7 @@ pub struct AccessToken { inner: Arc, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct AccessTokenInner { pub(crate) account_id: u32, pub(crate) tenant_id: Option, @@ -140,7 +140,7 @@ pub struct AccessTokenInner { pub(crate) obj_size: u64, } -#[derive(Debug, Default, Hash)] +#[derive(Debug, Default, Hash, Clone)] pub(crate) struct AccessScope { pub permissions: Permissions, pub credential_id: u32, @@ -148,7 +148,7 @@ pub(crate) struct AccessScope { pub allowed_ips: Box<[IpAddrOrMask]>, } -#[derive(Debug, Default, Hash, PartialEq, Eq)] +#[derive(Debug, Default, Hash, PartialEq, Eq, Clone)] pub(crate) struct AccessTo { pub account_id: u32, pub collections: Bitmap, diff --git a/crates/common/src/auth/permissions.rs b/crates/common/src/auth/permissions.rs index 2c96d0d8..5f200e80 100644 --- a/crates/common/src/auth/permissions.rs +++ b/crates/common/src/auth/permissions.rs @@ -114,12 +114,17 @@ impl Server { &account.permissions, match &account.roles { UserRoles::User => self.core.network.security.default_role_ids_user.as_slice(), - UserRoles::TenantAdmin => self - .core - .network - .security - .default_role_ids_tenant - .as_slice(), + UserRoles::Admin => { + if access_token.tenant_id().is_none() { + self.core.network.security.default_role_ids_admin.as_slice() + } else { + self.core + .network + .security + .default_role_ids_tenant + .as_slice() + } + } UserRoles::Custom(custom_roles) => custom_roles.role_ids.as_slice(), }, account.member_tenant_id.map(|t| t.document_id()), diff --git a/crates/common/src/auth/rate_limit.rs b/crates/common/src/auth/rate_limit.rs index 32020fb9..302f1e4d 100644 --- a/crates/common/src/auth/rate_limit.rs +++ b/crates/common/src/auth/rate_limit.rs @@ -53,7 +53,7 @@ impl Server { } } - pub async fn is_http_anonymous_request_allowed(&self, addr: &IpAddr) -> trc::Result<()> { + pub async fn is_http_anonymous_request_allowed(&self, addr: IpAddr) -> trc::Result<()> { if let Some(rate) = &self.core.network.http.rate_anonymous && !self.is_ip_allowed(addr) && self @@ -62,7 +62,7 @@ impl Server { .memory .is_rate_allowed( KV_RATE_LIMIT_HTTP_ANONYMOUS, - &ip_to_bytes(addr), + &ip_to_bytes(&addr), rate, false, ) diff --git a/crates/common/src/cache/invalidate.rs b/crates/common/src/cache/invalidate.rs index ae616aee..473c99f7 100644 --- a/crates/common/src/cache/invalidate.rs +++ b/crates/common/src/cache/invalidate.rs @@ -287,6 +287,7 @@ impl Server { match change { CacheInvalidation::AccessToken(id) => { cache.access_tokens.remove(id); + cache.http_auth.inner().retain(|_, v| v.account_id != *id); } CacheInvalidation::DavResources(id) => { cache.files.remove(id); diff --git a/crates/common/src/cache/reload.rs b/crates/common/src/cache/reload.rs index aa9748a5..72405218 100644 --- a/crates/common/src/cache/reload.rs +++ b/crates/common/src/cache/reload.rs @@ -12,7 +12,7 @@ use crate::{ telemetry::Telemetry, }, ipc::{QueueEvent, RegistryChange}, - network::security::BlockedIps, + network::security::{BlockedIps, IpWithTtl}, }; use ahash::AHashMap; use directory::Directories; @@ -35,14 +35,22 @@ impl Server { let object = match change { RegistryChange::Insert(id) => { if matches!(id.object(), ObjectType::BlockedIp) { - if let Some(ip) = bootstrap.get_infallible::(id.id()).await - && ip.expires_at.is_none_or(|ip| ip.timestamp() > now() as i64) - { - let mut ips = self.inner.data.blocked_ips.write(); - if let Some(ip) = ip.address.try_to_ip() { - ips.blocked_ip_addresses.insert(ip); - } else { - ips.blocked_ip_networks.push(ip.address); + if let Some(ip) = bootstrap.get_infallible::(id.id()).await { + let expires_at = ip + .expires_at + .as_ref() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(u64::MAX); + + if expires_at > now() { + let mut ips = self.inner.data.blocked_ips.write(); + if let Some(ip) = ip.address.try_to_ip() { + ips.blocked_ip_addresses + .insert(IpWithTtl::new(ip, expires_at)); + } else { + ips.blocked_ip_networks + .push(IpWithTtl::new(ip.address, expires_at)); + } } } return Ok(bootstrap.into()); diff --git a/crates/common/src/manager/defaults.rs b/crates/common/src/manager/defaults.rs index f75e329b..810bfdc6 100644 --- a/crates/common/src/manager/defaults.rs +++ b/crates/common/src/manager/defaults.rs @@ -329,7 +329,7 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> { ..Default::default() }, Role { - description: "Superuser".into(), + description: "System Administrator".into(), enabled_permissions: Map::new(permissions.superuser), ..Default::default() }, @@ -356,6 +356,7 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> { default_user_role_ids: Map::new(vec![role_ids[0]]), default_group_role_ids: Map::new(vec![role_ids[1]]), default_tenant_role_ids: Map::new(vec![role_ids[2], role_ids[0]]), + default_admin_role_ids: Map::new(vec![role_ids[3], role_ids[0]]), ..Default::default() } .into(), diff --git a/crates/common/src/network/listen.rs b/crates/common/src/network/listen.rs index b086af05..8753cbf4 100644 --- a/crates/common/src/network/listen.rs +++ b/crates/common/src/network/listen.rs @@ -220,7 +220,7 @@ impl BuildSession for Arc { let remote_port = remote_addr.port(); // Check if blocked - if server.is_ip_blocked(&remote_ip) { + if server.is_ip_blocked(remote_ip) { trc::event!( Security(trc::SecurityEvent::IpBlocked), ListenerId = self.id.clone(), diff --git a/crates/common/src/network/security.rs b/crates/common/src/network/security.rs index be85e039..20440d38 100644 --- a/crates/common/src/network/security.rs +++ b/crates/common/src/network/security.rs @@ -18,7 +18,7 @@ use registry::{ }, types::{datetime::UTCDateTime, ipmask::IpAddrOrMask}, }; -use std::{fmt::Debug, net::IpAddr}; +use std::{fmt::Debug, hash::Hash, net::IpAddr}; use store::{ registry::{ bootstrap::Bootstrap, @@ -33,8 +33,8 @@ use zxcvbn::Score; #[derive(Debug, Clone)] pub struct Security { - pub allowed_ip_addresses: AHashSet, - pub allowed_ip_networks: Vec, + pub allowed_ip_addresses: AHashSet>, + pub allowed_ip_networks: Vec>, pub has_allowed_networks: bool, pub blocked_ip_expiration: Option, @@ -48,37 +48,54 @@ pub struct Security { pub default_role_ids_user: Vec, pub default_role_ids_group: Vec, pub default_role_ids_tenant: Vec, + pub default_role_ids_admin: Vec, pub password_hash_algorithm: PasswordHashAlgorithm, pub password_max_length: u32, pub password_min_length: u32, pub password_min_strength: Score, + pub password_default_expiration: Option, } #[derive(Default)] pub struct BlockedIps { - pub blocked_ip_addresses: AHashSet, - pub blocked_ip_networks: Vec, + pub blocked_ip_addresses: AHashSet>, + pub blocked_ip_networks: Vec>, pub has_blocked_networks: bool, } +#[derive(Debug, Clone)] +pub struct IpWithTtl { + pub ip: T, + pub expires_at: u64, +} + impl Security { pub async fn parse(bp: &mut Bootstrap) -> Self { let mut allowed_ip_addresses = AHashSet::new(); let mut allowed_ip_networks = Vec::new(); let mut expired_allows = Vec::new(); - let now = now() as i64; + let now = now(); for ip in bp.list_infallible::().await { let id = ip.id; let revision = ip.revision; let ip = ip.object; + let expires_at = ip + .expires_at + .as_ref() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(u64::MAX); - if ip.expires_at.as_ref().is_none_or(|ip| ip.timestamp() > now) { + if expires_at > now { if let Some(ip) = ip.address.try_to_ip() { - allowed_ip_addresses.insert(ip); - } else if !allowed_ip_networks.contains(&ip.address) { - allowed_ip_networks.push(ip.address); + allowed_ip_addresses.insert(IpWithTtl::new(ip, expires_at)); + } else { + let ip_with_ttl = IpWithTtl::new(ip.address, expires_at); + + if !allowed_ip_networks.contains(&ip_with_ttl) { + allowed_ip_networks.push(ip_with_ttl); + } } } else { expired_allows.push(( @@ -96,9 +113,12 @@ impl Security { let system = bp.setting_infallible::().await; for ip in system.proxy_trusted_networks { if let Some(ip) = ip.try_to_ip() { - allowed_ip_addresses.insert(ip); - } else if !allowed_ip_networks.contains(&ip) { - allowed_ip_networks.push(ip); + allowed_ip_addresses.insert(IpWithTtl::new(ip, u64::MAX)); + } else { + let ip_with_ttl = IpWithTtl::new(ip, u64::MAX); + if !allowed_ip_networks.contains(&ip_with_ttl) { + allowed_ip_networks.push(ip_with_ttl); + } } } @@ -151,6 +171,7 @@ impl Security { default_role_ids_user: auth.default_user_role_ids.into_inner(), default_role_ids_group: auth.default_group_role_ids.into_inner(), default_role_ids_tenant: auth.default_tenant_role_ids.into_inner(), + default_role_ids_admin: auth.default_admin_role_ids.into_inner(), password_hash_algorithm: auth.password_hash_algorithm, password_max_length: auth.password_max_length as u32, password_min_length: auth.password_min_length as u32, @@ -161,6 +182,7 @@ impl Security { PasswordStrength::Three => Score::Three, PasswordStrength::Four => Score::Four, }, + password_default_expiration: auth.password_default_expiry.map(|v| v.as_secs()), } } } @@ -168,7 +190,7 @@ impl Security { impl Server { pub async fn is_rcpt_fail2banned(&self, ip: IpAddr, rcpt: &str) -> trc::Result { if let Some(rate) = &self.core.network.security.rcpt_fail_rate { - let is_allowed = self.is_ip_allowed(&ip) + let is_allowed = self.is_ip_allowed(ip) || (self .in_memory_store() .is_rate_allowed(KV_RATE_LIMIT_RCPT, &ip_to_bytes(&ip), rate, false) @@ -193,7 +215,7 @@ impl Server { pub async fn is_scanner_fail2banned(&self, ip: IpAddr) -> trc::Result { if let Some(rate) = &self.core.network.security.scanner_fail_rate { - let is_allowed = self.is_ip_allowed(&ip) + let is_allowed = self.is_ip_allowed(ip) || self .in_memory_store() .is_rate_allowed(KV_RATE_LIMIT_SCAN, &ip_to_bytes(&ip), rate, false) @@ -214,7 +236,7 @@ impl Server { pub async fn is_http_banned_path(&self, path: &str, ip: IpAddr) -> trc::Result { let paths = &self.core.network.security.http_banned_paths; - if !paths.is_empty() && paths.iter().any(|p| p.matches(path)) && !self.is_ip_allowed(&ip) { + if !paths.is_empty() && paths.iter().any(|p| p.matches(path)) && !self.is_ip_allowed(ip) { self.block_ip(ip, BlockReason::PortScanning) .await .map(|_| true) @@ -225,7 +247,7 @@ impl Server { pub async fn is_loiter_fail2banned(&self, ip: IpAddr) -> trc::Result { if let Some(rate) = &self.core.network.security.loiter_fail_rate { - let is_allowed = self.is_ip_allowed(&ip) + let is_allowed = self.is_ip_allowed(ip) || self .in_memory_store() .is_rate_allowed(KV_RATE_LIMIT_LOITER, &ip_to_bytes(&ip), rate, false) @@ -246,7 +268,7 @@ impl Server { pub async fn is_auth_fail2banned(&self, ip: IpAddr, login: Option<&str>) -> trc::Result { if let Some(rate) = &self.core.network.security.auth_fail_rate { let login = login.unwrap_or_default(); - let is_allowed = self.is_ip_allowed(&ip) + let is_allowed = self.is_ip_allowed(ip) || (self .in_memory_store() .is_rate_allowed(KV_RATE_LIMIT_AUTH, &ip_to_bytes(&ip), rate, false) @@ -271,27 +293,28 @@ impl Server { pub async fn block_ip(&self, ip: IpAddr, reason: BlockReason) -> trc::Result<()> { // Add IP to blocked list + let now = now(); + let expires_at = self + .core + .network + .security + .blocked_ip_expiration + .map(|v| now + v); self.inner .data .blocked_ips .write() .blocked_ip_addresses - .insert(ip); + .insert(IpWithTtl::new(ip, expires_at.unwrap_or(u64::MAX))); // Write blocked IP to config - let now = now() as i64; let RegistryWriteResult::Success(id) = self .registry() .write(RegistryWrite::insert( &BlockedIp { address: IpAddrOrMask::from_ip(ip), - created_at: UTCDateTime::from_timestamp(now), - expires_at: self - .core - .network - .security - .blocked_ip_expiration - .map(|v| UTCDateTime::from_timestamp(now + v as i64)), + created_at: UTCDateTime::from_timestamp(now as i64), + expires_at: expires_at.map(|ts| UTCDateTime::from_timestamp(ts as i64)), reason, } .into(), @@ -315,19 +338,27 @@ impl Server { self.core.network.security.auth_fail_rate.is_some() } - pub fn is_ip_blocked(&self, ip: &IpAddr) -> bool { + pub fn is_ip_blocked(&self, ip: IpAddr) -> bool { let blocked_ips = self.inner.data.blocked_ips.read(); - (blocked_ips.blocked_ip_addresses.contains(ip) + (blocked_ips + .blocked_ip_addresses + .get(&IpWithTtl::new(ip, 0)) + .is_some_and(|v| !v.is_expired()) || (blocked_ips.has_blocked_networks && blocked_ips .blocked_ip_networks .iter() - .any(|network| network.matches(ip)))) + .any(|network| network.ip.matches(&ip) && !network.is_expired()))) && !self.is_ip_allowed(ip) } - pub fn is_ip_allowed(&self, ip: &IpAddr) -> bool { - self.core.network.security.allowed_ip_addresses.contains(ip) + pub fn is_ip_allowed(&self, ip: IpAddr) -> bool { + self.core + .network + .security + .allowed_ip_addresses + .get(&IpWithTtl::new(ip, 0)) + .is_some_and(|v| !v.is_expired()) || (self.core.network.security.has_allowed_networks && self .core @@ -335,7 +366,7 @@ impl Server { .security .allowed_ip_networks .iter() - .any(|network| network.matches(ip))) + .any(|network| network.ip.matches(&ip) && !network.is_expired())) } pub fn is_secure_password(&self, password: &str, user_inputs: &[&str]) -> Result<(), String> { @@ -374,12 +405,19 @@ impl BlockedIps { let id = ip.id; let revision = ip.revision; let ip = ip.object; + let expires_at = ip + .expires_at + .as_ref() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(u64::MAX); if ip.expires_at.as_ref().is_none_or(|ip| ip.timestamp() > now) { if let Some(ip) = ip.address.try_to_ip() { - ips.blocked_ip_addresses.insert(ip); + ips.blocked_ip_addresses + .insert(IpWithTtl::new(ip, expires_at)); } else { - ips.blocked_ip_networks.push(ip.address); + ips.blocked_ip_networks + .push(IpWithTtl::new(ip.address, expires_at)); } } else { expired_blocks.push(( @@ -419,3 +457,27 @@ impl BlockedIps { ips } } + +impl Hash for IpWithTtl { + fn hash(&self, state: &mut H) { + self.ip.hash(state); + } +} + +impl PartialEq for IpWithTtl { + fn eq(&self, other: &Self) -> bool { + self.ip == other.ip + } +} + +impl Eq for IpWithTtl {} + +impl IpWithTtl { + pub fn new(ip: T, expires_at: u64) -> Self { + Self { ip, expires_at } + } + + pub fn is_expired(&self) -> bool { + self.expires_at <= now() + } +} diff --git a/crates/common/src/storage/blob.rs b/crates/common/src/storage/blob.rs index 86a6f34d..68555057 100644 --- a/crates/common/src/storage/blob.rs +++ b/crates/common/src/storage/blob.rs @@ -50,6 +50,11 @@ impl Server { let count = v >> COUNT_SHIFT; let size = v & SIZE_MASK; + let c = println!( + "count: {}, size: {}, expires in: {}", + count, size, expires_in + ); + (self.core.jmap.upload_tmp_quota_amount == 0 || count <= self.core.jmap.upload_tmp_quota_amount as u64) && (self.core.jmap.upload_tmp_quota_size == 0 diff --git a/crates/http/src/auth/authenticate.rs b/crates/http/src/auth/authenticate.rs index 160d5ed2..800120d5 100644 --- a/crates/http/src/auth/authenticate.rs +++ b/crates/http/src/auth/authenticate.rs @@ -62,7 +62,7 @@ impl Authenticator for Server { })? } else if mechanism.eq_ignore_ascii_case("bearer") { // Enforce anonymous rate limit - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; Credentials::Bearer { @@ -71,7 +71,7 @@ impl Authenticator for Server { } } else { // Enforce anonymous rate limit - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return Err(trc::AuthEvent::Error @@ -108,7 +108,7 @@ impl Authenticator for Server { .map(|in_flight| (in_flight, access_token)) } else { // Enforce anonymous rate limit - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; Err(trc::AuthEvent::Failed diff --git a/crates/http/src/auth/oauth/registration.rs b/crates/http/src/auth/oauth/registration.rs index 4bf783b8..9a64a83b 100644 --- a/crates/http/src/auth/oauth/registration.rs +++ b/crates/http/src/auth/oauth/registration.rs @@ -55,7 +55,7 @@ impl ClientRegistrationHandler for Server { access_token.enforce_permission(Permission::OAuthClientRegistration)?; access_token.tenant_id() } else { - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; None }; diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 299a5106..63e4be2d 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -271,14 +271,14 @@ impl ParseHttp for Server { } ("oauth-authorization-server", &Method::GET) => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self.handle_oauth_metadata(req, session).await; } ("openid-configuration", &Method::GET) => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self.handle_oidc_metadata(req, session).await; @@ -298,7 +298,7 @@ impl ParseHttp for Server { } ("mta-sts.txt", &Method::GET) => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return if let Some(policy) = self.build_mta_sts_policy() { @@ -310,7 +310,7 @@ impl ParseHttp for Server { } ("mail-v1.xml", &Method::GET) => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self.handle_autoconfig_request(&req).await; @@ -320,7 +320,7 @@ impl ParseHttp for Server { && path.next().unwrap_or_default() == "config-v1.1.xml" { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self.handle_autoconfig_request(&req).await; @@ -333,7 +333,7 @@ impl ParseHttp for Server { }, "auth" => match (path.next().unwrap_or_default(), req.method()) { ("login", &Method::POST) => { - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; let bytes = fetch_body(&mut req, 4096, session.session_id) @@ -343,13 +343,13 @@ impl ParseHttp for Server { return self.handle_login_request(session, bytes).await; } ("device", &Method::POST) => { - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self.handle_device_auth(&mut req, session).await; } ("token", &Method::POST) => { - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self.handle_token_request(&mut req, session).await; @@ -379,7 +379,7 @@ impl ParseHttp for Server { } ("jwks.json", &Method::GET) => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return Ok(self.core.oauth.oidc_jwks.clone().into_http_response()); @@ -440,7 +440,7 @@ impl ParseHttp for Server { && path.next().unwrap_or_default() == "config-v1.1.xml" { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self.handle_autoconfig_request(&req).await; @@ -448,7 +448,7 @@ impl ParseHttp for Server { } "calendar" => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; if self.core.groupware.itip_http_rsvp_url.is_some() @@ -483,7 +483,7 @@ impl ParseHttp for Server { .eq_ignore_ascii_case("autodiscover.xml") { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return self @@ -495,7 +495,7 @@ impl ParseHttp for Server { } "robots.txt" => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; return Ok( @@ -505,7 +505,7 @@ impl ParseHttp for Server { } "healthz" => { // Limit anonymous requests - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; match path.next().unwrap_or_default() { @@ -586,7 +586,7 @@ impl ParseHttp for Server { if let Some(form) = &self.core.network.contact_form { match *req.method() { Method::POST => { - self.is_http_anonymous_request_allowed(&session.remote_ip) + self.is_http_anonymous_request_allowed(session.remote_ip) .await?; let form_data = @@ -696,7 +696,7 @@ async fn handle_session(inner: Arc, session: SessionDat }) { // Check if the forwarded IP has been blocked - if server.is_ip_blocked(&forwarded_for) { + if server.is_ip_blocked(forwarded_for) { trc::event!( Security(trc::SecurityEvent::IpBlocked), ListenerId = instance.id.clone(), diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs index a23b7438..85b1303b 100644 --- a/crates/jmap/src/api/mod.rs +++ b/crates/jmap/src/api/mod.rs @@ -109,7 +109,9 @@ impl ToRequestError for trc::Error { | trc::SecurityEvent::AbuseBan | trc::SecurityEvent::LoiterBan | trc::SecurityEvent::IpBlocked => RequestError::too_many_auth_attempts(), - trc::SecurityEvent::Unauthorized => RequestError::forbidden(), + trc::SecurityEvent::Unauthorized | trc::SecurityEvent::IpUnauthorized => { + RequestError::forbidden() + } trc::SecurityEvent::IpBlockExpired | trc::SecurityEvent::IpAllowExpired => { RequestError::internal_server_error() } diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index be0e6790..76fbfb95 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -56,7 +56,7 @@ impl EmailImport for Server { let import_access_token = if account_id != access_token.account_id() { #[cfg(feature = "test_mode")] { - AccessToken::from_id(account_id).into() + AccessToken::from_id_maybe_invalid(account_id).into() } #[cfg(not(feature = "test_mode"))] diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index d9e30289..4c88d8f7 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -100,7 +100,7 @@ impl EmailSet for Server { let import_access_token = if account_id != access_token.account_id() { #[cfg(feature = "test_mode")] { - std::sync::Arc::new(AccessToken::from_id(account_id)).into() + std::sync::Arc::new(AccessToken::from_id_maybe_invalid(account_id)).into() } #[cfg(not(feature = "test_mode"))] diff --git a/crates/jmap/src/registry/mapping/account.rs b/crates/jmap/src/registry/mapping/account.rs index db357770..166603f9 100644 --- a/crates/jmap/src/registry/mapping/account.rs +++ b/crates/jmap/src/registry/mapping/account.rs @@ -39,9 +39,12 @@ use registry::{ types::{EnumImpl, datetime::UTCDateTime, id::ObjectId}, }; use std::str::FromStr; -use store::registry::{ - RegistryFilterOp, - write::{RegistryWrite, RegistryWriteResult}, +use store::{ + registry::{ + RegistryFilterOp, + write::{RegistryWrite, RegistryWriteResult}, + }, + write::now, }; use trc::AddContext; use types::id::Id; @@ -99,6 +102,8 @@ pub(crate) async fn account_set( break 'outer; } } + + set.response.updated.append(id, None); } } ObjectType::Credential => { @@ -460,6 +465,25 @@ pub(crate) async fn account_set( ); continue 'outer; } + + if let Some(expires_at) = set + .server + .core + .network + .security + .password_default_expiration + { + credential.expires_at = + Some(UTCDateTime::from_timestamp( + (now() + expires_at) as i64, + )); + } else if credential + .expires_at + .is_some_and(|exp| exp.timestamp() <= now() as i64) + { + credential.expires_at = None; + } + credential.secret = hash_secret( set.server .core diff --git a/crates/jmap/src/registry/mapping/principal.rs b/crates/jmap/src/registry/mapping/principal.rs index b9373fe3..a38cc709 100644 --- a/crates/jmap/src/registry/mapping/principal.rs +++ b/crates/jmap/src/registry/mapping/principal.rs @@ -11,7 +11,7 @@ use common::{ }; use directory::core::secret::hash_secret; use jmap_proto::error::set::SetError; -use registry::schema::structs::TaskStatus; +use registry::{schema::structs::TaskStatus, types::datetime::UTCDateTime}; use registry::{ schema::{ enums::{AccountType, Permission, TenantStorageQuota}, @@ -22,7 +22,7 @@ use registry::{ }; use store::{ registry::{RegistryObjectCounter, RegistryQuery}, - write::BatchBuilder, + write::{BatchBuilder, now}, }; use trc::AddContext; use types::id::Id; @@ -110,6 +110,22 @@ pub(crate) async fn validate_account( .with_description(err))); } + if credential.expires_at == old_credential.expires_at + && credential + .expires_at + .is_some_and(|exp| exp.timestamp() <= now() as i64) + && let Some(expires_at) = set + .server + .core + .network + .security + .password_default_expiration + { + credential.expires_at = Some(UTCDateTime::from_timestamp( + (now() + expires_at) as i64, + )); + } + credential.secret = hash_secret( set.server.core.network.security.password_hash_algorithm, std::mem::take(&mut credential.secret).into_bytes(), @@ -241,6 +257,14 @@ async fn validate_credential_creation( .with_property(Property::Secret) .with_description(err))) } else { + if credential.expires_at.is_none() + && let Some(expires_at) = + server.core.network.security.password_default_expiration + { + credential.expires_at = + Some(UTCDateTime::from_timestamp((now() + expires_at) as i64)); + } + credential.secret = hash_secret( server.core.network.security.password_hash_algorithm, std::mem::take(&mut credential.secret).into_bytes(), diff --git a/crates/jmap/src/registry/mapping/task.rs b/crates/jmap/src/registry/mapping/task.rs index e94a5c63..d2376a14 100644 --- a/crates/jmap/src/registry/mapping/task.rs +++ b/crates/jmap/src/registry/mapping/task.rs @@ -52,7 +52,9 @@ pub(crate) async fn task_set( 'outer: for (id, value) in set.create.drain() { let mut task = Task::default(); if let Err(err) = task.patch( - JsonPointerPatch::new(&JsonPointer::new(vec![])).with_create(true), + JsonPointerPatch::new(&JsonPointer::new(vec![])) + .with_create(true) + .with_can_set_account(true), value, ) { set.response.not_created.append(id, err.into()); @@ -189,7 +191,12 @@ pub(crate) async fn task_set( Key::Owned(other) => JsonPointer::parse(&other), }; - if let Err(err) = task.patch(JsonPointerPatch::new(&ptr).with_create(false), value) { + if let Err(err) = task.patch( + JsonPointerPatch::new(&ptr) + .with_create(false) + .with_can_set_account(true), + value, + ) { set.response.not_updated.append(id, err.into()); continue 'outer; } diff --git a/crates/jmap/src/registry/query.rs b/crates/jmap/src/registry/query.rs index 206312fc..ad70e3a6 100644 --- a/crates/jmap/src/registry/query.rs +++ b/crates/jmap/src/registry/query.rs @@ -32,6 +32,7 @@ use registry::{ types::{ EnumImpl, index::{IndexSchemaType, IndexSchemaValueType}, + ipmask::IpAddrOrMask, }, }; use std::str::FromStr; @@ -189,6 +190,11 @@ impl RegistryQuery for Server { .ok() .map(|id| RegistryFilterValue::from(id.id())) } + (IndexSchemaValueType::IpMask, serde_json::Value::String(value)) => { + IpAddrOrMask::from_str(&value) + .ok() + .map(|ip| RegistryFilterValue::Bytes(ip.to_index_key())) + } _ => None, }; diff --git a/crates/jmap/src/registry/set.rs b/crates/jmap/src/registry/set.rs index 0fa7e1bb..197f2a5c 100644 --- a/crates/jmap/src/registry/set.rs +++ b/crates/jmap/src/registry/set.rs @@ -479,9 +479,15 @@ impl RegistrySet for Server { } Modification::Update { id, object } => { if object.inner != new_object.inner { - self.registry() - .write(RegistryWrite::update(*id, &new_object, object)) - .await? + if !(is_singleton && object.revision == 0) { + self.registry() + .write(RegistryWrite::update(*id, &new_object, object)) + .await? + } else { + self.registry() + .write(RegistryWrite::insert(&new_object)) + .await? + } } else { set.response.updated.append(*id, None); continue; diff --git a/crates/registry/src/types/index.rs b/crates/registry/src/types/index.rs index 98b32fe9..a179232a 100644 --- a/crates/registry/src/types/index.rs +++ b/crates/registry/src/types/index.rs @@ -69,6 +69,7 @@ pub enum IndexSchemaValueType { Enum, Boolean, Id, + IpMask, } #[derive(Debug, Default)] @@ -192,20 +193,7 @@ impl From<&i64> for IndexValue<'_> { impl<'x> From<&'x IpAddrOrMask> for IndexValue<'x> { fn from(value: &'x IpAddrOrMask) -> Self { - match value { - IpAddrOrMask::V4 { addr, mask } => { - let mut bytes = Vec::with_capacity(8); - bytes.extend_from_slice(&addr.octets()); - bytes.extend_from_slice(&mask.to_be_bytes()); - IndexValue::Bytes(bytes) - } - IpAddrOrMask::V6 { addr, mask } => { - let mut bytes = Vec::with_capacity(24); - bytes.extend_from_slice(&addr.octets()); - bytes.extend_from_slice(&mask.to_be_bytes()); - IndexValue::Bytes(bytes) - } - } + IndexValue::Bytes(value.to_index_key()) } } diff --git a/crates/registry/src/types/ipmask.rs b/crates/registry/src/types/ipmask.rs index 7f3ca814..68ff6627 100644 --- a/crates/registry/src/types/ipmask.rs +++ b/crates/registry/src/types/ipmask.rs @@ -107,6 +107,23 @@ impl IpAddrOrMask { }, } } + + pub fn to_index_key(&self) -> Vec { + match self { + IpAddrOrMask::V4 { addr, mask } => { + let mut bytes = Vec::with_capacity(8); + bytes.extend_from_slice(&addr.octets()); + bytes.extend_from_slice(&mask.to_be_bytes()); + bytes + } + IpAddrOrMask::V6 { addr, mask } => { + let mut bytes = Vec::with_capacity(24); + bytes.extend_from_slice(&addr.octets()); + bytes.extend_from_slice(&mask.to_be_bytes()); + bytes + } + } + } } impl FromStr for IpAddrOrMask { diff --git a/crates/spam-filter/src/analysis/ip.rs b/crates/spam-filter/src/analysis/ip.rs index 79f1ed34..1ae312c2 100644 --- a/crates/spam-filter/src/analysis/ip.rs +++ b/crates/spam-filter/src/analysis/ip.rs @@ -37,7 +37,7 @@ impl SpamFilterAnalyzeIp for Server { { if let Some(ip) = received.from_ip() && !ip.is_loopback() - && !self.is_ip_allowed(&ip) + && !self.is_ip_allowed(ip) { ips.insert(ElementLocation::new(ip, Location::HeaderReceived)); } @@ -47,7 +47,7 @@ impl SpamFilterAnalyzeIp for Server { { if let Host::IpAddr(ip) = host && !ip.is_loopback() - && !self.is_ip_allowed(ip) + && !self.is_ip_allowed(*ip) { ips.insert(ElementLocation::new(*ip, Location::HeaderReceived)); } @@ -89,10 +89,10 @@ impl SpamFilterAnalyzeIp for Server { if ip.element.is_loopback() || ip.element.is_multicast() || ip.element.is_unspecified() - || self.is_ip_allowed(&ip.element) + || self.is_ip_allowed(ip.element) { continue; - } else if self.is_ip_blocked(&ip.element) { + } else if self.is_ip_blocked(ip.element) { ctx.result.add_tag("IP_BLOCKED"); continue; } diff --git a/crates/trc/src/event/enums.rs b/crates/trc/src/event/enums.rs index b64d5b04..104cdc48 100644 --- a/crates/trc/src/event/enums.rs +++ b/crates/trc/src/event/enums.rs @@ -111,6 +111,7 @@ pub enum AuthEvent { ClientRegistration = 555, Error = 34, Warning = 595, + CredentialExpired = 276, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -401,8 +402,6 @@ pub enum MailAuthEvent { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u16)] pub enum ManageEvent { - Reserved3 = 276, - Reserved4 = 279, Reserved5 = 280, Reserved6 = 277, } @@ -608,6 +607,7 @@ pub enum SecurityEvent { IpBlocked = 318, IpBlockExpired = 593, IpAllowExpired = 594, + IpUnauthorized = 279, Unauthorized = 552, } diff --git a/crates/trc/src/event/enums_impl.rs b/crates/trc/src/event/enums_impl.rs index 332b7b79..6ee7525a 100644 --- a/crates/trc/src/event/enums_impl.rs +++ b/crates/trc/src/event/enums_impl.rs @@ -49,6 +49,7 @@ impl EventType { b"auth.client-registration" => EventType::Auth(AuthEvent::ClientRegistration), b"auth.error" => EventType::Auth(AuthEvent::Error), b"auth.warning" => EventType::Auth(AuthEvent::Warning), + b"auth.credential-expired" => EventType::Auth(AuthEvent::CredentialExpired), b"calendar.rule-expansion-error" => EventType::Calendar(CalendarEvent::RuleExpansionError), b"calendar.alarm-sent" => EventType::Calendar(CalendarEvent::AlarmSent), b"calendar.alarm-skipped" => EventType::Calendar(CalendarEvent::AlarmSkipped), @@ -259,8 +260,6 @@ impl EventType { b"mail-auth.dns-record-not-found" => EventType::MailAuth(MailAuthEvent::DnsRecordNotFound), b"mail-auth.dns-invalid-record-type" => EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType), b"mail-auth.policy-not-aligned" => EventType::MailAuth(MailAuthEvent::PolicyNotAligned), - b"manage.reserved3" => EventType::Manage(ManageEvent::Reserved3), - b"manage.reserved4" => EventType::Manage(ManageEvent::Reserved4), b"manage.reserved5" => EventType::Manage(ManageEvent::Reserved5), b"manage.reserved6" => EventType::Manage(ManageEvent::Reserved6), b"manage-sieve.connection-start" => EventType::ManageSieve(ManageSieveEvent::ConnectionStart), @@ -401,6 +400,7 @@ impl EventType { b"security.ip-blocked" => EventType::Security(SecurityEvent::IpBlocked), b"security.ip-block-expired" => EventType::Security(SecurityEvent::IpBlockExpired), b"security.ip-allow-expired" => EventType::Security(SecurityEvent::IpAllowExpired), + b"security.ip-unauthorized" => EventType::Security(SecurityEvent::IpUnauthorized), b"security.unauthorized" => EventType::Security(SecurityEvent::Unauthorized), b"server.startup" => EventType::Server(ServerEvent::Startup), b"server.shutdown" => EventType::Server(ServerEvent::Shutdown), @@ -651,6 +651,7 @@ impl EventType { EventType::Auth(AuthEvent::ClientRegistration) => "auth.client-registration", EventType::Auth(AuthEvent::Error) => "auth.error", EventType::Auth(AuthEvent::Warning) => "auth.warning", + EventType::Auth(AuthEvent::CredentialExpired) => "auth.credential-expired", EventType::Calendar(CalendarEvent::RuleExpansionError) => { "calendar.rule-expansion-error" } @@ -917,8 +918,6 @@ impl EventType { "mail-auth.dns-invalid-record-type" } EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => "mail-auth.policy-not-aligned", - EventType::Manage(ManageEvent::Reserved3) => "manage.reserved3", - EventType::Manage(ManageEvent::Reserved4) => "manage.reserved4", EventType::Manage(ManageEvent::Reserved5) => "manage.reserved5", EventType::Manage(ManageEvent::Reserved6) => "manage.reserved6", EventType::ManageSieve(ManageSieveEvent::ConnectionStart) => { @@ -1109,6 +1108,7 @@ impl EventType { EventType::Security(SecurityEvent::IpBlocked) => "security.ip-blocked", EventType::Security(SecurityEvent::IpBlockExpired) => "security.ip-block-expired", EventType::Security(SecurityEvent::IpAllowExpired) => "security.ip-allow-expired", + EventType::Security(SecurityEvent::IpUnauthorized) => "security.ip-unauthorized", EventType::Security(SecurityEvent::Unauthorized) => "security.unauthorized", EventType::Server(ServerEvent::Startup) => "server.startup", EventType::Server(ServerEvent::Shutdown) => "server.shutdown", @@ -1378,6 +1378,7 @@ impl EventType { EventType::Auth(AuthEvent::ClientRegistration) => 555, EventType::Auth(AuthEvent::Error) => 34, EventType::Auth(AuthEvent::Warning) => 595, + EventType::Auth(AuthEvent::CredentialExpired) => 276, EventType::Calendar(CalendarEvent::RuleExpansionError) => 576, EventType::Calendar(CalendarEvent::AlarmSent) => 579, EventType::Calendar(CalendarEvent::AlarmSkipped) => 580, @@ -1588,8 +1589,6 @@ impl EventType { EventType::MailAuth(MailAuthEvent::DnsRecordNotFound) => 250, EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType) => 249, EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => 255, - EventType::Manage(ManageEvent::Reserved3) => 276, - EventType::Manage(ManageEvent::Reserved4) => 279, EventType::Manage(ManageEvent::Reserved5) => 280, EventType::Manage(ManageEvent::Reserved6) => 277, EventType::ManageSieve(ManageSieveEvent::ConnectionStart) => 259, @@ -1730,6 +1729,7 @@ impl EventType { EventType::Security(SecurityEvent::IpBlocked) => 318, EventType::Security(SecurityEvent::IpBlockExpired) => 593, EventType::Security(SecurityEvent::IpAllowExpired) => 594, + EventType::Security(SecurityEvent::IpUnauthorized) => 279, EventType::Security(SecurityEvent::Unauthorized) => 552, EventType::Server(ServerEvent::Startup) => 393, EventType::Server(ServerEvent::Shutdown) => 392, @@ -1979,6 +1979,7 @@ impl EventType { 555 => Some(EventType::Auth(AuthEvent::ClientRegistration)), 34 => Some(EventType::Auth(AuthEvent::Error)), 595 => Some(EventType::Auth(AuthEvent::Warning)), + 276 => Some(EventType::Auth(AuthEvent::CredentialExpired)), 576 => Some(EventType::Calendar(CalendarEvent::RuleExpansionError)), 579 => Some(EventType::Calendar(CalendarEvent::AlarmSent)), 580 => Some(EventType::Calendar(CalendarEvent::AlarmSkipped)), @@ -2207,8 +2208,6 @@ impl EventType { 250 => Some(EventType::MailAuth(MailAuthEvent::DnsRecordNotFound)), 249 => Some(EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType)), 255 => Some(EventType::MailAuth(MailAuthEvent::PolicyNotAligned)), - 276 => Some(EventType::Manage(ManageEvent::Reserved3)), - 279 => Some(EventType::Manage(ManageEvent::Reserved4)), 280 => Some(EventType::Manage(ManageEvent::Reserved5)), 277 => Some(EventType::Manage(ManageEvent::Reserved6)), 259 => Some(EventType::ManageSieve(ManageSieveEvent::ConnectionStart)), @@ -2369,6 +2368,7 @@ impl EventType { 318 => Some(EventType::Security(SecurityEvent::IpBlocked)), 593 => Some(EventType::Security(SecurityEvent::IpBlockExpired)), 594 => Some(EventType::Security(SecurityEvent::IpAllowExpired)), + 279 => Some(EventType::Security(SecurityEvent::IpUnauthorized)), 552 => Some(EventType::Security(SecurityEvent::Unauthorized)), 393 => Some(EventType::Server(ServerEvent::Startup)), 392 => Some(EventType::Server(ServerEvent::Shutdown)), @@ -2770,6 +2770,7 @@ impl EventType { EventType::Security(SecurityEvent::IpBlocked) => Level::Info, EventType::Security(SecurityEvent::IpBlockExpired) => Level::Info, EventType::Security(SecurityEvent::IpAllowExpired) => Level::Info, + EventType::Security(SecurityEvent::IpUnauthorized) => Level::Info, EventType::Security(SecurityEvent::Unauthorized) => Level::Info, EventType::Server(ServerEvent::Startup) => Level::Info, EventType::Server(ServerEvent::Shutdown) => Level::Info, @@ -2960,6 +2961,7 @@ impl EventType { EventType::Auth(AuthEvent::ClientRegistration) => "OAuth Client registration", EventType::Auth(AuthEvent::Error) => "Authentication error", EventType::Auth(AuthEvent::Warning) => "Authentication warning", + EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired", EventType::Calendar(CalendarEvent::RuleExpansionError) => { "Calendar rule expansion error" } @@ -3224,8 +3226,6 @@ impl EventType { EventType::MailAuth(MailAuthEvent::DnsRecordNotFound) => "DNS record not found", EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType) => "Invalid DNS record type", EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => "Policy not aligned", - EventType::Manage(ManageEvent::Reserved3) => "Assertion failed", - EventType::Manage(ManageEvent::Reserved4) => "Resource not found", EventType::Manage(ManageEvent::Reserved5) => "Management operation not supported", EventType::Manage(ManageEvent::Reserved6) => "Management error", EventType::ManageSieve(ManageSieveEvent::ConnectionStart) => { @@ -3430,6 +3430,7 @@ impl EventType { EventType::Security(SecurityEvent::IpBlocked) => "Blocked IP address", EventType::Security(SecurityEvent::IpBlockExpired) => "IP block expired", EventType::Security(SecurityEvent::IpAllowExpired) => "IP allow expired", + EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address", EventType::Security(SecurityEvent::Unauthorized) => "Unauthorized access", EventType::Server(ServerEvent::Startup) => "Starting Stalwart Server v0.15.4", EventType::Server(ServerEvent::Shutdown) => "Shutting down Stalwart Server v0.15.4", @@ -3709,6 +3710,7 @@ impl EventType { } EventType::Auth(AuthEvent::Error) => "An error occurred with authentication", EventType::Auth(AuthEvent::Warning) => "A warning occurred with authentication", + EventType::Auth(AuthEvent::CredentialExpired) => "A credential has expired", EventType::Calendar(CalendarEvent::RuleExpansionError) => { "An error occurred while expanding calendar recurrences" } @@ -4093,8 +4095,6 @@ impl EventType { "The DNS record type is invalid" } EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => "The policy is not aligned", - EventType::Manage(ManageEvent::Reserved3) => "A management assertion has failed", - EventType::Manage(ManageEvent::Reserved4) => "The managed resource was not found", EventType::Manage(ManageEvent::Reserved5) => { "The management operation is not supported" } @@ -4387,6 +4387,9 @@ impl EventType { EventType::Security(SecurityEvent::IpAllowExpired) => { "A previously allowed IP address allow has expired" } + EventType::Security(SecurityEvent::IpUnauthorized) => { + "IP address is not authorized to authenticate using this credential" + } EventType::Security(SecurityEvent::Unauthorized) => { "Account does not have permission to access resource" } @@ -4789,6 +4792,7 @@ impl EventType { EventType::Auth(AuthEvent::TooManyAttempts) => "Too many authentication attempts", EventType::Auth(AuthEvent::ClientRegistration) => "Authentication error", EventType::Auth(AuthEvent::Error) => "Authentication error", + EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired", EventType::Imap(ImapEvent::ConnectionStart) => "IMAP error", EventType::Imap(ImapEvent::ConnectionEnd) => "IMAP error", EventType::Imap(ImapEvent::GetAcl) => "IMAP error", @@ -4858,8 +4862,6 @@ impl EventType { EventType::Limit(LimitEvent::BlobQuota) => "Blob quota exceeded", EventType::Limit(LimitEvent::TenantQuota) => "Tenant quota exceeded", EventType::Limit(LimitEvent::TooManyRequests) => "Too many requests", - EventType::Manage(ManageEvent::Reserved3) => "Assertion failed", - EventType::Manage(ManageEvent::Reserved4) => "Not found", EventType::Manage(ManageEvent::Reserved5) => "Operation not supported", EventType::Manage(ManageEvent::Reserved6) => "Management API Error", EventType::ManageSieve(ManageSieveEvent::ConnectionStart) => "ManageSieve error", @@ -4923,6 +4925,7 @@ impl EventType { EventType::Security(SecurityEvent::IpBlocked) => "Insufficient permissions", EventType::Security(SecurityEvent::IpBlockExpired) => "Insufficient permissions", EventType::Security(SecurityEvent::IpAllowExpired) => "Insufficient permissions", + EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address", EventType::Security(SecurityEvent::Unauthorized) => "Insufficient permissions", EventType::Smtp(SmtpEvent::ConnectionStart) => "SMTP error", EventType::Smtp(SmtpEvent::ConnectionEnd) => "SMTP error", @@ -5085,6 +5088,7 @@ impl EventType { EventType::Auth(AuthEvent::ClientRegistration), EventType::Auth(AuthEvent::Error), EventType::Auth(AuthEvent::Warning), + EventType::Auth(AuthEvent::CredentialExpired), EventType::Calendar(CalendarEvent::RuleExpansionError), EventType::Calendar(CalendarEvent::AlarmSent), EventType::Calendar(CalendarEvent::AlarmSkipped), @@ -5295,8 +5299,6 @@ impl EventType { EventType::MailAuth(MailAuthEvent::DnsRecordNotFound), EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType), EventType::MailAuth(MailAuthEvent::PolicyNotAligned), - EventType::Manage(ManageEvent::Reserved3), - EventType::Manage(ManageEvent::Reserved4), EventType::Manage(ManageEvent::Reserved5), EventType::Manage(ManageEvent::Reserved6), EventType::ManageSieve(ManageSieveEvent::ConnectionStart), @@ -5437,6 +5439,7 @@ impl EventType { EventType::Security(SecurityEvent::IpBlocked), EventType::Security(SecurityEvent::IpBlockExpired), EventType::Security(SecurityEvent::IpAllowExpired), + EventType::Security(SecurityEvent::IpUnauthorized), EventType::Security(SecurityEvent::Unauthorized), EventType::Server(ServerEvent::Startup), EventType::Server(ServerEvent::Shutdown), diff --git a/tests/src/cluster/stress.rs b/tests/src/cluster/stress.rs index fc6ce73b..b0f8fa8f 100644 --- a/tests/src/cluster/stress.rs +++ b/tests/src/cluster/stress.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{assert_is_empty, mail::mailbox::destroy_all_mailboxes_no_wait, wait_for_index}; +use crate::jmap::{assert_is_empty, mail::mailbox::destroy_all_mailboxes_no_wait, wait_for_tasks}; use common::Server; use email::{ cache::{MessageCacheFetch, email::MessageCacheAccess}, @@ -264,7 +264,7 @@ async fn email_tests(server: Server, client: Arc) { } } - wait_for_index(&server).await; + test.wait_for_tasks().await; destroy_all_mailboxes_no_wait(&client).await; assert_is_empty(&server).await; } @@ -344,7 +344,7 @@ async fn mailbox_tests(server: Server, client: Arc) { join_all(futures).await; - wait_for_index(&server).await; + test.wait_for_tasks().await; for mailbox_id in client .mailbox_query(None::, None::>) .await diff --git a/tests/src/imap/append.rs b/tests/src/imap/append.rs index f8b7ea46..e510fcd1 100644 --- a/tests/src/imap/append.rs +++ b/tests/src/imap/append.rs @@ -8,7 +8,7 @@ use std::{fs, io}; use imap_proto::ResponseType; -use crate::jmap::wait_for_index; +use crate::jmap::wait_for_tasks; use super::{AssertResult, IMAPTest, ImapConnection, Type, resources_dir}; @@ -64,7 +64,7 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, h expected_uid += 1; } - wait_for_index(&handle.server).await; + wait_for_tasks(&handle.server).await; } pub async fn assert_append_message( diff --git a/tests/src/imap/store.rs b/tests/src/imap/store.rs index 65d8487c..a25f6dae 100644 --- a/tests/src/imap/store.rs +++ b/tests/src/imap/store.rs @@ -6,7 +6,7 @@ use imap_proto::ResponseType; -use crate::jmap::wait_for_index; +use crate::jmap::wait_for_tasks; use super::{AssertResult, IMAPTest, ImapConnection, Type}; @@ -60,7 +60,7 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, h .assert_contains("UIDNEXT 11"); // Store using saved searches - wait_for_index(&handle.server).await; + wait_for_tasks(&handle.server).await; imap.send("SEARCH RETURN (SAVE) FROM nathaniel").await; imap.assert_read(Type::Tagged, ResponseType::Ok).await; imap.send("UID STORE $ +FLAGS (\\Answered)").await; diff --git a/tests/src/jmap/auth/limits.rs b/tests/src/jmap/auth/limits.rs deleted file mode 100644 index d6b2b5e4..00000000 --- a/tests/src/jmap/auth/limits.rs +++ /dev/null @@ -1,263 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::{ - directory::internal::TestInternalDirectory, - imap::{ImapConnection, Type}, - jmap::JMAPTest, -}; -use imap_proto::ResponseType; -use jmap_client::{ - client::{Client, Credentials}, - core::set::{SetError, SetErrorType}, - mailbox::{self}, -}; -use std::{ - net::{IpAddr, Ipv4Addr}, - sync::Arc, - time::Duration, -}; -use store::write::now; - -pub async fn test(params: &mut JMAPTest) { - println!("Running Authorization tests..."); - - // Create test account - let server = params.server.clone(); - let account = params.account("jdoe@example.com"); - - // Remove unlimited requests permission - params - .server - .store() - .remove_permissions(account.name(), [Permission::UnlimitedRequests]) - .await; - params.server.inner.cache.access_tokens.clear(); - - // Reset rate limiters - params.webhook.clear(); - - // Incorrect passwords should be rejected with a 401 error - assert!(matches!( - Client::new() - .credentials(Credentials::basic("jdoe@example.com", "abcde")) - .accept_invalid_certs(true) .follow_redirects(["127.0.0.1"]) - .connect("https://127.0.0.1:8899") - .await, - Err(jmap_client::Error::Problem(err)) if err.status() == Some(401))); - - // Wait until the beginning of the 5 seconds bucket - const LIMIT: u64 = 5; - let now = now(); - let range_start = now / LIMIT; - let range_end = (range_start * LIMIT) + LIMIT; - tokio::time::sleep(Duration::from_secs(range_end - now)).await; - - // Test fail2ban - assert_eq!( - server - .core - .storage - .config - .get(format!("{BLOCKED_IP_KEY}.127.0.0.1")) - .await - .unwrap(), - None - ); - for n in 0..98 { - match Client::new() - .credentials(Credentials::basic( - "not_an_account@example.com", - &format!("brute_force{}", n), - )) - .accept_invalid_certs(true) - .follow_redirects(["127.0.0.1"]) - .connect("https://127.0.0.1:8899") - .await - { - Err(jmap_client::Error::Problem(_)) => {} - Err(err) => { - panic!("Unexpected response: {:?}", err); - } - Ok(_) => { - panic!("Unexpected success"); - } - } - } - - let mut imap = ImapConnection::connect(b"_x ").await; - imap.send("AUTHENTICATE PLAIN AGpvaG4AY2hpbWljaGFuZ2Fz") - .await; - imap.assert_read(Type::Tagged, ResponseType::No).await; - - // There are already 100 failed login attempts for this IP address - // so the next one should be rejected, even if done over IMAP - imap.send("AUTHENTICATE PLAIN AGpvaG4AY2hpbWljaGFuZ2Fz") - .await; - imap.assert_disconnect().await; - - // Make sure the IP address is blocked - assert_eq!( - server - .core - .storage - .config - .get(format!("{BLOCKED_IP_KEY}.127.0.0.1")) - .await - .unwrap(), - Some(String::new()) - ); - ImapConnection::connect(b"_y ") - .await - .assert_disconnect() - .await; - - // Lift ban - server - .core - .storage - .config - .clear(format!("{BLOCKED_IP_KEY}.127.0.0.1")) - .await - .unwrap(); - server - .inner - .data - .blocked_ips - .write() - .remove(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); - - // Valid authentication requests should not be rate limited - for _ in 0..110 { - Client::new() - .credentials(Credentials::basic(account.name(), account.secret())) - .accept_invalid_certs(true) - .follow_redirects(["127.0.0.1"]) - .connect("https://127.0.0.1:8899") - .await - .unwrap(); - } - - // Login with the correct credentials - let client = Client::new() - .credentials(Credentials::basic(account.name(), account.secret())) - .accept_invalid_certs(true) - .follow_redirects(["127.0.0.1"]) - .connect("https://127.0.0.1:8899") - .await - .unwrap(); - assert_eq!(client.session().username(), account.name()); - assert_eq!( - client - .session() - .account(account.id_string()) - .unwrap() - .name(), - account.name() - ); - assert!( - client - .session() - .account(account.id_string()) - .unwrap() - .is_personal() - ); - - // Uploads up to 5000000 bytes should be allowed - assert_eq!( - client - .upload(None, vec![b'A'; 5000000], None) - .await - .unwrap() - .size(), - 5000000 - ); - assert!( - client - .upload(None, vec![b'A'; 5000001], None) - .await - .is_err() - ); - - // Users should be allowed to create identities only - // using email addresses associated to their principal - let iid1 = client - .identity_create("John Doe", "jdoe@example.com") - .await - .unwrap() - .take_id(); - let iid2 = client - .identity_create("John Doe (secondary)", "john.doe@example.com") - .await - .unwrap() - .take_id(); - assert!(matches!( - client - .identity_create("John the Spammer", "spammy@mcspamface.com") - .await, - Err(jmap_client::Error::Set(SetError { - type_: SetErrorType::InvalidProperties, - .. - })) - )); - client.identity_destroy(&iid1).await.unwrap(); - client.identity_destroy(&iid2).await.unwrap(); - - // Concurrent requests check - let client = Arc::new(client); - for _ in 0..8 { - let client_ = client.clone(); - tokio::spawn(async move { - let _ = client_ - .mailbox_query( - mailbox::query::Filter::name("__sleep").into(), - [mailbox::query::Comparator::name()].into(), - ) - .await; - }); - } - tokio::time::sleep(Duration::from_millis(500)).await; - assert!(matches!( - client - .mailbox_query( - mailbox::query::Filter::name("__sleep").into(), - [mailbox::query::Comparator::name()].into(), - ) - .await, - Err(jmap_client::Error::Problem(err)) if err.status() == Some(400))); - - // Wait for sleep to be done - tokio::time::sleep(Duration::from_millis(1000)).await; - - // Concurrent upload test - for _ in 0..4 { - let client_ = client.clone(); - tokio::spawn(async move { - client_.upload(None, b"sleep".to_vec(), None).await.unwrap(); - }); - } - tokio::time::sleep(Duration::from_millis(500)).await; - assert!(matches!( - client.upload(None, b"sleep".to_vec(), None).await, - Err(jmap_client::Error::Problem(err)) if err.status() == Some(400))); - - // Add unlimited requests permission - params - .server - .store() - .add_permissions(account.name(), [Permission::UnlimitedRequests]) - .await; - params.server.inner.cache.access_tokens.clear(); - - // Destroy test accounts - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; - - // Check webhook events - params - .webhook - .assert_contains(&["auth.failed", "auth.success", "security.authentication-ban"]); -} diff --git a/tests/src/jmap/auth/mod.rs b/tests/src/jmap/auth/mod.rs deleted file mode 100644 index 8add5490..00000000 --- a/tests/src/jmap/auth/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -pub mod limits; -pub mod oauth; -pub mod permissions; -pub mod quota; diff --git a/tests/src/jmap/calendar/acl.rs b/tests/src/jmap/calendar/acl.rs index d4a78234..3f240f9a 100644 --- a/tests/src/jmap/calendar/acl.rs +++ b/tests/src/jmap/calendar/acl.rs @@ -707,5 +707,5 @@ pub async fn test(params: &mut JMAPTest) { // Destroy all mailboxes john.destroy_all_calendars().await; jane.destroy_all_calendars().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/calendar/alarm.rs b/tests/src/jmap/calendar/alarm.rs index f8acb485..b11fae93 100644 --- a/tests/src/jmap/calendar/alarm.rs +++ b/tests/src/jmap/calendar/alarm.rs @@ -169,5 +169,5 @@ pub async fn test(params: &mut JMAPTest) { // Cleanup account.destroy_all_calendars().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/calendar/calendars.rs b/tests/src/jmap/calendar/calendars.rs index 134c878c..9f14c2b6 100644 --- a/tests/src/jmap/calendar/calendars.rs +++ b/tests/src/jmap/calendar/calendars.rs @@ -370,5 +370,5 @@ pub async fn test(params: &mut JMAPTest) { // Destroy all mailboxes account.destroy_all_calendars().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/calendar/event.rs b/tests/src/jmap/calendar/event.rs index 9f4dc1db..2d35691b 100644 --- a/tests/src/jmap/calendar/event.rs +++ b/tests/src/jmap/calendar/event.rs @@ -5,7 +5,7 @@ */ use crate::{ - jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils, wait_for_index}, + jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils, wait_for_tasks}, webdav::DummyWebDavClient, }; use ahash::AHashSet; @@ -452,7 +452,7 @@ pub async fn test(params: &mut JMAPTest) { })); // Query tests - wait_for_index(¶ms.server).await; + wait_for_tasks(¶ms.server).await; assert_eq!( account .jmap_query( @@ -716,7 +716,7 @@ END:VCALENDAR // Clean up account.destroy_all_calendars().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } pub fn test_jscalendar_1() -> Value { diff --git a/tests/src/jmap/calendar/identity.rs b/tests/src/jmap/calendar/identity.rs index 5ff1f93d..5cabdd95 100644 --- a/tests/src/jmap/calendar/identity.rs +++ b/tests/src/jmap/calendar/identity.rs @@ -147,5 +147,5 @@ pub async fn test(params: &mut JMAPTest) { .with_document(0) .clear(PrincipalField::ParticipantIdentities); params.server.commit_batch(batch).await.unwrap(); - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/calendar/notification.rs b/tests/src/jmap/calendar/notification.rs index f844bb95..594db036 100644 --- a/tests/src/jmap/calendar/notification.rs +++ b/tests/src/jmap/calendar/notification.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{IntoJmapSet, JMAPTest, JmapUtils, wait_for_index}; +use crate::jmap::{IntoJmapSet, JMAPTest, JmapUtils, wait_for_tasks}; use calcard::jscalendar::JSCalendarProperty; use jmap_proto::{ object::calendar_event_notification::CalendarEventNotificationProperty, @@ -73,7 +73,7 @@ pub async fn test(params: &mut JMAPTest) { let john_event_id = response.created(0).id().to_string(); tokio::time::sleep(std::time::Duration::from_millis(600)).await; - wait_for_index(¶ms.server).await; + wait_for_tasks(¶ms.server).await; // Verify Jane and Bill received the share notification let mut jane_event_id = String::new(); @@ -386,9 +386,9 @@ pub async fn test(params: &mut JMAPTest) { for client in [john, jane, bill] { client.destroy_all_calendars().await; client.destroy_all_event_notifications().await; - params.destroy_all_mailboxes(client).await; + test.destroy_all_mailboxes(client).await; } - params.assert_is_empty().await; + test.assert_is_empty().await;; } fn test_event() -> Value { diff --git a/tests/src/jmap/contacts/acl.rs b/tests/src/jmap/contacts/acl.rs index 9636fbbf..7151ebb7 100644 --- a/tests/src/jmap/contacts/acl.rs +++ b/tests/src/jmap/contacts/acl.rs @@ -670,5 +670,5 @@ pub async fn test(params: &mut JMAPTest) { // Destroy all mailboxes john.destroy_all_addressbooks().await; jane.destroy_all_addressbooks().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/contacts/addressbook.rs b/tests/src/jmap/contacts/addressbook.rs index 737dd929..9152aa09 100644 --- a/tests/src/jmap/contacts/addressbook.rs +++ b/tests/src/jmap/contacts/addressbook.rs @@ -210,5 +210,5 @@ pub async fn test(params: &mut JMAPTest) { // Destroy all mailboxes account.destroy_all_addressbooks().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/contacts/contact.rs b/tests/src/jmap/contacts/contact.rs index a90ba96b..7a1a644c 100644 --- a/tests/src/jmap/contacts/contact.rs +++ b/tests/src/jmap/contacts/contact.rs @@ -5,7 +5,7 @@ */ use crate::{ - jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils, wait_for_index}, + jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils, wait_for_tasks}, webdav::DummyWebDavClient, }; use ahash::AHashSet; @@ -336,7 +336,7 @@ pub async fn test(params: &mut JMAPTest) { })); // Query tests - wait_for_index(¶ms.server).await; + wait_for_tasks(¶ms.server).await; let email = if !params.server.search_store().is_mysql() { "sarah.johnson@example.com" } else { @@ -496,7 +496,7 @@ END:VCARD"# // Clean up account.destroy_all_addressbooks().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } fn test_jscontact_1() -> Value { diff --git a/tests/src/jmap/core/blob.rs b/tests/src/jmap/core/blob.rs index ab043d68..8586b1ae 100644 --- a/tests/src/jmap/core/blob.rs +++ b/tests/src/jmap/core/blob.rs @@ -411,6 +411,6 @@ pub async fn test(params: &mut JMAPTest) { } // Remove test data - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/core/event_source.rs b/tests/src/jmap/core/event_source.rs index 8f53e284..31acf934 100644 --- a/tests/src/jmap/core/event_source.rs +++ b/tests/src/jmap/core/event_source.rs @@ -114,8 +114,8 @@ pub async fn test(params: &mut JMAPTest) { assert_ping(&mut event_rx).await; assert_ping(&mut event_rx).await; - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } async fn assert_state( diff --git a/tests/src/jmap/core/push_subscription.rs b/tests/src/jmap/core/push_subscription.rs index 26f54a73..b00cfcdd 100644 --- a/tests/src/jmap/core/push_subscription.rs +++ b/tests/src/jmap/core/push_subscription.rs @@ -202,8 +202,8 @@ pub async fn test(params: &mut JMAPTest) { client.mailbox_destroy(&mailbox_id, true).await.unwrap(); expect_nothing(&mut event_rx).await; - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await; } #[derive(Clone)] diff --git a/tests/src/jmap/core/websocket.rs b/tests/src/jmap/core/websocket.rs index 9d0f8a57..8bd9dac0 100644 --- a/tests/src/jmap/core/websocket.rs +++ b/tests/src/jmap/core/websocket.rs @@ -96,8 +96,8 @@ pub async fn test(params: &mut JMAPTest) { .unwrap(); expect_nothing(&mut stream_rx).await; - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await; } async fn expect_response( diff --git a/tests/src/jmap/files/acl.rs b/tests/src/jmap/files/acl.rs index 3fcdedd2..9db078df 100644 --- a/tests/src/jmap/files/acl.rs +++ b/tests/src/jmap/files/acl.rs @@ -460,5 +460,5 @@ pub async fn test(params: &mut JMAPTest) { ); // Destroy all mailboxes - params.assert_is_empty().await; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/files/node.rs b/tests/src/jmap/files/node.rs index ec0bb8d0..8084f750 100644 --- a/tests/src/jmap/files/node.rs +++ b/tests/src/jmap/files/node.rs @@ -329,5 +329,5 @@ pub async fn test(params: &mut JMAPTest) { ); // Make sure everything is gone - params.assert_is_empty().await; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/mail/acl.rs b/tests/src/jmap/mail/acl.rs index 43c2c5b3..93153aee 100644 --- a/tests/src/jmap/mail/acl.rs +++ b/tests/src/jmap/mail/acl.rs @@ -716,9 +716,9 @@ pub async fn test(params: &mut JMAPTest) { // Destroy test account data for id in [john, bill, jane, sales] { - params.destroy_all_mailboxes(id).await; + test.destroy_all_mailboxes(id).await; } - params.assert_is_empty().await; + test.assert_is_empty().await;; } pub fn assert_forbidden(result: Result) { diff --git a/tests/src/jmap/mail/antispam.rs b/tests/src/jmap/mail/antispam.rs index 09248025..002b34c3 100644 --- a/tests/src/jmap/mail/antispam.rs +++ b/tests/src/jmap/mail/antispam.rs @@ -170,6 +170,6 @@ pub async fn test(params: &mut JMAPTest) { assert_eq!(samples.spam_count, 10); assert_eq!(samples.samples.len(), 20); - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/mail/changes.rs b/tests/src/jmap/mail/changes.rs index a546b985..ef6d9fad 100644 --- a/tests/src/jmap/mail/changes.rs +++ b/tests/src/jmap/mail/changes.rs @@ -312,8 +312,8 @@ pub async fn test(params: &mut JMAPTest) { assert_eq!(created, vec![2, 3, 11, 12]); assert_eq!(changes.updated(), Vec::::new()); assert_eq!(changes.destroyed(), Vec::::new()); - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } #[derive(Debug, Clone, Copy)] diff --git a/tests/src/jmap/mail/copy.rs b/tests/src/jmap/mail/copy.rs index d930db4c..e6012588 100644 --- a/tests/src/jmap/mail/copy.rs +++ b/tests/src/jmap/mail/copy.rs @@ -98,5 +98,5 @@ pub async fn test(params: &mut JMAPTest) { // Empty store destroy_all_mailboxes_for_account(1).await; destroy_all_mailboxes_for_account(2).await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/mail/delivery.rs b/tests/src/jmap/mail/delivery.rs index d4711bd4..64c6ad53 100644 --- a/tests/src/jmap/mail/delivery.rs +++ b/tests/src/jmap/mail/delivery.rs @@ -434,9 +434,9 @@ END:VCARD // Remove test data for account in [john, jane, bill] { - params.destroy_all_mailboxes(account).await; + test.destroy_all_mailboxes(account).await; } - params.assert_is_empty().await; + test.assert_is_empty().await;; // Restore core params.server.inner.shared_core.store(old_core); diff --git a/tests/src/jmap/mail/get.rs b/tests/src/jmap/mail/get.rs index fd4e59ad..1d0e3281 100644 --- a/tests/src/jmap/mail/get.rs +++ b/tests/src/jmap/mail/get.rs @@ -165,8 +165,8 @@ pub async fn test(params: &mut JMAPTest) { } } - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } pub fn all_headers() -> Vec { diff --git a/tests/src/jmap/mail/mailbox.rs b/tests/src/jmap/mail/mailbox.rs index 5fde7804..eafeab11 100644 --- a/tests/src/jmap/mail/mailbox.rs +++ b/tests/src/jmap/mail/mailbox.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{Account, JMAPTest, wait_for_index}; +use crate::jmap::{Account, JMAPTest, wait_for_tasks}; use jmap_client::{ Error, Set, client::{Client, Credentials}, @@ -608,7 +608,7 @@ pub async fn test(params: &mut JMAPTest) { ); destroy_all_mailboxes_no_wait(&client).await; - params.assert_is_empty().await; + test.assert_is_empty().await; } async fn create_test_mailboxes(client: &Client) -> AHashMap { @@ -656,36 +656,6 @@ fn build_create_query( } } -impl JMAPTest { - pub async fn destroy_all_mailboxes(&self, account: &Account) { - wait_for_index(&self.server).await; - destroy_all_mailboxes_no_wait(account.client()).await; - } -} - -pub async fn destroy_all_mailboxes_for_account(account_id: u32) { - let mut client = Client::new() - .credentials(Credentials::basic("admin", "secret")) - .follow_redirects(["127.0.0.1"]) - .timeout(Duration::from_secs(3600)) - .accept_invalid_certs(true) - .connect("https://127.0.0.1:8899") - .await - .unwrap(); - client.set_default_account_id(Id::from(account_id)); - destroy_all_mailboxes_no_wait(&client).await; -} - -pub async fn destroy_all_mailboxes_no_wait(client: &Client) { - let mut request = client.build(); - request.query_mailbox().arguments().sort_as_tree(true); - let mut ids = request.send_query_mailbox().await.unwrap().take_ids(); - ids.reverse(); - for id in ids { - client.mailbox_destroy(&id, true).await.unwrap(); - } -} - #[derive(Serialize, Deserialize)] struct TestMailbox { id: String, diff --git a/tests/src/jmap/mail/parse.rs b/tests/src/jmap/mail/parse.rs index a9e7b7fa..fbb6ec3d 100644 --- a/tests/src/jmap/mail/parse.rs +++ b/tests/src/jmap/mail/parse.rs @@ -221,6 +221,6 @@ pub async fn test(params: &mut JMAPTest) { panic!("Test failed, output saved to {}", test_file.display()); } - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/mail/query.rs b/tests/src/jmap/mail/query.rs index 51dd5e0a..f10a934e 100644 --- a/tests/src/jmap/mail/query.rs +++ b/tests/src/jmap/mail/query.rs @@ -5,7 +5,7 @@ */ use crate::{ - jmap::{Account, JMAPTest, wait_for_index}, + jmap::{Account, JMAPTest, wait_for_tasks}, store::{deflate_test_resource, query::FIELDS}, }; use ::email::{cache::MessageCacheFetch, mailbox::Mailbox}; @@ -89,7 +89,7 @@ pub async fn test(params: &mut JMAPTest, insert: bool) { ); // Wait for indexing to complete - wait_for_index(&server).await; + test.wait_for_tasks().await; } let can_stem = !params.server.search_store().is_mysql(); @@ -112,8 +112,8 @@ pub async fn test(params: &mut JMAPTest, insert: bool) { .unwrap_set_email() .unwrap(); - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } pub async fn query(client: &Client, can_stem: bool) { @@ -867,7 +867,7 @@ pub async fn create(server: &Server, account: &Account) { task.await.unwrap(); } - wait_for_index(server).await; + wait_for_tasks(server).await; println!( "Imported {} messages in {} ms (single thread).", diff --git a/tests/src/jmap/mail/query_changes.rs b/tests/src/jmap/mail/query_changes.rs index 1d76d78c..b5447941 100644 --- a/tests/src/jmap/mail/query_changes.rs +++ b/tests/src/jmap/mail/query_changes.rs @@ -322,8 +322,8 @@ pub async fn test(params: &mut JMAPTest) { states.push(new_state); } - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } #[derive(Debug, Clone)] diff --git a/tests/src/jmap/mail/search_snippet.rs b/tests/src/jmap/mail/search_snippet.rs index 1a2454bc..59cf16f7 100644 --- a/tests/src/jmap/mail/search_snippet.rs +++ b/tests/src/jmap/mail/search_snippet.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, wait_for_index}; +use crate::jmap::{JMAPTest, wait_for_tasks}; use email::mailbox::INBOX_ID; use jmap_client::{core::query, email::query::Filter}; use std::{fs, path::PathBuf}; @@ -47,7 +47,7 @@ pub async fn test(params: &mut JMAPTest) { .take_id(); email_ids.insert(email_name, email_id); } - wait_for_index(&server).await; + test.wait_for_tasks().await; let can_stem = params.server.search_store().internal_fts().is_some(); @@ -170,6 +170,6 @@ pub async fn test(params: &mut JMAPTest) { } // Destroy test data - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/mail/set.rs b/tests/src/jmap/mail/set.rs index 6b27424c..9020b64c 100644 --- a/tests/src/jmap/mail/set.rs +++ b/tests/src/jmap/mail/set.rs @@ -26,8 +26,8 @@ pub async fn test(params: &mut JMAPTest) { create(client, &mailbox_id).await; update(client, &mailbox_id).await; - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } async fn create(client: &Client, mailbox_id: &str) { diff --git a/tests/src/jmap/mail/sieve_script.rs b/tests/src/jmap/mail/sieve_script.rs index 0b8677e8..f7b45816 100644 --- a/tests/src/jmap/mail/sieve_script.rs +++ b/tests/src/jmap/mail/sieve_script.rs @@ -496,8 +496,8 @@ pub async fn test(params: &mut JMAPTest) { for id in request.send_query_sieve_script().await.unwrap().take_ids() { client.sieve_script_destroy(&id).await.unwrap(); } - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } fn get_script(name: &str) -> Vec { diff --git a/tests/src/jmap/mail/submission.rs b/tests/src/jmap/mail/submission.rs index aa704838..195f3ad9 100644 --- a/tests/src/jmap/mail/submission.rs +++ b/tests/src/jmap/mail/submission.rs @@ -82,6 +82,30 @@ pub async fn test(params: &mut JMAPTest) { assert_eq!(identity.name().unwrap(), "John Doe"); } + // Users should be allowed to create identities only + // using email addresses associated to their principal + let iid1 = client + .identity_create("John Doe", "jdoe@example.com") + .await + .unwrap() + .take_id(); + let iid2 = client + .identity_create("John Doe (secondary)", "john.doe@example.com") + .await + .unwrap() + .take_id(); + assert!(matches!( + client + .identity_create("John the Spammer", "spammy@mcspamface.com") + .await, + Err(jmap_client::Error::Set(SetError { + type_: SetErrorType::InvalidProperties, + .. + })) + )); + client.identity_destroy(&iid1).await.unwrap(); + client.identity_destroy(&iid2).await.unwrap(); + // Create an identity without using a valid address should fail match client .identity_create("John Doe", "someaddress@domain.com") @@ -476,8 +500,8 @@ pub async fn test(params: &mut JMAPTest) { .await; client.email_submission_destroy(&id).await.unwrap(); } - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } pub fn spawn_mock_smtp_server() -> (mpsc::Receiver, Arc>) { diff --git a/tests/src/jmap/mail/thread_get.rs b/tests/src/jmap/mail/thread_get.rs index b11ab9b3..6685e39e 100644 --- a/tests/src/jmap/mail/thread_get.rs +++ b/tests/src/jmap/mail/thread_get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, wait_for_index}; +use crate::jmap::{JMAPTest, wait_for_tasks}; use jmap_client::mailbox::Role; pub async fn test(params: &mut JMAPTest) { @@ -35,7 +35,7 @@ pub async fn test(params: &mut JMAPTest) { expected_result[num - 1] = email.take_id(); } - wait_for_index(¶ms.server).await; + wait_for_tasks(¶ms.server).await; assert_eq!( client @@ -47,6 +47,6 @@ pub async fn test(params: &mut JMAPTest) { expected_result ); - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/mail/thread_merge.rs b/tests/src/jmap/mail/thread_merge.rs index 2a582be0..085fa826 100644 --- a/tests/src/jmap/mail/thread_merge.rs +++ b/tests/src/jmap/mail/thread_merge.rs @@ -5,7 +5,7 @@ */ use crate::{ - jmap::{JMAPTest, mail::mailbox::destroy_all_mailboxes_no_wait, wait_for_index}, + jmap::{JMAPTest, mail::mailbox::destroy_all_mailboxes_no_wait, wait_for_tasks}, store::deflate_test_resource, }; use ::email::{ @@ -141,7 +141,7 @@ async fn test_single_thread(params: &mut JMAPTest) { } } - wait_for_index(¶ms.server).await; + wait_for_tasks(¶ms.server).await; for test_num in 0..=5 { let result = client @@ -206,7 +206,7 @@ async fn test_single_thread(params: &mut JMAPTest) { } } - params.assert_is_empty().await; + test.assert_is_empty().await;; } #[allow(dead_code)] @@ -277,8 +277,8 @@ async fn test_multi_thread(params: &mut JMAPTest) { .len(), ); println!("Deleting all messages..."); - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } fn build_message(message: usize, in_reply_to: Option, thread_num: usize) -> String { diff --git a/tests/src/jmap/mail/vacation_response.rs b/tests/src/jmap/mail/vacation_response.rs index ece0f5be..fe1240ec 100644 --- a/tests/src/jmap/mail/vacation_response.rs +++ b/tests/src/jmap/mail/vacation_response.rs @@ -161,6 +161,6 @@ pub async fn test(params: &mut JMAPTest) { // Remove test data client.vacation_response_destroy().await.unwrap(); - params.destroy_all_mailboxes(account).await; - params.assert_is_empty().await; + test.destroy_all_mailboxes(account).await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 04819ccd..d337e007 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -53,7 +53,6 @@ use store::{ use tokio::sync::watch; use types::id::Id; -pub mod auth; pub mod calendar; pub mod contacts; pub mod core; diff --git a/tests/src/jmap/principal/availability.rs b/tests/src/jmap/principal/availability.rs index 142583d7..103f253b 100644 --- a/tests/src/jmap/principal/availability.rs +++ b/tests/src/jmap/principal/availability.rs @@ -251,5 +251,5 @@ pub async fn test(params: &mut JMAPTest) { // Cleanup john.destroy_all_calendars().await; - params.assert_is_empty().await; + test.assert_is_empty().await;; } diff --git a/tests/src/jmap/server/enterprise.rs b/tests/src/jmap/server/enterprise.rs index 4981672b..4966a2e1 100644 --- a/tests/src/jmap/server/enterprise.rs +++ b/tests/src/jmap/server/enterprise.rs @@ -16,7 +16,7 @@ use crate::{ JMAPTest, ManagementApi, mail::delivery::{AssertResult, SmtpConnection}, server::List, - wait_for_index, + wait_for_tasks, }, }; use common::{ @@ -151,7 +151,7 @@ pub async fn test(params: &mut JMAPTest) { destroy_account_data(&server, account_id, true) .await .unwrap(); - params.assert_is_empty().await; + test.assert_is_empty().await;; params.server.inner.shared_core.store( params @@ -292,7 +292,7 @@ async fn tracing(params: &mut JMAPTest) { tokio::time::sleep(Duration::from_millis(300)).await; params.server.notify_task_queue(); - wait_for_index(¶ms.server).await; + wait_for_tasks(¶ms.server).await; // Purge should not delete anything at this point store @@ -437,7 +437,7 @@ async fn undelete(params: &mut JMAPTest) { api.get::("/api/store/purge/account/jdoe@example.com") .await .unwrap(); - wait_for_index(¶ms.server).await; + wait_for_tasks(¶ms.server).await; tokio::time::sleep(Duration::from_millis(200)).await; let deleted = api .get::>("/api/store/undelete/jdoe@example.com") diff --git a/tests/src/jmap/server/purge.rs b/tests/src/jmap/server/purge.rs index 94832a6e..6915b7dd 100644 --- a/tests/src/jmap/server/purge.rs +++ b/tests/src/jmap/server/purge.rs @@ -6,7 +6,7 @@ use crate::{ imap::{AssertResult, ImapConnection, Type}, - jmap::{JMAPTest, wait_for_index}, + jmap::{JMAPTest, wait_for_tasks}, }; use ahash::AHashSet; use common::Server; @@ -147,7 +147,7 @@ pub async fn test(params: &mut JMAPTest) { } // Delete account - wait_for_index(&server).await; + test.wait_for_tasks().await; server .store() .delete_principal(QueryBy::Id(account.id().document_id())) @@ -156,7 +156,7 @@ pub async fn test(params: &mut JMAPTest) { destroy_account_data(&server, account.id().document_id(), true) .await .unwrap(); - params.assert_is_empty().await; + test.assert_is_empty().await;; } async fn get_changes(server: &Server) -> (AHashSet<(u64, u8)>, bool) { diff --git a/tests/src/smtp/queue/mod.rs b/tests/src/smtp/queue/mod.rs index c562faa7..36a1add6 100644 --- a/tests/src/smtp/queue/mod.rs +++ b/tests/src/smtp/queue/mod.rs @@ -33,14 +33,3 @@ pub fn build_rcpt(address: &str, retry: u64, notify: u64, expires: u64) -> Recip queue: QueueName::default(), } } - -pub trait QueuedEvents: Sync + Send { - fn all_queued_messages(&self) -> impl Future + Send; -} - -impl QueuedEvents for Server { - async fn all_queued_messages(&self) -> QueuedMessages { - self.next_event(&mut Queue::new(self.inner.clone(), mpsc::channel(100).1)) - .await - } -} diff --git a/tests/src/system/authentication.rs b/tests/src/system/authentication.rs index a629f227..59c9c2be 100644 --- a/tests/src/system/authentication.rs +++ b/tests/src/system/authentication.rs @@ -15,10 +15,11 @@ use registry::{ Account, Credential, Http, PasswordCredential, SecondaryCredential, UserAccount, }, }, - types::{EnumImpl, ipmask::IpAddrOrMask, list::List, map::Map}, + types::{EnumImpl, datetime::UTCDateTime, ipmask::IpAddrOrMask, list::List, map::Map}, }; use serde_json::json; use std::str::FromStr; +use store::write::now; pub async fn test(test: &TestServer) { let admin = test.account("admin@example.org"); @@ -134,7 +135,16 @@ pub async fn test(test: &TestServer) { validate_password("user@example.org", "this is a very strong password", false).await; validate_password("user@example.org", "very strong password indeed", true).await; - // Change password as user + // Set password expiration in two seconds and verify it works + admin + .registry_update_object( + ObjectType::Account, + user_id, + json!({ + "credentials/0/expiresAt": UTCDateTime::from_timestamp((now() + 2) as i64) + }), + ) + .await; let mut user = crate::utils::account::Account::new( "user@example.org", "very strong password indeed", @@ -142,8 +152,28 @@ pub async fn test(test: &TestServer) { user_id, ) .await; + user.registry_query_ids( + ObjectType::PublicKey, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await; + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + assert_eq!( + user.registry_query( + ObjectType::PublicKey, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .method_response() + .text_field("type"), + "forbidden" + ); + + // Change password as user and reset expiration let credential_id = user - .registry_query( + .registry_query_ids( ObjectType::Credential, [(Property::Type, CredentialType::Password.as_str())], Vec::<&str>::new(), @@ -191,7 +221,15 @@ pub async fn test(test: &TestServer) { validate_password("user@example.org", "user provided strong password", true).await; user.update_secret("user provided strong password"); - // Users should not be allowed to change allowedIps of expiration + // After a successful password change, the user permissions should be restored + user.registry_query_ids( + ObjectType::PublicKey, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await; + + // Users should not be allowed to change allowedIps or expiration user.registry_update_object_expect_err( ObjectType::Credential, credential_id, @@ -306,6 +344,20 @@ pub async fn test(test: &TestServer) { .assert_type(SetErrorType::OverQuota) .assert_description_contains("You have exceeded your quota of 1 API keys."); + // Set a credential expiration in the past and verify it is rejected + for credential_id in [app_password_id, api_key_id] { + user.registry_update_object( + ObjectType::Credential, + credential_id, + json!({ + Property::ExpiresAt: UTCDateTime::now() + }), + ) + .await; + } + validate_token_with_ip(&api_key_secret, "10.0.0.2", false).await; + validate_password_with_ip("user@example.org", &app_password_secret, "10.0.0.2", false).await; + // Destroy the API key and app password, then verify they no longer work let response = user .registry_destroy(ObjectType::Credential, [app_password_id, api_key_id]) @@ -328,13 +380,25 @@ pub async fn test(test: &TestServer) { vec![user_id] ); validate_password("user@example.org", "user provided strong password", false).await; + + // Disable X-Forwarded-For processing + admin + .registry_update_setting( + Http { + use_x_forwarded: false, + ..Default::default() + }, + &[Property::UseXForwarded], + ) + .await; + admin.reload_settings().await; } -async fn validate_password(username: &str, password: &str, is_valid: bool) { +pub async fn validate_password(username: &str, password: &str, is_valid: bool) { validate_password_with_ip(username, password, "127.0.0.1", is_valid).await; } -async fn validate_password_with_ip( +pub async fn validate_password_with_ip( username: &str, password: &str, remote_ip: &str, @@ -367,7 +431,7 @@ async fn validate_password_with_ip( } } -async fn validate_token_with_ip(token: &str, remote_ip: &str, is_valid: bool) { +pub async fn validate_token_with_ip(token: &str, remote_ip: &str, is_valid: bool) { let response = reqwest::Client::builder() .danger_accept_invalid_certs(true) .build() diff --git a/tests/src/system/authorization.rs b/tests/src/system/authorization.rs index 20b4870b..ec2f44a6 100644 --- a/tests/src/system/authorization.rs +++ b/tests/src/system/authorization.rs @@ -4,445 +4,230 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::utils::server::TestServer; +use common::auth::{BuildAccessToken, permissions::DefaultPermissions}; +use jmap_proto::error::set::SetErrorType; +use registry::{ + schema::{ + enums::Permission, + prelude::{ObjectType, Property}, + structs::{ + self, AccountSettings, Credential, CustomRoles, PasswordCredential, Role, UserAccount, + UserRoles, + }, + }, + types::{EnumImpl, list::List, map::Map}, +}; +use serde_json::json; +use types::id::Id; + +use crate::utils::{jmap::JmapUtils, server::TestServer}; pub async fn test(test: &mut TestServer) { println!("Running authorization tests..."); - /* + let admin = test.account("admin@example.org"); + let domain_id = admin.find_or_create_domain("example.org").await; - pub async fn test(params: &JMAPTest) { - println!("Running permissions tests..."); - let server = params.server.clone(); + // Create nested roles + let l3_role_id = admin + .registry_create_object(Role { + description: "Level 3 role".to_string(), + enabled_permissions: Map::new(vec![Permission::SysAccountSettingsGet]), + ..Default::default() + }) + .await; + let l2_role_id = admin + .registry_create_object(Role { + description: "Level 2 role".to_string(), + enabled_permissions: Map::new(vec![ + Permission::AuthenticateWithAlias, + Permission::SysAccountSettingsUpdate, + ]), + role_ids: Map::new(vec![l3_role_id]), + ..Default::default() + }) + .await; + let l1_role_id = admin + .registry_create_object(Role { + description: "Level 1 role".to_string(), + enabled_permissions: Map::new(vec![Permission::Authenticate]), + role_ids: Map::new(vec![l2_role_id]), + ..Default::default() + }) + .await; - // Disable spam filtering to avoid adding extra headers - let old_core = params.server.core.clone(); - let mut new_core = old_core.as_ref().clone(); - new_core.spam.enabled = false; - new_core.smtp.session.data.add_delivered_to = false; - params.server.inner.shared_core.store(Arc::new(new_core)); + // Create a user with the nested role + let user_id = admin + .registry_create_object(structs::Account::User(UserAccount { + name: "user".to_string(), + domain_id, + credentials: List::from_iter([Credential::Password(PasswordCredential { + secret: "this is a very strong password".to_string(), + ..Default::default() + })]), + roles: UserRoles::Custom(CustomRoles { + role_ids: Map::new(vec![l1_role_id]), + }), + ..Default::default() + })) + .await; + let user = crate::utils::account::Account::new( + "user@example.org", + "this is a very strong password", + &[], + user_id, + ) + .await; - // Remove unlimited requests permission - for &account in params.accounts.keys() { - params - .server - .store() - .remove_permissions(account, [Permission::UnlimitedRequests]) - .await; - } - - // Prepare management API - let api = ManagementApi::new(8899, "admin", "secret"); - - // Create a user with the default 'user' role - let account_id = api - .post::( - "/api/principal", - &PrincipalSet::new(u32::MAX, Type::Individual) - .with_field(PrincipalField::Name, "role_player") - .with_field(PrincipalField::Roles, vec!["user".to_string()]) - .with_field( - PrincipalField::DisabledPermissions, - vec![Permission::Pop3Dele.name().to_string()], - ), - ) + // Verify user permissions include all permissions from the nested roles + user.registry_update_object( + ObjectType::AccountSettings, + Id::singleton(), + json!({ + Property::Description: "Updated description" + }), + ) + .await; + assert_eq!( + user.registry_get::(Id::singleton()) .await - .unwrap() - .unwrap_data(); - let revision = server - .get_access_token(account_id) - .await - .unwrap() - .validate_permissions( - Permission::all().filter(|p| p.is_user_permission() && *p != Permission::Pop3Dele), - ) - .revision; - - // Create multiple roles - for (role, permissions, parent_role) in &[ - ( - "pop3_user", - vec![Permission::Pop3Authenticate, Permission::Pop3List], - vec![], - ), - ( - "imap_user", - vec![Permission::ImapAuthenticate, Permission::ImapList], - vec![], - ), - ( - "jmap_user", - vec![ - Permission::JmapEmailQuery, - Permission::AuthenticateOauth, - Permission::ManageEncryption, - ], - vec![], - ), - ( - "email_user", - vec![Permission::EmailSend, Permission::EmailReceive], - vec!["pop3_user", "imap_user", "jmap_user"], - ), - ] { - api.post::( - "/api/principal", - &PrincipalSet::new(u32::MAX, Type::Role) - .with_field(PrincipalField::Name, role.to_string()) - .with_field( - PrincipalField::EnabledPermissions, - permissions - .iter() - .map(|p| p.name().to_string()) - .collect::>(), - ) - .with_field( - PrincipalField::Roles, - parent_role - .iter() - .map(|r| r.to_string()) - .collect::>(), - ), - ) - .await - .unwrap() - .unwrap_data(); - } - - // Update email_user role - api.patch::<()>( - "/api/principal/email_user", - &vec![PrincipalUpdate::add_item( - PrincipalField::DisabledPermissions, - PrincipalValue::String(Permission::ManageEncryption.name().to_string()), - )], - ) - .await - .unwrap() - .unwrap_data(); - - // Update the user role to the nested 'email_user' role - api.patch::<()>( - "/api/principal/role_player", - &vec![PrincipalUpdate::set( - PrincipalField::Roles, - PrincipalValue::StringList(vec!["email_user".to_string()]), - )], - ) - .await - .unwrap() - .unwrap_data(); - assert_ne!( - server - .get_access_token(account_id) - .await - .unwrap() - .validate_permissions([ - Permission::EmailSend, - Permission::EmailReceive, - Permission::JmapEmailQuery, - Permission::AuthenticateOauth, - Permission::ImapAuthenticate, - Permission::ImapList, - Permission::Pop3Authenticate, - Permission::Pop3List, - ]) - .revision, - revision - ); - - // Query all principals - api.get::>("/api/principal") - .await - .unwrap() - .unwrap_data() - .assert_count(12) - .assert_exists( - "admin", - Type::Individual, - [ - (PrincipalField::Roles, &["admin"][..]), - (PrincipalField::Members, &[][..]), - (PrincipalField::EnabledPermissions, &[][..]), - (PrincipalField::DisabledPermissions, &[][..]), - ], - ) - .assert_exists( - "role_player", - Type::Individual, - [ - (PrincipalField::Roles, &["email_user"][..]), - (PrincipalField::Members, &[][..]), - (PrincipalField::EnabledPermissions, &[][..]), - ( - PrincipalField::DisabledPermissions, - &[Permission::Pop3Dele.name()][..], - ), - ], - ) - .assert_exists( - "email_user", - Type::Role, - [ - ( - PrincipalField::Roles, - &["pop3_user", "imap_user", "jmap_user"][..], - ), - (PrincipalField::Members, &["role_player"][..]), - ( - PrincipalField::EnabledPermissions, - &[ - Permission::EmailReceive.name(), - Permission::EmailSend.name(), - ][..], - ), - ( - PrincipalField::DisabledPermissions, - &[Permission::ManageEncryption.name()][..], - ), - ], - ) - .assert_exists( - "pop3_user", - Type::Role, - [ - (PrincipalField::Roles, &[][..]), - (PrincipalField::Members, &["email_user"][..]), - ( - PrincipalField::EnabledPermissions, - &[ - Permission::Pop3Authenticate.name(), - Permission::Pop3List.name(), - ][..], - ), - (PrincipalField::DisabledPermissions, &[][..]), - ], - ) - .assert_exists( - "imap_user", - Type::Role, - [ - (PrincipalField::Roles, &[][..]), - (PrincipalField::Members, &["email_user"][..]), - ( - PrincipalField::EnabledPermissions, - &[ - Permission::ImapAuthenticate.name(), - Permission::ImapList.name(), - ][..], - ), - (PrincipalField::DisabledPermissions, &[][..]), - ], - ) - .assert_exists( - "jmap_user", - Type::Role, - [ - (PrincipalField::Roles, &[][..]), - (PrincipalField::Members, &["email_user"][..]), - ( - PrincipalField::EnabledPermissions, - &[ - Permission::JmapEmailQuery.name(), - Permission::AuthenticateOauth.name(), - Permission::ManageEncryption.name(), - ][..], - ), - (PrincipalField::DisabledPermissions, &[][..]), - ], - ); - - // Verify permissions - server - .get_access_token(tenant_admin_id) - .await - .unwrap() - .validate_permissions(Permission::all().filter(|p| p.is_tenant_admin_permission())) - .validate_tenant(tenant_id, TENANT_QUOTA); - - // Prepare tenant admin API - let tenant_api = ManagementApi::new(8899, "admin@foobar.org", "mytenantpass"); - - // John should not be allowed to receive email - let (message_blob, _) = server - .put_temporary_blob(tenant_user_id, TEST_MESSAGE.as_bytes(), 60) - .await - .unwrap(); - assert_eq!( - server - .deliver_message(IngestMessage { - sender_address: "bill@foobar.org".to_string(), - sender_authenticated: true, - recipients: vec![IngestRecipient { - address: "john@foobar.org".to_string(), - is_spam: false - }], - message_blob: message_blob.clone(), - message_size: TEST_MESSAGE.len() as u64, - session_id: 0, - }) - .await - .status, - vec![LocalDeliveryStatus::PermanentFailure { - code: [5, 5, 0], - reason: "This account is not authorized to receive email.".into() - }] - ); - - // Remove the restriction - tenant_api - .patch::<()>( - "/api/principal/john.doe@foobar.org", - &vec![PrincipalUpdate::remove_item( - PrincipalField::Roles, - PrincipalValue::String("no-mail-for-you@foobar.com".to_string()), - )], - ) - .await - .unwrap() - .unwrap_data(); - server - .get_access_token(tenant_user_id) - .await - .unwrap() - .validate_permissions( - Permission::all().filter(|p| p.is_tenant_admin_permission() || p.is_user_permission()), - ); - } - - const TENANT_QUOTA: u64 = TEST_MESSAGE.len() as u64; - const TEST_MESSAGE: &str = concat!( - "From: bill@foobar.org\r\n", - "To: jdoe@foobar.com\r\n", - "Subject: TPS Report\r\n", - "\r\n", - "I'm going to need those TPS reports ASAP. ", - "So, if you could do that, that'd be great." + .description + .as_deref(), + Some("Updated description") ); - trait ValidatePrincipalList { - fn assert_exists<'x>( - self, - name: &str, - typ: Type, - items: impl IntoIterator, - ) -> Self; - fn assert_count(self, count: usize) -> Self; - } + // Remove read permissions from the l3 role and verify the user can no longer read account settings + admin + .registry_update_object( + ObjectType::Role, + l3_role_id, + json!({ + Property::EnabledPermissions: {} + }), + ) + .await; + assert_eq!( + user.registry_get_many(ObjectType::AccountSettings, [Id::singleton()]) + .await + .method_response() + .text_field("type"), + "forbidden" + ); - impl ValidatePrincipalList for List { - fn assert_exists<'x>( - self, - name: &str, - typ: Type, - items: impl IntoIterator, - ) -> Self { - for item in &self.items { - if item.name() == name { - item.validate(typ, items); - return self; - } - } + // User should still be able to update account settings due to permissions from the l2 role + user.registry_update_object( + ObjectType::AccountSettings, + Id::singleton(), + json!({ + Property::Description: "Updated description v2" + }), + ) + .await; - panic!("Principal not found: {}", name); - } - - fn assert_count(self, count: usize) -> Self { - assert_eq!(self.items.len(), count, "Principal count failed validation"); - assert_eq!(self.total, count, "Principal total failed validation"); - self - } - } - - trait ValidatePrincipal { - fn validate<'x>( - &self, - typ: Type, - items: impl IntoIterator, - ); - } - - impl ValidatePrincipal for PrincipalSet { - fn validate<'x>( - &self, - typ: Type, - items: impl IntoIterator, - ) { - assert_eq!(self.typ(), typ, "Type failed validation"); - - for (field, values) in items { - match ( - self.get_str_array(field).filter(|v| !v.is_empty()), - (!values.is_empty()).then_some(values), - ) { - (Some(values), Some(expected)) => { - assert_eq!( - values.iter().map(|s| s.as_str()).collect::>(), - expected.iter().copied().collect::>(), - "Field {field:?} failed validation: {values:?} != {expected:?}" - ); - } - (None, None) => {} - (values, expected) => { - panic!("Field {field:?} failed validation: {values:?} != {expected:?}"); - } - } - } - } - } - - trait ValidatePermissions { - fn validate_permissions( - self, - expected_permissions: impl IntoIterator, - ) -> Self; - fn validate_tenant(self, tenant_id: u32, tenant_quota: u64) -> Self; - } - - impl ValidatePermissions for Arc { - fn validate_permissions( - self, - expected_permissions: impl IntoIterator, - ) -> Self { - let expected_permissions: AHashSet<_> = expected_permissions.into_iter().collect(); - - let permissions = self.permissions(); - for permission in &permissions { - assert!( - expected_permissions.contains(permission), - "Permission {:?} failed validation", - permission - ); - } - assert_eq!( - permissions.into_iter().collect::>(), - expected_permissions - ); - - for permission in Permission::all() { - if self.has_permission(permission) { - assert!( - expected_permissions.contains(&permission), - "Permission {:?} failed validation", - permission - ); - } - } - self - } - - fn validate_tenant(self, tenant_id: u32, tenant_quota: u64) -> Self { - assert_eq!( - self.tenant, - Some(TenantInfo { - id: tenant_id, - quota: tenant_quota + // Disable account settings update permission in the l3 role + admin + .registry_update_object( + ObjectType::Role, + l3_role_id, + json!({ + Property::DisabledPermissions: Map::new(vec![Permission::SysAccountSettingsUpdate]), + }), + ) + .await; + assert_eq!( + user.registry_update( + ObjectType::AccountSettings, + [( + Id::singleton(), + json!({ + Property::Description: "Updated description v3" }) + )] + ) + .await + .method_response() + .text_field("type"), + "forbidden" + ); + + // Assign user to the default user role + admin + .registry_update_object( + ObjectType::Account, + user_id, + json!({ + Property::Roles: UserRoles::User + }), + ) + .await; + + // Make sure the user does not have any administrator permissions + let permissions = DefaultPermissions::default(); + let mut num_permissions_verified = 0; + let mut num_objects_verified = 0; + let user_access_token = test + .server + .access_token(user_id.document_id()) + .await + .unwrap() + .build(); + for permission in permissions.superuser { + if permissions.user.contains(&permission) { + continue; + } + num_permissions_verified += 1; + assert!( + !user_access_token.has_permission(permission), + "User should not have {:?} permission", + permission + ); + + if let Some(name) = permission + .as_str() + .strip_prefix("sys") + .and_then(|perm| perm.strip_suffix("Get")) + { + let object_type = ObjectType::parse(name).unwrap(); + + assert_eq!( + user.registry_get_many(object_type, Vec::<&str>::new()) + .await + .method_response() + .text_field("type"), + "forbidden", + "User should not have permission to read {:?} objects", + object_type ); - self + + num_objects_verified += 1; } } + assert_ne!( + num_permissions_verified, 0, + "No permissions were verified in the test" + ); + assert_ne!( + num_objects_verified, 0, + "No object read permissions were verified in the test" + ); + // Deleting a linked role should not be allowed + admin + .registry_destroy_object_expect_err(ObjectType::Role, l2_role_id) + .await + .assert_type(SetErrorType::ObjectIsLinked); - */ + // Delete the account and roles in the correct order + admin + .registry_destroy(ObjectType::Account, [user_id]) + .await + .assert_destroyed(&[user_id]); + for role_id in [l1_role_id, l2_role_id, l3_role_id] { + admin + .registry_destroy(ObjectType::Role, [role_id]) + .await + .assert_destroyed(&[role_id]); + } + + test.assert_is_empty().await; } diff --git a/tests/src/system/directory.rs b/tests/src/system/directory.rs index 4b3500b3..a1a695e5 100644 --- a/tests/src/system/directory.rs +++ b/tests/src/system/directory.rs @@ -330,7 +330,7 @@ pub async fn test(test: &TestServer) { // Query tests assert_eq!( account - .registry_query( + .registry_query_ids( ObjectType::Domain, [(Property::Name, "example.com")], [Property::Name] @@ -340,7 +340,7 @@ pub async fn test(test: &TestServer) { ); assert_eq!( account - .registry_query( + .registry_query_ids( ObjectType::Account, [ (Property::Name, "johndoe"), diff --git a/tests/src/system/mod.rs b/tests/src/system/mod.rs index ce2cbc37..4bb5b29a 100644 --- a/tests/src/system/mod.rs +++ b/tests/src/system/mod.rs @@ -8,6 +8,8 @@ pub mod authentication; pub mod authorization; pub mod directory; pub mod oidc; +pub mod quota; +pub mod security; pub mod tenant; use crate::utils::server::TestServerBuilder; @@ -31,11 +33,14 @@ pub async fn system_tests() { ) .await; test.account("admin") - .assign_roles_to_account(admin_id, &["user", "superuser"]) + .assign_roles_to_account(admin_id, &["user", "system"]) .await; //directory::test(&test).await; //authentication::test(&test).await; //oidc::test(&mut test).await; - tenant::test(&mut test).await; + //authorization::test(&mut test).await; + //tenant::test(&mut test).await; + //security::test(&mut test).await; + quota::test(&mut test).await; } diff --git a/tests/src/jmap/auth/quota.rs b/tests/src/system/quota.rs similarity index 59% rename from tests/src/jmap/auth/quota.rs rename to tests/src/system/quota.rs index bf39e14b..32e6d86f 100644 --- a/tests/src/jmap/auth/quota.rs +++ b/tests/src/system/quota.rs @@ -4,12 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - directory::internal::TestInternalDirectory, - jmap::{JMAPTest, mail::delivery::SmtpConnection, wait_for_index}, - smtp::queue::QueuedEvents, - store::cleanup::store_blob_expire_all, -}; +use crate::utils::{account::Account, jmap::JmapUtils, server::TestServer, smtp::SmtpConnection}; use common::config::smtp::queue::QueueName; use email::{cache::MessageCacheFetch, mailbox::INBOX_ID}; use jmap::blob::upload::DISABLE_UPLOAD_QUOTA; @@ -17,33 +12,106 @@ use jmap_client::{ core::set::{SetErrorType, SetObject}, email::EmailBodyPart, }; +use registry::{ + schema::{ + enums::{Permission, StorageQuota, TaskAccountMaintenanceType}, + prelude::{ObjectType, Property}, + structs::{ + self, Credential, Expression, Jmap, MtaStageAuth, PasswordCredential, PermissionsList, + Task, TaskAccountMaintenance, TaskStatus, UserAccount, + }, + }, + types::{EnumImpl, list::List, map::Map}, +}; use serde_json::json; use smtp::queue::spool::SmtpSpool; use types::id::Id; +use utils::map::vec_map::VecMap; -pub async fn test(params: &mut JMAPTest) { +pub async fn test(test: &mut TestServer) { println!("Running quota tests..."); - let server = params.server.clone(); + let admin = test.account("admin@example.org"); + let domain_id = admin.find_or_create_domain("example.org").await; - let account = params.account("robert@example.com"); - let other_account = params.account("jdoe@example.com"); + // Set test settings + admin + .registry_update_setting( + Jmap { + upload_quota: 50000, + max_upload_count: 3, + upload_ttl: registry::types::duration::Duration::from_millis(1000), + ..Default::default() + }, + &[ + Property::UploadQuota, + Property::MaxUploadCount, + Property::UploadTtl, + ], + ) + .await; + admin + .registry_update_setting( + MtaStageAuth { + require: Expression { + else_: "false".to_string(), + ..Default::default() + }, + ..Default::default() + }, + &[Property::Require], + ) + .await; + admin.reload_settings().await; - server - .core - .storage - .data - .set_test_quota("robert@example.com", 1024) + // Create test accounts + let account_id = admin + .registry_create_object(structs::Account::User(UserAccount { + name: "user1".to_string(), + domain_id, + credentials: List::from_iter([Credential::Password(PasswordCredential { + secret: "this is a very strong password1".to_string(), + ..Default::default() + })]), + quotas: VecMap::from_iter([(StorageQuota::MaxDiskQuota, 1024)]), + permissions: structs::Permissions::Merge(PermissionsList { + enabled_permissions: Map::new(vec![Permission::Impersonate]), + disabled_permissions: Default::default(), + }), + ..Default::default() + })) .await; - server - .core - .storage - .data - .add_to_group("robert@example.com", "jdoe@example.com") + let other_account_id = admin + .registry_create_object(structs::Account::User(UserAccount { + name: "user2".to_string(), + domain_id, + credentials: List::from_iter([Credential::Password(PasswordCredential { + secret: "this is a very strong password2".to_string(), + ..Default::default() + })]), + permissions: structs::Permissions::Merge(PermissionsList { + enabled_permissions: Map::new(vec![Permission::Impersonate]), + disabled_permissions: Default::default(), + }), + ..Default::default() + })) .await; - server.inner.cache.access_tokens.clear(); + let account = Account::new( + "user1@example.org", + "this is a very strong password1", + &[], + account_id, + ) + .await; + let other_account = Account::new( + "user2@example.org", + "this is a very strong password2", + &[], + other_account_id, + ) + .await; // Delete temporary blobs from previous tests - store_blob_expire_all(&server.core.storage.data).await; + test.blob_expire_all().await; // Test temporary blob quota (3 files) DISABLE_UPLOAD_QUOTA.store(false, std::sync::atomic::Ordering::Relaxed); @@ -66,9 +134,10 @@ pub async fn test(params: &mut JMAPTest) { jmap_client::Error::Problem(err) if err.detail().unwrap().contains("quota") => (), other => panic!("Unexpected error: {:?}", other), } - store_blob_expire_all(&server.core.storage.data).await; + test.blob_expire_all().await; // Test temporary blob quota (50000 bytes) + tokio::time::sleep(std::time::Duration::from_millis(1100)).await; for i in 0..2 { assert_eq!( client @@ -87,7 +156,8 @@ pub async fn test(params: &mut JMAPTest) { jmap_client::Error::Problem(err) if err.detail().unwrap().contains("quota") => (), other => panic!("Unexpected error: {:?}", other), } - store_blob_expire_all(&server.core.storage.data).await; + test.blob_expire_all().await; + tokio::time::sleep(std::time::Duration::from_millis(1100)).await; // Test JMAP Quotas extension let response = account @@ -103,7 +173,7 @@ pub async fn test(params: &mut JMAPTest) { assert!(response.contains("\"hardLimit\":1024"), "{}", response); assert!(response.contains("\"scope\":\"account\""), "{}", response); assert!( - response.contains("\"name\":\"robert@example.com\""), + response.contains("\"name\":\"user1@example.org\""), "{}", response ); @@ -116,8 +186,8 @@ pub async fn test(params: &mut JMAPTest) { client .email_import( create_message_with_size( - "jdoe@example.com", - "robert@example.com", + "user2@example.org", + "user1@example.org", &format!("Test {i}"), 512, ), @@ -134,7 +204,7 @@ pub async fn test(params: &mut JMAPTest) { assert_over_quota( client .email_import( - create_message_with_size("test@example.com", "jdoe@example.com", "Test 3", 100), + create_message_with_size("test@example.org", "user2@example.org", "Test 3", 100), vec![&inbox_id], None::>, None, @@ -155,16 +225,26 @@ pub async fn test(params: &mut JMAPTest) { assert!(response.contains("\"used\":1024"), "{}", response); assert!(response.contains("\"hardLimit\":1024"), "{}", response); + // Test registry quota + assert_eq!( + admin + .registry_get_many(ObjectType::Account, [account_id]) + .await + .list()[0] + .integer_field(Property::UsedDiskQuota.as_str()), + 1024 + ); + // Delete messages and check available quota for message_id in message_ids { client.email_destroy(&message_id).await.unwrap(); } // Wait for pending index tasks - wait_for_index(&server).await; + test.wait_for_tasks().await; assert_eq!( - server - .get_used_quota(account.id().document_id()) + test.server + .get_used_quota_account(account.id().document_id()) .await .unwrap(), 0 @@ -178,8 +258,8 @@ pub async fn test(params: &mut JMAPTest) { create_item .mailbox_ids([&inbox_id]) .subject(format!("Test {i}")) - .from(["jdoe@example.com"]) - .to(["robert@example.com"]) + .from(["user2@example.org"]) + .to(["user1@example.org"]) .body_value("a".to_string(), String::from_utf8(vec![b'A'; 200]).unwrap()) .text_body(EmailBodyPart::new().part_id("a")); let create_id = create_item.create_id().unwrap(); @@ -198,24 +278,30 @@ pub async fn test(params: &mut JMAPTest) { create_item .mailbox_ids([&inbox_id]) .subject("Test 3") - .from(["jdoe@example.com"]) - .to(["robert@example.com"]) + .from(["user2@example.org"]) + .to(["user1@example.org"]) .body_value("a".to_string(), String::from_utf8(vec![b'A'; 400]).unwrap()) .text_body(EmailBodyPart::new().part_id("a")); let create_id = create_item.create_id().unwrap(); assert_over_quota(request.send_set_email().await.unwrap().created(&create_id)); // Recalculate quota - let prev_quota = server - .get_used_quota(account.id().document_id()) - .await - .unwrap(); - recalculate_quota(&server, account.id().document_id()) + let prev_quota = test + .server + .get_used_quota_account(account.id().document_id()) .await .unwrap(); + admin + .registry_create_object(Task::AccountMaintenance(TaskAccountMaintenance { + account_id, + maintenance_type: TaskAccountMaintenanceType::RecalculateQuota, + status: TaskStatus::now(), + })) + .await; + test.wait_for_tasks().await; assert_eq!( - server - .get_used_quota(account.id().document_id()) + test.server + .get_used_quota_account(account.id().document_id()) .await .unwrap(), prev_quota @@ -226,10 +312,10 @@ pub async fn test(params: &mut JMAPTest) { client.email_destroy(&message_id).await.unwrap(); } // Wait for pending index tasks - wait_for_index(&server).await; + test.wait_for_tasks().await; assert_eq!( - server - .get_used_quota(account.id().document_id()) + test.server + .get_used_quota_account(account.id().document_id()) .await .unwrap(), 0 @@ -244,8 +330,8 @@ pub async fn test(params: &mut JMAPTest) { other_client .email_import( create_message_with_size( - "jane@example.com", - "jdoe@example.com", + "jane@example.org", + "user2@example.org", &format!("Other Test {i}"), 512, ), @@ -290,10 +376,10 @@ pub async fn test(params: &mut JMAPTest) { client.email_destroy(&message_id).await.unwrap(); } // Wait for pending index tasks - wait_for_index(&server).await; + test.wait_for_tasks().await; assert_eq!( - server - .get_used_quota(account.id().document_id()) + test.server + .get_used_quota_account(account.id().document_id()) .await .unwrap(), 0 @@ -303,11 +389,11 @@ pub async fn test(params: &mut JMAPTest) { let mut lmtp = SmtpConnection::connect().await; for i in 0..2 { lmtp.ingest( - "jane@example.com", - &["robert@example.com"], + "jane@example.org", + &["user1@example.org"], &String::from_utf8(create_message_with_size( - "jane@example.com", - "robert@example.com", + "jane@example.org", + "user1@example.org", &format!("Ingest test {i}"), 513, )) @@ -315,13 +401,14 @@ pub async fn test(params: &mut JMAPTest) { ) .await; } - let quota = server - .get_used_quota(account.id().document_id()) + let quota = test + .server + .get_used_quota_account(account.id().document_id()) .await .unwrap(); assert!(quota > 0 && quota <= 1024, "Quota is {}", quota); assert_eq!( - server + test.server .get_cached_messages(account.id().document_id()) .await .unwrap() @@ -334,18 +421,23 @@ pub async fn test(params: &mut JMAPTest) { DISABLE_UPLOAD_QUOTA.store(true, std::sync::atomic::Ordering::Relaxed); // Remove test data - params.destroy_all_mailboxes(account).await; - params.destroy_all_mailboxes(other_account).await; + test.destroy_all_mailboxes(&account).await; + test.destroy_all_mailboxes(&other_account).await; - for event in server.all_queued_messages().await.messages { - server + for event in test.all_queued_messages().await.messages { + test.server .read_message(event.queue_id, QueueName::default()) .await .unwrap() - .remove(&server, event.due.into()) + .remove(&test.server, event.due.into()) .await; } - params.assert_is_empty().await; + test.assert_is_empty().await; + + admin + .registry_destroy(ObjectType::Account, [account_id, other_account_id]) + .await + .assert_destroyed(&[account_id, other_account_id]); } fn assert_over_quota(result: Result) { diff --git a/tests/src/system/security.rs b/tests/src/system/security.rs new file mode 100644 index 00000000..704a7b25 --- /dev/null +++ b/tests/src/system/security.rs @@ -0,0 +1,321 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + system::authentication::validate_password_with_ip, + utils::{ + imap::{ImapConnection, Type}, + registry::UnwrapRegistryId, + server::TestServer, + }, +}; +use common::ipc::RegistryChange; +use imap_proto::ResponseType; +use jmap_client::{ + client::{Client, Credentials}, + mailbox::{self}, +}; +use registry::{ + schema::{ + enums::BlockReason, + prelude::{ObjectType, Property}, + structs::{ + self, Action, BlockedIp, Credential, Http, Jmap, PasswordCredential, UserAccount, + }, + }, + types::{ipmask::IpAddrOrMask, list::List}, +}; +use serde_json::json; +use std::{net::Ipv4Addr, sync::Arc, time::Duration}; +use store::{registry::write::RegistryWrite, write::now}; +use types::id::Id; + +pub async fn test(test: &mut TestServer) { + println!("Running Security tests..."); + + let admin = test.account("admin@example.org"); + let domain_id = admin.find_or_create_domain("example.org").await; + + // Set security settings + admin + .registry_update_setting( + Http { + use_x_forwarded: true, + ..Default::default() + }, + &[Property::UseXForwarded], + ) + .await; + admin + .registry_update_setting( + Jmap { + max_concurrent_uploads: Some(4), + max_concurrent_requests: Some(8), + max_upload_size: 5000000, + ..Default::default() + }, + &[ + Property::MaxConcurrentUploads, + Property::MaxConcurrentRequests, + Property::MaxUploadSize, + ], + ) + .await; + admin.reload_settings().await; + + // Create a user with the nested role + let user_id = admin + .registry_create_object(structs::Account::User(UserAccount { + name: "user".to_string(), + domain_id, + credentials: List::from_iter([Credential::Password(PasswordCredential { + secret: "this is a very strong password".to_string(), + ..Default::default() + })]), + ..Default::default() + })) + .await; + + // Incorrect passwords should be rejected with a 401 error + assert!(matches!( + Client::new() + .credentials(Credentials::basic("user@example.org", "abcde")) + .accept_invalid_certs(true) .follow_redirects(["127.0.0.1"]) + .connect("https://127.0.0.1:8899") + .await, + Err(jmap_client::Error::Problem(err)) if err.status() == Some(401))); + + // Wait until the beginning of the 5 seconds bucket + const LIMIT: u64 = 5; + let now = now(); + let range_start = now / LIMIT; + let range_end = (range_start * LIMIT) + LIMIT; + tokio::time::sleep(Duration::from_secs(range_end - now)).await; + + // Make sure that the IP address is not blocked before the test + assert_eq!( + admin + .registry_query_ids( + ObjectType::BlockedIp, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new() + ) + .await, + Vec::::new() + ); + + for _ in 0..98 { + validate_password_with_ip("unknown@example.org", "wrong password", "127.0.0.1", false) + .await; + } + + let mut imap = ImapConnection::connect(b"_x ").await; + imap.send("AUTHENTICATE PLAIN AGpvaG4AY2hpbWljaGFuZ2Fz") + .await; + imap.assert_read(Type::Tagged, ResponseType::No).await; + + // There are already 100 failed login attempts for this IP address + // so the next one should be rejected, even if done over IMAP + imap.send("AUTHENTICATE PLAIN AGpvaG4AY2hpbWljaGFuZ2Fz") + .await; + imap.assert_disconnect().await; + + // Make sure the IP address is blocked + let blocked_id = test + .server + .registry() + .primary_key( + ObjectType::BlockedIp.into(), + Property::Address, + IpAddrOrMask::from_ip(Ipv4Addr::LOCALHOST.into()).to_index_key(), + ) + .await + .unwrap() + .expect("Blocked IP should have been created after too many failed login attempts"); + let blocked_ip = test + .server + .registry() + .object::(blocked_id.id()) + .await + .unwrap() + .unwrap(); + assert_eq!(blocked_ip.reason, BlockReason::AuthFailure); + + ImapConnection::connect(b"_y ") + .await + .assert_disconnect() + .await; + + // Lift ban + test.server + .registry() + .write(RegistryWrite::delete(blocked_id)) + .await + .unwrap() + .unwrap_id(trc::location!()); + test.server + .reload_registry(RegistryChange::Delete(blocked_id)) + .await + .unwrap(); + + // Valid authentication requests should not be rate limited + for _ in 0..110 { + validate_password_with_ip( + "user@example.org", + "this is a very strong password", + "127.0.0.1", + true, + ) + .await; + } + + // Set fail2ban expiration + admin + .registry_update_object( + ObjectType::Security, + Id::singleton(), + json!({ + Property::AuthBanPeriod: registry::types::duration::Duration::from_millis(1000) + }), + ) + .await; + admin.reload_settings().await; + + // Block IP 10.0.0.2 + for _ in 0..105 { + validate_password_with_ip("unknown@example.org", "wrong password", "10.0.0.2", false).await; + } + validate_password_with_ip( + "user@example.org", + "this is a very strong password", + "10.0.0.2", + false, + ) + .await; + + // Check that the IP is blocked + let blocked_ids = admin + .registry_query_ids( + ObjectType::BlockedIp, + [(Property::Address, "10.0.0.2")], + Vec::<&str>::new(), + ) + .await; + assert_eq!(blocked_ids.len(), 1); + let blocked_ip = admin.registry_get::(blocked_ids[0]).await; + assert_eq!(blocked_ip.reason, BlockReason::AuthFailure); + assert!(blocked_ip.expires_at.is_some()); + + // After 1 second the ban should be lifted + tokio::time::sleep(Duration::from_secs(2)).await; + validate_password_with_ip( + "user@example.org", + "this is a very strong password", + "10.0.0.2", + true, + ) + .await; + + // Make sure the IP remains unblocked after reload + admin.registry_create_object(Action::ReloadBlockedIps).await; + validate_password_with_ip( + "user@example.org", + "this is a very strong password", + "10.0.0.2", + true, + ) + .await; + + // Login with the correct credentials + let client = Client::new() + .credentials(Credentials::basic( + "user@example.org", + "this is a very strong password", + )) + .accept_invalid_certs(true) + .follow_redirects(["127.0.0.1"]) + .connect("https://127.0.0.1:8899") + .await + .unwrap(); + assert_eq!(client.session().username(), "user@example.org"); + assert_eq!( + client + .session() + .account(&user_id.to_string()) + .unwrap() + .name(), + "user@example.org" + ); + assert!( + client + .session() + .account(&user_id.to_string()) + .unwrap() + .is_personal() + ); + + // Uploads up to 5000000 bytes should be allowed + assert_eq!( + client + .upload(None, vec![b'A'; 5000000], None) + .await + .unwrap() + .size(), + 5000000 + ); + assert!( + client + .upload(None, vec![b'A'; 5000001], None) + .await + .is_err() + ); + + // Concurrent requests check + let client = Arc::new(client); + for _ in 0..8 { + let client_ = client.clone(); + tokio::spawn(async move { + let _ = client_ + .mailbox_query( + mailbox::query::Filter::name("__sleep").into(), + [mailbox::query::Comparator::name()].into(), + ) + .await; + }); + } + tokio::time::sleep(Duration::from_millis(500)).await; + assert!(matches!( + client + .mailbox_query( + mailbox::query::Filter::name("__sleep").into(), + [mailbox::query::Comparator::name()].into(), + ) + .await, + Err(jmap_client::Error::Problem(err)) if err.status() == Some(400))); + + // Wait for sleep to be done + tokio::time::sleep(Duration::from_millis(1000)).await; + + // Concurrent upload test + for _ in 0..4 { + let client_ = client.clone(); + tokio::spawn(async move { + client_.upload(None, b"sleep".to_vec(), None).await.unwrap(); + }); + } + tokio::time::sleep(Duration::from_millis(500)).await; + assert!(matches!( + client.upload(None, b"sleep".to_vec(), None).await, + Err(jmap_client::Error::Problem(err)) if err.status() == Some(400))); + + // Destroy account + admin + .registry_destroy(ObjectType::Account, [user_id]) + .await + .assert_destroyed(&[user_id]); + + test.assert_is_empty().await; +} diff --git a/tests/src/system/tenant.rs b/tests/src/system/tenant.rs index bb5bb46e..3804c0d0 100644 --- a/tests/src/system/tenant.rs +++ b/tests/src/system/tenant.rs @@ -28,13 +28,13 @@ use utils::map::vec_map::VecMap; pub async fn test(test: &mut TestServer) { println!("Running multi-tenancy tests..."); - let account = test.account("admin@example.org"); + let admin_system = test.account("admin@example.org"); // Create tenants let mut tenant_x_ids = AHashMap::new(); let mut tenant_y_ids = AHashMap::new(); for (tenant_ids, name) in [(&mut tenant_x_ids, "x"), (&mut tenant_y_ids, "y")] { - let tenant_id = account + let tenant_id = admin_system .registry_create_object(Tenant { name: format!("Tenant {}", name), quotas: VecMap::from_iter([ @@ -52,7 +52,7 @@ pub async fn test(test: &mut TestServer) { }) .await; - let domain_id = account + let domain_id = admin_system .registry_create_object(Domain { name: format!("tenant{name}.org"), member_tenant_id: tenant_id.into(), @@ -60,12 +60,12 @@ pub async fn test(test: &mut TestServer) { }) .await; - let tenant_admin_id = account + let tenant_admin_id = admin_system .registry_create_object(Account::User(UserAccount { name: "admin".to_string(), domain_id, member_tenant_id: tenant_id.into(), - roles: UserRoles::TenantAdmin, + roles: UserRoles::Admin, description: format!("Tenant {name} Admin").into(), credentials: List::from_iter([Credential::Password(PasswordCredential { secret: format!("tenant {name} secret"), @@ -333,7 +333,7 @@ pub async fn test(test: &mut TestServer) { let expected = vec![tenant_y_ids[&ObjectType::TaskManager], expected_id]; assert_eq!( admin_y - .registry_query( + .registry_query_ids( ObjectType::Account, [(Property::Type, AccountType::User.as_str())], Vec::<&str>::new() @@ -353,7 +353,7 @@ pub async fn test(test: &mut TestServer) { ObjectType::AccountSettings => { assert_eq!( admin_y - .registry_query( + .registry_query_ids( ObjectType::Account, [(Property::Type, AccountType::Group.as_str())], Vec::<&str>::new() @@ -379,7 +379,11 @@ pub async fn test(test: &mut TestServer) { _ => { assert_eq!( admin_y - .registry_query(object_type, Vec::<(&str, &str)>::new(), Vec::<&str>::new()) + .registry_query_ids( + object_type, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new() + ) .await, vec![expected_id] ); @@ -603,7 +607,7 @@ pub async fn test(test: &mut TestServer) { object_type }; assert_eq!( - account + admin_system .registry_destroy(object_type, [id]) .await .destroyed_ids() diff --git a/tests/src/utils/account.rs b/tests/src/utils/account.rs index 76c4e004..4b616f10 100644 --- a/tests/src/utils/account.rs +++ b/tests/src/utils/account.rs @@ -146,7 +146,7 @@ impl Account { pub async fn find_or_create_domain(&self, name: &'static str) -> Id { let ids = self - .registry_query( + .registry_query_ids( ObjectType::Domain, [(Property::Name, name)], Vec::<&str>::new(), @@ -173,7 +173,7 @@ impl Account { let mut role_ids = Vec::new(); for name in names { let role_id = *self - .registry_query( + .registry_query_ids( ObjectType::Role, [(Property::Description, *name)], Vec::<&str>::new(), diff --git a/tests/src/utils/jmap.rs b/tests/src/utils/jmap.rs index 2a14e6ca..72d9e918 100644 --- a/tests/src/utils/jmap.rs +++ b/tests/src/utils/jmap.rs @@ -504,6 +504,18 @@ impl JmapResponse { }) } + pub fn assert_destroyed(&self, expected: &[Id]) -> &Self { + let destroyed_ids = self.destroyed_ids().collect::>(); + for expected in expected { + if !destroyed_ids.contains(expected) { + panic!( + "Expected id {expected} to be destroyed but got destroyed ids {destroyed_ids:?}: {self:?}" + ); + } + } + self + } + pub fn not_destroyed(&self, id: &str) -> &Value { self.0 .pointer(&format!("/methodResponses/0/1/notDestroyed/{id}")) @@ -638,6 +650,8 @@ pub trait JmapUtils { fn text_field(&self, field: &str) -> &str; + fn integer_field(&self, field: &str) -> i64; + fn assert_is_equal(&self, other: Value); } @@ -647,6 +661,13 @@ impl JmapUtils for Value { .and_then(|v| v.as_str()) .unwrap_or_else(|| panic!("Missing {field} in object: {self:?}")) } + + fn integer_field(&self, field: &str) -> i64 { + self.pointer(&format!("/{field}")) + .and_then(|v| v.as_i64()) + .unwrap_or_else(|| panic!("Missing {field} in object: {self:?}")) + } + fn assert_is_equal(&self, expected: Value) { if self != &expected { panic!( @@ -656,6 +677,7 @@ impl JmapUtils for Value { ); } } + fn with_property(mut self, field: impl Display, value: impl Into) -> Self { if let Value::Object(map) = &mut self { map.insert(field.to_string(), value.into()); diff --git a/tests/src/utils/registry.rs b/tests/src/utils/registry.rs index 03d764a8..941ba762 100644 --- a/tests/src/utils/registry.rs +++ b/tests/src/utils/registry.rs @@ -77,12 +77,24 @@ impl Account { .await } - pub async fn registry_query( + pub async fn registry_query_ids( &self, object: ObjectType, filter: impl IntoIterator)>, sort_by: impl IntoIterator, ) -> Vec { + self.registry_query(object, filter, sort_by) + .await + .object_ids() + .collect() + } + + pub async fn registry_query( + &self, + object: ObjectType, + filter: impl IntoIterator)>, + sort_by: impl IntoIterator, + ) -> JmapResponse { let name = object.as_str(); self.jmap_query( @@ -92,8 +104,6 @@ impl Account { Vec::<(&str, &str)>::new(), ) .await - .object_ids() - .collect() } pub async fn registry_destroy( diff --git a/tests/src/utils/server.rs b/tests/src/utils/server.rs index 36303c2e..408c654d 100644 --- a/tests/src/utils/server.rs +++ b/tests/src/utils/server.rs @@ -9,9 +9,9 @@ use crate::{ store::TempDir, utils::{ account::Account, - cleanup::{search_store_destroy, store_destroy}, + cleanup::{search_store_destroy, store_blob_expire_all, store_destroy}, registry::UnwrapRegistryId, - storage::{RegistryEnvStores, assert_is_empty, build_data_store}, + storage::{RegistryEnvStores, assert_is_empty, build_data_store, wait_for_tasks}, }, }; use ahash::AHashMap; @@ -27,6 +27,7 @@ use common::{ }; use http::HttpSessionManager; use imap::core::ImapSessionManager; +use jmap_client::client::{Client, Credentials}; use managesieve::core::ManageSieveSessionManager; use pop3::Pop3SessionManager; use registry::{ @@ -38,13 +39,20 @@ use registry::{ types::{EnumImpl, map::Map}, }; use services::{SpawnServices, broadcast::subscriber::spawn_broadcast_subscriber}; -use smtp::{SpawnQueueManager, core::SmtpSessionManager}; -use std::{str::FromStr, sync::Arc}; +use smtp::{ + SpawnQueueManager, + core::SmtpSessionManager, + queue::{ + manager::Queue, + spool::{QueuedMessages, SmtpSpool}, + }, +}; +use std::{str::FromStr, sync::Arc, time::Duration}; use store::{ RegistryStore, Store, registry::{bootstrap::Bootstrap, write::RegistryWrite}, }; -use tokio::sync::watch; +use tokio::sync::{mpsc, watch}; use trc::EventType; use types::id::Id; @@ -287,6 +295,14 @@ impl TestServer { self.accounts.get(name).unwrap() } + pub async fn wait_for_tasks(&self) { + wait_for_tasks(&self.server).await; + } + + pub async fn blob_expire_all(&self) { + store_blob_expire_all(&self.server.core.storage.data).await; + } + pub async fn assert_is_empty(&self) { assert_is_empty(&self.server).await; } @@ -302,4 +318,41 @@ impl TestServer { pub fn shutdown(&self) { let _ = self.shutdown_tx.send(true); } + + pub async fn all_queued_messages(&self) -> QueuedMessages { + self.server + .next_event(&mut Queue::new( + self.server.inner.clone(), + mpsc::channel(100).1, + )) + .await + } + + pub async fn destroy_all_mailboxes(&self, account: &Account) { + self.wait_for_tasks().await; + destroy_all_mailboxes_no_wait(account.client()).await; + } +} + +pub async fn destroy_all_mailboxes_for_account(account_id: u32) { + let mut client = Client::new() + .credentials(Credentials::basic("admin", "secret")) + .follow_redirects(["127.0.0.1"]) + .timeout(Duration::from_secs(3600)) + .accept_invalid_certs(true) + .connect("https://127.0.0.1:8899") + .await + .unwrap(); + client.set_default_account_id(Id::from(account_id)); + destroy_all_mailboxes_no_wait(&client).await; +} + +async fn destroy_all_mailboxes_no_wait(client: &Client) { + let mut request = client.build(); + request.query_mailbox().arguments().sort_as_tree(true); + let mut ids = request.send_query_mailbox().await.unwrap().take_ids(); + ids.reverse(); + for id in ids { + client.mailbox_destroy(&id, true).await.unwrap(); + } } diff --git a/tests/src/utils/storage.rs b/tests/src/utils/storage.rs index 0fdacd0c..a5a174cc 100644 --- a/tests/src/utils/storage.rs +++ b/tests/src/utils/storage.rs @@ -154,7 +154,7 @@ fn build_search_store(typ: SearchStoreType, _path: &str) -> SearchStore { } } -pub async fn wait_for_index(server: &Server) { +pub async fn wait_for_tasks(server: &Server) { let mut count = 0; loop { let mut has_index_tasks = None; @@ -193,7 +193,7 @@ pub async fn wait_for_index(server: &Server) { pub async fn assert_is_empty(server: &Server) { // Wait for pending index tasks - wait_for_index(server).await; + wait_for_tasks(server).await; // Assert is empty store_assert_is_empty(server.store(), server.core.storage.blob.clone(), false).await; diff --git a/tests/src/webdav/cal_scheduling.rs b/tests/src/webdav/cal_scheduling.rs index d1ca6d67..983e3122 100644 --- a/tests/src/webdav/cal_scheduling.rs +++ b/tests/src/webdav/cal_scheduling.rs @@ -258,7 +258,7 @@ pub async fn test(test: &WebDavTest) { // Check that John received the RSVP tokio::time::sleep(std::time::Duration::from_millis(200)).await; - test.wait_for_index().await; + test.wait_for_tasks().await; let itips = fetch_and_remove_itips(john_client).await; assert_eq!(itips.len(), 1); assert!( @@ -449,7 +449,7 @@ pub async fn test(test: &WebDavTest) { // Check that Bill received the update tokio::time::sleep(std::time::Duration::from_millis(200)).await; - test.wait_for_index().await; + test.wait_for_tasks().await; let mut itips = fetch_and_remove_itips(bill_client).await; itips.sort_unstable_by(|a, _| { if a.contains("Lunch") { diff --git a/tests/src/webdav/mod.rs b/tests/src/webdav/mod.rs index c3951cc2..5ef4995c 100644 --- a/tests/src/webdav/mod.rs +++ b/tests/src/webdav/mod.rs @@ -7,7 +7,7 @@ use crate::{ AssertConfig, TEST_USERS, add_test_certs, directory::internal::TestInternalDirectory, - jmap::{assert_is_empty, wait_for_index}, + jmap::{assert_is_empty, wait_for_tasks}, store::{ TempDir, build_store_config, cleanup::{search_store_destroy, store_destroy}, @@ -277,8 +277,8 @@ impl WebDavTest { self.clear_cache(); } - pub async fn wait_for_index(&self) { - wait_for_index(&self.server).await; + pub async fn wait_for_tasks(&self) { + wait_for_tasks(&self.server).await; } }