diff options
100 files changed, 1780 insertions, 1193 deletions
diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs new file mode 100644 index 00000000..be143005 --- /dev/null +++ b/crates/common/src/auth/access_token.rs @@ -0,0 +1,433 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use directory::{backend::internal::PrincipalField, Permission, Principal, QueryBy}; +use jmap_proto::{ + request::RequestMethod, + types::{acl::Acl, collection::Collection, id::Id}, +}; +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + sync::Arc, + time::Instant, +}; +use store::query::acl::AclQuery; +use trc::AddContext; +use utils::map::{ + bitmap::{Bitmap, BitmapItem}, + ttl_dashmap::TtlMap, + vec_map::VecMap, +}; + +use crate::Core; + +use super::{roles::RolePermissions, AccessToken}; + +impl Core { + pub async fn build_access_token(&self, mut principal: Principal) -> trc::Result<AccessToken> { + let mut role_permissions = RolePermissions::default(); + + // Apply role permissions + for role_id in principal.iter_int(PrincipalField::Roles) { + role_permissions.union(self.get_role_permissions(role_id as u32).await?.as_ref()); + } + + // Add principal permissions + for (permissions, field) in [ + ( + &mut role_permissions.enabled, + PrincipalField::EnabledPermissions, + ), + ( + &mut role_permissions.disabled, + PrincipalField::DisabledPermissions, + ), + ] { + for permission in principal.iter_int(field) { + let permission = permission as usize; + if permission < Permission::COUNT { + permissions.set(permission); + } + } + } + + // Apply principal permissions + let mut permissions = role_permissions.finalize(); + + // Limit tenant permissions + let mut tenant_id = None; + #[cfg(feature = "enterprise")] + if self.is_enterprise_edition() { + tenant_id = principal.get_int(PrincipalField::Tenant).map(|v| v as u32); + if let Some(tenant_id) = tenant_id { + permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled); + } + } + + Ok(AccessToken { + primary_id: principal.id(), + member_of: principal + .iter_int(PrincipalField::MemberOf) + .map(|v| v as u32) + .collect(), + access_to: VecMap::new(), + tenant_id, + name: principal.take_str(PrincipalField::Name).unwrap_or_default(), + description: principal.take_str(PrincipalField::Description), + quota: principal.quota(), + permissions, + }) + } + + pub async fn get_access_token(&self, account_id: u32) -> trc::Result<AccessToken> { + let err = match self + .storage + .directory + .query(QueryBy::Id(account_id), true) + .await + { + Ok(Some(principal)) => { + return self + .update_access_token(self.build_access_token(principal).await?) + .await + } + Ok(None) => Err(trc::AuthEvent::Error + .into_err() + .details("Account not found.") + .caused_by(trc::location!())), + Err(err) => Err(err), + }; + + match &self.jmap.fallback_admin { + Some((_, secret)) if account_id == u32::MAX => { + self.update_access_token( + self.build_access_token(Principal::fallback_admin(secret)) + .await?, + ) + .await + } + _ => err, + } + } + + pub async fn update_access_token( + &self, + mut access_token: AccessToken, + ) -> trc::Result<AccessToken> { + for grant_account_id in [access_token.primary_id] + .into_iter() + .chain(access_token.member_of.iter().copied()) + { + for acl_item in self + .storage + .data + .acl_query(AclQuery::HasAccess { grant_account_id }) + .await + .caused_by(trc::location!())? + { + if !access_token.is_member(acl_item.to_account_id) { + let acl = Bitmap::<Acl>::from(acl_item.permissions); + let collection = Collection::from(acl_item.to_collection); + if !collection.is_valid() { + return Err(trc::StoreEvent::DataCorruption + .ctx(trc::Key::Reason, "Corrupted collection found in ACL key.") + .details(format!("{acl_item:?}")) + .account_id(grant_account_id) + .caused_by(trc::location!())); + } + + let mut collections: Bitmap<Collection> = Bitmap::new(); + if acl.contains(Acl::Read) || acl.contains(Acl::Administer) { + collections.insert(collection); + } + if collection == Collection::Mailbox + && (acl.contains(Acl::ReadItems) || acl.contains(Acl::Administer)) + { + collections.insert(Collection::Email); + } + + if !collections.is_empty() { + access_token + .access_to + .get_mut_or_insert_with(acl_item.to_account_id, Bitmap::new) + .union(&collections); + } + } + } + } + + Ok(access_token) + } + + pub fn cache_access_token(&self, access_token: Arc<AccessToken>) { + self.security.access_tokens.insert_with_ttl( + access_token.primary_id(), + access_token, + Instant::now() + self.jmap.session_cache_ttl, + ); + } + + pub async fn get_cached_access_token(&self, primary_id: u32) -> trc::Result<Arc<AccessToken>> { + if let Some(access_token) = self.security.access_tokens.get_with_ttl(&primary_id) { + Ok(access_token) + } else { + // Refresh ACL token + self.get_access_token(primary_id).await.map(|access_token| { + let access_token = Arc::new(access_token); + self.cache_access_token(access_token.clone()); + access_token + }) + } + } +} + +impl AccessToken { + pub fn from_id(primary_id: u32) -> Self { + Self { + primary_id, + ..Default::default() + } + } + + pub fn with_access_to(self, access_to: VecMap<u32, Bitmap<Collection>>) -> Self { + Self { access_to, ..self } + } + + pub fn with_permission(mut self, permission: Permission) -> Self { + self.permissions.set(permission.id()); + self + } + + pub fn state(&self) -> u32 { + // Hash state + let mut s = DefaultHasher::new(); + self.member_of.hash(&mut s); + self.access_to.hash(&mut s); + s.finish() as u32 + } + + pub fn primary_id(&self) -> u32 { + self.primary_id + } + + pub fn secondary_ids(&self) -> impl Iterator<Item = &u32> { + self.member_of + .iter() + .chain(self.access_to.iter().map(|(id, _)| id)) + } + + pub fn is_member(&self, account_id: u32) -> bool { + self.primary_id == account_id + || self.member_of.contains(&account_id) + || self.has_permission(Permission::Impersonate) + } + + pub fn is_primary_id(&self, account_id: u32) -> bool { + self.primary_id == account_id + } + + #[inline(always)] + pub fn has_permission(&self, permission: Permission) -> bool { + self.permissions.get(permission.id()) + } + + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + if self.has_permission(permission) { + Ok(()) + } else { + Err(trc::SecurityEvent::Unauthorized + .into_err() + .details(permission.name())) + } + } + + pub fn permissions(&self) -> Vec<Permission> { + let mut permissions = Vec::new(); + for (block_num, bytes) in self.permissions.inner().iter().enumerate() { + let mut bytes = *bytes; + + while bytes != 0 { + let item = std::mem::size_of::<usize>() - 1 - bytes.leading_zeros() as usize; + bytes ^= 1 << item; + permissions.push( + Permission::from_id((block_num * std::mem::size_of::<usize>()) + item).unwrap(), + ); + } + } + permissions + } + + pub fn is_shared(&self, account_id: u32) -> bool { + !self.is_member(account_id) && self.access_to.iter().any(|(id, _)| *id == account_id) + } + + pub fn shared_accounts(&self, collection: impl Into<Collection>) -> impl Iterator<Item = &u32> { + let collection = collection.into(); + self.member_of + .iter() + .chain(self.access_to.iter().filter_map(move |(id, cols)| { + if cols.contains(collection) { + id.into() + } else { + None + } + })) + } + + pub fn has_access(&self, to_account_id: u32, to_collection: impl Into<Collection>) -> bool { + let to_collection = to_collection.into(); + self.is_member(to_account_id) + || self.access_to.iter().any(|(id, collections)| { + *id == to_account_id && collections.contains(to_collection) + }) + } + + pub fn assert_has_access( + &self, + to_account_id: Id, + to_collection: Collection, + ) -> trc::Result<&Self> { + if self.has_access(to_account_id.document_id(), to_collection) { + Ok(self) + } else { + Err(trc::JmapEvent::Forbidden.into_err().details(format!( + "You do not have access to account {}", + to_account_id + ))) + } + } + + pub fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self> { + if self.is_member(account_id.document_id()) { + Ok(self) + } else { + Err(trc::JmapEvent::Forbidden + .into_err() + .details(format!("You are not an owner of account {}", account_id))) + } + } + + pub fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()> { + let permission = match request { + RequestMethod::Get(m) => match &m.arguments { + jmap_proto::method::get::RequestArguments::Email(_) => Permission::JmapEmailGet, + jmap_proto::method::get::RequestArguments::Mailbox => Permission::JmapMailboxGet, + jmap_proto::method::get::RequestArguments::Thread => Permission::JmapThreadGet, + jmap_proto::method::get::RequestArguments::Identity => Permission::JmapIdentityGet, + jmap_proto::method::get::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionGet + } + jmap_proto::method::get::RequestArguments::PushSubscription => { + Permission::JmapPushSubscriptionGet + } + jmap_proto::method::get::RequestArguments::SieveScript => { + Permission::JmapSieveScriptGet + } + jmap_proto::method::get::RequestArguments::VacationResponse => { + Permission::JmapVacationResponseGet + } + jmap_proto::method::get::RequestArguments::Principal => { + Permission::JmapPrincipalGet + } + jmap_proto::method::get::RequestArguments::Quota => Permission::JmapQuotaGet, + jmap_proto::method::get::RequestArguments::Blob(_) => Permission::JmapBlobGet, + }, + RequestMethod::Set(m) => match &m.arguments { + jmap_proto::method::set::RequestArguments::Email => Permission::JmapEmailSet, + jmap_proto::method::set::RequestArguments::Mailbox(_) => Permission::JmapMailboxSet, + jmap_proto::method::set::RequestArguments::Identity => Permission::JmapIdentitySet, + jmap_proto::method::set::RequestArguments::EmailSubmission(_) => { + Permission::JmapEmailSubmissionSet + } + jmap_proto::method::set::RequestArguments::PushSubscription => { + Permission::JmapPushSubscriptionSet + } + jmap_proto::method::set::RequestArguments::SieveScript(_) => { + Permission::JmapSieveScriptSet + } + jmap_proto::method::set::RequestArguments::VacationResponse => { + Permission::JmapVacationResponseSet + } + }, + RequestMethod::Changes(m) => match m.arguments { + jmap_proto::method::changes::RequestArguments::Email => { + Permission::JmapEmailChanges + } + jmap_proto::method::changes::RequestArguments::Mailbox => { + Permission::JmapMailboxChanges + } + jmap_proto::method::changes::RequestArguments::Thread => { + Permission::JmapThreadChanges + } + jmap_proto::method::changes::RequestArguments::Identity => { + Permission::JmapIdentityChanges + } + jmap_proto::method::changes::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionChanges + } + jmap_proto::method::changes::RequestArguments::Quota => { + Permission::JmapQuotaChanges + } + }, + RequestMethod::Copy(m) => match m.arguments { + jmap_proto::method::copy::RequestArguments::Email => Permission::JmapEmailCopy, + }, + RequestMethod::CopyBlob(_) => Permission::JmapBlobCopy, + RequestMethod::ImportEmail(_) => Permission::JmapEmailImport, + RequestMethod::ParseEmail(_) => Permission::JmapEmailParse, + RequestMethod::QueryChanges(m) => match m.arguments { + jmap_proto::method::query::RequestArguments::Email(_) => { + Permission::JmapEmailQueryChanges + } + jmap_proto::method::query::RequestArguments::Mailbox(_) => { + Permission::JmapMailboxQueryChanges + } + jmap_proto::method::query::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionQueryChanges + } + jmap_proto::method::query::RequestArguments::SieveScript => { + Permission::JmapSieveScriptQueryChanges + } + jmap_proto::method::query::RequestArguments::Principal => { + Permission::JmapPrincipalQueryChanges + } + jmap_proto::method::query::RequestArguments::Quota => { + Permission::JmapQuotaQueryChanges + } + }, + RequestMethod::Query(m) => match m.arguments { + jmap_proto::method::query::RequestArguments::Email(_) => Permission::JmapEmailQuery, + jmap_proto::method::query::RequestArguments::Mailbox(_) => { + Permission::JmapMailboxQuery + } + jmap_proto::method::query::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionQuery + } + jmap_proto::method::query::RequestArguments::SieveScript => { + Permission::JmapSieveScriptQuery + } + jmap_proto::method::query::RequestArguments::Principal => { + Permission::JmapPrincipalQuery + } + jmap_proto::method::query::RequestArguments::Quota => Permission::JmapQuotaQuery, + }, + RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, + RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, + RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup, + RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload, + RequestMethod::Echo(_) => Permission::JmapEcho, + RequestMethod::Error(_) => return Ok(()), + }; + + if self.has_permission(permission) { + Ok(()) + } else { + Err(trc::JmapEvent::Forbidden + .into_err() + .details("You are not authorized to perform this action")) + } + } +} diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs new file mode 100644 index 00000000..af98eb11 --- /dev/null +++ b/crates/common/src/auth/mod.rs @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use directory::Permissions; +use jmap_proto::types::collection::Collection; +use utils::map::{bitmap::Bitmap, vec_map::VecMap}; + +pub mod access_token; +pub mod roles; + +#[derive(Debug, Clone, Default)] +pub struct AccessToken { + pub primary_id: u32, + pub tenant_id: Option<u32>, + pub member_of: Vec<u32>, + pub access_to: VecMap<u32, Bitmap<Collection>>, + pub name: String, + pub description: Option<String>, + pub quota: u64, + pub permissions: Permissions, +} diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs new file mode 100644 index 00000000..4b2e5a5f --- /dev/null +++ b/crates/common/src/auth/roles.rs @@ -0,0 +1,192 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::sync::Arc; + +use ahash::AHashSet; +use directory::{ + backend::internal::{lookup::DirectoryStore, PrincipalField}, + Permission, Permissions, QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, +}; +use trc::AddContext; +use utils::map::ttl_dashmap::TtlMap; + +use crate::Core; + +#[derive(Debug, Clone, Default)] +pub struct RolePermissions { + pub enabled: Permissions, + pub disabled: Permissions, +} + +const USER_PERMISSIONS: RolePermissions = user_permissions(); +const ADMIN_PERMISSIONS: RolePermissions = admin_permissions(); +const TENANT_ADMIN_PERMISSIONS: RolePermissions = tenant_admin_permissions(); + +impl Core { + pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> { + let todo = "create default permissions"; + + match role_id { + ROLE_USER => Ok(Arc::new(USER_PERMISSIONS.clone())), + ROLE_ADMIN => Ok(Arc::new(ADMIN_PERMISSIONS.clone())), + ROLE_TENANT_ADMIN => Ok(Arc::new(TENANT_ADMIN_PERMISSIONS.clone())), + role_id => { + if let Some(role_permissions) = self.security.permissions.get(&role_id) { + Ok(role_permissions.clone()) + } else { + self.build_role_permissions(role_id).await + } + } + } + } + + async fn build_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> { + let mut role_ids = vec![role_id as u64].into_iter(); + let mut role_ids_stack = vec![]; + let mut fetched_role_ids = AHashSet::new(); + let mut return_permissions = RolePermissions::default(); + + 'outer: loop { + if let Some(role_id) = role_ids.next() { + let role_id = role_id as u32; + + // Skip if already fetched + if !fetched_role_ids.insert(role_id) { + continue; + } + + match role_id { + ROLE_USER => { + return_permissions.enabled.union(&USER_PERMISSIONS.enabled); + return_permissions + .disabled + .union(&USER_PERMISSIONS.disabled); + } + ROLE_ADMIN => { + return_permissions.enabled.union(&ADMIN_PERMISSIONS.enabled); + return_permissions + .disabled + .union(&ADMIN_PERMISSIONS.disabled); + break 'outer; + } + ROLE_TENANT_ADMIN => { + return_permissions + .enabled + .union(&TENANT_ADMIN_PERMISSIONS.enabled); + return_permissions + .disabled + .union(&TENANT_ADMIN_PERMISSIONS.disabled); + } + role_id => { + // Try with the cache + if let Some(role_permissions) = self.security.permissions.get(&role_id) { + return_permissions.union(role_permissions.as_ref()); + } else { + let mut role_permissions = RolePermissions::default(); + + // Obtain principal + let mut principal = self + .storage + .data + .query(QueryBy::Id(role_id), true) + .await + .caused_by(trc::location!())? + .ok_or_else(|| { + trc::SecurityEvent::Unauthorized + .into_err() + .details( + "Principal not found while building role permissions", + ) + .ctx(trc::Key::Id, role_id) + })?; + + // Add permissions + for (permissions, field) in [ + ( + &mut role_permissions.enabled, + PrincipalField::EnabledPermissions, + ), + ( + &mut role_permissions.disabled, + PrincipalField::DisabledPermissions, + ), + ] { + for permission in principal.iter_int(field) { + let permission = permission as usize; + if permission < Permission::COUNT { + permissions.set(permission); + } + } + } + + // Add permissions + return_permissions.union(&role_permissions); + + // Add parent roles + if let Some(parent_role_ids) = principal + .take_int_array(PrincipalField::Roles) + .filter(|r| !r.is_empty()) + { + role_ids_stack.push(role_ids); + role_ids = parent_role_ids.into_iter(); + } else { + // Cache role + self.security + .permissions + .insert(role_id, Arc::new(role_permissions)); + } + } + } + } + } else if let Some(prev_role_ids) = role_ids_stack.pop() { + role_ids = prev_role_ids; + } else { + break; + } + } + + // Cache role + let return_permissions = Arc::new(return_permissions); + self.security + .permissions + .insert(role_id, return_permissions.clone()); + Ok(return_permissions) + } +} + +impl RolePermissions { + pub fn union(&mut self, other: &RolePermissions) { + self.enabled.union(&other.enabled); + self.disabled.union(&other.disabled); + } + + pub fn finalize(mut self) -> Permissions { + self.enabled.difference(&self.disabled); + self.enabled + } +} + +const fn admin_permissions() -> RolePermissions { + RolePermissions { + enabled: Permissions::all(), + disabled: Permissions::new(), + } +} + +const fn tenant_admin_permissions() -> RolePermissions { + RolePermissions { + enabled: Permissions::all(), + disabled: Permissions::new(), + } +} + +const fn user_permissions() -> RolePermissions { + RolePermissions { + enabled: Permissions::new(), + disabled: Permissions::all(), + } +} diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 75dbd200..6062b77a 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -10,9 +10,14 @@ use arc_swap::ArcSwap; use directory::{Directories, Directory}; use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores}; use telemetry::Metrics; -use utils::config::Config; +use utils::{ + config::Config, + map::ttl_dashmap::{ADashMap, TtlDashMap, TtlMap}, +}; -use crate::{expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network}; +use crate::{ + expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network, Security, +}; use self::{ imap::ImapConfig, jmap::settings::JmapConfig, scripts::Scripting, smtp::SmtpConfig, @@ -162,6 +167,15 @@ impl Core { imap: ImapConfig::parse(config), tls: TlsManager::parse(config), metrics: Metrics::parse(config), + security: Security { + access_tokens: TtlDashMap::with_capacity(32, 100), + permissions: ADashMap::with_capacity_and_hasher_and_shard_amount( + 32, + ahash::RandomState::new(), + 100, + ), + permissions_version: Default::default(), + }, storage: Storage { data, blob, diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index 6374156a..e285c540 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -10,6 +10,7 @@ use std::time::Duration; +use directory::Type; use store::{Store, Stores}; use trc::{EventType, MetricType, TOTAL_EVENT_COUNT}; use utils::config::{ @@ -20,7 +21,7 @@ use utils::config::{ use crate::{ expr::{tokenizer::TokenMap, Expression}, - total_accounts, + total_principals, }; use super::{ @@ -42,7 +43,7 @@ impl Enterprise { } }; - match total_accounts(data).await { + match total_principals(data, Type::Individual).await { Ok(total) if total > license.accounts as u64 => { config.new_build_warning( "enterprise.license-key", diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 7ff3b897..af48b8f0 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,9 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, net::IpAddr, sync::Arc}; +use std::{ + borrow::Cow, + net::IpAddr, + sync::{atomic::AtomicU8, Arc}, +}; use arc_swap::ArcSwap; +use auth::{roles::RolePermissions, AccessToken}; use config::{ imap::ImapConfig, jmap::settings::JmapConfig, @@ -19,7 +24,10 @@ use config::{ storage::Storage, telemetry::Metrics, }; -use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy}; +use directory::{ + backend::internal::PrincipalInfo, core::secret::verify_secret_hash, Directory, Principal, + QueryBy, Type, +}; use expr::if_block::IfBlock; use listener::{ blocked::{AllowedIps, BlockedIps}, @@ -30,13 +38,17 @@ use mail_send::Credentials; use sieve::Sieve; use store::{ write::{DirectoryClass, QueueClass, ValueClass}, - IterateParams, LookupStore, ValueKey, + Deserialize, IterateParams, LookupStore, ValueKey, }; use tokio::sync::{mpsc, oneshot}; use trc::AddContext; -use utils::BlobHash; +use utils::{ + map::ttl_dashmap::{ADashMap, TtlDashMap}, + BlobHash, +}; pub mod addresses; +pub mod auth; pub mod config; #[cfg(feature = "enterprise")] pub mod enterprise; @@ -63,10 +75,19 @@ pub struct Core { pub jmap: JmapConfig, pub imap: ImapConfig, pub metrics: Metrics, + pub security: Security, #[cfg(feature = "enterprise")] pub enterprise: Option<enterprise::Enterprise>, } +//TODO: temporary hack until OIDC is implemented +#[derive(Default)] +pub struct Security { + pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>, + pub permissions: ADashMap<u32, Arc<RolePermissions>>, + pub permissions_version: AtomicU8, +} + #[derive(Clone)] pub struct Network { pub node_id: u64, @@ -341,35 +362,15 @@ impl Core { } pub async fn total_accounts(&self) -> trc::Result<u64> { - total_accounts(&self.storage.data).await + total_principals(&self.storage.data, Type::Individual).await } pub async fn total_domains(&self) -> trc::Result<u64> { - let mut total = 0; - self.storage - .data - .iterate( - IterateParams::new( - ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![]))), - ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![ - u8::MAX; - 10 - ]))), - ) - .no_values() - .ascending(), - |_, _| { - total += 1; - Ok(true) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| total) + total_principals(&self.storage.data, Type::Domain).await } } -pub(crate) async fn total_accounts(store: &store::Store) -> trc::Result<u64> { +pub(crate) async fn total_principals(store: &store::Store, typ: Type) -> trc::Result<u64> { let mut total = 0; store .iterate( @@ -382,9 +383,14 @@ pub(crate) async fn total_accounts(store: &store::Store) -> trc::Result<u64> { ) .ascending(), |_, value| { - if matches!(value.last(), Some(0u8 | 4u8)) { + if PrincipalInfo::deserialize(value) + .caused_by(trc::location!())? + .typ + == typ + { total += 1; } + Ok(true) }, ) @@ -406,3 +412,16 @@ impl CredentialsUsername for Credentials<String> { } } } + +impl Clone for Security { + fn clone(&self) -> Self { + Self { + access_tokens: self.access_tokens.clone(), + permissions: self.permissions.clone(), + permissions_version: AtomicU8::new( + self.permissions_version + .load(std::sync::atomic::Ordering::Relaxed), + ), + } + } +} diff --git a/crates/common/src/manager/restore.rs b/crates/common/src/manager/restore.rs index 0f5cf44c..236a7a2c 100644 --- a/crates/common/src/manager/restore.rs +++ b/crates/common/src/manager/restore.rs @@ -195,11 +195,11 @@ async fn restore_file(store: Store, blob_store: BlobStore, path: &Path) { .deserialize_leb128::<u32>() .expect("Failed to deserialize principal id"), )), - 3 => DirectoryClass::Domain( + /*3 => DirectoryClass::Domain( key.get(1..) .expect("Failed to read directory string") .to_vec(), - ), + ),*/ 4 => { batch.add( ValueClass::Directory(DirectoryClass::UsedQuota( diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index 3d86b831..7732ca5d 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -12,7 +12,7 @@ use store::{ use crate::{Principal, QueryBy, Type}; -use super::{manage::ManageDirectory, PrincipalField, PrincipalIdType}; +use super::{manage::ManageDirectory, PrincipalField, PrincipalInfo}; #[allow(async_fn_in_trait)] pub trait DirectoryStore: Sync + Send { @@ -36,66 +36,63 @@ impl DirectoryStore for Store { return_member_of: bool, ) -> trc::Result<Option<Principal>> { let (account_id, secret) = match by { - QueryBy::Name(name) => (self.get_account_id(name).await?, None), + QueryBy::Name(name) => (self.get_principal_id(name).await?, None), QueryBy::Id(account_id) => (account_id.into(), None), QueryBy::Credentials(credentials) => match credentials { - Credentials::Plain { username, secret } => { - (self.get_account_id(username).await?, secret.as_str().into()) - } + Credentials::Plain { username, secret } => ( + self.get_principal_id(username).await?, + secret.as_str().into(), + ), Credentials::OAuthBearer { token } => { - (self.get_account_id(token).await?, token.as_str().into()) - } - Credentials::XOauth2 { username, secret } => { - (self.get_account_id(username).await?, secret.as_str().into()) + (self.get_principal_id(token).await?, token.as_str().into()) } + Credentials::XOauth2 { username, secret } => ( + self.get_principal_id(username).await?, + secret.as_str().into(), + ), }, }; if let Some(account_id) = account_id { - match ( - self.get_value::<Principal>(ValueKey::from(ValueClass::Directory( + if let Some(mut principal) = self + .get_value::<Principal>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(account_id), ))) - .await?, - secret, - ) { - (Some(mut principal), Some(secret)) if principal.verify_secret(secret).await? => { - if return_member_of { - principal.set( - PrincipalField::MemberOf, - self.get_member_of(principal.id).await?, - ); + .await? + { + if let Some(secret) = secret { + if principal.verify_secret(secret).await? { + return Ok(None); } - Ok(Some(principal)) } - (Some(mut principal), None) => { - if return_member_of { - principal.set( - PrincipalField::MemberOf, - self.get_member_of(principal.id).await?, - ); - } - Ok(Some(principal)) + if return_member_of { + for member in self.get_member_of(principal.id).await? { + let field = match member.typ { + Type::List => PrincipalField::Lists, + Type::Role => PrincipalField::Roles, + _ => PrincipalField::MemberOf, + }; + principal.append_int(field, member.principal_id); + } } - _ => Ok(None), + return Ok(Some(principal)); } - } else { - Ok(None) } + Ok(None) } async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>> { if let Some(ptype) = self - .get_value::<PrincipalIdType>(ValueKey::from(ValueClass::Directory( + .get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory( DirectoryClass::EmailToId(email.as_bytes().to_vec()), ))) .await? { if ptype.typ != Type::List { - Ok(vec![ptype.account_id]) + Ok(vec![ptype.id]) } else { - self.get_members(ptype.account_id).await + self.get_members(ptype.id).await } } else { Ok(Vec::new()) @@ -103,11 +100,11 @@ impl DirectoryStore for Store { } async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> { - self.get_value::<()>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Domain(domain.as_bytes().to_vec()), + self.get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory( + DirectoryClass::NameToId(domain.as_bytes().to_vec()), ))) .await - .map(|ids| ids.is_some()) + .map(|p| p.map_or(false, |p| p.typ == Type::Domain)) } async fn rcpt(&self, address: &str) -> trc::Result<bool> { diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 0838b391..8502e276 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -14,83 +14,82 @@ use store::{ }; use trc::AddContext; -use crate::{Principal, QueryBy, Type}; +use crate::{Permission, Principal, QueryBy, Type, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER}; use super::{ - lookup::DirectoryStore, PrincipalAction, PrincipalField, PrincipalIdType, PrincipalUpdate, + lookup::DirectoryStore, PrincipalAction, PrincipalField, PrincipalInfo, PrincipalUpdate, PrincipalValue, SpecialSecrets, }; +pub struct MemberOf { + pub principal_id: u32, + pub typ: Type, +} + #[allow(async_fn_in_trait)] pub trait ManageDirectory: Sized { - async fn get_account_id(&self, name: &str) -> trc::Result<Option<u32>>; - async fn get_or_create_account_id(&self, name: &str) -> trc::Result<u32>; - async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>>; - async fn get_member_of(&self, account_id: u32) -> trc::Result<Vec<u32>>; - async fn get_members(&self, account_id: u32) -> trc::Result<Vec<u32>>; - async fn create_account(&self, principal: Principal) -> trc::Result<u32>; - async fn update_account( + async fn get_principal_id(&self, name: &str) -> trc::Result<Option<u32>>; + async fn get_principal_info(&self, name: &str) -> trc::Result<Option<PrincipalInfo>>; + async fn get_or_create_principal_id(&self, name: &str, typ: Type) -> trc::Result<u32>; + async fn get_principal_name(&self, principal_id: u32) -> trc::Result<Option<String>>; + async fn get_member_of(&self, principal_id: u32) -> trc::Result<Vec<MemberOf>>; + async fn get_members(&self, principal_id: u32) -> trc::Result<Vec<u32>>; + async fn create_principal( + &self, + principal: Principal, + tenant_id: Option<u32>, + ) -> trc::Result<u32>; + async fn update_principal( &self, by: QueryBy<'_>, changes: Vec<PrincipalUpdate>, ) -> trc::Result<()>; - async fn delete_account(&self, by: QueryBy<'_>) -> trc::Result<()>; - async fn list_accounts( + async fn delete_principal(&self, by: QueryBy<'_>) -> trc::Result<()>; + async fn list_principals( &self, filter: Option<&str>, typ: Option<Type>, + tenant_id: Option<u32>, ) -> trc::Result<Vec<String>>; - async fn map_group_ids(&self, principal: Principal) -> trc::Result<Principal>; - async fn map_principal( - &self, - principal: Principal, - create_if_missing: bool, - ) -> trc::Result<Principal>; - async fn map_group_names( - &self, - members: Vec<String>, - create_if_missing: bool, - ) -> trc::Result<Vec<u32>>; - async fn create_domain(&self, domain: &str) -> trc::Result<()>; - async fn delete_domain(&self, domain: &str) -> trc::Result<()>; - async fn list_domains(&self, filter: Option<&str>) -> trc::Result<Vec<String>>; } impl ManageDirectory for Store { - async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>> { + async fn get_principal_name(&self, principal_id: u32) -> trc::Result<Option<String>> { self.get_value::<Principal>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(account_id), + DirectoryClass::Principal(principal_id), ))) .await .map(|v| v.and_then(|mut v| v.take_str(PrincipalField::Name))) .caused_by(trc::location!()) } - async fn get_account_id(&self, name: &str) -> trc::Result<Option<u32>> { - self.get_value::<PrincipalIdType>(ValueKey::from(ValueClass::Directory( + async fn get_principal_id(&self, name: &str) -> trc::Result<Option<u32>> { + self.get_principal_info(name).await.map(|v| v.map(|v| v.id)) + } + async fn get_principal_info(&self, name: &str) -> trc::Result<Option<PrincipalInfo>> { + self.get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory( DirectoryClass::NameToId(name.as_bytes().to_vec()), ))) .await - .map(|v| v.map(|v| v.account_id)) .caused_by(trc::location!()) } // Used by all directories except internal - async fn get_or_create_account_id(&self, name: &str) -> trc::Result<u32> { + async fn get_or_create_principal_id(&self, name: &str, typ: Type) -> trc::Result<u32> { let mut try_count = 0; let name = name.to_lowercase(); loop { // Try to obtain ID - if let Some(account_id) = self - .get_account_id(&name) + if let Some(principal_id) = self + .get_principal_id(&name) .await .caused_by(trc::location!())? { - return Ok(account_id); + return Ok(principal_id); } - // Write account ID + // Write principal ID let name_key = ValueClass::Directory(DirectoryClass::NameToId(name.as_bytes().to_vec())); let mut batch = BatchBuilder::new(); @@ -99,11 +98,11 @@ impl ManageDirectory for Store { .with_collection(Collection::Principal) .assert_value(name_key.clone(), ()) .create_document() - .set(name_key, DynamicPrincipalIdType(Type::Individual)) + .set(name_key, DynamicPrincipalInfo::new(typ, None)) .set( ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Dynamic(0))), Principal { - typ: Type::Individual, + typ, ..Default::default() } .with_field(PrincipalField::Name, name.to_string()), @@ -114,8 +113,8 @@ impl ManageDirectory for Store { .await .and_then(|r| r.last_document_id()) { - Ok(account_id) => { - return Ok(account_id); + Ok(principal_id) => { + return Ok(principal_id); } Err(err) => { if err.is_assertion_failure() && try_count < 3 { @@ -129,32 +128,43 @@ impl ManageDirectory for Store { } } - async fn create_account(&self, mut principal: Principal) -> trc::Result<u32> { + async fn create_principal( + &self, + mut principal: Principal, + mut tenant_id: Option<u32>, + ) -> trc::Result<u32> { // Make sure the principal has a name let name = principal.name().to_lowercase(); if name.is_empty() { return Err(err_missing(PrincipalField::Name)); } - // Map group names - let members = self - .map_group_names( - principal - .take(PrincipalField::Members) - .map(|v| v.into_str_array()) - .unwrap_or_default(), - false, - ) - .await - .caused_by(trc::location!())?; - let mut principal = self - .map_principal(principal, false) - .await - .caused_by(trc::location!())?; + // Tenants must provide principal names including a valid domain + if tenant_id.is_some() { + let mut name_is_valid = false; + if let Some(domain) = name.split('@').nth(1) { + if self + .get_principal_info(domain) + .await + .caused_by(trc::location!())? + .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id)) + .is_some() + { + name_is_valid = true; + } + } + + if !name_is_valid { + return Err(error( + "Invalid principal name", + "Principal name must include a valid domain".into(), + )); + } + } // Make sure new name is not taken if self - .get_account_id(&name) + .get_principal_id(&name) .await .caused_by(trc::location!())? .is_some() @@ -163,6 +173,67 @@ impl ManageDirectory for Store { } principal.set(PrincipalField::Name, name); + // Map member names + let mut members = Vec::new(); + let mut member_of = Vec::new(); + for (field, expected_type) in [ + (PrincipalField::Members, None), + (PrincipalField::MemberOf, Some(Type::Group)), + (PrincipalField::Lists, Some(Type::List)), + (PrincipalField::Roles, Some(Type::Role)), + ] { + if let Some(names) = principal.take_str_array(field) { + let list = if field == PrincipalField::Members { + &mut members + } else { + &mut member_of + }; + + for name in names { + let id = match name.strip_prefix("_") { + Some("admin") if field == PrincipalField::Roles && tenant_id.is_none() => { + PrincipalInfo::new(ROLE_ADMIN, Type::Role, None) + } + Some("tenant_admin") if field == PrincipalField::Roles => { + PrincipalInfo::new(ROLE_TENANT_ADMIN, Type::Role, None) + } + Some("user") if field == PrincipalField::Roles => { + PrincipalInfo::new(ROLE_USER, Type::Role, None) + } + _ => self + .get_principal_info(&name) + .await + .caused_by(trc::location!())? + .filter(|v| { + expected_type.map_or(true, |t| v.typ == t) + && v.has_tenant_access(tenant_id) + }) + .ok_or_else(|| not_found(name))?, + }; + + list.push(id); + } + } + } + + // Map permissions + for field in [ + PrincipalField::EnabledPermissions, + PrincipalField::DisabledPermissions, + ] { + if let Some(names) = principal.take_str_array(field) { + for name in names { + let permission = Permission::from_name(&name).ok_or_else(|| { + error( + "Invalid permission", + format!("Permission {name:?} is invalid").into(), + ) + })?; + principal.append_int(field, permission.id() as u64); + } + } + } + // Make sure the e-mail is not taken and validate domain for email in principal.iter_mut_str(PrincipalField::Emails) { *email = email.to_lowercase(); @@ -170,19 +241,32 @@ impl ManageDirectory for Store { return Err(err_exists(PrincipalField::Emails, email.to_string())); } if let Some(domain) = email.split('@').nth(1) { - if !self - .is_local_domain(domain) + self.get_principal_info(domain) .await .caused_by(trc::location!())? - { - return Err(not_found(domain.to_string())); - } + .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id)) + .ok_or_else(|| not_found(domain.to_string()))?; } } + // Obtain tenant id + if let Some(tenant_id) = tenant_id { + principal.set(PrincipalField::Tenant, tenant_id); + } else if let Some(tenant_name) = principal.take_str(PrincipalField::Tenant) { + tenant_id = self + .get_principal_info(&tenant_name) + .await + .caused_by(trc::location!())? + .filter(|v| v.typ == Type::Tenant) + .ok_or_else(|| not_found(tenant_name.clone()))? + .id + .into(); + } + // Write principal let mut batch = BatchBuilder::new(); - let ptype = DynamicPrincipalIdType(principal.typ); + let pinfo_name = DynamicPrincipalInfo::new(principal.typ, tenant_id); + let pinfo_email = DynamicPrincipalInfo::new(principal.typ, None); batch .with_account_id(u32::MAX) .with_collection(Collection::Principal) @@ -204,7 +288,7 @@ impl ManageDirectory for Store { .unwrap() .into_bytes(), )), - ptype, + pinfo_name, ); // Write email to id mapping @@ -215,40 +299,40 @@ impl ManageDirectory for Store { for email in emails { batch.set( ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())), - ptype, + pinfo_email, ); } } // Write membership - for member_of in principal.iter_int(PrincipalField::MemberOf) { + for member_of in member_of { batch.set( ValueClass::Directory(DirectoryClass::MemberOf { principal_id: MaybeDynamicId::Dynamic(0), - member_of: MaybeDynamicId::Static(member_of as u32), + member_of: MaybeDynamicId::Static(member_of.id), }), - vec![], + vec![member_of.typ as u8], ); batch.set( ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(member_of as u32), + principal_id: MaybeDynamicId::Static(member_of.id), has_member: MaybeDynamicId::Dynamic(0), }), vec![], ); } - for member_id in members { + for member in members { batch.set( ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(member_id), + principal_id: MaybeDynamicId::Static(member.id), member_of: MaybeDynamicId::Dynamic(0), }), - vec![], + vec![member.typ as u8], ); batch.set( ValueClass::Directory(DirectoryClass::Members { principal_id: MaybeDynamicId::Dynamic(0), - has_member: MaybeDynamicId::Static(member_id), + has_member: MaybeDynamicId::Static(member.id), }), vec![], ); @@ -259,44 +343,46 @@ impl ManageDirectory for Store { .and_then(|r| r.last_document_id()) } - async fn delete_account(&self, by: QueryBy<'_>) -> trc::Result<()> { - let account_id = match by { + async fn delete_principal(&self, by: QueryBy<'_>) -> trc::Result<()> { + let todo = "do not delete tenants with children"; + + let principal_id = match by { QueryBy::Name(name) => self - .get_account_id(name) + .get_principal_id(name) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(name.to_string()))?, - QueryBy::Id(account_id) => account_id, + QueryBy::Id(principal_id) => principal_id, QueryBy::Credentials(_) => unreachable!(), }; let mut principal = self .get_value::<Principal>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(account_id), + DirectoryClass::Principal(principal_id), ))) .await .caused_by(trc::location!())? - .ok_or_else(|| not_found(account_id.to_string()))?; + .ok_or_else(|| not_found(principal_id.to_string()))?; - // Unlink all account's blobs - self.blob_hash_unlink_account(account_id) + // Unlink all principal's blobs + self.blob_hash_unlink_account(principal_id) .await .caused_by(trc::location!())?; // Revoke ACLs - self.acl_revoke_all(account_id) + self.acl_revoke_all(principal_id) .await .caused_by(trc::location!())?; - // Delete account data - self.purge_account(account_id) + // Delete principal data + self.purge_account(principal_id) .await .caused_by(trc::location!())?; - // Delete account + // Delete principal let mut batch = BatchBuilder::new(); batch - .with_account_id(account_id) + .with_account_id(principal_id) .clear(DirectoryClass::NameToId( principal .take_str(PrincipalField::Name) @@ -304,9 +390,9 @@ impl ManageDirectory for Store { .into_bytes(), )) .clear(DirectoryClass::Principal(MaybeDynamicId::Static( - account_id, + principal_id, ))) - .clear(DirectoryClass::UsedQuota(account_id)); + .clear(DirectoryClass::UsedQuota(principal_id)); if let Some(emails) = principal.take_str_array(PrincipalField::Emails) { for email in emails { @@ -314,32 +400,32 @@ impl ManageDirectory for Store { } } - for member_id in self - .get_member_of(account_id) + for member in self + .get_member_of(principal_id) .await .caused_by(trc::location!())? { batch.clear(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(account_id), - member_of: MaybeDynamicId::Static(member_id), + principal_id: MaybeDynamicId::Static(principal_id), + member_of: MaybeDynamicId::Static(member.principal_id), }); batch.clear(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(member_id), - has_member: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(member.principal_id), + has_member: MaybeDynamicId::Static(principal_id), }); } for member_id in self - .get_members(account_id) + .get_members(principal_id) .await .caused_by(trc::location!())? { batch.clear(DirectoryClass::MemberOf { principal_id: MaybeDynamicId::Static(member_id), - member_of: MaybeDynamicId::Static(account_id), + member_of: MaybeDynamicId::Static(principal_id), }); batch.clear(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(principal_id), has_member: MaybeDynamicId::Static(member_id), }); } @@ -351,43 +437,49 @@ impl ManageDirectory for Store { Ok(()) } - async fn update_account( + async fn update_principal( &self, by: QueryBy<'_>, changes: Vec<PrincipalUpdate>, ) -> trc::Result<()> { - let account_id = match by { + let principal_id = match by { QueryBy::Name(name) => self - .get_account_id(name) + .get_principal_id(name) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(name.to_string()))?, - QueryBy::Id(account_id) => account_id, + QueryBy::Id(principal_id) => principal_id, QueryBy::Credentials(_) => unreachable!(), }; // Fetch principal let mut principal = self .get_value::<HashedValue<Principal>>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(account_id), + DirectoryClass::Principal(principal_id), ))) .await .caused_by(trc::location!())? - .ok_or_else(|| not_found(account_id.to_string()))?; + .ok_or_else(|| not_found(principal_id.to_string()))?; // Obtain members and memberOf let mut member_of = self - .get_member_of(account_id) + .get_member_of(principal_id) .await - .caused_by(trc::location!())?; + .caused_by(trc::location!())? + .into_iter() + .map(|v| v.principal_id) + .collect::<Vec<_>>(); let mut members = self - .get_members(account_id) + .get_members(principal_id) .await .caused_by(trc::location!())?; // Apply changes let mut batch = BatchBuilder::new(); - let ptype = PrincipalIdType::new(account_id, principal.inner.typ).serialize(); + let mut pinfo_name = + PrincipalInfo::new(principal_id, principal.inner.typ, principal.inner.tenant()) + .serialize(); + let pinfo_email = PrincipalInfo::new(principal_id, principal.inner.typ, None).serialize(); let update_principal = !changes.is_empty() && !changes .iter() @@ -396,7 +488,7 @@ impl ManageDirectory for Store { if update_principal { batch.assert_value( ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Static( - account_id, + principal_id, ))), &principal, ); @@ -408,7 +500,7 @@ impl ManageDirectory for Store { let new_name = new_name.to_lowercase(); if principal.inner.name() != new_name { if self - .get_account_id(&new_name) + .get_principal_id(&new_name) .await .caused_by(trc::location!())? .is_some() @@ -424,12 +516,56 @@ impl ManageDirectory for Store { batch.set( ValueClass::Directory(DirectoryClass::NameToId(new_name.into_bytes())), - ptype.clone(), + pinfo_name.clone(), ); } } ( PrincipalAction::Set, + PrincipalField::Tenant, + PrincipalValue::String(tenant_name), + ) => { + if !tenant_name.is_empty() { + let tenant_info = self + .get_principal_info(&tenant_name) + .await + .caused_by(trc::location!())? + .ok_or_else(|| not_found(tenant_name.clone()))?; + if tenant_info.typ != Type::Tenant { + return Err(error( + "Not a tenant", + format!("Principal {tenant_name:?} is not a tenant").into(), + )); + } + + if principal.inner.tenant() != Some(tenant_info.id) { + principal.inner.set(PrincipalField::Tenant, tenant_info.id); + pinfo_name = PrincipalInfo::new( + principal_id, + principal.inner.typ, + tenant_info.id.into(), + ) + .serialize(); + } else { + continue; + } + } else if principal.inner.tenant().is_some() { + principal.inner.remove(PrincipalField::Tenant); + pinfo_name = + PrincipalInfo::new(principal_id, principal.inner.typ, None).serialize(); + } else { + continue; + } + + batch.set( + ValueClass::Directory(DirectoryClass::NameToId( + principal.inner.name().as_bytes().to_vec(), + )), + pinfo_name.clone(), + ); + } + ( + PrincipalAction::Set, PrincipalField::Secrets, value @ (PrincipalValue::StringList(_) | PrincipalValue::String(_)), ) => { @@ -517,7 +653,7 @@ impl ManageDirectory for Store { ValueClass::Directory(DirectoryClass::EmailToId( email.as_bytes().to_vec(), )), - ptype.clone(), + pinfo_email.clone(), ); } } @@ -558,7 +694,7 @@ impl ManageDirectory for Store { ValueClass::Directory(DirectoryClass::EmailToId( email.as_bytes().to_vec(), )), - ptype.clone(), + pinfo_email.clone(), ); principal.inner.append_str(PrincipalField::Emails, email); } @@ -590,40 +726,40 @@ impl ManageDirectory for Store { ) => { let mut new_member_of = Vec::new(); for member in members { - let member_id = self - .get_account_id(&member) + let member_info = self + .get_principal_info(&member) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(member))?; - if !member_of.contains(&member_id) { + if !member_of.contains(&member_info.id) { batch.set( ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(account_id), - member_of: MaybeDynamicId::Static(member_id), + principal_id: MaybeDynamicId::Static(principal_id), + member_of: MaybeDynamicId::Static(member_info.id), }), - vec![], + vec![member_info.typ as u8], ); batch.set( ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(member_id), - has_member: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(member_info.id), + has_member: MaybeDynamicId::Static(principal_id), }), vec![], ); } - new_member_of.push(member_id); + new_member_of.push(member_info.id); } for member_id in &member_of { if !new_member_of.contains(member_id) { batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(principal_id), member_of: MaybeDynamicId::Static(*member_id), })); batch.clear(ValueClass::Directory(DirectoryClass::Members { principal_id: MaybeDynamicId::Static(*member_id), - has_member: MaybeDynamicId::Static(account_id), + has_member: MaybeDynamicId::Static(principal_id), })); } } @@ -635,27 +771,27 @@ impl ManageDirectory for Store { PrincipalField::MemberOf, PrincipalValue::String(member), ) => { - let member_id = self - .get_account_id(&member) + let member_info = self + .get_principal_info(&member) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(member))?; - if !member_of.contains(&member_id) { + if !member_of.contains(&member_info.id) { batch.set( ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(account_id), - member_of: MaybeDynamicId::Static(member_id), + principal_id: MaybeDynamicId::Static(principal_id), + member_of: MaybeDynamicId::Static(member_info.id), }), - vec![], + vec![member_info.typ as u8], ); batch.set( ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(member_id), - has_member: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(member_info.id), + has_member: MaybeDynamicId::Static(principal_id), }), vec![], ); - member_of.push(member_id); + member_of.push(member_info.id); } } ( @@ -664,18 +800,18 @@ impl ManageDirectory for Store { PrincipalValue::String(member), ) => { if let Some(member_id) = self - .get_account_id(&member) + .get_principal_id(&member) .await .caused_by(trc::location!())? { if let Some(pos) = member_of.iter().position(|v| *v == member_id) { batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(principal_id), member_of: MaybeDynamicId::Static(member_id), })); batch.clear(ValueClass::Directory(DirectoryClass::Members { principal_id: MaybeDynamicId::Static(member_id), - has_member: MaybeDynamicId::Static(account_id), + has_member: MaybeDynamicId::Static(principal_id), })); member_of.remove(pos); } @@ -689,39 +825,39 @@ impl ManageDirectory for Store { ) => { let mut new_members = Vec::new(); for member in members_ { - let member_id = self - .get_account_id(&member) + let member_info = self + .get_principal_info(&member) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(member))?; - if !members.contains(&member_id) { + if !members.contains(&member_info.id) { batch.set( ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(member_id), - member_of: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(member_info.id), + member_of: MaybeDynamicId::Static(principal_id), }), - vec![], + vec![member_info.typ as u8], ); batch.set( ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(account_id), - has_member: MaybeDynamicId::Static(member_id), + principal_id: MaybeDynamicId::Static(principal_id), + has_member: MaybeDynamicId::Static(member_info.id), }), vec![], ); } - new_members.push(member_id); + new_members.push(member_info.id); } for member_id in &members { if !new_members.contains(member_id) { batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { principal_id: MaybeDynamicId::Static(*member_id), - member_of: MaybeDynamicId::Static(account_id), + member_of: MaybeDynamicId::Static(principal_id), })); batch.clear(ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(principal_id), has_member: MaybeDynamicId::Static(*member_id), })); } @@ -734,27 +870,27 @@ impl ManageDirectory for Store { PrincipalField::Members, PrincipalValue::String(member), ) => { - let member_id = self - .get_account_id(&member) + let member_info = self + .get_principal_info(&member) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(member))?; - if !members.contains(&member_id) { + if !members.contains(&member_info.id) { batch.set( ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: MaybeDynamicId::Static(member_id), - member_of: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(member_info.id), + member_of: MaybeDynamicId::Static(principal_id), }), - vec![], + vec![member_info.typ as u8], ); batch.set( ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(account_id), - has_member: MaybeDynamicId::Static(member_id), + principal_id: MaybeDynamicId::Static(principal_id), + has_member: MaybeDynamicId::Static(member_info.id), }), vec![], ); - members.push(member_id); + members.push(member_info.id); } } ( @@ -763,17 +899,17 @@ impl ManageDirectory for Store { PrincipalValue::String(member), ) => { if let Some(member_id) = self - .get_account_id(&member) + .get_principal_id(&member) .await .caused_by(trc::location!())? { if let Some(pos) = members.iter().position(|v| *v == member_id) { batch.clear(ValueClass::Directory(DirectoryClass::MemberOf { principal_id: MaybeDynamicId::Static(member_id), - member_of: MaybeDynamicId::Static(account_id), + member_of: MaybeDynamicId::Static(principal_id), })); batch.clear(ValueClass::Directory(DirectoryClass::Members { - principal_id: MaybeDynamicId::Static(account_id), + principal_id: MaybeDynamicId::Static(principal_id), has_member: MaybeDynamicId::Static(member_id), })); members.remove(pos); @@ -790,7 +926,7 @@ impl ManageDirectory for Store { if update_principal { batch.set( ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Static( - account_id, + principal_id, ))), principal.inner.serialize(), ); @@ -803,90 +939,11 @@ impl ManageDirectory for Store { Ok(()) } - async fn create_domain(&self, domain: &str) -> trc::Result<()> { - if !domain.contains('.') { - return Err(err_missing(PrincipalField::Name)); - } - let mut batch = BatchBuilder::new(); - batch.set( - ValueClass::Directory(DirectoryClass::Domain(domain.to_lowercase().into_bytes())), - vec![], - ); - self.write(batch.build()).await.map(|_| ()) - } - - async fn delete_domain(&self, domain: &str) -> trc::Result<()> { - if !domain.contains('.') { - return Err(err_missing(PrincipalField::Name)); - } - let mut batch = BatchBuilder::new(); - batch.clear(ValueClass::Directory(DirectoryClass::Domain( - domain.to_lowercase().into_bytes(), - ))); - self.write(batch.build()).await.map(|_| ()) - } - - async fn map_group_ids(&self, mut principal: Principal) -> trc::Result<Principal> { - if let Some(member_of) = principal.take_int_array(PrincipalField::MemberOf) { - for account_id in member_of { - if let Some(name) = self - .get_account_name(account_id as u32) - .await - .caused_by(trc::location!())? - { - principal.append_str(PrincipalField::MemberOf, name); - } - } - } - - Ok(principal) - } - - async fn map_principal( - &self, - mut principal: Principal, - create_if_missing: bool, - ) -> trc::Result<Principal> { - if let Some(member_of) = principal.take_str_array(PrincipalField::MemberOf) { - principal.set( - PrincipalField::MemberOf, - self.map_group_names(member_of, create_if_missing) - .await - .caused_by(trc::location!())?, - ); - } - - Ok(principal) - } - - async fn map_group_names( - &self, - members: Vec<String>, - create_if_missing: bool, - ) -> trc::Result<Vec<u32>> { - let mut member_ids = Vec::with_capacity(members.len()); - - for member in members { - let account_id = if create_if_missing { - self.get_or_create_account_id(&member) - .await - .caused_by(trc::location!())? - } else { - self.get_account_id(&member) - .await - .caused_by(trc::location!())? - .ok_or_else(|| not_found(member))? - }; - member_ids.push(account_id); - } - - Ok(member_ids) - } - - async fn list_accounts( + async fn list_principals( &self, filter: Option<&str>, typ: Option<Type>, + tenant_id: Option<u32>, ) -> trc::Result<Vec<String>> { let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![]))); let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![ @@ -898,11 +955,11 @@ impl ManageDirectory for Store { self.iterate( IterateParams::new(from_key, to_key).ascending(), |key, value| { - let pt = PrincipalIdType::deserialize(value).caused_by(trc::location!())?; + let pt = PrincipalInfo::deserialize(value).caused_by(trc::location!())?; - if typ.map_or(true, |t| pt.typ == t) { + if typ.map_or(true, |t| pt.typ == t) && pt.has_tenant_access(tenant_id) { results.push(( - pt.account_id, + pt.id, String::from_utf8_lossy(key.get(1..).unwrap_or_default()).into_owned(), )); } @@ -920,25 +977,16 @@ impl ManageDirectory for Store { .map(|r| r.to_lowercase()) .collect::<Vec<_>>(); - for (account_id, account_name) in results { + for (principal_id, principal_name) in results { let principal = self .get_value::<Principal>(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(account_id), + DirectoryClass::Principal(principal_id), ))) .await .caused_by(trc::location!())? - .ok_or_else(|| not_found(account_id.to_string()))?; - if filters.iter().all(|f| { - principal.name().to_lowercase().contains(f) - || principal - .description() - .as_ref() - .map_or(false, |d| d.to_lowercase().contains(f)) - || principal - .iter_str(PrincipalField::Emails) - .any(|email| email.to_lowercase().contains(f)) - }) { - filtered.push(account_name); + .ok_or_else(|| not_found(principal_id.to_string()))?; + if filters.iter().all(|f| principal.find_str(f)) { + filtered.push(principal_name); } } @@ -948,59 +996,38 @@ impl ManageDirectory for Store { } } - async fn list_domains(&self, filter: Option<&str>) -> trc::Result<Vec<String>> { - let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![]))); - let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![ - u8::MAX; - 10 - ]))); - - let mut results = Vec::new(); - self.iterate( - IterateParams::new(from_key, to_key).no_values().ascending(), - |key, _| { - let domain = String::from_utf8_lossy(key.get(1..).unwrap_or_default()).into_owned(); - if filter.map_or(true, |f| domain.contains(f)) { - results.push(domain); - } - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - - Ok(results) - } - - async fn get_member_of(&self, account_id: u32) -> trc::Result<Vec<u32>> { + async fn get_member_of(&self, principal_id: u32) -> trc::Result<Vec<MemberOf>> { let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: account_id, + principal_id, member_of: 0, })); let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: account_id, + principal_id, member_of: u32::MAX, })); let mut results = Vec::new(); - self.iterate( - IterateParams::new(from_key, to_key).no_values(), - |key, _| { - results.push(key.deserialize_be_u32(key.len() - U32_LEN)?); - Ok(true) - }, - ) + self.iterate(IterateParams::new(from_key, to_key), |key, value| { + results.push(MemberOf { + principal_id: key.deserialize_be_u32(key.len() - U32_LEN)?, + typ: value + .first() + .map(|v| Type::from_u8(*v)) + .unwrap_or(Type::Group), + }); + Ok(true) + }) .await .caused_by(trc::location!())?; Ok(results) } - async fn get_members(&self, account_id: u32) -> trc::Result<Vec<u32>> { + async fn get_members(&self, principal_id: u32) -> trc::Result<Vec<u32>> { let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Members { - principal_id: account_id, + principal_id, has_member: 0, })); let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Members { - principal_id: account_id, + principal_id, has_member: u32::MAX, })); let mut results = Vec::new(); @@ -1032,17 +1059,26 @@ impl From<Principal> for MaybeDynamicValue { } #[derive(Clone, Copy)] -struct DynamicPrincipalIdType(Type); +struct DynamicPrincipalInfo { + typ: Type, + tenant: Option<u32>, +} + +impl DynamicPrincipalInfo { + fn new(typ: Type, tenant: Option<u32>) -> Self { + Self { typ, tenant } + } +} -impl SerializeWithId for DynamicPrincipalIdType { +impl SerializeWithId for DynamicPrincipalInfo { fn serialize_with_id(&self, ids: &AssignedIds) -> trc::Result<Vec<u8>> { ids.last_document_id() - .map(|account_id| PrincipalIdType::new(account_id, self.0).serialize()) + .map(|principal_id| PrincipalInfo::new(principal_id, self.typ, self.tenant).serialize()) } } -impl From<DynamicPrincipalIdType> for MaybeDynamicValue { - fn from(value: DynamicPrincipalIdType) -> Self { +impl From<DynamicPrincipalInfo> for MaybeDynamicValue { + fn from(value: DynamicPrincipalInfo) -> Self { MaybeDynamicValue::Dynamic(Box::new(value)) } } diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index b8ac95df..d0f4a459 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -7,19 +7,20 @@ pub mod lookup; pub mod manage; -use std::{fmt::Display, slice::Iter, str::FromStr}; +use std::{fmt::Display, slice::Iter}; use ahash::AHashMap; use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN}; use utils::codec::leb128::Leb128Iterator; -use crate::{Principal, Type}; +use crate::{Principal, Type, ROLE_ADMIN, ROLE_USER}; const INT_MARKER: u8 = 1 << 7; -pub(super) struct PrincipalIdType { - pub account_id: u32, +pub struct PrincipalInfo { + pub id: u32, pub typ: Type, + pub tenant: Option<u32>, } impl Serialize for Principal { @@ -90,20 +91,36 @@ impl Deserialize for Principal { } } -impl Serialize for PrincipalIdType { +impl PrincipalInfo { + pub fn has_tenant_access(&self, tenant_id: Option<u32>) -> bool { + tenant_id.map_or(true, |tenant_id| { + self.tenant.map_or(false, |t| tenant_id == t) + }) + } +} + +impl Serialize for PrincipalInfo { fn serialize(self) -> Vec<u8> { - KeySerializer::new(U32_LEN + 1) - .write_leb128(self.account_id) - .write(self.typ as u8) - .finalize() + if let Some(tenant) = self.tenant { + KeySerializer::new((U32_LEN * 2) + 1) + .write_leb128(self.id) + .write(self.typ as u8) + .write_leb128(tenant) + .finalize() + } else { + KeySerializer::new(U32_LEN + 1) + .write_leb128(self.id) + .write(self.typ as u8) + .finalize() + } } } -impl Deserialize for PrincipalIdType { +impl Deserialize for PrincipalInfo { fn deserialize(bytes_: &[u8]) -> trc::Result<Self> { let mut bytes = bytes_.iter(); - Ok(PrincipalIdType { - account_id: bytes.next_leb128().ok_or_else(|| { + Ok(PrincipalInfo { + id: bytes.next_leb128().ok_or_else(|| { trc::StoreEvent::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, bytes_) @@ -113,13 +130,18 @@ impl Deserialize for PrincipalIdType { .caused_by(trc::location!()) .ctx(trc::Key::Value, bytes_) })?), + tenant: bytes.next_leb128(), }) } } -impl PrincipalIdType { - pub fn new(account_id: u32, typ: Type) -> Self { - Self { account_id, typ } +impl PrincipalInfo { + pub fn new(principal_id: u32, typ: Type, tenant: Option<u32>) -> Self { + Self { + id: principal_id, + typ, + tenant, + } } } @@ -151,12 +173,12 @@ fn deserialize(bytes: &[u8]) -> Option<Principal> { } } - if type_id != 4 { - principal - } else { - principal.into_superuser() - } - .into() + principal + .with_field( + PrincipalField::Roles, + if type_id != 4 { ROLE_USER } else { ROLE_ADMIN }, + ) + .into() } 2 => { // Version 2 @@ -206,23 +228,22 @@ fn deserialize(bytes: &[u8]) -> Option<Principal> { #[derive( Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] +#[serde(rename_all = "camelCase")] pub enum PrincipalField { - #[serde(rename = "name")] Name, - #[serde(rename = "type")] Type, - #[serde(rename = "quota")] Quota, - #[serde(rename = "description")] + UsedQuota, Description, - #[serde(rename = "secrets")] Secrets, - #[serde(rename = "emails")] Emails, - #[serde(rename = "memberOf")] MemberOf, - #[serde(rename = "members")] Members, + Tenant, + Roles, + Lists, + EnabledPermissions, + DisabledPermissions, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -294,6 +315,12 @@ impl PrincipalField { PrincipalField::Emails => 5, PrincipalField::MemberOf => 6, PrincipalField::Members => 7, + PrincipalField::Tenant => 8, + PrincipalField::Roles => 9, + PrincipalField::Lists => 10, + PrincipalField::EnabledPermissions => 11, + PrincipalField::DisabledPermissions => 12, + PrincipalField::UsedQuota => 13, } } @@ -307,6 +334,12 @@ impl PrincipalField { 5 => Some(PrincipalField::Emails), 6 => Some(PrincipalField::MemberOf), 7 => Some(PrincipalField::Members), + 8 => Some(PrincipalField::Tenant), + 9 => Some(PrincipalField::Roles), + 10 => Some(PrincipalField::Lists), + 11 => Some(PrincipalField::EnabledPermissions), + 12 => Some(PrincipalField::DisabledPermissions), + 13 => Some(PrincipalField::UsedQuota), _ => None, } } @@ -316,11 +349,37 @@ impl PrincipalField { PrincipalField::Name => "name", PrincipalField::Type => "type", PrincipalField::Quota => "quota", + PrincipalField::UsedQuota => "usedQuota", PrincipalField::Description => "description", PrincipalField::Secrets => "secrets", PrincipalField::Emails => "emails", PrincipalField::MemberOf => "memberOf", PrincipalField::Members => "members", + PrincipalField::Tenant => "tenant", + PrincipalField::Roles => "roles", + PrincipalField::Lists => "lists", + PrincipalField::EnabledPermissions => "enabledPermissions", + PrincipalField::DisabledPermissions => "disabledPermissions", + } + } + + pub fn try_parse(s: &str) -> Option<Self> { + match s { + "name" => Some(PrincipalField::Name), + "type" => Some(PrincipalField::Type), + "quota" => Some(PrincipalField::Quota), + "usedQuota" => Some(PrincipalField::UsedQuota), + "description" => Some(PrincipalField::Description), + "secrets" => Some(PrincipalField::Secrets), + "emails" => Some(PrincipalField::Emails), + "memberOf" => Some(PrincipalField::MemberOf), + "members" => Some(PrincipalField::Members), + "tenant" => Some(PrincipalField::Tenant), + "roles" => Some(PrincipalField::Roles), + "lists" => Some(PrincipalField::Lists), + "enabledPermissions" => Some(PrincipalField::EnabledPermissions), + "disabledPermissions" => Some(PrincipalField::DisabledPermissions), + _ => None, } } } @@ -334,42 +393,6 @@ fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option<String> { String::from_utf8(string).ok() } -impl Type { - pub fn parse(value: &str) -> Option<Self> { - match value { - "individual" => Some(Type::Individual), - "group" => Some(Type::Group), - "resource" => Some(Type::Resource), - "location" => Some(Type::Location), - "list" => Some(Type::List), - "tenant" => Some(Type::Tenant), - "superuser" => Some(Type::Individual), // legacy - _ => None, - } - } - - pub fn from_u8(value: u8) -> Self { - match value { - 0 => Type::Individual, - 1 => Type::Group, - 2 => Type::Resource, - 3 => Type::Location, - 4 => Type::Individual, // legacy - 5 => Type::List, - 7 => Type::Tenant, - _ => Type::Other, - } - } -} - -impl FromStr for Type { - type Err = (); - - fn from_str(s: &str) -> Result<Self, Self::Err> { - Type::parse(s).ok_or(()) - } -} - pub trait SpecialSecrets { fn is_otp_auth(&self) -> bool; fn is_app_password(&self) -> bool; diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index 1576de22..f7f558cd 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -6,10 +6,11 @@ use ldap3::{Ldap, LdapConnAsync, Scope, SearchEntry}; use mail_send::Credentials; +use trc::AddContext; use crate::{ backend::internal::{manage::ManageDirectory, PrincipalField}, - IntoError, Principal, QueryBy, Type, + IntoError, Principal, QueryBy, Type, ROLE_ADMIN, ROLE_USER, }; use super::{LdapDirectory, LdapMappings}; @@ -38,7 +39,7 @@ impl LdapDirectory { } } QueryBy::Id(uid) => { - if let Some(username) = self.data_store.get_account_name(uid).await? { + if let Some(username) = self.data_store.get_principal_name(uid).await? { account_name = username; } else { return Ok(None); @@ -125,18 +126,22 @@ impl LdapDirectory { } else { principal.id = self .data_store - .get_or_create_account_id(&account_name) + .get_or_create_principal_id(&account_name, Type::Individual) .await?; } principal.append_str(PrincipalField::Name, account_name); // Obtain groups if return_member_of && principal.has_field(PrincipalField::MemberOf) { - for member_of in principal.iter_mut_str(PrincipalField::MemberOf) { - if member_of.contains('=') { + let mut member_of = Vec::new(); + for mut name in principal + .take_str_array(PrincipalField::MemberOf) + .unwrap_or_default() + { + if name.contains('=') { let (rs, _res) = conn .search( - member_of, + &name, Scope::Base, "objectClass=*", &self.mappings.attr_name, @@ -150,7 +155,7 @@ impl LdapDirectory { if self.mappings.attr_name.contains(&attr) { if let Some(group) = value.into_iter().next() { if !group.is_empty() { - *member_of = group; + name = group; break 'outer; } } @@ -158,17 +163,22 @@ impl LdapDirectory { } } } + + member_of.push( + self.data_store + .get_or_create_principal_id(&name, Type::Group) + .await + .caused_by(trc::location!())?, + ); } // Map ids - self.data_store - .map_principal(principal, true) - .await - .map(Some) + principal.set(PrincipalField::MemberOf, member_of); } else { principal.remove(PrincipalField::MemberOf); - Ok(Some(principal)) } + + Ok(Some(principal)) } pub async fn email_to_ids(&self, address: &str) -> trc::Result<Vec<u32>> { @@ -205,7 +215,11 @@ impl LdapDirectory { 'outer: for attr in &self.mappings.attr_name { if let Some(name) = entry.attrs.get(attr).and_then(|v| v.first()) { if !name.is_empty() { - ids.push(self.data_store.get_or_create_account_id(name).await?); + ids.push( + self.data_store + .get_or_create_principal_id(name, Type::Individual) + .await?, + ); break 'outer; } } @@ -405,6 +419,7 @@ impl LdapDirectory { impl LdapMappings { fn entry_to_principal(&self, entry: SearchEntry) -> Principal { let mut principal = Principal::default(); + let mut role = ROLE_USER; for (attr, value) in entry.attrs { if self.attr_name.contains(&attr) { @@ -443,7 +458,8 @@ impl LdapMappings { for value in value { match value.to_ascii_lowercase().as_str() { "admin" | "administrator" | "root" | "superuser" => { - principal = principal.into_superuser(); + role = ROLE_ADMIN; + principal.typ = Type::Individual } "posixaccount" | "individual" | "person" | "inetorgperson" => { principal.typ = Type::Individual @@ -458,6 +474,6 @@ impl LdapMappings { } } - principal + principal.with_field(PrincipalField::Roles, role) } } diff --git a/crates/directory/src/backend/memory/config.rs b/crates/directory/src/backend/memory/config.rs index 2ed6bf08..a4f966d2 100644 --- a/crates/directory/src/backend/memory/config.rs +++ b/crates/directory/src/backend/memory/config.rs @@ -9,7 +9,7 @@ use utils::config::{utils::AsKey, Config}; use crate::{ backend::internal::{manage::ManageDirectory, PrincipalField}, - Principal, Type, + Principal, Type, ROLE_ADMIN, ROLE_USER, }; use super::{EmailType, MemoryDirectory}; @@ -48,7 +48,7 @@ impl MemoryDirectory { // Obtain id let id = directory .data_store - .get_or_create_account_id(&name) + .get_or_create_principal_id(&name, Type::Individual) .await .map_err(|err| { config.new_build_error( @@ -62,20 +62,15 @@ impl MemoryDirectory { .ok()?; // Create principal - let mut principal = if is_superuser { - Principal { - id, - typ, - ..Default::default() - } - .into_superuser() - } else { - Principal { - id, - typ, - ..Default::default() - } - }; + let mut principal = Principal { + id, + typ, + ..Default::default() + } + .with_field( + PrincipalField::Roles, + if is_superuser { ROLE_ADMIN } else { ROLE_USER }, + ); // Obtain group ids for group in config @@ -87,7 +82,7 @@ impl MemoryDirectory { PrincipalField::MemberOf, directory .data_store - .get_or_create_account_id(&group) + .get_or_create_principal_id(&group, Type::Group) .await .map_err(|err| { config.new_build_error( diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index e86b12e4..38a96638 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -10,7 +10,7 @@ use trc::AddContext; use crate::{ backend::internal::{manage::ManageDirectory, PrincipalField, PrincipalValue}, - Principal, QueryBy, Type, + Principal, QueryBy, Type, ROLE_ADMIN, ROLE_USER, }; use super::{SqlDirectory, SqlMappings}; @@ -37,7 +37,7 @@ impl SqlDirectory { QueryBy::Id(uid) => { if let Some(username) = self .data_store - .get_account_name(uid) + .get_principal_name(uid) .await .caused_by(trc::location!())? { @@ -98,7 +98,7 @@ impl SqlDirectory { } else { principal.id = self .data_store - .get_or_create_account_id(&account_name) + .get_or_create_principal_id(&account_name, Type::Individual) .await .caused_by(trc::location!())?; } @@ -117,7 +117,7 @@ impl SqlDirectory { principal.append_int( PrincipalField::MemberOf, self.data_store - .get_or_create_account_id(account_id) + .get_or_create_principal_id(account_id, Type::Group) .await .caused_by(trc::location!())?, ); @@ -155,7 +155,7 @@ impl SqlDirectory { if let Some(Value::Text(name)) = row.values.first() { ids.push( self.data_store - .get_or_create_account_id(name) + .get_or_create_principal_id(name, Type::Individual) .await .caused_by(trc::location!())?, ); @@ -208,6 +208,7 @@ impl SqlDirectory { impl SqlMappings { pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal> { let mut principal = Principal::default(); + let mut role = ROLE_USER; if let Some(row) = rows.rows.into_iter().next() { for (name, value) in rows.names.into_iter().zip(row.values) { @@ -221,11 +222,13 @@ impl SqlMappings { } } else if name.eq_ignore_ascii_case(&self.column_type) { match value.to_str().as_ref() { - "individual" | "person" | "user" => principal.typ = Type::Individual, + "individual" | "person" | "user" => { + principal.typ = Type::Individual; + } "group" => principal.typ = Type::Group, "admin" | "superuser" | "administrator" => { principal.typ = Type::Individual; - principal = principal.into_superuser(); + role = ROLE_ADMIN; } _ => (), } @@ -241,6 +244,6 @@ impl SqlMappings { } } - Ok(principal) + Ok(principal.with_field(PrincipalField::Roles, role)) } } diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index bdec5d7a..4c6d45e0 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -4,13 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::collections::hash_map::Entry; +use std::{collections::hash_map::Entry, str::FromStr}; +use serde::{ser::SerializeMap, Serializer}; use store::U64_LEN; use crate::{ backend::internal::{PrincipalField, PrincipalValue}, - Principal, Type, + Principal, Type, ROLE_ADMIN, }; impl Principal { @@ -42,6 +43,10 @@ impl Principal { self.get_int(PrincipalField::Quota).unwrap_or_default() } + pub fn tenant(&self) -> Option<u32> { + self.get_int(PrincipalField::Tenant).map(|v| v as u32) + } + pub fn description(&self) -> Option<&str> { self.get_str(PrincipalField::Description) } @@ -256,6 +261,10 @@ impl Principal { }) } + pub fn find_str(&self, value: &str) -> bool { + self.fields.values().any(|v| v.find_str(value)) + } + pub fn field_len(&self, key: PrincipalField) -> usize { self.fields.get(&key).map_or(0, |v| match v { PrincipalValue::String(_) => 1, @@ -324,12 +333,7 @@ impl Principal { PrincipalField::Secrets, PrincipalValue::String(fallback_pass.into()), ) - .into_superuser() - } - - pub fn into_superuser(mut self) -> Self { - let todo = "add role"; - self + .with_field(PrincipalField::Roles, ROLE_ADMIN) } } @@ -419,6 +423,14 @@ impl PrincipalValue { PrincipalValue::IntegerList(l) => l.len() * U64_LEN, } } + + pub fn find_str(&self, value: &str) -> bool { + match self { + PrincipalValue::String(s) => s.to_lowercase().contains(value), + PrincipalValue::StringList(l) => l.iter().any(|s| s.to_lowercase().contains(value)), + _ => false, + } + } } impl From<u64> for PrincipalValue { @@ -473,6 +485,8 @@ impl Type { Self::Other => "other", Self::List => "list", Self::Tenant => "tenant", + Self::Role => "role", + Self::Domain => "domain", } } @@ -485,6 +499,143 @@ impl Type { Self::Tenant => "Tenant", Self::List => "List", Self::Other => "Other", + Self::Role => "Role", + Self::Domain => "Domain", + } + } + + pub fn parse(value: &str) -> Option<Self> { + match value { + "individual" => Some(Type::Individual), + "group" => Some(Type::Group), + "resource" => Some(Type::Resource), + "location" => Some(Type::Location), + "list" => Some(Type::List), + "tenant" => Some(Type::Tenant), + "superuser" => Some(Type::Individual), // legacy + "role" => Some(Type::Role), + "domain" => Some(Type::Domain), + _ => None, + } + } + + pub fn from_u8(value: u8) -> Self { + match value { + 0 => Type::Individual, + 1 => Type::Group, + 2 => Type::Resource, + 3 => Type::Location, + 4 => Type::Individual, // legacy + 5 => Type::List, + 6 => Type::Other, + 7 => Type::Domain, + 8 => Type::Tenant, + 9 => Type::Role, + _ => Type::Other, + } + } +} + +impl FromStr for Type { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Type::parse(s).ok_or(()) + } +} + +impl serde::Serialize for Principal { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut map = serializer.serialize_map(None)?; + + map.serialize_entry("id", &self.id)?; + map.serialize_entry("type", &self.typ.to_jmap())?; + + for (key, value) in &self.fields { + match value { + PrincipalValue::String(v) => map.serialize_entry(key.as_str(), v)?, + PrincipalValue::StringList(v) => map.serialize_entry(key.as_str(), v)?, + PrincipalValue::Integer(v) => map.serialize_entry(key.as_str(), v)?, + PrincipalValue::IntegerList(v) => map.serialize_entry(key.as_str(), v)?, + }; } + + map.end() + } +} + +impl<'de> serde::Deserialize<'de> for Principal { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct PrincipalVisitor; + + // Deserialize the principal + impl<'de> serde::de::Visitor<'de> for PrincipalVisitor { + type Value = Principal; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid principal") + } + + fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + where + A: serde::de::MapAccess<'de>, + { + let mut principal = Principal::default(); + + while let Some(key) = map.next_key::<&str>()? { + let key = PrincipalField::try_parse(key).ok_or_else(|| { + serde::de::Error::custom(format!("invalid principal field: {}", key)) + })?; + let value = match key { + PrincipalField::Name => PrincipalValue::String(map.next_value()?), + PrincipalField::Description | PrincipalField::Tenant => { + if let Some(v) = map.next_value::<Option<String>>()? { + PrincipalValue::String(v) + } else { + continue; + } + } + + PrincipalField::Type => { + principal.typ = Type::parse(map.next_value()?).ok_or_else(|| { + serde::de::Error::custom("invalid principal type") + })?; + continue; + } + PrincipalField::Quota => PrincipalValue::Integer( + map.next_value::<Option<u64>>()?.unwrap_or_default(), + ), + + PrincipalField::Secrets + | PrincipalField::Emails + | PrincipalField::MemberOf + | PrincipalField::Members + | PrincipalField::Roles + | PrincipalField::Lists + | PrincipalField::EnabledPermissions + | PrincipalField::DisabledPermissions => { + PrincipalValue::StringList(map.next_value()?) + } + PrincipalField::UsedQuota => { + // consume and ignore + let _ = map.next_value::<Option<u64>>()?; + continue; + } + }; + + principal.set(key, value); + } + + Ok(principal) + } + } + + deserializer.deserialize_map(PrincipalVisitor) } } diff --git a/crates/directory/src/core/secret.rs b/crates/directory/src/core/secret.rs index cbe27d6a..b4945ea4 100644 --- a/crates/directory/src/core/secret.rs +++ b/crates/directory/src/core/secret.rs @@ -31,8 +31,6 @@ impl Principal { let mut is_authenticated = false; let mut is_app_authenticated = false; - let todo = "validate authenticate permission"; - for secret in self.iter_str(PrincipalField::Secrets) { if secret.is_otp_auth() { if !is_totp_verified && !is_totp_token_missing { diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 07d320d3..e4ea79e8 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -21,6 +21,7 @@ use ldap3::LdapError; use mail_send::Credentials; use proc_macros::EnumMethods; use store::Store; +use trc::ipc::bitset::Bitset; pub mod backend; pub mod core; @@ -53,8 +54,12 @@ pub enum Type { List = 5, #[serde(rename = "other")] Other = 6, + #[serde(rename = "domain")] + Domain = 7, #[serde(rename = "tenant")] - Tenant = 7, + Tenant = 8, + #[serde(rename = "role")] + Role = 9, } #[derive( @@ -81,16 +86,41 @@ pub enum Permission { SettingsUpdate, SettingsDelete, SettingsReload, - PrincipalList, - PrincipalGet, - PrincipalUpdate, - PrincipalDelete, - PrincipalCreate, + IndividualList, + IndividualGet, + IndividualUpdate, + IndividualDelete, + IndividualCreate, + GroupList, + GroupGet, + GroupUpdate, + GroupDelete, + GroupCreate, DomainList, DomainGet, DomainCreate, DomainUpdate, DomainDelete, + TenantList, + TenantGet, + TenantCreate, + TenantUpdate, + TenantDelete, + MailingListList, + MailingListGet, + MailingListCreate, + MailingListUpdate, + MailingListDelete, + RoleList, + RoleGet, + RoleCreate, + RoleUpdate, + RoleDelete, + PrincipalList, + PrincipalGet, + PrincipalCreate, + PrincipalUpdate, + PrincipalDelete, BlobFetch, PurgeBlobStore, PurgeDataStore, @@ -113,6 +143,8 @@ pub enum Permission { // Generic Authenticate, AuthenticateOauth, + EmailSend, + EmailReceive, // Account Management ManageEncryption, @@ -195,9 +227,6 @@ pub enum Permission { ImapSubscribe, ImapThread, - // SMTP - SmtpAuthenticate, - // POP3 Pop3Authenticate, Pop3List, @@ -218,8 +247,13 @@ pub enum Permission { SieveHaveSpace, } -pub const PERMISSION_BITMAP_SIZE: usize = - (Permission::COUNT + std::mem::size_of::<usize>() - 1) / std::mem::size_of::<usize>(); +pub type Permissions = Bitset< + { (Permission::COUNT + std::mem::size_of::<usize>() - 1) / std::mem::size_of::<usize>() }, +>; + +pub const ROLE_ADMIN: u32 = u32::MAX; +pub const ROLE_TENANT_ADMIN: u32 = u32::MAX - 1; +pub const ROLE_USER: u32 = u32::MAX - 2; pub enum DirectoryInner { Internal(Store), diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index abed8c8a..4a780ee6 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -5,15 +5,13 @@ use std::{ use ahash::AHashMap; use common::{ + auth::AccessToken, config::jmap::settings::SpecialUse, listener::{limiter::InFlight, SessionStream}, }; use directory::{backend::internal::PrincipalField, QueryBy}; use imap_proto::protocol::list::Attribute; -use jmap::{ - auth::{acl::EffectiveAcl, AccessToken}, - mailbox::INBOX_ID, -}; +use jmap::{auth::acl::EffectiveAcl, mailbox::INBOX_ID}; use jmap_proto::{ object::Object, types::{acl::Acl, collection::Collection, id::Id, property::Property, value::Value}, @@ -335,6 +333,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain access token let access_token = self .jmap + .core .get_cached_access_token(self.account_id) .await .caused_by(trc::location!())?; diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs index d6834906..7af304b7 100644 --- a/crates/imap/src/core/mod.rs +++ b/crates/imap/src/core/mod.rs @@ -11,17 +11,17 @@ use std::{ }; use ahash::AHashMap; -use common::listener::{limiter::InFlight, ServerInstance, SessionStream}; +use common::{ + auth::AccessToken, + listener::{limiter::InFlight, ServerInstance, SessionStream}, +}; use dashmap::DashMap; use imap_proto::{ protocol::{list::Attribute, ProtocolVersion}, receiver::Receiver, Command, }; -use jmap::{ - auth::{rate_limit::ConcurrencyLimiters, AccessToken}, - JmapInstance, JMAP, -}; +use jmap::{auth::rate_limit::ConcurrencyLimiters, JmapInstance, JMAP}; use tokio::{ io::{ReadHalf, WriteHalf}, sync::watch, @@ -222,6 +222,7 @@ impl<T: SessionStream> State<T> { impl<T: SessionStream> SessionData<T> { pub async fn get_access_token(&self) -> trc::Result<Arc<AccessToken>> { self.jmap + .core .get_cached_access_token(self.account_id) .await .caused_by(trc::location!()) diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index 499da72b..b2bc1585 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::Instant}; -use common::listener::SessionStream; +use common::{auth::AccessToken, listener::SessionStream}; use directory::{backend::internal::PrincipalField, Permission, QueryBy}; use imap_proto::{ protocol::acl::{ @@ -16,10 +16,7 @@ use imap_proto::{ Command, ResponseCode, StatusResponse, }; -use jmap::{ - auth::{acl::EffectiveAcl, AccessToken}, - mailbox::set::SCHEMA, -}; +use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{ @@ -368,7 +365,11 @@ impl<T: SessionStream> Session<T> { } // Invalidate ACLs - data.jmap.inner.access_tokens.remove(&acl_account_id); + data.jmap + .core + .security + .access_tokens + .remove(&acl_account_id); trc::event!( Imap(trc::ImapEvent::SetAcl), diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index 92ba8916..88a31fec 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -88,7 +88,7 @@ impl<T: SessionStream> Session<T> { .validate_access_token("access_token", &token) .await { - Ok((account_id, _, _)) => self.jmap.get_access_token(account_id).await, + Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await, Err(err) => Err(err), } } @@ -127,7 +127,7 @@ impl<T: SessionStream> Session<T> { // Cache access token let access_token = Arc::new(access_token); - self.jmap.cache_access_token(access_token.clone()); + self.jmap.core.cache_access_token(access_token.clone()); // Create session self.state = State::Authenticated { diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs index 84043f62..da0ed0a1 100644 --- a/crates/imap/src/op/copy_move.rs +++ b/crates/imap/src/op/copy_move.rs @@ -243,6 +243,7 @@ impl<T: SessionStream> SessionData<T> { let dest_account_id = dest_mailbox.account_id; let dest_quota = self .jmap + .core .get_cached_access_token(dest_account_id) .await .imap_ctx(&arguments.tag, trc::location!())? diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs index a1a61355..362eba1f 100644 --- a/crates/jmap/src/api/event_source.rs +++ b/crates/jmap/src/api/event_source.rs @@ -9,6 +9,7 @@ use std::{ time::{Duration, Instant}, }; +use common::auth::AccessToken; use http_body_util::{combinators::BoxBody, StreamBody}; use hyper::{ body::{Bytes, Frame}, @@ -17,7 +18,7 @@ use hyper::{ use jmap_proto::types::type_state::DataType; use utils::map::bitmap::Bitmap; -use crate::{auth::AccessToken, JMAP, LONG_SLUMBER}; +use crate::{JMAP, LONG_SLUMBER}; use super::{HttpRequest, HttpResponse, HttpResponseBody, StateChangeResponse}; diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 120777db..d3053d4c 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -7,6 +7,7 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc}; use common::{ + auth::AccessToken, expr::{functions::ResolveVariable, *}, listener::{ServerInstance, SessionData, SessionManager, SessionStream}, manager::webadmin::Resource, @@ -30,7 +31,7 @@ use jmap_proto::{ }; use crate::{ - auth::{authenticate::HttpHeaders, oauth::OAuthMetadata, AccessToken}, + auth::{authenticate::HttpHeaders, oauth::OAuthMetadata}, blob::{DownloadResponse, UploadResponse}, services::state, JmapInstance, JMAP, diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs index 06ddaa59..09e0c383 100644 --- a/crates/jmap/src/api/management/dkim.rs +++ b/crates/jmap/src/api/management/dkim.rs @@ -6,7 +6,7 @@ use std::str::FromStr; -use common::config::smtp::auth::simple_pem_parse; +use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse}; use directory::{backend::internal::manage, Permission}; use hyper::Method; use mail_auth::{ @@ -23,7 +23,6 @@ use store::write::now; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/api/management/domain.rs b/crates/jmap/src/api/management/dns.rs index eba1312b..d2577007 100644 --- a/crates/jmap/src/api/management/domain.rs +++ b/crates/jmap/src/api/management/dns.rs @@ -4,8 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use directory::{ - backend::internal::manage::{self, ManageDirectory}, + backend::internal::manage::{self}, Permission, }; @@ -13,7 +14,7 @@ use hyper::Method; use serde::{Deserialize, Serialize}; use serde_json::json; use sha1::Digest; -use utils::{config::Config, url_params::UrlParams}; +use utils::config::Config; use x509_parser::parse_x509_certificate; use crate::{ @@ -22,7 +23,6 @@ use crate::{ management::dkim::{obtain_dkim_public_key, Algorithm}, HttpRequest, HttpResponse, JsonResponse, }, - auth::AccessToken, JMAP, }; @@ -37,43 +37,18 @@ struct DnsRecord { } impl JMAP { - pub async fn handle_manage_domain( + pub async fn handle_manage_dns( &self, req: &HttpRequest, path: Vec<&str>, access_token: &AccessToken, ) -> trc::Result<HttpResponse> { - match (path.get(1), req.method()) { - (None, &Method::GET) => { - // Validate the access token - access_token.assert_has_permission(Permission::DomainList)?; - - // List domains - let params = UrlParams::new(req.uri().query()); - let filter = params.get("filter"); - let page: usize = params.parse("page").unwrap_or(0); - let limit: usize = params.parse("limit").unwrap_or(0); - - let domains = self.core.storage.data.list_domains(filter).await?; - let (total, domains) = if limit > 0 { - let offset = page.saturating_sub(1) * limit; - ( - domains.len(), - domains.into_iter().skip(offset).take(limit).collect(), - ) - } else { - (domains.len(), domains) - }; - - Ok(JsonResponse::new(json!({ - "data": { - "items": domains, - "total": total, - }, - })) - .into_http_response()) - } - (Some(domain), &Method::GET) => { + match ( + path.get(1).copied().unwrap_or_default(), + path.get(2), + req.method(), + ) { + ("records", Some(domain), &Method::GET) => { // Validate the access token access_token.assert_has_permission(Permission::DomainGet)?; @@ -84,56 +59,6 @@ impl JMAP { })) .into_http_response()) } - (Some(domain), &Method::POST) => { - // Validate the access token - access_token.assert_has_permission(Permission::DomainCreate)?; - - // Create domain - let domain = decode_path_element(domain); - self.core - .storage - .data - .create_domain(domain.as_ref()) - .await?; - // Set default domain name if missing - if self - .core - .storage - .config - .get("lookup.default.domain") - .await? - .is_none() - { - self.core - .storage - .config - .set([("lookup.default.domain", domain.as_ref())]) - .await?; - } - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - (Some(domain), &Method::DELETE) => { - // Validate the access token - access_token.assert_has_permission(Permission::DomainDelete)?; - - // Delete domain - let domain = decode_path_element(domain); - self.core - .storage - .data - .delete_domain(domain.as_ref()) - .await?; - - Ok(JsonResponse::new(json!({ - "data": (), - })) - .into_http_response()) - } - _ => Err(trc::ResourceEvent::NotFound.into_err()), } } diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs index 80ea5548..b5b06e8f 100644 --- a/crates/jmap/src/api/management/enterprise/telemetry.rs +++ b/crates/jmap/src/api/management/enterprise/telemetry.rs @@ -13,9 +13,12 @@ use std::{ time::{Duration, Instant}, }; -use common::telemetry::{ - metrics::store::{Metric, MetricsStore}, - tracers::store::{TracingQuery, TracingStore}, +use common::{ + auth::AccessToken, + telemetry::{ + metrics::store::{Metric, MetricsStore}, + tracers::store::{TracingQuery, TracingStore}, + }, }; use directory::{backend::internal::manage, Permission}; use http_body_util::{combinators::BoxBody, StreamBody}; @@ -38,7 +41,6 @@ use crate::{ http::ToHttpResponse, management::Timestamp, HttpRequest, HttpResponse, HttpResponseBody, JsonResponse, }, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/api/management/enterprise/undelete.rs b/crates/jmap/src/api/management/enterprise/undelete.rs index 08fff17b..1fef99df 100644 --- a/crates/jmap/src/api/management/enterprise/undelete.rs +++ b/crates/jmap/src/api/management/enterprise/undelete.rs @@ -65,7 +65,7 @@ impl JMAP { .core .storage .data - .get_account_id(account_name) + .get_principal_id(account_name) .await? .ok_or_else(|| trc::ResourceEvent::NotFound.into_err())?; let mut deleted = self.core.list_deleted(account_id).await?; @@ -115,7 +115,7 @@ impl JMAP { .core .storage .data - .get_account_id(account_name) + .get_principal_id(account_name) .await? .ok_or_else(|| trc::ResourceEvent::NotFound.into_err())?; diff --git a/crates/jmap/src/api/management/log.rs b/crates/jmap/src/api/management/log.rs index 0bebbf48..94a87af2 100644 --- a/crates/jmap/src/api/management/log.rs +++ b/crates/jmap/src/api/management/log.rs @@ -5,6 +5,7 @@ use std::{ }; use chrono::DateTime; +use common::auth::AccessToken; use directory::{backend::internal::manage, Permission}; use rev_lines::RevLines; use serde::Serialize; @@ -14,7 +15,6 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs index 9972a86f..b88f78fa 100644 --- a/crates/jmap/src/api/management/mod.rs +++ b/crates/jmap/src/api/management/mod.rs @@ -5,7 +5,7 @@ */ pub mod dkim; -pub mod domain; +pub mod dns; #[cfg(feature = "enterprise")] pub mod enterprise; pub mod log; @@ -19,6 +19,7 @@ pub mod stores; use std::{borrow::Cow, str::FromStr, sync::Arc}; +use common::auth::AccessToken; use directory::{backend::internal::manage, Permission}; use hyper::Method; use mail_parser::DateTime; @@ -26,7 +27,7 @@ use serde::Serialize; use store::write::now; use super::{http::HttpSessionData, HttpRequest, HttpResponse}; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; #[derive(Serialize)] #[serde(tag = "error")] @@ -62,7 +63,7 @@ impl JMAP { self.handle_manage_principal(req, path, body, &access_token) .await } - "domain" => self.handle_manage_domain(req, path, &access_token).await, + "dns" => self.handle_manage_dns(req, path, &access_token).await, "store" => { self.handle_manage_store(req, path, body, session, &access_token) .await diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs index e47d56d4..61284c5c 100644 --- a/crates/jmap/src/api/management/principal.rs +++ b/crates/jmap/src/api/management/principal.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use common::auth::AccessToken; use directory::{ backend::internal::{ lookup::DirectoryStore, @@ -17,43 +18,17 @@ use directory::{ use hyper::{header, Method}; use serde_json::json; +use trc::AddContext; use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; use super::decode_path_element; #[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct PrincipalPayload { - #[serde(default)] - pub id: u32, - #[serde(rename = "type")] - pub typ: Type, - #[serde(default)] - pub quota: u64, - #[serde(rename = "usedQuota")] - #[serde(default)] - pub used_quota: u64, - #[serde(default)] - pub name: String, - #[serde(default)] - pub emails: Vec<String>, - #[serde(default)] - pub secrets: Vec<String>, - #[serde(rename = "memberOf")] - #[serde(default)] - pub member_of: Vec<String>, - #[serde(default)] - pub members: Vec<String>, - #[serde(default)] - pub description: Option<String>, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] #[serde(rename_all = "camelCase")] pub enum AccountAuthRequest { @@ -82,36 +57,62 @@ impl JMAP { ) -> trc::Result<HttpResponse> { match (path.get(1), req.method()) { (None, &Method::POST) => { - // Validate the access token - access_token.assert_has_permission(Permission::PrincipalCreate)?; - - // Make sure the current directory supports updates - self.assert_supported_directory()?; + let todo = "increment role list version + implement gossip"; - // Create principal + // Parse principal let principal = - serde_json::from_slice::<PrincipalPayload>(body.as_deref().unwrap_or_default()) + serde_json::from_slice::<Principal>(body.as_deref().unwrap_or_default()) .map_err(|err| { trc::EventType::Resource(trc::ResourceEvent::BadParameters) .from_json_error(err) })?; - let principal = Principal::new(principal.id, principal.typ) - .with_field(PrincipalField::Name, principal.name) - .with_field(PrincipalField::Secrets, principal.secrets) - .with_field(PrincipalField::Quota, principal.quota) - .with_field(PrincipalField::Emails, principal.emails) - .with_field(PrincipalField::MemberOf, principal.member_of) - .with_field(PrincipalField::Members, principal.members) - .with_opt_field(PrincipalField::Description, principal.description); + // Validate the access token + access_token.assert_has_permission(match principal.typ() { + Type::Individual => Permission::IndividualCreate, + Type::Group => Permission::GroupCreate, + Type::List => Permission::MailingListCreate, + Type::Domain => Permission::DomainCreate, + Type::Tenant => Permission::TenantCreate, + Type::Role => Permission::RoleCreate, + Type::Resource | Type::Location | Type::Other => Permission::PrincipalCreate, + })?; - Ok(JsonResponse::new(json!({ - "data": self + // Make sure the current directory supports updates + if matches!(principal.typ(), Type::Individual | Type::Group | Type::List) { + self.assert_supported_directory()?; + } + + // Validate tenant limits + #[cfg(feature = "enterprise")] + if self.core.is_enterprise_edition() { + if let Some(tenant_id) = access_token.tenant_id { + let tenant = self + .core + .storage + .data + .query(QueryBy::Id(tenant_id), false) + .await? + .ok_or_else(|| { + trc::ManageEvent::NotFound + .into_err() + .caused_by(trc::location!()) + })?; + + let todo = "check limits"; + } + } + + // Create principal + let result = self .core .storage .data - .create_account(principal) - .await?, + .create_principal(principal, access_token.tenant_id) + .await?; + + Ok(JsonResponse::new(json!({ + "data": result, })) .into_http_response()) } @@ -126,7 +127,28 @@ impl JMAP { let page: usize = params.parse("page").unwrap_or(0); let limit: usize = params.parse("limit").unwrap_or(0); - let accounts = self.core.storage.data.list_accounts(filter, typ).await?; + let mut tenant_id = access_token.tenant_id; + + #[cfg(feature = "enterprise")] + if self.core.is_enterprise_edition() && tenant_id.is_none() { + if let Some(tenant_name) = params.get("tenant") { + tenant_id = self + .core + .storage + .data + .get_principal_info(tenant_name) + .await? + .filter(|p| p.typ == Type::Tenant) + .map(|p| p.id); + } + } + + let accounts = self + .core + .storage + .data + .list_principals(filter, typ, tenant_id) + .await?; let (total, accounts) = if limit > 0 { let offset = page.saturating_sub(1) * limit; ( @@ -166,24 +188,42 @@ impl JMAP { .core .storage .data - .get_account_id(name.as_ref()) + .get_principal_id(name.as_ref()) .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; match *method { Method::GET => { - let principal = self + let mut principal = self .core .storage .data .query(QueryBy::Id(account_id), true) .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - let principal = self.core.storage.data.map_group_ids(principal).await?; + + // Map groups + if let Some(member_of) = principal.take_int_array(PrincipalField::MemberOf) + { + for principal_id in member_of { + if let Some(name) = self + .core + .storage + .data + .get_principal_name(principal_id as u32) + .await + .caused_by(trc::location!())? + { + principal.append_str(PrincipalField::MemberOf, name); + } + } + } // Obtain quota usage - let mut principal = PrincipalPayload::from(principal); - principal.used_quota = self.get_used_quota(account_id).await? as u64; + principal.set( + PrincipalField::UsedQuota, + self.get_used_quota(account_id).await? as u64, + ); // Obtain member names for member_id in self.core.storage.data.get_members(account_id).await? { @@ -194,11 +234,10 @@ impl JMAP { .query(QueryBy::Id(member_id), false) .await? { - principal.members.push( - member_principal - .take_str(PrincipalField::Name) - .unwrap_or_default(), - ); + if let Some(name) = member_principal.take_str(PrincipalField::Name) + { + principal.append_str(PrincipalField::Members, name); + } } } @@ -215,7 +254,7 @@ impl JMAP { self.core .storage .data - .delete_account(QueryBy::Id(account_id)) + .delete_principal(QueryBy::Id(account_id)) .await?; // Remove entries from cache self.inner.sessions.retain(|_, id| id.item != account_id); @@ -251,7 +290,7 @@ impl JMAP { self.core .storage .data - .update_account(QueryBy::Id(account_id), changes) + .update_principal(QueryBy::Id(account_id), changes) .await?; if is_password_change { // Remove entries from cache @@ -412,7 +451,7 @@ impl JMAP { self.core .storage .data - .update_account(QueryBy::Id(access_token.primary_id()), actions) + .update_principal(QueryBy::Id(access_token.primary_id()), actions) .await?; // Remove entries from cache @@ -446,26 +485,3 @@ impl JMAP { ))) } } - -impl From<Principal> for PrincipalPayload { - fn from(mut principal: Principal) -> Self { - PrincipalPayload { - id: principal.id(), - typ: principal.typ(), - quota: principal.quota(), - name: principal.take_str(PrincipalField::Name).unwrap_or_default(), - emails: principal - .take_str_array(PrincipalField::Emails) - .unwrap_or_default(), - member_of: principal - .take_str_array(PrincipalField::MemberOf) - .unwrap_or_default(), - description: principal.take_str(PrincipalField::Description), - secrets: principal - .take_str_array(PrincipalField::Secrets) - .unwrap_or_default(), - used_quota: 0, - members: Vec::new(), - } - } -} diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs index 46fa0616..db2e86db 100644 --- a/crates/jmap/src/api/management/queue.rs +++ b/crates/jmap/src/api/management/queue.rs @@ -5,6 +5,7 @@ */ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use common::auth::AccessToken; use directory::Permission; use hyper::Method; use mail_auth::{ @@ -24,7 +25,6 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/api/management/reload.rs b/crates/jmap/src/api/management/reload.rs index c8410fc2..f396e1b6 100644 --- a/crates/jmap/src/api/management/reload.rs +++ b/crates/jmap/src/api/management/reload.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use directory::Permission; use hyper::Method; use serde_json::json; @@ -11,7 +12,6 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, services::housekeeper::Event, JMAP, }; diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs index a53db1f9..4afe1e2f 100644 --- a/crates/jmap/src/api/management/report.rs +++ b/crates/jmap/src/api/management/report.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use directory::Permission; use hyper::Method; use mail_auth::report::{ @@ -20,7 +21,6 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs index 1d46799a..6844c503 100644 --- a/crates/jmap/src/api/management/settings.rs +++ b/crates/jmap/src/api/management/settings.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use directory::Permission; use hyper::Method; use serde_json::json; @@ -12,7 +13,6 @@ use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams}; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/api/management/sieve.rs b/crates/jmap/src/api/management/sieve.rs index 8e56ed11..eb918185 100644 --- a/crates/jmap/src/api/management/sieve.rs +++ b/crates/jmap/src/api/management/sieve.rs @@ -6,7 +6,7 @@ use std::time::SystemTime; -use common::{scripts::ScriptModification, IntoString}; +use common::{auth::AccessToken, scripts::ScriptModification, IntoString}; use directory::Permission; use hyper::Method; use serde_json::json; @@ -16,7 +16,6 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/api/management/stores.rs b/crates/jmap/src/api/management/stores.rs index 038f7286..f4c95ada 100644 --- a/crates/jmap/src/api/management/stores.rs +++ b/crates/jmap/src/api/management/stores.rs @@ -5,7 +5,7 @@ */ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use common::manager::webadmin::Resource; +use common::{auth::AccessToken, manager::webadmin::Resource}; use directory::{ backend::internal::manage::{self, ManageDirectory}, Permission, @@ -19,7 +19,6 @@ use crate::{ http::{HttpSessionData, ToHttpResponse}, HttpRequest, HttpResponse, JsonResponse, }, - auth::AccessToken, services::housekeeper::{Event, PurgeType}, JMAP, }; @@ -128,7 +127,7 @@ impl JMAP { self.core .storage .data - .get_account_id(decode_path_element(id).as_ref()) + .get_principal_id(decode_path_element(id).as_ref()) .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())? .into() diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 59f2d81a..4b24eda9 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -6,6 +6,7 @@ use std::{sync::Arc, time::Instant}; +use common::auth::AccessToken; use jmap_proto::{ method::{ get, query, @@ -17,7 +18,7 @@ use jmap_proto::{ }; use trc::JmapEvent; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; use super::http::HttpSessionData; diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 3a426e9b..1480487f 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use common::auth::AccessToken; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ request::capability::{Capability, Session}, @@ -13,7 +14,7 @@ use jmap_proto::{ }; use trc::AddContext; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn handle_session_resource( diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index d9ec2d2b..c574da61 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ error::set::SetError, @@ -22,63 +23,11 @@ use store::{ ValueKey, }; use trc::AddContext; -use utils::map::bitmap::{Bitmap, BitmapItem}; +use utils::map::bitmap::Bitmap; use crate::JMAP; -use super::AccessToken; - impl JMAP { - pub async fn update_access_token( - &self, - mut access_token: AccessToken, - ) -> trc::Result<AccessToken> { - for &grant_account_id in [access_token.primary_id] - .iter() - .chain(access_token.member_of.clone().iter()) - { - for acl_item in self - .core - .storage - .data - .acl_query(AclQuery::HasAccess { grant_account_id }) - .await - .caused_by(trc::location!())? - { - if !access_token.is_member(acl_item.to_account_id) { - let acl = Bitmap::<Acl>::from(acl_item.permissions); - let collection = Collection::from(acl_item.to_collection); - if !collection.is_valid() { - return Err(trc::StoreEvent::DataCorruption - .ctx(trc::Key::Reason, "Corrupted collection found in ACL key.") - .details(format!("{acl_item:?}")) - .account_id(grant_account_id) - .caused_by(trc::location!())); - } - - let mut collections: Bitmap<Collection> = Bitmap::new(); - if acl.contains(Acl::Read) || acl.contains(Acl::Administer) { - collections.insert(collection); - } - if collection == Collection::Mailbox - && (acl.contains(Acl::ReadItems) || acl.contains(Acl::Administer)) - { - collections.insert(Collection::Email); - } - - if !collections.is_empty() { - access_token - .access_to - .get_mut_or_insert_with(acl_item.to_account_id, Bitmap::new) - .union(&collections); - } - } - } - } - - Ok(access_token) - } - pub async fn shared_documents( &self, access_token: &AccessToken, @@ -344,7 +293,7 @@ impl JMAP { current: &Option<HashedValue<Object<Value>>>, ) { if let Value::Acl(acl_changes) = changes.get(&Property::Acl) { - let access_tokens = &self.inner.access_tokens; + let access_tokens = &self.core.security.access_tokens; if let Some(Value::Acl(acl_current)) = current .as_ref() .and_then(|current| current.inner.properties.get(&Property::Acl)) diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 1d2168df..9bb5cc2d 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -7,7 +7,7 @@ use std::{net::IpAddr, sync::Arc, time::Instant}; use common::listener::limiter::InFlight; -use directory::{Principal, QueryBy}; +use directory::Permission; use hyper::header; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; @@ -18,7 +18,7 @@ use crate::{ JMAP, }; -use super::AccessToken; +use common::auth::AccessToken; impl JMAP { pub async fn authenticate_headers( @@ -28,7 +28,7 @@ impl JMAP { ) -> trc::Result<(InFlight, Arc<AccessToken>)> { if let Some((mechanism, token)) = req.authorization() { let access_token = if let Some(account_id) = self.inner.sessions.get_with_ttl(token) { - self.get_cached_access_token(account_id).await? + self.core.get_cached_access_token(account_id).await? } else { let access_token = if mechanism.eq_ignore_ascii_case("basic") { // Enforce rate limit for authentication requests @@ -64,7 +64,7 @@ impl JMAP { let (account_id, _, _) = self.validate_access_token("access_token", token).await?; - self.get_access_token(account_id).await? + self.core.get_access_token(account_id).await? } else { // Enforce anonymous rate limit self.is_anonymous_allowed(&session.remote_ip).await?; @@ -78,7 +78,7 @@ impl JMAP { // Cache session let access_token = Arc::new(access_token); self.cache_session(token.to_string(), &access_token); - self.cache_access_token(access_token.clone()); + self.core.cache_access_token(access_token.clone()); access_token }; @@ -105,27 +105,6 @@ impl JMAP { ); } - pub fn cache_access_token(&self, access_token: Arc<AccessToken>) { - self.inner.access_tokens.insert_with_ttl( - access_token.primary_id(), - access_token, - Instant::now() + self.core.jmap.session_cache_ttl, - ); - } - - pub async fn get_cached_access_token(&self, primary_id: u32) -> trc::Result<Arc<AccessToken>> { - if let Some(access_token) = self.inner.access_tokens.get_with_ttl(&primary_id) { - Ok(access_token) - } else { - // Refresh ACL token - self.get_access_token(primary_id).await.map(|access_token| { - let access_token = Arc::new(access_token); - self.cache_access_token(access_token.clone()); - access_token - }) - } - } - pub async fn authenticate_plain( &self, username: &str, @@ -147,7 +126,15 @@ impl JMAP { ) .await { - Ok(principal) => Ok(AccessToken::new(principal)), + Ok(principal) => self + .core + .build_access_token(principal) + .await + .and_then(|token| { + token + .assert_has_permission(Permission::Authenticate) + .map(|_| token) + }), Err(err) => { if !err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) { let _ = self.is_auth_allowed_hard(&remote_ip).await; @@ -156,33 +143,6 @@ impl JMAP { } } } - - pub async fn get_access_token(&self, account_id: u32) -> trc::Result<AccessToken> { - let err = match self - .core - .storage - .directory - .query(QueryBy::Id(account_id), true) - .await - { - Ok(Some(principal)) => { - return self.update_access_token(AccessToken::new(principal)).await - } - Ok(None) => Err(trc::AuthEvent::Error - .into_err() - .details("Account not found.") - .caused_by(trc::location!())), - Err(err) => Err(err), - }; - - match &self.core.jmap.fallback_admin { - Some((_, secret)) if account_id == u32::MAX => { - self.update_access_token(AccessToken::new(Principal::fallback_admin(secret))) - .await - } - _ => err, - } - } } pub trait HttpHeaders { diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs index 3e5c9f66..53e062a4 100644 --- a/crates/jmap/src/auth/mod.rs +++ b/crates/jmap/src/auth/mod.rs @@ -4,304 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; - use aes_gcm_siv::{ aead::{generic_array::GenericArray, Aead}, AeadInPlace, Aes256GcmSiv, KeyInit, Nonce, }; -use directory::{backend::internal::PrincipalField, Permission, Principal, PERMISSION_BITMAP_SIZE}; -use jmap_proto::{ - request::RequestMethod, - types::{collection::Collection, id::Id}, -}; use store::blake3; -use trc::ipc::bitset::Bitset; -use utils::map::{bitmap::Bitmap, vec_map::VecMap}; pub mod acl; pub mod authenticate; pub mod oauth; pub mod rate_limit; -#[derive(Debug, Clone, Default)] -pub struct AccessToken { - pub primary_id: u32, - pub member_of: Vec<u32>, - pub access_to: VecMap<u32, Bitmap<Collection>>, - pub name: String, - pub description: Option<String>, - pub quota: u64, - pub permissions: Bitset<PERMISSION_BITMAP_SIZE>, -} - -impl AccessToken { - pub fn new(mut principal: Principal) -> Self { - Self { - primary_id: principal.id(), - member_of: principal - .iter_int(PrincipalField::MemberOf) - .map(|v| v as u32) - .collect(), - access_to: VecMap::new(), - name: principal.take_str(PrincipalField::Name).unwrap_or_default(), - description: principal.take_str(PrincipalField::Description), - quota: principal.quota(), - permissions: Default::default(), - } - } - - pub fn from_id(primary_id: u32) -> Self { - Self { - primary_id, - ..Default::default() - } - } - - pub fn with_access_to(self, access_to: VecMap<u32, Bitmap<Collection>>) -> Self { - Self { access_to, ..self } - } - - pub fn with_permission(mut self, permission: Permission) -> Self { - self.permissions.set(permission.id()); - self - } - - pub fn state(&self) -> u32 { - // Hash state - let mut s = DefaultHasher::new(); - self.member_of.hash(&mut s); - self.access_to.hash(&mut s); - s.finish() as u32 - } - - pub fn primary_id(&self) -> u32 { - self.primary_id - } - - pub fn secondary_ids(&self) -> impl Iterator<Item = &u32> { - self.member_of - .iter() - .chain(self.access_to.iter().map(|(id, _)| id)) - } - - pub fn is_member(&self, account_id: u32) -> bool { - self.primary_id == account_id - || self.member_of.contains(&account_id) - || self.has_permission(Permission::Impersonate) - } - - pub fn is_primary_id(&self, account_id: u32) -> bool { - self.primary_id == account_id - } - - #[inline(always)] - pub fn has_permission(&self, permission: Permission) -> bool { - self.permissions.get(permission.id()) - } - - pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { - if self.has_permission(permission) { - Ok(()) - } else { - Err(trc::SecurityEvent::Unauthorized - .into_err() - .details(permission.name())) - } - } - - pub fn permissions(&self) -> Vec<Permission> { - let mut permissions = Vec::new(); - for (block_num, bytes) in self.permissions.inner().iter().enumerate() { - let mut bytes = *bytes; - - while bytes != 0 { - let item = std::mem::size_of::<usize>() - 1 - bytes.leading_zeros() as usize; - bytes ^= 1 << item; - permissions.push( - Permission::from_id((block_num * std::mem::size_of::<usize>()) + item).unwrap(), - ); - } - } - permissions - } - - pub fn is_shared(&self, account_id: u32) -> bool { - !self.is_member(account_id) && self.access_to.iter().any(|(id, _)| *id == account_id) - } - - pub fn shared_accounts(&self, collection: impl Into<Collection>) -> impl Iterator<Item = &u32> { - let collection = collection.into(); - self.member_of - .iter() - .chain(self.access_to.iter().filter_map(move |(id, cols)| { - if cols.contains(collection) { - id.into() - } else { - None - } - })) - } - - pub fn has_access(&self, to_account_id: u32, to_collection: impl Into<Collection>) -> bool { - let to_collection = to_collection.into(); - self.is_member(to_account_id) - || self.access_to.iter().any(|(id, collections)| { - *id == to_account_id && collections.contains(to_collection) - }) - } - - pub fn assert_has_access( - &self, - to_account_id: Id, - to_collection: Collection, - ) -> trc::Result<&Self> { - if self.has_access(to_account_id.document_id(), to_collection) { - Ok(self) - } else { - Err(trc::JmapEvent::Forbidden.into_err().details(format!( - "You do not have access to account {}", - to_account_id - ))) - } - } - - pub fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self> { - if self.is_member(account_id.document_id()) { - Ok(self) - } else { - Err(trc::JmapEvent::Forbidden - .into_err() - .details(format!("You are not an owner of account {}", account_id))) - } - } - - pub fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()> { - let permission = match request { - RequestMethod::Get(m) => match &m.arguments { - jmap_proto::method::get::RequestArguments::Email(_) => Permission::JmapEmailGet, - jmap_proto::method::get::RequestArguments::Mailbox => Permission::JmapMailboxGet, - jmap_proto::method::get::RequestArguments::Thread => Permission::JmapThreadGet, - jmap_proto::method::get::RequestArguments::Identity => Permission::JmapIdentityGet, - jmap_proto::method::get::RequestArguments::EmailSubmission => { - Permission::JmapEmailSubmissionGet - } - jmap_proto::method::get::RequestArguments::PushSubscription => { - Permission::JmapPushSubscriptionGet - } - jmap_proto::method::get::RequestArguments::SieveScript => { - Permission::JmapSieveScriptGet - } - jmap_proto::method::get::RequestArguments::VacationResponse => { - Permission::JmapVacationResponseGet - } - jmap_proto::method::get::RequestArguments::Principal => { - Permission::JmapPrincipalGet - } - jmap_proto::method::get::RequestArguments::Quota => Permission::JmapQuotaGet, - jmap_proto::method::get::RequestArguments::Blob(_) => Permission::JmapBlobGet, - }, - RequestMethod::Set(m) => match &m.arguments { - jmap_proto::method::set::RequestArguments::Email => Permission::JmapEmailSet, - jmap_proto::method::set::RequestArguments::Mailbox(_) => Permission::JmapMailboxSet, - jmap_proto::method::set::RequestArguments::Identity => Permission::JmapIdentitySet, - jmap_proto::method::set::RequestArguments::EmailSubmission(_) => { - Permission::JmapEmailSubmissionSet - } - jmap_proto::method::set::RequestArguments::PushSubscription => { - Permission::JmapPushSubscriptionSet - } - jmap_proto::method::set::RequestArguments::SieveScript(_) => { - Permission::JmapSieveScriptSet - } - jmap_proto::method::set::RequestArguments::VacationResponse => { - Permission::JmapVacationResponseSet - } - }, - RequestMethod::Changes(m) => match m.arguments { - jmap_proto::method::changes::RequestArguments::Email => { - Permission::JmapEmailChanges - } - jmap_proto::method::changes::RequestArguments::Mailbox => { - Permission::JmapMailboxChanges - } - jmap_proto::method::changes::RequestArguments::Thread => { - Permission::JmapThreadChanges - } - jmap_proto::method::changes::RequestArguments::Identity => { - Permission::JmapIdentityChanges - } - jmap_proto::method::changes::RequestArguments::EmailSubmission => { - Permission::JmapEmailSubmissionChanges - } - jmap_proto::method::changes::RequestArguments::Quota => { - Permission::JmapQuotaChanges - } - }, - RequestMethod::Copy(m) => match m.arguments { - jmap_proto::method::copy::RequestArguments::Email => Permission::JmapEmailCopy, - }, - RequestMethod::CopyBlob(_) => Permission::JmapBlobCopy, - RequestMethod::ImportEmail(_) => Permission::JmapEmailImport, - RequestMethod::ParseEmail(_) => Permission::JmapEmailParse, - RequestMethod::QueryChanges(m) => match m.arguments { - jmap_proto::method::query::RequestArguments::Email(_) => { - Permission::JmapEmailQueryChanges - } - jmap_proto::method::query::RequestArguments::Mailbox(_) => { - Permission::JmapMailboxQueryChanges - } - jmap_proto::method::query::RequestArguments::EmailSubmission => { - Permission::JmapEmailSubmissionQueryChanges - } - jmap_proto::method::query::RequestArguments::SieveScript => { - Permission::JmapSieveScriptQueryChanges - } - jmap_proto::method::query::RequestArguments::Principal => { - Permission::JmapPrincipalQueryChanges - } - jmap_proto::method::query::RequestArguments::Quota => { - Permission::JmapQuotaQueryChanges - } - }, - RequestMethod::Query(m) => match m.arguments { - jmap_proto::method::query::RequestArguments::Email(_) => Permission::JmapEmailQuery, - jmap_proto::method::query::RequestArguments::Mailbox(_) => { - Permission::JmapMailboxQuery - } - jmap_proto::method::query::RequestArguments::EmailSubmission => { - Permission::JmapEmailSubmissionQuery - } - jmap_proto::method::query::RequestArguments::SieveScript => { - Permission::JmapSieveScriptQuery - } - jmap_proto::method::query::RequestArguments::Principal => { - Permission::JmapPrincipalQuery - } - jmap_proto::method::query::RequestArguments::Quota => Permission::JmapQuotaQuery, - }, - RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, - RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, - RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup, - RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload, - RequestMethod::Echo(_) => Permission::JmapEcho, - RequestMethod::Error(_) => return Ok(()), - }; - - if self.has_permission(permission) { - Ok(()) - } else { - Err(trc::JmapEvent::Forbidden - .into_err() - .details("You are not authorized to perform this action")) - } - } -} - pub struct SymmetricEncrypt { aes: Aes256GcmSiv, } diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs index b25c13cf..78f53454 100644 --- a/crates/jmap/src/auth/oauth/auth.rs +++ b/crates/jmap/src/auth/oauth/auth.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use common::auth::AccessToken; use rand::distributions::Standard; use serde_json::json; use store::{ @@ -16,7 +17,7 @@ use store::{ use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - auth::{oauth::OAuthStatus, AccessToken}, + auth::oauth::OAuthStatus, JMAP, }; diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index 97548d20..25b1ec9b 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -12,7 +12,7 @@ use trc::AddContext; use crate::JMAP; -use super::AccessToken; +use common::auth::AccessToken; pub struct ConcurrencyLimiters { pub concurrent_requests: ConcurrencyLimiter, diff --git a/crates/jmap/src/blob/copy.rs b/crates/jmap/src/blob/copy.rs index 160eb28f..635b6bc0 100644 --- a/crates/jmap/src/blob/copy.rs +++ b/crates/jmap/src/blob/copy.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::copy::{CopyBlobRequest, CopyBlobResponse}, @@ -16,7 +17,7 @@ use store::{ }; use utils::map::vec_map::VecMap; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn blob_copy( diff --git a/crates/jmap/src/blob/download.rs b/crates/jmap/src/blob/download.rs index ca2f8093..8b289e59 100644 --- a/crates/jmap/src/blob/download.rs +++ b/crates/jmap/src/blob/download.rs @@ -6,6 +6,7 @@ use std::ops::Range; +use common::auth::AccessToken; use jmap_proto::types::{ acl::Acl, blob::{BlobId, BlobSection}, @@ -19,7 +20,7 @@ use store::BlobClass; use trc::AddContext; use utils::BlobHash; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { #[allow(clippy::blocks_in_conditions)] diff --git a/crates/jmap/src/blob/get.rs b/crates/jmap/src/blob/get.rs index 1ace52c0..3add516d 100644 --- a/crates/jmap/src/blob/get.rs +++ b/crates/jmap/src/blob/get.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::{ get::{GetRequest, GetResponse}, @@ -25,7 +26,7 @@ use sha2::{Sha256, Sha512}; use store::BlobClass; use utils::map::vec_map::VecMap; -use crate::{auth::AccessToken, mailbox::UidMailbox, JMAP}; +use crate::{mailbox::UidMailbox, JMAP}; impl JMAP { pub async fn blob_get( diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index 867b6cb7..dfa902dc 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use common::auth::AccessToken; use directory::Permission; use jmap_proto::{ error::set::SetError, @@ -22,7 +23,7 @@ use store::{ use trc::AddContext; use utils::BlobHash; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; use super::UploadResponse; diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs index e1bc9fa2..cd68d95a 100644 --- a/crates/jmap/src/changes/get.rs +++ b/crates/jmap/src/changes/get.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::changes::{ChangesRequest, ChangesResponse, RequestArguments}, types::{collection::Collection, property::Property, state::State}, @@ -11,7 +12,7 @@ use jmap_proto::{ use store::query::log::{Change, Changes, Query}; use trc::AddContext; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn changes( diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index bad2925d..a6d31831 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -4,13 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::method::{ changes::{self, ChangesRequest}, query::{self, QueryRequest}, query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse}, }; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn query_changes( diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index 7be5e780..2fcaaaae 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ error::set::SetError, method::{ @@ -41,7 +42,7 @@ use store::{ use trc::AddContext; use utils::map::vec_map::VecMap; -use crate::{api::http::HttpSessionData, auth::AccessToken, mailbox::UidMailbox, JMAP}; +use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP}; use super::{ index::{EmailIndexBuilder, TrimTextValue, VisitValues, MAX_ID_LENGTH, MAX_SORT_FIELD_LENGTH}, diff --git a/crates/jmap/src/email/crypto.rs b/crates/jmap/src/email/crypto.rs index 3c864d0f..815de86d 100644 --- a/crates/jmap/src/email/crypto.rs +++ b/crates/jmap/src/email/crypto.rs @@ -8,10 +8,10 @@ use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor, sync::Ar use crate::{ api::{http::ToHttpResponse, HttpResponse, JsonResponse}, - auth::AccessToken, JMAP, }; use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit}; +use common::auth::AccessToken; use directory::backend::internal::manage; use jmap_proto::types::{collection::Collection, property::Property}; use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary}; diff --git a/crates/jmap/src/email/get.rs b/crates/jmap/src/email/get.rs index 1709e36c..cc891d37 100644 --- a/crates/jmap/src/email/get.rs +++ b/crates/jmap/src/email/get.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::{email::GetArguments, Object}, @@ -22,7 +23,7 @@ use mail_parser::HeaderName; use store::{write::Bincode, BlobClass}; use trc::{AddContext, StoreEvent}; -use crate::{auth::AccessToken, email::headers::HeaderToValue, mailbox::UidMailbox, JMAP}; +use crate::{email::headers::HeaderToValue, mailbox::UidMailbox, JMAP}; use super::{ body::{ToBodyPart, TruncateBody}, diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index 4fb94ad9..f5b83f70 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::import::{ImportEmailRequest, ImportEmailResponse}, @@ -19,7 +20,7 @@ use jmap_proto::{ use mail_parser::MessageParser; use utils::map::vec_map::VecMap; -use crate::{api::http::HttpSessionData, auth::AccessToken, JMAP}; +use crate::{api::http::HttpSessionData, JMAP}; use super::ingest::{IngestEmail, IngestSource}; diff --git a/crates/jmap/src/email/parse.rs b/crates/jmap/src/email/parse.rs index 81578cec..83ec8162 100644 --- a/crates/jmap/src/email/parse.rs +++ b/crates/jmap/src/email/parse.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::parse::{ParseEmailRequest, ParseEmailResponse}, object::Object, @@ -14,7 +15,7 @@ use mail_parser::{ }; use utils::map::vec_map::VecMap; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; use super::{ body::{ToBodyPart, TruncateBody}, diff --git a/crates/jmap/src/email/query.rs b/crates/jmap/src/email/query.rs index 7d59b5e3..56942279 100644 --- a/crates/jmap/src/email/query.rs +++ b/crates/jmap/src/email/query.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty}, object::email::QueryArguments, @@ -19,7 +20,7 @@ use store::{ ValueKey, }; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn email_query( diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index c5658d5f..db144262 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -6,6 +6,7 @@ use std::{borrow::Cow, collections::HashMap, slice::IterMut}; +use common::auth::AccessToken; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -40,7 +41,7 @@ use store::{ }; use trc::AddContext; -use crate::{api::http::HttpSessionData, auth::AccessToken, mailbox::UidMailbox, JMAP}; +use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP}; use super::{ headers::{BuildHeader, ValueToHeader}, diff --git a/crates/jmap/src/email/snippet.rs b/crates/jmap/src/email/snippet.rs index ed6c5259..dbb6e67c 100644 --- a/crates/jmap/src/email/snippet.rs +++ b/crates/jmap/src/email/snippet.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::{ query::Filter, @@ -15,7 +16,7 @@ use mail_parser::{decoders::html::html_to_text, GetHeader, HeaderName, PartType} use nlp::language::{search_snippet::generate_snippet, stemmer::Stemmer, Language}; use store::{backend::MAX_TOKEN_LENGTH, write::Bincode}; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; use super::metadata::{MessageMetadata, MetadataPartType}; diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 5eb6beb6..e07d4740 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -11,8 +11,10 @@ use std::{ time::Duration, }; -use auth::{rate_limit::ConcurrencyLimiters, AccessToken}; -use common::{manager::webadmin::WebAdminManager, Core, DeliveryEvent, SharedCore}; +use auth::rate_limit::ConcurrencyLimiters; +use common::{ + auth::AccessToken, manager::webadmin::WebAdminManager, Core, DeliveryEvent, SharedCore, +}; use dashmap::DashMap; use directory::QueryBy; use email::cache::Threads; @@ -87,7 +89,6 @@ pub struct JmapInstance { pub struct Inner { pub sessions: TtlDashMap<String, u32>, - pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>, pub snowflake_id: SnowflakeIdGenerator, pub webadmin: WebAdminManager, pub config_version: AtomicU8, @@ -121,7 +122,6 @@ impl JMAP { let inner = Inner { webadmin: WebAdminManager::new(), sessions: TtlDashMap::with_capacity(capacity, shard_amount), - access_tokens: TtlDashMap::with_capacity(capacity, shard_amount), snowflake_id: config .property::<u64>("cluster.node-id") .map(SnowflakeIdGenerator::with_node_id) diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs index 852613a1..7b7ac2ed 100644 --- a/crates/jmap/src/mailbox/get.rs +++ b/crates/jmap/src/mailbox/get.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -12,10 +13,7 @@ use jmap_proto::{ use store::{ahash::AHashSet, query::Filter, roaring::RoaringBitmap}; use trc::AddContext; -use crate::{ - auth::{acl::EffectiveAcl, AccessToken}, - JMAP, -}; +use crate::{auth::acl::EffectiveAcl, JMAP}; impl JMAP { pub async fn mailbox_get( diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index 3ad0718e..16295772 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty}, object::{mailbox::QueryArguments, Object}, @@ -15,7 +16,7 @@ use store::{ roaring::RoaringBitmap, }; -use crate::{auth::AccessToken, UpdateResults, JMAP}; +use crate::{UpdateResults, JMAP}; impl JMAP { pub async fn mailbox_query( diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index eea8fb13..1a150171 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::config::jmap::settings::SpecialUse; +use common::{auth::AccessToken, config::jmap::settings::SpecialUse}; use directory::Permission; use jmap_proto::{ error::set::{SetError, SetErrorType}, @@ -36,10 +36,7 @@ use store::{ }; use trc::AddContext; -use crate::{ - auth::{acl::EffectiveAcl, AccessToken}, - JMAP, -}; +use crate::{auth::acl::EffectiveAcl, JMAP}; #[allow(unused_imports)] use super::{UidMailbox, INBOX_ID, JUNK_ID, TRASH_ID}; diff --git a/crates/jmap/src/push/get.rs b/crates/jmap/src/push/get.rs index 2feb52c6..175b8be6 100644 --- a/crates/jmap/src/push/get.rs +++ b/crates/jmap/src/push/get.rs @@ -5,6 +5,7 @@ */ use base64::{engine::general_purpose, Engine}; +use common::auth::AccessToken; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -16,7 +17,7 @@ use store::{ }; use utils::map::bitmap::Bitmap; -use crate::{auth::AccessToken, services::state, JMAP}; +use crate::{services::state, JMAP}; use super::{EncryptionKeys, PushSubscription, UpdateSubscription}; diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs index e6f44b67..22f56f3e 100644 --- a/crates/jmap/src/push/set.rs +++ b/crates/jmap/src/push/set.rs @@ -5,6 +5,7 @@ */ use base64::{engine::general_purpose, Engine}; +use common::auth::AccessToken; use jmap_proto::{ error::set::SetError, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -23,7 +24,7 @@ use store::{ write::{now, BatchBuilder, F_CLEAR, F_VALUE}, }; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; const EXPIRES_MAX: i64 = 7 * 24 * 3600; // 7 days const VERIFICATION_CODE_LEN: usize = 32; diff --git a/crates/jmap/src/quota/get.rs b/crates/jmap/src/quota/get.rs index 7449e0c6..193c77fb 100644 --- a/crates/jmap/src/quota/get.rs +++ b/crates/jmap/src/quota/get.rs @@ -4,13 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, types::{id::Id, property::Property, state::State, type_state::DataType, value::Value}, }; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn quota_get( diff --git a/crates/jmap/src/quota/query.rs b/crates/jmap/src/quota/query.rs index a3e7dedb..40d65a52 100644 --- a/crates/jmap/src/quota/query.rs +++ b/crates/jmap/src/quota/query.rs @@ -4,12 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ method::query::{QueryRequest, QueryResponse, RequestArguments}, types::{id::Id, state::State}, }; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn quota_query( diff --git a/crates/jmap/src/services/housekeeper.rs b/crates/jmap/src/services/housekeeper.rs index 87967a6e..ed05f8c6 100644 --- a/crates/jmap/src/services/housekeeper.rs +++ b/crates/jmap/src/services/housekeeper.rs @@ -398,9 +398,12 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } ActionClass::Session => { let inner = core.jmap_inner.clone(); + let core = core_.clone(); + tokio::spawn(async move { trc::event!(Housekeeper(HousekeeperEvent::PurgeSessions)); inner.purge(); + core.security.access_tokens.cleanup(); }); queue.schedule( Instant::now() @@ -685,7 +688,6 @@ impl PartialOrd for Action { impl Inner { pub fn purge(&self) { self.sessions.cleanup(); - self.access_tokens.cleanup(); self.concurrency_limiter .retain(|_, limiter| limiter.is_active()); } diff --git a/crates/jmap/src/services/state.rs b/crates/jmap/src/services/state.rs index 2ab5e916..6f448d50 100644 --- a/crates/jmap/src/services/state.rs +++ b/crates/jmap/src/services/state.rs @@ -101,7 +101,11 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve } Event::UpdateSharedAccounts { account_id } => { // Obtain account membership and shared mailboxes - let acl = match JMAP::from(core.clone()).get_access_token(account_id).await { + let acl = match JMAP::from(core.clone()) + .core + .get_access_token(account_id) + .await + { Ok(result) => result, Err(err) => { trc::error!(err diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 5faa2b82..d18903e3 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{SetRequest, SetResponse}, @@ -33,7 +34,7 @@ use store::{ BlobClass, }; -use crate::{api::http::HttpSessionData, auth::AccessToken, JMAP}; +use crate::{api::http::HttpSessionData, JMAP}; struct SetContext<'x> { account_id: u32, diff --git a/crates/jmap/src/sieve/validate.rs b/crates/jmap/src/sieve/validate.rs index 4fd9fce8..2413a572 100644 --- a/crates/jmap/src/sieve/validate.rs +++ b/crates/jmap/src/sieve/validate.rs @@ -4,12 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse}, }; -use crate::{auth::AccessToken, JMAP}; +use crate::JMAP; impl JMAP { pub async fn sieve_script_validate( diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs index 3a3c0109..20fbac55 100644 --- a/crates/jmap/src/websocket/stream.rs +++ b/crates/jmap/src/websocket/stream.rs @@ -6,6 +6,7 @@ use std::{sync::Arc, time::Instant}; +use common::auth::AccessToken; use futures_util::{SinkExt, StreamExt}; use hyper::upgrade::Upgraded; use hyper_util::rt::TokioIo; @@ -23,7 +24,6 @@ use utils::map::bitmap::Bitmap; use crate::{ api::http::{HttpSessionData, ToRequestError}, - auth::AccessToken, JMAP, }; diff --git a/crates/jmap/src/websocket/upgrade.rs b/crates/jmap/src/websocket/upgrade.rs index a0fb2515..ee9aef1a 100644 --- a/crates/jmap/src/websocket/upgrade.rs +++ b/crates/jmap/src/websocket/upgrade.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use common::auth::AccessToken; use hyper::StatusCode; use hyper_util::rt::TokioIo; use tokio_tungstenite::WebSocketStream; @@ -14,7 +15,6 @@ use tungstenite::{handshake::derive_accept_key, protocol::Role}; use crate::{ api::{http::HttpSessionData, HttpRequest, HttpResponse, HttpResponseBody}, - auth::AccessToken, JMAP, }; diff --git a/crates/managesieve/src/core/mod.rs b/crates/managesieve/src/core/mod.rs index 3f9aadc1..1f89b895 100644 --- a/crates/managesieve/src/core/mod.rs +++ b/crates/managesieve/src/core/mod.rs @@ -9,10 +9,13 @@ pub mod session; use std::{borrow::Cow, net::IpAddr, sync::Arc}; -use common::listener::{limiter::InFlight, ServerInstance}; +use common::{ + auth::AccessToken, + listener::{limiter::InFlight, ServerInstance}, +}; use imap::core::{ImapInstance, Inner}; use imap_proto::receiver::{CommandParser, Receiver}; -use jmap::{auth::AccessToken, JMAP}; +use jmap::JMAP; use tokio::io::{AsyncRead, AsyncWrite}; pub struct Session<T: AsyncRead + AsyncWrite> { diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs index 13198e70..da67f2e5 100644 --- a/crates/managesieve/src/op/authenticate.rs +++ b/crates/managesieve/src/op/authenticate.rs @@ -81,7 +81,7 @@ impl<T: SessionStream> Session<T> { .validate_access_token("access_token", &token) .await { - Ok((account_id, _, _)) => self.jmap.get_access_token(account_id).await, + Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await, Err(err) => Err(err), } } @@ -122,7 +122,7 @@ impl<T: SessionStream> Session<T> { // Cache access token let access_token = Arc::new(access_token); - self.jmap.cache_access_token(access_token.clone()); + self.jmap.core.cache_access_token(access_token.clone()); // Create session self.state = State::Authenticated { diff --git a/crates/pop3/src/lib.rs b/crates/pop3/src/lib.rs index 63b2b3d0..5212dafe 100644 --- a/crates/pop3/src/lib.rs +++ b/crates/pop3/src/lib.rs @@ -6,9 +6,12 @@ use std::{net::IpAddr, sync::Arc}; -use common::listener::{limiter::InFlight, ServerInstance, SessionStream}; +use common::{ + auth::AccessToken, + listener::{limiter::InFlight, ServerInstance, SessionStream}, +}; use imap::core::{ImapInstance, Inner}; -use jmap::{auth::AccessToken, JMAP}; +use jmap::JMAP; use mailbox::Mailbox; use protocol::request::Parser; diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs index 39d129a0..fe7ca09d 100644 --- a/crates/pop3/src/op/authenticate.rs +++ b/crates/pop3/src/op/authenticate.rs @@ -75,7 +75,7 @@ impl<T: SessionStream> Session<T> { .validate_access_token("access_token", &token) .await { - Ok((account_id, _, _)) => self.jmap.get_access_token(account_id).await, + Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await, Err(err) => Err(err), } } @@ -118,7 +118,7 @@ impl<T: SessionStream> Session<T> { // Cache access token let access_token = Arc::new(access_token); - self.jmap.cache_access_token(access_token.clone()); + self.jmap.core.cache_access_token(access_token.clone()); // Fetch mailbox let mailbox = self.fetch_mailbox(access_token.primary_id()).await?; diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index d7ff8c24..419b4dab 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -5,11 +5,11 @@ */ use common::listener::SessionStream; -use directory::backend::internal::PrincipalField; +use directory::{backend::internal::PrincipalField, Permission}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use smtp_proto::{IntoString, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; -use trc::{AuthEvent, SmtpEvent}; +use trc::{AddContext, AuthEvent, SmtpEvent}; use crate::core::Session; @@ -165,7 +165,9 @@ impl<T: SessionStream> Session<T> { | Credentials::XOauth2 { username, .. } | Credentials::OAuthBearer { token: username } => username.to_string(), }; - match self + + // Authenticate + let mut result = self .core .core .authenticate( @@ -175,10 +177,35 @@ impl<T: SessionStream> Session<T> { self.data.remote_ip, false, ) - .await - { + .await; + + // Validate permissions + if let Ok(principal) = &result { + match self + .core + .core + .get_cached_access_token(principal.id()) + .await + .caused_by(trc::location!()) + { + Ok(access_token) => { + if let Err(err) = access_token + .assert_has_permission(Permission::EmailSend) + .and_then(|_| { + access_token.assert_has_permission(Permission::Authenticate) + }) + { + result = Err(err); + } + } + Err(err) => { + result = Err(err); + } + } + } + + match result { Ok(principal) => { - let todo = "check smtp auth permissions"; self.data.authenticated_as = authenticated_as.to_lowercase(); self.data.authenticated_emails = principal .iter_str(PrincipalField::Emails) @@ -207,6 +234,11 @@ impl<T: SessionStream> Session<T> { ) .await; } + trc::EventType::Security(trc::SecurityEvent::Unauthorized) => { + self.write(b"550 5.7.1 Your account is not authorized to use this service.\r\n") + .await?; + return Ok(false); + } trc::EventType::Security(_) => { return Err(()); } diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index eec920f9..0918e278 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -304,7 +304,6 @@ impl<T: ResolveId> ValueClass<T> { DirectoryClass::Principal(uid) => serializer .write(2u8) .write_leb128(uid.resolve_id(assigned_ids)), - DirectoryClass::Domain(name) => serializer.write(3u8).write(name.as_slice()), DirectoryClass::UsedQuota(uid) => serializer.write(4u8).write_leb128(*uid), DirectoryClass::MemberOf { principal_id, @@ -533,9 +532,7 @@ impl<T> ValueClass<T> { ValueClass::Lookup(LookupClass::Counter(v) | LookupClass::Key(v)) | ValueClass::Config(v) => v.len(), ValueClass::Directory(d) => match d { - DirectoryClass::NameToId(v) - | DirectoryClass::EmailToId(v) - | DirectoryClass::Domain(v) => v.len(), + DirectoryClass::NameToId(v) | DirectoryClass::EmailToId(v) => v.len(), DirectoryClass::Principal(_) | DirectoryClass::UsedQuota(_) => U32_LEN, DirectoryClass::Members { .. } | DirectoryClass::MemberOf { .. } => U32_LEN * 2, }, diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 671ad340..9d513cad 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -181,7 +181,6 @@ pub enum DirectoryClass<T> { EmailToId(Vec<u8>), MemberOf { principal_id: T, member_of: T }, Members { principal_id: T, has_member: T }, - Domain(Vec<u8>), Principal(T), UsedQuota(u32), } diff --git a/crates/trc/src/ipc/bitset.rs b/crates/trc/src/ipc/bitset.rs index e4d6b0f1..d42d603a 100644 --- a/crates/trc/src/ipc/bitset.rs +++ b/crates/trc/src/ipc/bitset.rs @@ -37,6 +37,24 @@ impl<const N: usize> Bitset<N> { self.0[index / USIZE_BITS] & (1 << (index & USIZE_BITS_MASK)) != 0 } + pub fn union(&mut self, other: &Self) { + for i in 0..N { + self.0[i] |= other.0[i]; + } + } + + pub fn intersection(&mut self, other: &Self) { + for i in 0..N { + self.0[i] &= other.0[i]; + } + } + + pub fn difference(&mut self, other: &Self) { + for i in 0..N { + self.0[i] &= !other.0[i]; + } + } + pub fn clear_all(&mut self) { for i in 0..N { self.0[i] = 0; diff --git a/crates/utils/src/map/ttl_dashmap.rs b/crates/utils/src/map/ttl_dashmap.rs index 5d351aa7..089e01f7 100644 --- a/crates/utils/src/map/ttl_dashmap.rs +++ b/crates/utils/src/map/ttl_dashmap.rs @@ -9,6 +9,7 @@ use std::{borrow::Borrow, hash::Hash, time::Instant}; use dashmap::DashMap; pub type TtlDashMap<K, V> = DashMap<K, LruItem<V>, ahash::RandomState>; +pub type ADashMap<K, V> = DashMap<K, V, ahash::RandomState>; #[derive(Debug, Clone)] pub struct LruItem<V> { diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index dc7acf3a..14989ed9 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -439,7 +439,7 @@ async fn internal_directory() { member_of: vec!["list".to_string(), "sales".to_string()], } ); - assert_eq!(store.get_account_id("john").await.unwrap(), None); + assert_eq!(store.get_principal_id("john").await.unwrap(), None); assert!(!store.rcpt("john@example.org").await.unwrap()); assert!(store.rcpt("john.doe@example.org").await.unwrap()); @@ -589,7 +589,7 @@ async fn internal_directory() { // Delete John's account and make sure his records are gone store.delete_account(QueryBy::Id(john_id)).await.unwrap(); - assert_eq!(store.get_account_id("john.doe").await.unwrap(), None); + assert_eq!(store.get_principal_id("john.doe").await.unwrap(), None); assert_eq!( store.email_to_ids("john.doe@example.org").await.unwrap(), Vec::<u32>::new() @@ -633,7 +633,7 @@ async fn internal_directory() { ); // Make sure Jane's records are still there - assert_eq!(store.get_account_id("jane").await.unwrap(), Some(jane_id)); + assert_eq!(store.get_principal_id("jane").await.unwrap(), Some(jane_id)); assert_eq!( store.email_to_ids("jane@example.org").await.unwrap(), vec![jane_id] diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 41245794..7db2313f 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -43,7 +43,7 @@ async fn ldap_directory() { .into_test() .into_sorted(), TestPrincipal { - id: base_store.get_account_id("john").await.unwrap().unwrap(), + id: base_store.get_principal_id("john").await.unwrap().unwrap(), name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], @@ -76,7 +76,7 @@ async fn ldap_directory() { .into_test() .into_sorted(), TestPrincipal { - id: base_store.get_account_id("bill").await.unwrap().unwrap(), + id: base_store.get_principal_id("bill").await.unwrap().unwrap(), name: "bill".to_string(), description: "Bill Foobar".to_string().into(), secrets: vec![ @@ -111,7 +111,7 @@ async fn ldap_directory() { .into_test() .into_sorted(), TestPrincipal { - id: base_store.get_account_id("jane").await.unwrap().unwrap(), + id: base_store.get_principal_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, @@ -136,7 +136,7 @@ async fn ldap_directory() { .unwrap() .into_test(), TestPrincipal { - id: base_store.get_account_id("sales").await.unwrap().unwrap(), + id: base_store.get_principal_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), description: "sales".to_string().into(), typ: Type::Group, diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index 732e2e90..754a3841 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -632,7 +632,13 @@ async fn address_mappings() { async fn map_account_ids(store: &Store, names: Vec<impl AsRef<str>>) -> Vec<u32> { let mut ids = Vec::with_capacity(names.len()); for name in names { - ids.push(store.get_account_id(name.as_ref()).await.unwrap().unwrap()); + ids.push( + store + .get_principal_id(name.as_ref()) + .await + .unwrap() + .unwrap(), + ); } ids } diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index 5fd22d71..1b8ced94 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -113,7 +113,7 @@ async fn sql_directory() { .unwrap() .into_test(), TestPrincipal { - id: base_store.get_account_id("john").await.unwrap().unwrap(), + id: base_store.get_principal_id("john").await.unwrap().unwrap(), name: "john".to_string(), description: "John Doe".to_string().into(), secrets: vec!["12345".to_string()], @@ -145,7 +145,7 @@ async fn sql_directory() { .unwrap() .into_test(), TestPrincipal { - id: base_store.get_account_id("bill").await.unwrap().unwrap(), + id: base_store.get_principal_id("bill").await.unwrap().unwrap(), name: "bill".to_string(), description: "Bill Foobar".to_string().into(), secrets: vec![ @@ -178,7 +178,7 @@ async fn sql_directory() { .unwrap() .into_test(), TestPrincipal { - id: base_store.get_account_id("jane").await.unwrap().unwrap(), + id: base_store.get_principal_id("jane").await.unwrap().unwrap(), name: "jane".to_string(), description: "Jane Doe".to_string().into(), typ: Type::Individual, @@ -202,7 +202,7 @@ async fn sql_directory() { .unwrap() .into_test(), TestPrincipal { - id: base_store.get_account_id("sales").await.unwrap().unwrap(), + id: base_store.get_principal_id("sales").await.unwrap().unwrap(), name: "sales".to_string(), description: "Sales Team".to_string().into(), typ: Type::Group, diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index cd5d90dc..2af174ad 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -424,7 +424,7 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest { } // Assign Id 0 to admin (required for some tests) - store.get_or_create_account_id("admin").await.unwrap(); + store.get_or_create_principal_id("admin").await.unwrap(); IMAPTest { jmap: JMAP::from(jmap.clone()).into(), diff --git a/tests/src/jmap/auth_acl.rs b/tests/src/jmap/auth_acl.rs index a45496a7..b3774cfc 100644 --- a/tests/src/jmap/auth_acl.rs +++ b/tests/src/jmap/auth_acl.rs @@ -51,7 +51,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap() .into(); @@ -59,7 +59,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jane.smith@example.com") + .get_or_create_principal_id("jane.smith@example.com") .await .unwrap() .into(); @@ -67,7 +67,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("bill@example.com") + .get_or_create_principal_id("bill@example.com") .await .unwrap() .into(); @@ -75,7 +75,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("sales@example.com") + .get_or_create_principal_id("sales@example.com") .await .unwrap() .into(); diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs index 3cfeee38..288e37ae 100644 --- a/tests/src/jmap/auth_limits.rs +++ b/tests/src/jmap/auth_limits.rs @@ -42,7 +42,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs index c89ff168..8ef8ee77 100644 --- a/tests/src/jmap/auth_oauth.rs +++ b/tests/src/jmap/auth_oauth.rs @@ -45,7 +45,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/blob.rs b/tests/src/jmap/blob.rs index 5121be12..debdb5ae 100644 --- a/tests/src/jmap/blob.rs +++ b/tests/src/jmap/blob.rs @@ -25,7 +25,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ); diff --git a/tests/src/jmap/crypto.rs b/tests/src/jmap/crypto.rs index 6d90d35b..7d38a5c7 100644 --- a/tests/src/jmap/crypto.rs +++ b/tests/src/jmap/crypto.rs @@ -32,7 +32,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/delivery.rs b/tests/src/jmap/delivery.rs index 8cf3bad0..d83c4bee 100644 --- a/tests/src/jmap/delivery.rs +++ b/tests/src/jmap/delivery.rs @@ -41,7 +41,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) @@ -51,7 +51,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jane@example.com") + .get_or_create_principal_id("jane@example.com") .await .unwrap(), ) @@ -61,7 +61,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("bill@example.com") + .get_or_create_principal_id("bill@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index af1eb3d7..b98e9cef 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -89,7 +89,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/event_source.rs b/tests/src/jmap/event_source.rs index d868bbee..3cf77676 100644 --- a/tests/src/jmap/event_source.rs +++ b/tests/src/jmap/event_source.rs @@ -34,7 +34,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/purge.rs b/tests/src/jmap/purge.rs index 1d8315ce..4126f742 100644 --- a/tests/src/jmap/purge.rs +++ b/tests/src/jmap/purge.rs @@ -38,7 +38,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(); let mut imap = ImapConnection::connect(b"_x ").await; diff --git a/tests/src/jmap/push_subscription.rs b/tests/src/jmap/push_subscription.rs index 5b97bb68..97e47c45 100644 --- a/tests/src/jmap/push_subscription.rs +++ b/tests/src/jmap/push_subscription.rs @@ -73,7 +73,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ); diff --git a/tests/src/jmap/quota.rs b/tests/src/jmap/quota.rs index 2e2a2a58..927c5575 100644 --- a/tests/src/jmap/quota.rs +++ b/tests/src/jmap/quota.rs @@ -34,7 +34,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ); @@ -43,7 +43,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("robert@example.com") + .get_or_create_principal_id("robert@example.com") .await .unwrap(), ); diff --git a/tests/src/jmap/sieve_script.rs b/tests/src/jmap/sieve_script.rs index 04232136..fa0dd0e1 100644 --- a/tests/src/jmap/sieve_script.rs +++ b/tests/src/jmap/sieve_script.rs @@ -42,7 +42,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/stress_test.rs b/tests/src/jmap/stress_test.rs index 771e8b6f..b780eded 100644 --- a/tests/src/jmap/stress_test.rs +++ b/tests/src/jmap/stress_test.rs @@ -29,7 +29,7 @@ pub async fn test(server: Arc<JMAP>, mut client: Client) { .core .storage .data - .get_or_create_account_id("john") + .get_or_create_principal_id("john") .await .unwrap(); client.set_default_account_id(Id::from(TEST_USER_ID).to_string()); diff --git a/tests/src/jmap/vacation_response.rs b/tests/src/jmap/vacation_response.rs index 60c1bdae..c4ab0233 100644 --- a/tests/src/jmap/vacation_response.rs +++ b/tests/src/jmap/vacation_response.rs @@ -36,7 +36,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) diff --git a/tests/src/jmap/websocket.rs b/tests/src/jmap/websocket.rs index 1b6d21d8..b2b9a13c 100644 --- a/tests/src/jmap/websocket.rs +++ b/tests/src/jmap/websocket.rs @@ -38,7 +38,7 @@ pub async fn test(params: &mut JMAPTest) { .core .storage .data - .get_or_create_account_id("jdoe@example.com") + .get_or_create_principal_id("jdoe@example.com") .await .unwrap(), ) |