summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2024-09-12 17:42:14 +0200
committermdecimus <mauro@stalw.art>2024-09-12 17:42:14 +0200
commitd214468c54415f3048f0246eb176f6d25a3acdd2 (patch)
treeadc9eaefdbd9c7a57f5c7a83fed41c92fe5d590b
parentfbcf55d8e1891b72499fed7d5ff54964c8a2f256 (diff)
Roles and multi-tenancy - part 1
-rw-r--r--crates/common/src/auth/access_token.rs433
-rw-r--r--crates/common/src/auth/mod.rs24
-rw-r--r--crates/common/src/auth/roles.rs192
-rw-r--r--crates/common/src/config/mod.rs18
-rw-r--r--crates/common/src/enterprise/config.rs5
-rw-r--r--crates/common/src/lib.rs75
-rw-r--r--crates/common/src/manager/restore.rs4
-rw-r--r--crates/directory/src/backend/internal/lookup.rs73
-rw-r--r--crates/directory/src/backend/internal/manage.rs632
-rw-r--r--crates/directory/src/backend/internal/mod.rs153
-rw-r--r--crates/directory/src/backend/ldap/lookup.rs46
-rw-r--r--crates/directory/src/backend/memory/config.rs29
-rw-r--r--crates/directory/src/backend/sql/lookup.rs19
-rw-r--r--crates/directory/src/core/principal.rs167
-rw-r--r--crates/directory/src/core/secret.rs2
-rw-r--r--crates/directory/src/lib.rs56
-rw-r--r--crates/imap/src/core/mailbox.rs7
-rw-r--r--crates/imap/src/core/mod.rs11
-rw-r--r--crates/imap/src/op/acl.rs13
-rw-r--r--crates/imap/src/op/authenticate.rs4
-rw-r--r--crates/imap/src/op/copy_move.rs1
-rw-r--r--crates/jmap/src/api/event_source.rs3
-rw-r--r--crates/jmap/src/api/http.rs3
-rw-r--r--crates/jmap/src/api/management/dkim.rs3
-rw-r--r--crates/jmap/src/api/management/dns.rs (renamed from crates/jmap/src/api/management/domain.rs)95
-rw-r--r--crates/jmap/src/api/management/enterprise/telemetry.rs10
-rw-r--r--crates/jmap/src/api/management/enterprise/undelete.rs4
-rw-r--r--crates/jmap/src/api/management/log.rs2
-rw-r--r--crates/jmap/src/api/management/mod.rs7
-rw-r--r--crates/jmap/src/api/management/principal.rs182
-rw-r--r--crates/jmap/src/api/management/queue.rs2
-rw-r--r--crates/jmap/src/api/management/reload.rs2
-rw-r--r--crates/jmap/src/api/management/report.rs2
-rw-r--r--crates/jmap/src/api/management/settings.rs2
-rw-r--r--crates/jmap/src/api/management/sieve.rs3
-rw-r--r--crates/jmap/src/api/management/stores.rs5
-rw-r--r--crates/jmap/src/api/request.rs3
-rw-r--r--crates/jmap/src/api/session.rs3
-rw-r--r--crates/jmap/src/auth/acl.rs57
-rw-r--r--crates/jmap/src/auth/authenticate.rs68
-rw-r--r--crates/jmap/src/auth/mod.rs286
-rw-r--r--crates/jmap/src/auth/oauth/auth.rs3
-rw-r--r--crates/jmap/src/auth/rate_limit.rs2
-rw-r--r--crates/jmap/src/blob/copy.rs3
-rw-r--r--crates/jmap/src/blob/download.rs3
-rw-r--r--crates/jmap/src/blob/get.rs3
-rw-r--r--crates/jmap/src/blob/upload.rs3
-rw-r--r--crates/jmap/src/changes/get.rs3
-rw-r--r--crates/jmap/src/changes/query.rs3
-rw-r--r--crates/jmap/src/email/copy.rs3
-rw-r--r--crates/jmap/src/email/crypto.rs2
-rw-r--r--crates/jmap/src/email/get.rs3
-rw-r--r--crates/jmap/src/email/import.rs3
-rw-r--r--crates/jmap/src/email/parse.rs3
-rw-r--r--crates/jmap/src/email/query.rs3
-rw-r--r--crates/jmap/src/email/set.rs3
-rw-r--r--crates/jmap/src/email/snippet.rs3
-rw-r--r--crates/jmap/src/lib.rs8
-rw-r--r--crates/jmap/src/mailbox/get.rs6
-rw-r--r--crates/jmap/src/mailbox/query.rs3
-rw-r--r--crates/jmap/src/mailbox/set.rs7
-rw-r--r--crates/jmap/src/push/get.rs3
-rw-r--r--crates/jmap/src/push/set.rs3
-rw-r--r--crates/jmap/src/quota/get.rs3
-rw-r--r--crates/jmap/src/quota/query.rs3
-rw-r--r--crates/jmap/src/services/housekeeper.rs4
-rw-r--r--crates/jmap/src/services/state.rs6
-rw-r--r--crates/jmap/src/sieve/set.rs3
-rw-r--r--crates/jmap/src/sieve/validate.rs3
-rw-r--r--crates/jmap/src/websocket/stream.rs2
-rw-r--r--crates/jmap/src/websocket/upgrade.rs2
-rw-r--r--crates/managesieve/src/core/mod.rs7
-rw-r--r--crates/managesieve/src/op/authenticate.rs4
-rw-r--r--crates/pop3/src/lib.rs7
-rw-r--r--crates/pop3/src/op/authenticate.rs4
-rw-r--r--crates/smtp/src/inbound/auth.rs44
-rw-r--r--crates/store/src/write/key.rs5
-rw-r--r--crates/store/src/write/mod.rs1
-rw-r--r--crates/trc/src/ipc/bitset.rs18
-rw-r--r--crates/utils/src/map/ttl_dashmap.rs1
-rw-r--r--tests/src/directory/internal.rs6
-rw-r--r--tests/src/directory/ldap.rs8
-rw-r--r--tests/src/directory/mod.rs8
-rw-r--r--tests/src/directory/sql.rs8
-rw-r--r--tests/src/imap/mod.rs2
-rw-r--r--tests/src/jmap/auth_acl.rs8
-rw-r--r--tests/src/jmap/auth_limits.rs2
-rw-r--r--tests/src/jmap/auth_oauth.rs2
-rw-r--r--tests/src/jmap/blob.rs2
-rw-r--r--tests/src/jmap/crypto.rs2
-rw-r--r--tests/src/jmap/delivery.rs6
-rw-r--r--tests/src/jmap/email_submission.rs2
-rw-r--r--tests/src/jmap/event_source.rs2
-rw-r--r--tests/src/jmap/purge.rs2
-rw-r--r--tests/src/jmap/push_subscription.rs2
-rw-r--r--tests/src/jmap/quota.rs4
-rw-r--r--tests/src/jmap/sieve_script.rs2
-rw-r--r--tests/src/jmap/stress_test.rs2
-rw-r--r--tests/src/jmap/vacation_response.rs2
-rw-r--r--tests/src/jmap/websocket.rs2
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(),
)