mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Roles and multi-tenancy - part 1
This commit is contained in:
parent
fbcf55d8e1
commit
d214468c54
433
crates/common/src/auth/access_token.rs
Normal file
433
crates/common/src/auth/access_token.rs
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
24
crates/common/src/auth/mod.rs
Normal file
24
crates/common/src/auth/mod.rs
Normal 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,
|
||||
}
|
192
crates/common/src/auth/roles.rs
Normal file
192
crates/common/src/auth/roles.rs
Normal 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(),
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
return Ok(Some(principal));
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
} else {
|
||||
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
|
@ -7,19 +7,20 @@
|
|||
pub mod lookup;
|
||||
pub mod manage;
|
||||
|
||||
use std::{fmt::Display, slice::Iter, str::FromStr};
|
||||
use std::{fmt::Display, slice::Iter};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN};
|
||||
use utils::codec::leb128::Leb128Iterator;
|
||||
|
||||
use crate::{Principal, Type};
|
||||
use crate::{Principal, Type, ROLE_ADMIN, ROLE_USER};
|
||||
|
||||
const INT_MARKER: u8 = 1 << 7;
|
||||
|
||||
pub(super) struct PrincipalIdType {
|
||||
pub account_id: u32,
|
||||
pub struct PrincipalInfo {
|
||||
pub id: u32,
|
||||
pub typ: Type,
|
||||
pub tenant: Option<u32>,
|
||||
}
|
||||
|
||||
impl Serialize for Principal {
|
||||
|
@ -90,20 +91,36 @@ impl Deserialize for Principal {
|
|||
}
|
||||
}
|
||||
|
||||
impl Serialize for PrincipalIdType {
|
||||
impl PrincipalInfo {
|
||||
pub fn has_tenant_access(&self, tenant_id: Option<u32>) -> bool {
|
||||
tenant_id.map_or(true, |tenant_id| {
|
||||
self.tenant.map_or(false, |t| tenant_id == t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PrincipalInfo {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
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.account_id)
|
||||
.write_leb128(self.id)
|
||||
.write(self.typ as u8)
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for PrincipalIdType {
|
||||
impl Deserialize for PrincipalInfo {
|
||||
fn deserialize(bytes_: &[u8]) -> trc::Result<Self> {
|
||||
let mut bytes = bytes_.iter();
|
||||
Ok(PrincipalIdType {
|
||||
account_id: bytes.next_leb128().ok_or_else(|| {
|
||||
Ok(PrincipalInfo {
|
||||
id: bytes.next_leb128().ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes_)
|
||||
|
@ -113,13 +130,18 @@ impl Deserialize for PrincipalIdType {
|
|||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes_)
|
||||
})?),
|
||||
tenant: bytes.next_leb128(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PrincipalIdType {
|
||||
pub fn new(account_id: u32, typ: Type) -> Self {
|
||||
Self { account_id, typ }
|
||||
impl PrincipalInfo {
|
||||
pub fn new(principal_id: u32, typ: Type, tenant: Option<u32>) -> Self {
|
||||
Self {
|
||||
id: principal_id,
|
||||
typ,
|
||||
tenant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,11 +173,11 @@ fn deserialize(bytes: &[u8]) -> Option<Principal> {
|
|||
}
|
||||
}
|
||||
|
||||
if type_id != 4 {
|
||||
principal
|
||||
} else {
|
||||
principal.into_superuser()
|
||||
}
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
if type_id != 4 { ROLE_USER } else { ROLE_ADMIN },
|
||||
)
|
||||
.into()
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
let mut principal = Principal {
|
||||
id,
|
||||
typ,
|
||||
..Default::default()
|
||||
}
|
||||
.into_superuser()
|
||||
} else {
|
||||
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(
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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!())?;
|
||||
|
|
|
@ -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!())
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!())?
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -13,9 +13,12 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::telemetry::{
|
||||
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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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())?;
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
.create_account(principal)
|
||||
.await?,
|
||||
.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_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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ use trc::AddContext;
|
|||
|
||||
use crate::JMAP;
|
||||
|
||||
use super::AccessToken;
|
||||
use common::auth::AccessToken;
|
||||
|
||||
pub struct ConcurrencyLimiters {
|
||||
pub concurrent_requests: ConcurrencyLimiter,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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;
|
||||
|
||||
// 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(());
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue