diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 32d0ab3d..5204cd8a 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -12,7 +12,7 @@ use crate::{ }; use ahash::AHashSet; use directory::{ - Permission, Principal, PrincipalData, QueryBy, Type, + Permission, Principal, PrincipalData, QueryParams, Type, backend::internal::{ lookup::DirectoryStore, manage::{ChangedPrincipals, ManageDirectory}, @@ -72,6 +72,8 @@ impl Server { if self.is_enterprise_edition() { if let Some(tenant_id) = principal.tenant { // Limit tenant permissions + + use directory::QueryParams; permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled); // Obtain tenant quota @@ -79,7 +81,7 @@ impl Server { id: tenant_id, quota: self .store() - .query(QueryBy::Id(tenant_id), false) + .query(QueryParams::id(tenant_id).with_return_member_of(false)) .await .caused_by(trc::location!())? .ok_or_else(|| { @@ -178,7 +180,11 @@ impl Server { } async fn build_access_token(&self, account_id: u32, revision: u64) -> trc::Result { - let err = match self.directory().query(QueryBy::Id(account_id), true).await { + let err = match self + .directory() + .query(QueryParams::id(account_id).with_return_member_of(true)) + .await + { Ok(Some(principal)) => { return self .build_access_token_from_principal(principal, revision) diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 5424f92b..cddcc2a7 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -4,22 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{net::IpAddr, sync::Arc}; - +use crate::{Server, listener::limiter::ConcurrencyLimiter}; use directory::{ - Directory, Permission, Permissions, Principal, QueryBy, Type, + Directory, FALLBACK_ADMIN_ID, Permission, Permissions, Principal, QueryParams, Type, backend::internal::lookup::DirectoryStore, core::secret::verify_secret_hash, }; use jmap_proto::types::collection::Collection; use mail_send::Credentials; use oauth::GrantType; +use std::{net::IpAddr, sync::Arc}; use utils::{ cache::CacheItemWeight, map::{bitmap::Bitmap, vec_map::VecMap}, }; -use crate::{Server, listener::limiter::ConcurrencyLimiter}; - pub mod access_token; pub mod oauth; pub mod rate_limit; @@ -102,7 +100,10 @@ impl Server { ) -> trc::Result { // First try to authenticate the user against the default directory let result = match directory - .query(QueryBy::Credentials(&req.credentials), req.return_member_of) + .query( + QueryParams::credentials(&req.credentials) + .with_return_member_of(req.return_member_of), + ) .await { Ok(Some(principal)) => { @@ -125,64 +126,104 @@ impl Server { } }; - // Then check if the credentials match the fallback admin or master user - if let Credentials::Plain { username, secret } = &req.credentials { - match (&self.core.jmap.fallback_admin, &self.core.jmap.master_user) { - (Some((fallback_admin, fallback_pass)), _) if username == fallback_admin => { - if verify_secret_hash(fallback_pass, secret).await? { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = username.clone(), - SpanId = req.session_id, - ); - - return Ok(Principal::fallback_admin(fallback_pass)); - } - } - (_, Some((master_user, master_pass))) if username.ends_with(master_user) => { - if verify_secret_hash(master_pass, secret).await? { - let username = username.strip_suffix(master_user).unwrap(); - let username = username.strip_suffix('%').unwrap_or(username); - - if let Some(principal) = directory - .query(QueryBy::Name(username), req.return_member_of) - .await? - { + match &req.credentials { + Credentials::Plain { username, secret } => { + // Then check if the credentials match the fallback admin or master user + match (&self.core.jmap.fallback_admin, &self.core.jmap.master_user) { + (Some((fallback_admin, fallback_pass)), _) if username == fallback_admin => { + if verify_secret_hash(fallback_pass, secret).await? { trc::event!( Auth(trc::AuthEvent::Success), - AccountName = username.to_string(), + AccountName = username.clone(), SpanId = req.session_id, - AccountId = principal.id(), - Type = principal.typ().as_str(), ); - return Ok(principal); + return Ok(Principal::fallback_admin(fallback_pass)); } } - } - _ => { - // Validate API credentials - if req.allow_api_access { - if let Ok(Some(principal)) = self - .store() - .query(QueryBy::Credentials(&req.credentials), req.return_member_of) - .await - { - if principal.typ == Type::ApiKey { + (_, Some((master_user, master_pass))) if username.ends_with(master_user) => { + if verify_secret_hash(master_pass, secret).await? { + let username = username.strip_suffix(master_user).unwrap(); + let username = username.strip_suffix('%').unwrap_or(username); + + if let Some(principal) = directory + .query( + QueryParams::name(username) + .with_return_member_of(req.return_member_of), + ) + .await? + { trc::event!( Auth(trc::AuthEvent::Success), - AccountName = principal.name().to_string(), - AccountId = principal.id(), + AccountName = username.to_string(), SpanId = req.session_id, + AccountId = principal.id(), + Type = principal.typ().as_str(), ); return Ok(principal); } } } + _ => { + // Validate API credentials + if req.allow_api_access { + if let Ok(Some(principal)) = self + .store() + .query( + QueryParams::credentials(&req.credentials) + .with_return_member_of(req.return_member_of), + ) + .await + { + if principal.typ == Type::ApiKey { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = principal.name().to_string(), + AccountId = principal.id(), + SpanId = req.session_id, + ); + + return Ok(principal); + } + } + } + } } } - } + Credentials::OAuthBearer { token } if directory.has_bearer_token_support() => { + // Check for bearer tokens issued locally + if let Ok(token_info) = self + .validate_access_token(GrantType::AccessToken.into(), token) + .await + { + let principal = if token_info.account_id != FALLBACK_ADMIN_ID { + directory + .query( + QueryParams::id(token_info.account_id) + .with_return_member_of(req.return_member_of), + ) + .await + .unwrap_or_default() + } else if let Some((_, fallback_pass)) = &self.core.jmap.fallback_admin { + Principal::fallback_admin(fallback_pass).into() + } else { + None + }; + if let Some(principal) = principal { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = principal.name().to_string(), + AccountId = principal.id(), + SpanId = req.session_id, + ); + + return Ok(principal); + } + } + } + _ => (), + }; if let Err(err) = result { Err(err) diff --git a/crates/common/src/auth/oauth/token.rs b/crates/common/src/auth/oauth/token.rs index 20819e50..d680f267 100644 --- a/crates/common/src/auth/oauth/token.rs +++ b/crates/common/src/auth/oauth/token.rs @@ -4,11 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::SystemTime; - -use directory::QueryBy; +use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncrypt}; +use crate::Server; +use directory::QueryParams; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; +use std::time::SystemTime; use store::{ blake3, rand::{Rng, rng}, @@ -16,10 +17,6 @@ use store::{ use trc::AddContext; use utils::codec::leb128::{Leb128Iterator, Leb128Vec}; -use crate::Server; - -use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncrypt}; - pub struct TokenInfo { pub grant_type: GrantType, pub account_id: u32, @@ -223,7 +220,7 @@ impl Server { self.core .storage .directory - .query(QueryBy::Id(account_id), false) + .query(QueryParams::id(account_id).with_return_member_of(false)) .await .caused_by(trc::location!())? .ok_or_else(|| { diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs index 1da9da1f..cf07d073 100644 --- a/crates/common/src/auth/roles.rs +++ b/crates/common/src/auth/roles.rs @@ -4,18 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::sync::{Arc, LazyLock}; - +use crate::Server; use ahash::AHashSet; use directory::{ - Permission, Permissions, QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, + Permission, Permissions, QueryParams, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, backend::internal::lookup::DirectoryStore, }; +use std::sync::{Arc, LazyLock}; use trc::AddContext; use utils::cache::CacheItemWeight; -use crate::Server; - #[derive(Debug, Clone, Default)] pub struct RolePermissions { pub enabled: Permissions, @@ -97,7 +95,7 @@ impl Server { // Obtain principal let mut principal = self .store() - .query(QueryBy::Id(role_id), true) + .query(QueryParams::id(role_id).with_return_member_of(true)) .await .caused_by(trc::location!())? .ok_or_else(|| { diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index 966b2da1..167a2bc2 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -16,7 +16,7 @@ use crate::{ }, ipc::{BroadcastEvent, StateEvent}, }; -use directory::{Directory, QueryBy, Type, backend::internal::manage::ManageDirectory}; +use directory::{Directory, QueryParams, Type, backend::internal::manage::ManageDirectory}; use jmap_proto::types::{ blob::BlobId, collection::{Collection, SyncCollection}, @@ -451,7 +451,7 @@ impl Server { .core .storage .directory - .query(QueryBy::Id(account_id), false) + .query(QueryParams::id(account_id).with_return_member_of(false)) .await .add_context(|err| err.caused_by(trc::location!()).account_id(account_id))? { @@ -470,7 +470,7 @@ impl Server { .core .storage .directory - .query(QueryBy::Id(tenant_id), false) + .query(QueryParams::id(tenant_id).with_return_member_of(false)) .await .add_context(|err| { err.caused_by(trc::location!()).account_id(tenant_id) diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index 8933402a..71487e51 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -14,14 +14,12 @@ pub mod license; pub mod llm; pub mod undelete; -use std::{sync::Arc, time::Duration}; - use ahash::{AHashMap, AHashSet}; - -use directory::{QueryBy, Type, backend::internal::lookup::DirectoryStore}; +use directory::{QueryParams, Type, backend::internal::lookup::DirectoryStore}; use license::LicenseKey; use llm::AiApiConfig; use mail_parser::DateTime; +use std::{sync::Arc, time::Duration}; use store::Store; use trc::{AddContext, EventType, MetricType}; use utils::{HttpLimitResponse, config::cron::SimpleCron, template::Template}; @@ -165,7 +163,7 @@ impl Server { // Try fetching the logo for the domain let logo_url = if let Some(mut principal) = self .store() - .query(QueryBy::Name(domain), false) + .query(QueryParams::name(domain).with_return_member_of(false)) .await .caused_by(trc::location!())? .filter(|p| p.typ() == Type::Domain) @@ -175,7 +173,7 @@ impl Server { } else if let Some(tenant_id) = principal.tenant { if let Some(logo) = self .store() - .query(QueryBy::Id(tenant_id), false) + .query(QueryParams::id(tenant_id).with_return_member_of(false)) .await .caused_by(trc::location!())? .and_then(|mut p| p.picture_mut().map(std::mem::take)) diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index ae8cb1d6..688068e3 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -36,7 +36,7 @@ use std::{ hash::{BuildHasher, Hash, Hasher}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::{Arc, atomic::AtomicBool}, - time::Duration, + time::{Duration, Instant}, }; use tinyvec::TinyVec; use tokio::sync::{Notify, Semaphore, mpsc}; @@ -239,10 +239,11 @@ pub struct MailboxCache { pub acls: TinyVec<[AclGrant; 2]>, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct HttpAuthCache { pub account_id: u32, pub revision: u64, + pub expires: Instant, } pub struct Ipc { diff --git a/crates/common/src/sharing/acl.rs b/crates/common/src/sharing/acl.rs index 24402f39..f11bbe6b 100644 --- a/crates/common/src/sharing/acl.rs +++ b/crates/common/src/sharing/acl.rs @@ -4,8 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{Server, auth::AccessToken}; use directory::{ - QueryBy, Type, + QueryParams, Type, backend::internal::{ PrincipalField, manage::{ChangedPrincipals, ManageDirectory}, @@ -21,8 +22,6 @@ use jmap_proto::{ }; use utils::map::bitmap::Bitmap; -use crate::{Server, auth::AccessToken}; - impl Server { pub async fn acl_set( &self, @@ -215,7 +214,7 @@ impl Server { .core .storage .directory - .query(QueryBy::Name(account_name), false) + .query(QueryParams::name(account_name).with_return_member_of(false)) .await { Ok(Some(principal)) => { @@ -256,7 +255,7 @@ impl Server { .core .storage .directory - .query(QueryBy::Name(account_name), false) + .query(QueryParams::name(account_name).with_return_member_of(false)) .await { Ok(Some(principal)) => Ok(( diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index f5de25ab..d7c1ec18 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -17,7 +17,7 @@ use dav_proto::{ response::{Ace, BaseCondition, GrantDeny, Href, MultiStatus, Principal}, }, }; -use directory::{QueryBy, Type, backend::internal::manage::ManageDirectory}; +use directory::{QueryParams, Type, backend::internal::manage::ManageDirectory}; use groupware::RFC_3986; use groupware::{cache::GroupwareCache, calendar::Calendar, contact::AddressBook, file::FileNode}; use http_proto::HttpResponse; @@ -412,7 +412,7 @@ impl DavAclHandler for Server { // Verify that the principal is a valid principal let principal = self .directory() - .query(QueryBy::Id(principal_id), false) + .query(QueryParams::id(principal_id).with_return_member_of(false)) .await .caused_by(trc::location!())? .ok_or_else(|| { diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index d39c1529..5ba9becb 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -4,8 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; - +use super::CurrentUserPrincipal; +use crate::{ + DavResourceName, + common::propfind::{PropFindRequestHandler, SyncTokenUrn}, +}; use common::{Server, auth::AccessToken}; use dav_proto::schema::{ Namespace, @@ -16,20 +19,14 @@ use dav_proto::schema::{ request::{DavPropertyValue, PropFind}, response::{Href, MultiStatus, PropStat, Response}, }; -use directory::{QueryBy, Type, backend::internal::manage::ManageDirectory}; +use directory::{QueryParams, Type, backend::internal::manage::ManageDirectory}; use groupware::RFC_3986; use groupware::cache::GroupwareCache; use hyper::StatusCode; use jmap_proto::types::collection::Collection; +use std::borrow::Cow; use trc::AddContext; -use crate::{ - DavResourceName, - common::propfind::{PropFindRequestHandler, SyncTokenUrn}, -}; - -use super::CurrentUserPrincipal; - pub(crate) trait PrincipalPropFind: Sync + Send { fn prepare_principal_propfind_response( &self, @@ -120,7 +117,7 @@ impl PrincipalPropFind for Server { ) } else { self.directory() - .query(QueryBy::Id(account_id), false) + .query(QueryParams::id(account_id).with_return_member_of(false)) .await .caused_by(trc::location!())? .map(|p| { diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index a98c2ad1..bd2b4e31 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -5,7 +5,7 @@ */ use super::{PrincipalInfo, manage::ManageDirectory}; -use crate::{Principal, PrincipalData, QueryBy, Type, backend::RcptType}; +use crate::{Principal, PrincipalData, QueryBy, QueryParams, Type, backend::RcptType}; use mail_send::Credentials; use store::{ @@ -16,11 +16,7 @@ use trc::AddContext; #[allow(async_fn_in_trait)] pub trait DirectoryStore: Sync + Send { - async fn query( - &self, - by: QueryBy<'_>, - return_member_of: bool, - ) -> trc::Result>; + async fn query(&self, by: QueryParams<'_>) -> trc::Result>; async fn email_to_id(&self, address: &str) -> trc::Result>; async fn is_local_domain(&self, domain: &str) -> trc::Result; async fn rcpt(&self, address: &str) -> trc::Result; @@ -30,12 +26,8 @@ pub trait DirectoryStore: Sync + Send { } impl DirectoryStore for Store { - async fn query( - &self, - by: QueryBy<'_>, - return_member_of: bool, - ) -> trc::Result> { - let (account_id, secret) = match by { + async fn query(&self, by: QueryParams<'_>) -> trc::Result> { + let (account_id, secret) = match by.by { QueryBy::Name(name) => (self.get_principal_id(name).await?, None), QueryBy::Id(account_id) => (account_id.into(), None), QueryBy::Credentials(credentials) => match credentials { @@ -56,12 +48,12 @@ impl DirectoryStore for Store { if let Some(account_id) = account_id { if let Some(mut principal) = self.get_principal(account_id).await? { if let Some(secret) = secret { - if !principal.verify_secret(secret).await? { + if !principal.verify_secret(secret, by.only_app_pass).await? { return Ok(None); } } - if return_member_of { + if by.return_member_of { let mut roles = vec![]; let mut lists = vec![]; let mut member_of = vec![]; diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 5d19134b..b5dfb2e2 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -10,7 +10,7 @@ use super::{ }; use crate::{ MemberOf, Permission, PermissionGrant, Permissions, Principal, PrincipalData, PrincipalQuota, - QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, Type, backend::RcptType, + QueryBy, QueryParams, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, Type, backend::RcptType, core::principal::build_search_index, }; use ahash::{AHashMap, AHashSet}; @@ -268,7 +268,7 @@ impl ManageDirectory for Store { #[cfg(feature = "enterprise")] if let Some(tenant_id) = tenant_id { let tenant = self - .query(QueryBy::Id(tenant_id), false) + .query(crate::QueryParams::id(tenant_id).with_return_member_of(false)) .await? .ok_or_else(|| { trc::ManageEvent::NotFound @@ -2025,7 +2025,7 @@ impl ManageDirectory for Store { for principal in result.items { items.push( - self.query(QueryBy::Id(principal.id), fetch) + self.query(QueryParams::id(principal.id).with_return_member_of(fetch)) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(principal.name().to_string()))?, @@ -2236,8 +2236,9 @@ impl ManageDirectory for Store { match principal.typ { Type::Group | Type::List | Type::Role => { for member_id in self.get_members(principal.id).await? { - if let Some(member_principal) = - self.query(QueryBy::Id(member_id), false).await? + if let Some(member_principal) = self + .query(QueryParams::id(member_id).with_return_member_of(false)) + .await? { result.append_str(PrincipalField::Members, member_principal.name); } diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index d0420a3b..274e9738 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -10,7 +10,7 @@ use store::xxhash_rust; use trc::AddContext; use crate::{ - IntoError, Principal, PrincipalData, QueryBy, ROLE_ADMIN, ROLE_USER, Type, + IntoError, Principal, PrincipalData, QueryBy, QueryParams, ROLE_ADMIN, ROLE_USER, Type, backend::{ RcptType, internal::{ @@ -23,13 +23,9 @@ use crate::{ use super::{AuthBind, LdapDirectory, LdapMappings}; impl LdapDirectory { - pub async fn query( - &self, - by: QueryBy<'_>, - return_member_of: bool, - ) -> trc::Result> { + pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { let mut conn = self.pool.get().await.map_err(|err| err.into_error())?; - let (mut external_principal, member_of, stored_principal) = match by { + let (mut external_principal, member_of, stored_principal) = match by.by { QueryBy::Name(username) => { let filter = self.mappings.filter_name.build(username); if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { @@ -49,7 +45,7 @@ impl LdapDirectory { QueryBy::Id(uid) => { if let Some(stored_principal_) = self .data_store - .query(QueryBy::Id(uid), return_member_of) + .query(QueryParams::id(uid).with_return_member_of(by.return_member_of)) .await? { if let Some(result) = self @@ -190,7 +186,7 @@ impl LdapDirectory { AuthBind::None => { let filter = self.mappings.filter_name.build(username); if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { - if result.principal.verify_secret(secret).await? { + if result.principal.verify_secret(secret, false).await? { if result.principal.name.is_empty() { result.principal.name = username.into(); } @@ -217,7 +213,7 @@ impl LdapDirectory { }; // Query groups - if !member_of.is_empty() && return_member_of { + if !member_of.is_empty() && by.return_member_of { let mut data = Vec::with_capacity(member_of.len()); for mut name in member_of { if name.contains('=') { @@ -268,7 +264,7 @@ impl LdapDirectory { .caused_by(trc::location!())?; self.data_store - .query(QueryBy::Id(id), return_member_of) + .query(QueryParams::id(id).with_return_member_of(by.return_member_of)) .await .caused_by(trc::location!())? .ok_or_else(|| manage::not_found(id).caused_by(trc::location!()))? diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs index 4c633694..8c1c476d 100644 --- a/crates/directory/src/backend/memory/lookup.rs +++ b/crates/directory/src/backend/memory/lookup.rs @@ -5,13 +5,13 @@ */ use super::{EmailType, MemoryDirectory}; -use crate::{Principal, QueryBy, backend::RcptType}; +use crate::{Principal, QueryBy, QueryParams, backend::RcptType}; use mail_send::Credentials; impl MemoryDirectory { - pub async fn query(&self, by: QueryBy<'_>) -> trc::Result> { - match by { + pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { + match by.by { QueryBy::Name(name) => { for principal in &self.principals { if principal.name() == name { @@ -35,7 +35,7 @@ impl MemoryDirectory { for principal in &self.principals { if principal.name() == username { - return if principal.verify_secret(secret).await? { + return if principal.verify_secret(secret, false).await? { Ok(Some(principal.clone())) } else { Ok(None) diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index c0390d67..105cf840 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -11,7 +11,7 @@ use reqwest::{StatusCode, header::AUTHORIZATION}; use trc::{AddContext, AuthEvent}; use crate::{ - Principal, PrincipalData, QueryBy, ROLE_USER, Type, + Principal, PrincipalData, QueryBy, QueryParams, ROLE_USER, Type, backend::{ RcptType, internal::{ @@ -27,12 +27,8 @@ use super::{OpenIdConfig, OpenIdDirectory}; type OpenIdResponse = HashMap; impl OpenIdDirectory { - pub async fn query( - &self, - by: QueryBy<'_>, - return_member_of: bool, - ) -> trc::Result> { - match &by { + pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { + match &by.by { QueryBy::Credentials(Credentials::OAuthBearer { token }) => { // Send request #[cfg(feature = "test_mode")] @@ -102,7 +98,7 @@ impl OpenIdDirectory { .caused_by(trc::location!())?; let mut principal = self .data_store - .query(QueryBy::Id(id), return_member_of) + .query(QueryParams::id(id).with_return_member_of(by.return_member_of)) .await .caused_by(trc::location!())? .ok_or_else(|| manage::not_found(id).caused_by(trc::location!()))?; @@ -133,7 +129,7 @@ impl OpenIdDirectory { .details("Unexpected status code")), } } - _ => self.data_store.query(by, return_member_of).await, + _ => self.data_store.query(by.with_only_app_pass(true)).await, } } diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index e6538da4..856aa0a3 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -6,7 +6,7 @@ use super::{SqlDirectory, SqlMappings}; use crate::{ - Principal, PrincipalData, QueryBy, ROLE_ADMIN, ROLE_USER, Type, + Principal, PrincipalData, QueryBy, QueryParams, ROLE_ADMIN, ROLE_USER, Type, backend::{ RcptType, internal::{ @@ -21,12 +21,8 @@ use store::{NamedRows, Rows, Value}; use trc::AddContext; impl SqlDirectory { - pub async fn query( - &self, - by: QueryBy<'_>, - return_member_of: bool, - ) -> trc::Result> { - let (external_principal, stored_principal) = match by { + pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { + let (external_principal, stored_principal) = match by.by { QueryBy::Name(username) => ( self.mappings .row_to_principal( @@ -48,7 +44,7 @@ impl SqlDirectory { QueryBy::Id(uid) => { if let Some(principal) = self .data_store - .query(QueryBy::Id(uid), return_member_of) + .query(QueryParams::id(uid).with_return_member_of(by.return_member_of)) .await .caused_by(trc::location!())? { @@ -108,7 +104,7 @@ impl SqlDirectory { } if principal - .verify_secret(secret) + .verify_secret(secret, false) .await .caused_by(trc::location!())? { @@ -131,7 +127,7 @@ impl SqlDirectory { }; // Obtain members - if return_member_of && !self.mappings.query_members.is_empty() { + if by.return_member_of && !self.mappings.query_members.is_empty() { let mut data = Vec::new(); for row in self .sql_store @@ -185,7 +181,7 @@ impl SqlDirectory { .caused_by(trc::location!())?; self.data_store - .query(QueryBy::Id(id), return_member_of) + .query(QueryParams::id(id).with_return_member_of(by.return_member_of)) .await .caused_by(trc::location!())? .ok_or_else(|| manage::not_found(id).caused_by(trc::location!()))? diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs index 03a85035..71ceac2f 100644 --- a/crates/directory/src/core/dispatch.rs +++ b/crates/directory/src/core/dispatch.rs @@ -7,24 +7,20 @@ use trc::AddContext; use crate::{ - Directory, DirectoryInner, Principal, QueryBy, + Directory, DirectoryInner, Principal, QueryParams, backend::{RcptType, internal::lookup::DirectoryStore}, }; impl Directory { - pub async fn query( - &self, - by: QueryBy<'_>, - return_member_of: bool, - ) -> trc::Result> { + pub async fn query(&self, by: QueryParams<'_>) -> trc::Result> { match &self.store { - DirectoryInner::Internal(store) => store.query(by, return_member_of).await, - DirectoryInner::Ldap(store) => store.query(by, return_member_of).await, - DirectoryInner::Sql(store) => store.query(by, return_member_of).await, - DirectoryInner::Imap(store) => store.query(by).await, - DirectoryInner::Smtp(store) => store.query(by).await, + DirectoryInner::Internal(store) => store.query(by).await, + DirectoryInner::Ldap(store) => store.query(by).await, + DirectoryInner::Sql(store) => store.query(by).await, + DirectoryInner::Imap(store) => store.query(by.by).await, + DirectoryInner::Smtp(store) => store.query(by.by).await, DirectoryInner::Memory(store) => store.query(by).await, - DirectoryInner::OpenId(store) => store.query(by, return_member_of).await, + DirectoryInner::OpenId(store) => store.query(by).await, } .caused_by(trc::location!()) } diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index 63989c3c..5ccbb7c7 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -4,8 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{collections::hash_map::Entry, fmt, str::FromStr}; - +use crate::{ + ArchivedPrincipal, FALLBACK_ADMIN_ID, Permission, PermissionGrant, Principal, PrincipalData, + ROLE_ADMIN, Type, + backend::internal::{PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue}, +}; use ahash::AHashSet; use nlp::tokenizers::word::WordTokenizer; use serde::{ @@ -13,17 +16,13 @@ use serde::{ de::{self, IgnoredAny, Visitor}, ser::SerializeMap, }; +use std::{collections::hash_map::Entry, fmt, str::FromStr}; use store::{ U64_LEN, backend::MAX_TOKEN_LENGTH, write::{BatchBuilder, DirectoryClass}, }; -use crate::{ - ArchivedPrincipal, Permission, PermissionGrant, Principal, PrincipalData, ROLE_ADMIN, Type, - backend::internal::{PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue}, -}; - impl Principal { pub fn new(id: u32, typ: Type) -> Self { Self { @@ -313,7 +312,7 @@ impl Principal { pub fn fallback_admin(fallback_pass: impl Into) -> Self { Principal { - id: u32::MAX, + id: FALLBACK_ADMIN_ID, typ: Type::Individual, name: "Fallback Administrator".into(), secrets: vec![fallback_pass.into()], diff --git a/crates/directory/src/core/secret.rs b/crates/directory/src/core/secret.rs index 49ab0f2e..a0cf8bd5 100644 --- a/crates/directory/src/core/secret.rs +++ b/crates/directory/src/core/secret.rs @@ -23,7 +23,7 @@ use crate::Principal; use crate::backend::internal::SpecialSecrets; impl Principal { - pub async fn verify_secret(&self, mut code: &str) -> trc::Result { + pub async fn verify_secret(&self, mut code: &str, only_app_pass: bool) -> trc::Result { let mut totp_token = None; let mut is_totp_token_missing = false; let mut is_totp_required = false; @@ -68,7 +68,7 @@ impl Principal { secret.strip_prefix("$app$").and_then(|s| s.split_once('$')) { is_app_authenticated = verify_secret_hash(app_secret, code).await?; - } else { + } else if !only_app_pass { is_authenticated = verify_secret_hash(secret, code).await?; } } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 41703101..cd3b6452 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -33,6 +33,8 @@ pub struct Directory { pub cache: Option, } +pub const FALLBACK_ADMIN_ID: u32 = u32::MAX; + #[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] pub struct Principal { pub id: u32, @@ -403,6 +405,12 @@ pub enum QueryBy<'x> { Credentials(&'x Credentials), } +pub struct QueryParams<'x> { + pub by: QueryBy<'x>, + pub return_member_of: bool, + pub only_app_pass: bool, +} + impl Default for Directory { fn default() -> Self { Self { @@ -504,3 +512,47 @@ impl From<&ArchivedType> for Type { } } } + +impl<'x> QueryParams<'x> { + pub fn name(name: &'x str) -> Self { + QueryParams { + by: QueryBy::Name(name), + return_member_of: false, + only_app_pass: false, + } + } + + pub fn credentials(credentials: &'x Credentials) -> Self { + QueryParams { + by: QueryBy::Credentials(credentials), + return_member_of: false, + only_app_pass: false, + } + } + + pub fn id(id: u32) -> Self { + QueryParams { + by: QueryBy::Id(id), + return_member_of: false, + only_app_pass: false, + } + } + + pub fn by(by: QueryBy<'x>) -> Self { + QueryParams { + by, + return_member_of: false, + only_app_pass: false, + } + } + + pub fn with_return_member_of(mut self, return_member_of: bool) -> Self { + self.return_member_of = return_member_of; + self + } + + pub fn with_only_app_pass(mut self, only_app_pass: bool) -> Self { + self.only_app_pass = only_app_pass; + self + } +} diff --git a/crates/email/src/sieve/ingest.rs b/crates/email/src/sieve/ingest.rs index 48c3214e..93c3d7f3 100644 --- a/crates/email/src/sieve/ingest.rs +++ b/crates/email/src/sieve/ingest.rs @@ -16,7 +16,7 @@ use crate::{ use common::{ Server, auth::AccessToken, config::jmap::settings::SpecialUse, scripts::plugins::PluginContext, }; -use directory::{Permission, QueryBy}; +use directory::{Permission, QueryParams}; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::MessageParser; use sieve::{Envelope, Event, Input, Mailbox, Recipient, Sieve}; @@ -109,7 +109,7 @@ impl SieveScriptIngest for Server { .core .storage .directory - .query(QueryBy::Id(account_id), false) + .query(QueryParams::id(account_id).with_return_member_of(false)) .await .caused_by(trc::location!())? .and_then(|p| { diff --git a/crates/http/src/auth/authenticate.rs b/crates/http/src/auth/authenticate.rs index 5b6aff4a..eaae76c8 100644 --- a/crates/http/src/auth/authenticate.rs +++ b/crates/http/src/auth/authenticate.rs @@ -4,16 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::sync::Arc; - +use common::auth::AccessToken; use common::{HttpAuthCache, Server, auth::AuthRequest, listener::limiter::InFlight}; use http_proto::{HttpRequest, HttpSessionData}; use hyper::header; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; - -use common::auth::AccessToken; use std::future::Future; +use std::sync::Arc; +use std::time::{Duration, Instant}; pub trait Authenticator: Sync + Send { fn authenticate_headers( @@ -34,19 +33,20 @@ impl Authenticator for Server { if let Some((mechanism, token)) = req.authorization() { // Check if the credentials are cached if let Some(http_cache) = self.inner.cache.http_auth.get(token) { - let access_token = self.get_access_token(http_cache.account_id).await?; - // Make sure the revision is still valid - if access_token.revision == http_cache.revision { - // Enforce authenticated rate limit - return self - .is_http_authenticated_request_allowed(&access_token) - .await - .map(|in_flight| (in_flight, access_token)); - } else { - // If the revision is not valid, remove the cached credentials - self.inner.cache.http_auth.remove(token); + if http_cache.expires <= Instant::now() { + let access_token = self.get_access_token(http_cache.account_id).await?; + if access_token.revision == http_cache.revision { + // Enforce authenticated rate limit + return self + .is_http_authenticated_request_allowed(&access_token) + .await + .map(|in_flight| (in_flight, access_token)); + } } + + // If the revision is not valid, remove the cached credentials + self.inner.cache.http_auth.remove(token); } let credentials = if mechanism.eq_ignore_ascii_case("basic") { @@ -100,6 +100,8 @@ impl Authenticator for Server { HttpAuthCache { account_id: access_token.primary_id(), revision: access_token.revision, + expires: Instant::now() + + Duration::from_secs(self.core.oauth.oauth_expiry_token), }, ); diff --git a/crates/http/src/auth/oauth/registration.rs b/crates/http/src/auth/oauth/registration.rs index 66077ce2..82f5e3f2 100644 --- a/crates/http/src/auth/oauth/registration.rs +++ b/crates/http/src/auth/oauth/registration.rs @@ -12,7 +12,7 @@ use common::{ }; use directory::{ - Permission, QueryBy, Type, + Permission, QueryParams, Type, backend::internal::{ PrincipalField, PrincipalSet, lookup::DirectoryStore, manage::ManageDirectory, }, @@ -113,7 +113,7 @@ impl ClientRegistrationHandler for Server { // Fetch client registration let found_registration = if let Some(client) = self .store() - .query(QueryBy::Name(client_id), false) + .query(QueryParams::name(client_id).with_return_member_of(false)) .await .caused_by(trc::location!())? .filter(|p| p.typ() == Type::OauthClient) diff --git a/crates/http/src/autoconfig/mod.rs b/crates/http/src/autoconfig/mod.rs index dca4b44a..6ea76b64 100644 --- a/crates/http/src/autoconfig/mod.rs +++ b/crates/http/src/autoconfig/mod.rs @@ -4,19 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::fmt::Write; - use common::{Server, manager::webadmin::Resource}; - -use directory::QueryBy; +use directory::QueryParams; +use http_proto::*; use quick_xml::Reader; use quick_xml::events::Event; +use std::fmt::Write; +use std::future::Future; use trc::AddContext; use utils::url_params::UrlParams; -use http_proto::*; -use std::future::Future; - pub trait Autoconfig: Sync + Send { fn handle_autoconfig_request( &self, @@ -211,7 +208,7 @@ impl Autoconfig for Server { .core .storage .directory - .query(QueryBy::Id(id), false) + .query(QueryParams::id(id).with_return_member_of(false)) .await { if principal diff --git a/crates/http/src/management/principal.rs b/crates/http/src/management/principal.rs index 58e37f80..2524c295 100644 --- a/crates/http/src/management/principal.rs +++ b/crates/http/src/management/principal.rs @@ -6,7 +6,7 @@ use common::{KV_BAYES_MODEL_USER, Server, auth::AccessToken}; use directory::{ - DirectoryInner, Permission, QueryBy, Type, + DirectoryInner, Permission, QueryBy, QueryParams, Type, backend::internal::{ PrincipalAction, PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue, SpecialSecrets, @@ -476,7 +476,7 @@ impl PrincipalManager for Server { let principal = self .store() - .query(QueryBy::Id(account_id), true) + .query(QueryParams::id(account_id).with_return_member_of(true)) .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; @@ -707,7 +707,7 @@ impl PrincipalManager for Server { if access_token.primary_id() != u32::MAX { let principal = self .directory() - .query(QueryBy::Id(access_token.primary_id()), false) + .query(QueryParams::id(access_token.primary_id()).with_return_member_of(false)) .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; @@ -803,7 +803,16 @@ impl PrincipalManager for Server { } // Make sure the current directory supports updates - self.assert_supported_directory(false)?; + if requests.iter().any(|r| { + matches!( + r, + AccountAuthRequest::SetPassword { .. } + | AccountAuthRequest::EnableOtpAuth { .. } + | AccountAuthRequest::DisableOtpAuth { .. } + ) + }) { + self.assert_supported_directory(false)?; + } // Build actions let mut actions = Vec::with_capacity(requests.len()); diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index baaa6947..2e2a687b 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -4,16 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{sync::Arc, time::Instant}; - +use crate::{ + core::{MailboxId, Session, SessionData, State}, + op::ImapContext, + spawn_op, +}; use common::{ auth::AccessToken, listener::SessionStream, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, }; - use compact_str::ToCompactString; use directory::{ - Permission, QueryBy, Type, + Permission, QueryParams, Type, backend::internal::{ PrincipalField, manage::{ChangedPrincipals, ManageDirectory}, @@ -26,18 +28,12 @@ use imap_proto::{ }, receiver::Request, }; - use jmap_proto::types::{acl::Acl, collection::Collection, value::AclGrant}; +use std::{sync::Arc, time::Instant}; use store::write::{AlignedBytes, Archive, BatchBuilder}; use trc::AddContext; use utils::map::bitmap::Bitmap; -use crate::{ - core::{MailboxId, Session, SessionData, State}, - op::ImapContext, - spawn_op, -}; - impl Session { pub async fn handle_get_acl(&mut self, request: Request) -> trc::Result<()> { // Validate access @@ -248,7 +244,10 @@ impl Session { .core .storage .directory - .query(QueryBy::Name(arguments.identifier.as_ref().unwrap()), false) + .query( + QueryParams::name(arguments.identifier.as_ref().unwrap()) + .with_return_member_of(false), + ) .await .imap_ctx(&arguments.tag, trc::location!())? .ok_or_else(|| { diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 3fe07ee2..71bd02a8 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -5,7 +5,7 @@ */ use common::{Server, storage::index::ObjectIndexBuilder}; -use directory::QueryBy; +use directory::QueryParams; use email::identity::{ArchivedEmailAddress, Identity}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, @@ -147,7 +147,7 @@ impl IdentityGet for Server { .core .storage .directory - .query(QueryBy::Id(account_id), false) + .query(QueryParams::id(account_id).with_return_member_of(false)) .await .caused_by(trc::location!())? { diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 03c42b9d..f2e8b84c 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -5,7 +5,7 @@ */ use common::{Server, storage::index::ObjectIndexBuilder}; -use directory::QueryBy; +use directory::QueryParams; use email::identity::{EmailAddress, Identity}; use jmap_proto::{ error::set::SetError, @@ -61,7 +61,7 @@ impl IdentitySet for Server { if !identity.email.is_empty() { if self .directory() - .query(QueryBy::Id(account_id), false) + .query(QueryParams::id(account_id).with_return_member_of(false)) .await? .is_none_or(|p| !p.emails.iter().any(|e| e == &identity.email)) { diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 58ded126..e9d4abd6 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -5,7 +5,7 @@ */ use common::Server; -use directory::QueryBy; +use directory::QueryParams; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, types::{ @@ -65,7 +65,7 @@ impl PrincipalGet for Server { .core .storage .directory - .query(QueryBy::Id(id.document_id()), false) + .query(QueryParams::id(id.document_id()).with_return_member_of(false)) .await? { principal diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index 1434e7ae..dc5cf5ab 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -4,17 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::JmapMethods; use common::Server; -use directory::QueryBy; +use directory::QueryParams; use http_proto::HttpSessionData; use jmap_proto::{ method::query::{Filter, QueryRequest, QueryResponse, RequestArguments}, types::{collection::Collection, state::State}, }; -use store::{query::ResultSet, roaring::RoaringBitmap}; - -use crate::JmapMethods; use std::future::Future; +use store::{query::ResultSet, roaring::RoaringBitmap}; pub trait PrincipalQuery: Sync + Send { fn principal_query( @@ -45,7 +44,7 @@ impl PrincipalQuery for Server { .core .storage .directory - .query(QueryBy::Name(name.as_str()), false) + .query(QueryParams::name(name.as_str()).with_return_member_of(false)) .await? { if is_set || result_set.results.contains(principal.id()) { diff --git a/tests/src/directory/imap.rs b/tests/src/directory/imap.rs index 73e0d83f..7015941a 100644 --- a/tests/src/directory/imap.rs +++ b/tests/src/directory/imap.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use common::listener::limiter::{ConcurrencyLimiter, InFlight}; -use directory::QueryBy; +use directory::QueryParams; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use tokio::{ @@ -61,7 +61,9 @@ async fn imap_directory() { assert_eq!( &LookupResult::from( handle - .query(QueryBy::Credentials(item.as_credentials()), true) + .query( + QueryParams::credentials(item.as_credentials()).with_return_member_of(true) + ) .await .unwrap() .is_some() @@ -81,7 +83,10 @@ async fn imap_directory() { tokio::spawn(async move { LookupResult::from( handle - .query(QueryBy::Credentials(item.as_credentials()), true) + .query( + QueryParams::credentials(item.as_credentials()) + .with_return_member_of(true), + ) .await .unwrap() .is_some(), diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index 69627eb2..70c3ac8f 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -6,7 +6,7 @@ use ahash::AHashSet; use directory::{ - Permission, QueryBy, Type, + Permission, QueryBy, QueryParams, Type, backend::{ RcptType, internal::{ @@ -178,8 +178,8 @@ async fn internal_directory() { assert_eq!( store .query( - QueryBy::Credentials(&Credentials::new("jane".into(), "my_secret".into())), - true + QueryParams::credentials(&Credentials::new("jane".into(), "my_secret".into())) + .with_return_member_of(true) ) .await .unwrap() @@ -197,8 +197,11 @@ async fn internal_directory() { assert_eq!( store .query( - QueryBy::Credentials(&Credentials::new("jane".into(), "wrong_password".into())), - true + QueryParams::credentials(&Credentials::new( + "jane".into(), + "wrong_password".into() + )) + .with_return_member_of(true) ) .await .unwrap(), @@ -275,7 +278,7 @@ async fn internal_directory() { assert_eq!( store - .query(QueryBy::Name("list"), true) + .query(QueryParams::name("list").with_return_member_of(true)) .await .unwrap() .unwrap() @@ -353,7 +356,7 @@ async fn internal_directory() { .is_ok() ); let principal = store - .query(QueryBy::Name("john"), true) + .query(QueryParams::name("john").with_return_member_of(true)) .await .unwrap() .unwrap(); @@ -398,7 +401,7 @@ async fn internal_directory() { .is_ok() ); let principal = store - .query(QueryBy::Name("john"), true) + .query(QueryParams::name("john").with_return_member_of(true)) .await .unwrap() .unwrap(); @@ -448,7 +451,7 @@ async fn internal_directory() { ); let principal = store - .query(QueryBy::Name("john.doe"), true) + .query(QueryParams::name("john.doe").with_return_member_of(true)) .await .unwrap() .unwrap(); @@ -791,7 +794,11 @@ impl TestInternalDirectory for Store { ) -> u32 { let role = if login == "admin" { "admin" } else { "user" }; self.create_test_domains(emails).await; - if let Some(principal) = self.query(QueryBy::Name(login), false).await.unwrap() { + if let Some(principal) = self + .query(QueryParams::name(login).with_return_member_of(false)) + .await + .unwrap() + { self.update_principal(UpdatePrincipal::by_id(principal.id()).with_updates(vec![ PrincipalUpdate::set( PrincipalField::Secrets, @@ -841,7 +848,11 @@ impl TestInternalDirectory for Store { async fn create_test_group(&self, login: &str, name: &str, emails: &[&str]) -> u32 { self.create_test_domains(emails).await; - if let Some(principal) = self.query(QueryBy::Name(login), false).await.unwrap() { + if let Some(principal) = self + .query(QueryParams::name(login).with_return_member_of(false)) + .await + .unwrap() + { principal.id() } else { self.create_principal( @@ -866,7 +877,11 @@ impl TestInternalDirectory for Store { } async fn create_test_list(&self, login: &str, name: &str, members: &[&str]) -> u32 { - if let Some(principal) = self.query(QueryBy::Name(login), false).await.unwrap() { + if let Some(principal) = self + .query(QueryParams::name(login).with_return_member_of(false)) + .await + .unwrap() + { principal.id() } else { self.create_test_domains(&[login]).await; @@ -958,7 +973,7 @@ impl TestInternalDirectory for Store { for domain in domains { let domain = domain.rsplit_once('@').map_or(*domain, |(_, d)| d); if self - .query(QueryBy::Name(domain), false) + .query(QueryParams::name(domain).with_return_member_of(false)) .await .unwrap() .is_none() diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 95df41e5..2cc0fb1b 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -7,7 +7,7 @@ use std::fmt::Debug; use directory::{ - QueryBy, ROLE_USER, Type, + QueryParams, ROLE_USER, Type, backend::{RcptType, internal::manage::ManageDirectory}, }; use mail_send::Credentials; @@ -51,11 +51,11 @@ async fn ldap_directory() { assert_eq!( handle .query( - QueryBy::Credentials(&Credentials::Plain { + QueryParams::credentials(&Credentials::Plain { username: "john".into(), secret: "12345".into() - }), - true + }) + .with_return_member_of(true) ) .await .unwrap() @@ -82,11 +82,11 @@ async fn ldap_directory() { assert_eq!( handle .query( - QueryBy::Credentials(&Credentials::Plain { + QueryParams::credentials(&Credentials::Plain { username: "bill".into(), secret: "password".into() - }), - true + }) + .with_return_member_of(true) ) .await .unwrap() @@ -111,11 +111,11 @@ async fn ldap_directory() { assert!( handle .query( - QueryBy::Credentials(&Credentials::Plain { + QueryParams::credentials(&Credentials::Plain { username: "bill".into(), secret: "invalid".into() - }), - true + }) + .with_return_member_of(true) ) .await .unwrap() @@ -126,7 +126,7 @@ async fn ldap_directory() { // Get user by name assert_eq!( handle - .query(QueryBy::Name("jane"), true) + .query(QueryParams::name("jane").with_return_member_of(true)) .await .unwrap() .unwrap() @@ -153,7 +153,7 @@ async fn ldap_directory() { // Get group by name assert_eq!( handle - .query(QueryBy::Name("sales"), true) + .query(QueryParams::name("sales").with_return_member_of(true)) .await .unwrap() .unwrap() diff --git a/tests/src/directory/oidc.rs b/tests/src/directory/oidc.rs index e6bcbbb6..7221d89d 100644 --- a/tests/src/directory/oidc.rs +++ b/tests/src/directory/oidc.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use base64::{Engine, engine::general_purpose}; -use directory::QueryBy; +use directory::QueryParams; use http_proto::{JsonProblemResponse, JsonResponse, ToHttpResponse}; use hyper::{Method, StatusCode}; use mail_send::Credentials; @@ -106,10 +106,10 @@ async fn oidc_directory() { // Test an invalid token let err = directory .query( - QueryBy::Credentials(&Credentials::OAuthBearer { + QueryParams::credentials(&Credentials::OAuthBearer { token: "invalid_or_expired_token".to_string(), - }), - false, + }) + .with_return_member_of(false), ) .await .unwrap_err(); @@ -122,10 +122,10 @@ async fn oidc_directory() { // Test a valid token let principal = directory .query( - QueryBy::Credentials(&Credentials::OAuthBearer { + QueryParams::credentials(&Credentials::OAuthBearer { token: TEST_TOKEN.to_string(), - }), - false, + }) + .with_return_member_of(false), ) .await .unwrap() diff --git a/tests/src/directory/smtp.rs b/tests/src/directory/smtp.rs index ce455959..15bf34a9 100644 --- a/tests/src/directory/smtp.rs +++ b/tests/src/directory/smtp.rs @@ -7,7 +7,7 @@ use super::dummy_tls_acceptor; use crate::directory::{DirectoryTest, Item, LookupResult}; use common::listener::limiter::{ConcurrencyLimiter, InFlight}; -use directory::{QueryBy, backend::RcptType}; +use directory::{QueryParams, backend::RcptType}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use std::sync::Arc; @@ -76,7 +76,7 @@ async fn lmtp_directory() { (core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into() } Item::Authenticate(v) => handle - .query(QueryBy::Credentials(v), true) + .query(QueryParams::credentials(v).with_return_member_of(true)) .await .unwrap() .is_some() @@ -122,7 +122,7 @@ async fn lmtp_directory() { (core.rcpt(&handle, v, 0).await.unwrap() == RcptType::Mailbox).into() } Item::Authenticate(v) => handle - .query(QueryBy::Credentials(v), true) + .query(QueryParams::credentials(v).with_return_member_of(true)) .await .unwrap() .is_some() diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index 330d184e..b1ef824d 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -5,7 +5,7 @@ */ use directory::{ - QueryBy, ROLE_USER, Type, + QueryParams, ROLE_USER, Type, backend::{RcptType, internal::manage::ManageDirectory}, }; use mail_send::Credentials; @@ -113,11 +113,11 @@ async fn sql_directory() { assert_eq!( handle .query( - QueryBy::Credentials(&Credentials::Plain { + QueryParams::credentials(&Credentials::Plain { username: "john".into(), secret: "12345".into() - }), - true + }) + .with_return_member_of(true) ) .await .unwrap() @@ -146,11 +146,11 @@ async fn sql_directory() { assert_eq!( handle .query( - QueryBy::Credentials(&Credentials::Plain { + QueryParams::credentials(&Credentials::Plain { username: "bill".into(), secret: "password".into() - }), - true + }) + .with_return_member_of(true) ) .await .unwrap() @@ -173,11 +173,11 @@ async fn sql_directory() { assert_eq!( handle .query( - QueryBy::Credentials(&Credentials::Plain { + QueryParams::credentials(&Credentials::Plain { username: "admin".into(), secret: "very_secret".into() - }), - true + }) + .with_return_member_of(true) ) .await .unwrap() @@ -196,11 +196,11 @@ async fn sql_directory() { assert!( handle .query( - QueryBy::Credentials(&Credentials::Plain { + QueryParams::credentials(&Credentials::Plain { username: "bill".into(), secret: "invalid".into() - }), - true + }) + .with_return_member_of(true) ) .await .unwrap() @@ -210,7 +210,7 @@ async fn sql_directory() { // Get user by name assert_eq!( handle - .query(QueryBy::Name("jane"), true) + .query(QueryParams::name("jane").with_return_member_of(true)) .await .unwrap() .unwrap() @@ -235,7 +235,7 @@ async fn sql_directory() { // Get group by name assert_eq!( handle - .query(QueryBy::Name("sales"), true) + .query(QueryParams::name("sales").with_return_member_of(true)) .await .unwrap() .unwrap() diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index edcfb602..49d1822a 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -12,7 +12,7 @@ use common::{ }; use directory::{ - QueryBy, Type, + QueryParams, Type, backend::internal::{PrincipalField, PrincipalSet, PrincipalValue, manage::ManageDirectory}, }; use mail_auth::MX; @@ -279,7 +279,7 @@ async fn lookup_sql() { .core .storage .directory - .query(QueryBy::Name("john@foobar.org"), true) + .query(QueryParams::name("john@foobar.org").with_return_member_of(true)) .await .unwrap() .unwrap(); @@ -288,7 +288,7 @@ async fn lookup_sql() { .core .storage .directory - .query(QueryBy::Name("jane@foobar.org"), true) + .query(QueryParams::name("jane@foobar.org").with_return_member_of(true)) .await .unwrap() .unwrap();