Roles and multi-tenancy - part 1

This commit is contained in:
mdecimus 2024-09-12 17:42:14 +02:00
parent fbcf55d8e1
commit d214468c54
100 changed files with 1783 additions and 1196 deletions

View file

@ -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"))
}
}
}

View file

@ -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,
}

View file

@ -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(),
}
}

View file

@ -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,

View file

@ -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",

View file

@ -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),
),
}
}
}

View file

@ -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(

View file

@ -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> {

File diff suppressed because it is too large Load diff

View file

@ -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 {
fn serialize(self) -> Vec<u8> {
KeySerializer::new(U32_LEN + 1)
.write_leb128(self.account_id)
.write(self.typ as u8)
.finalize()
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 Deserialize for PrincipalIdType {
impl Serialize for PrincipalInfo {
fn serialize(self) -> Vec<u8> {
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 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;

View file

@ -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)
}
}

View file

@ -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(

View file

@ -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))
}
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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),

View file

@ -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!())?;

View file

@ -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!())

View file

@ -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),

View file

@ -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 {

View file

@ -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!())?

View file

@ -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};

View file

@ -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,

View file

@ -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,
};

View file

@ -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()),
}
}

View file

@ -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,
};

View file

@ -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())?;

View file

@ -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,
};

View file

@ -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

View file

@ -6,6 +6,7 @@
use std::sync::Arc;
use common::auth::AccessToken;
use directory::{
backend::internal::{
lookup::DirectoryStore,
@ -17,42 +18,16 @@ 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")]
@ -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)?;
let todo = "increment role list version + implement gossip";
// Make sure the current directory supports updates
self.assert_supported_directory()?;
// 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(),
}
}
}

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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()

View file

@ -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;

View file

@ -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(

View file

@ -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))

View file

@ -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 {

View file

@ -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,
}

View file

@ -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,
};

View file

@ -12,7 +12,7 @@ use trc::AddContext;
use crate::JMAP;
use super::AccessToken;
use common::auth::AccessToken;
pub struct ConcurrencyLimiters {
pub concurrent_requests: ConcurrencyLimiter,

View file

@ -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(

View file

@ -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)]

View file

@ -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(

View file

@ -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;

View file

@ -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(

View file

@ -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(

View file

@ -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},

View file

@ -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};

View file

@ -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},

View file

@ -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};

View file

@ -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},

View file

@ -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(

View file

@ -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},

View file

@ -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};

View file

@ -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)

View file

@ -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(

View file

@ -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(

View file

@ -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};

View file

@ -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};

View file

@ -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;

View file

@ -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(

View file

@ -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(

View file

@ -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());
}

View file

@ -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

View file

@ -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,

View file

@ -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(

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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> {

View file

@ -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 {

View file

@ -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;

View file

@ -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?;

View file

@ -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(());
}

View file

@ -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,
},

View file

@ -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),
}

View file

@ -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;

View file

@ -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> {

View file

@ -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]

View file

@ -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,

View file

@ -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
}

View file

@ -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,

View file

@ -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(),

View file

@ -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();

View file

@ -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(),
)

View file

@ -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(),
)

View file

@ -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(),
);

View file

@ -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(),
)

View file

@ -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(),
)

View file

@ -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(),
)

View file

@ -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(),
)

View file

@ -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;

View file

@ -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(),
);

View file

@ -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(),
);

View file

@ -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(),
)

View file

@ -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());

View file

@ -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(),
)

View file

@ -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(),
)