mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-28 09:07:32 +00:00
Test fixes - part 1
This commit is contained in:
parent
49bce9a3de
commit
1e08e56672
31 changed files with 579 additions and 302 deletions
|
@ -267,15 +267,18 @@ impl AccessToken {
|
|||
}
|
||||
|
||||
pub fn permissions(&self) -> Vec<Permission> {
|
||||
const BYTES_LEN: u32 = (std::mem::size_of::<usize>() * 8) as u32 - 1;
|
||||
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;
|
||||
let item = BYTES_LEN - bytes.leading_zeros();
|
||||
bytes ^= 1 << item;
|
||||
permissions.push(
|
||||
Permission::from_id((block_num * std::mem::size_of::<usize>()) + item).unwrap(),
|
||||
Permission::from_id((block_num * std::mem::size_of::<usize>()) + item as usize)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,11 +168,11 @@ impl Core {
|
|||
tls: TlsManager::parse(config),
|
||||
metrics: Metrics::parse(config),
|
||||
security: Security {
|
||||
access_tokens: TtlDashMap::with_capacity(32, 100),
|
||||
access_tokens: TtlDashMap::with_capacity(100, 32),
|
||||
permissions: ADashMap::with_capacity_and_hasher_and_shard_amount(
|
||||
32,
|
||||
ahash::RandomState::new(),
|
||||
100,
|
||||
ahash::RandomState::new(),
|
||||
32,
|
||||
),
|
||||
permissions_version: Default::default(),
|
||||
},
|
||||
|
|
|
@ -22,7 +22,6 @@ pub trait DirectoryStore: Sync + Send {
|
|||
return_member_of: bool,
|
||||
) -> trc::Result<Option<Principal>>;
|
||||
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>>;
|
||||
|
||||
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>;
|
||||
async fn rcpt(&self, address: &str) -> trc::Result<bool>;
|
||||
async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>>;
|
||||
|
|
|
@ -27,6 +27,12 @@ pub struct MemberOf {
|
|||
pub typ: Type,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PrincipalList {
|
||||
pub items: Vec<Principal>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait ManageDirectory: Sized {
|
||||
async fn get_principal_id(&self, name: &str) -> trc::Result<Option<u32>>;
|
||||
|
@ -50,15 +56,23 @@ pub trait ManageDirectory: Sized {
|
|||
async fn list_principals(
|
||||
&self,
|
||||
filter: Option<&str>,
|
||||
typ: Option<Type>,
|
||||
tenant_id: Option<u32>,
|
||||
) -> trc::Result<Vec<String>>;
|
||||
types: &[Type],
|
||||
fields: &[PrincipalField],
|
||||
page: usize,
|
||||
limit: usize,
|
||||
) -> trc::Result<PrincipalList>;
|
||||
async fn count_principals(
|
||||
&self,
|
||||
filter: Option<&str>,
|
||||
typ: Option<Type>,
|
||||
tenant_id: Option<u32>,
|
||||
) -> trc::Result<u64>;
|
||||
async fn map_field_ids(
|
||||
&self,
|
||||
principal: &mut Principal,
|
||||
fields: &[PrincipalField],
|
||||
) -> trc::Result<()>;
|
||||
}
|
||||
|
||||
impl ManageDirectory for Store {
|
||||
|
@ -147,15 +161,50 @@ impl ManageDirectory for Store {
|
|||
return Err(err_missing(PrincipalField::Name));
|
||||
}
|
||||
|
||||
// Tenants must provide principal names including a valid domain
|
||||
// Validate tenant
|
||||
let mut valid_domains = AHashSet::new();
|
||||
if tenant_id.is_some() {
|
||||
if let Some(tenant_id) = tenant_id {
|
||||
let tenant = self
|
||||
.query(QueryBy::Id(tenant_id), false)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
trc::ManageEvent::NotFound
|
||||
.into_err()
|
||||
.id(tenant_id)
|
||||
.details("Tenant not found")
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
|
||||
// Enforce tenant quotas
|
||||
if let Some(limit) = tenant
|
||||
.get_int_array(PrincipalField::Quota)
|
||||
.and_then(|quotas| quotas.get(principal.typ() as usize + 1))
|
||||
.copied()
|
||||
.filter(|q| *q > 0)
|
||||
{
|
||||
// Obtain number of principals
|
||||
let total = self
|
||||
.count_principals(None, principal.typ().into(), tenant_id.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if total >= limit {
|
||||
trc::bail!(trc::LimitEvent::TenantQuota
|
||||
.into_err()
|
||||
.details("Tenant principal quota exceeded")
|
||||
.ctx(trc::Key::Details, principal.typ().as_str())
|
||||
.ctx(trc::Key::Limit, limit)
|
||||
.ctx(trc::Key::Total, total));
|
||||
}
|
||||
}
|
||||
|
||||
// Tenants must provide principal names including a valid domain
|
||||
if let Some(domain) = name.split('@').nth(1) {
|
||||
if self
|
||||
.get_principal_info(domain)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id))
|
||||
.filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id.into()))
|
||||
.is_some()
|
||||
{
|
||||
valid_domains.insert(domain.to_string());
|
||||
|
@ -388,27 +437,35 @@ impl ManageDirectory for Store {
|
|||
}
|
||||
Type::Tenant => {
|
||||
let tenant_members = self
|
||||
.list_principals(None, None, principal.id().into())
|
||||
.list_principals(
|
||||
None,
|
||||
principal.id().into(),
|
||||
&[],
|
||||
&[PrincipalField::Name],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !tenant_members.is_empty() {
|
||||
let tenant_members = if tenant_members.len() > 5 {
|
||||
tenant_members[..5].join(", ")
|
||||
+ " and "
|
||||
+ &(&tenant_members.len() - 5).to_string()
|
||||
+ " others"
|
||||
} else {
|
||||
tenant_members.join(", ")
|
||||
};
|
||||
if tenant_members.total > 0 {
|
||||
let mut message =
|
||||
String::from("Tenant must have no members to be deleted: Found: ");
|
||||
|
||||
return Err(error(
|
||||
"Tenant has members",
|
||||
format!(
|
||||
"Tenant must have no members to be deleted: Found: {tenant_members}"
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
for (num, principal) in tenant_members.items.iter().enumerate() {
|
||||
if num > 0 {
|
||||
message.push_str(", ");
|
||||
}
|
||||
message.push_str(principal.name());
|
||||
}
|
||||
|
||||
if tenant_members.total > 5 {
|
||||
message.push_str(" and ");
|
||||
message.push_str(&(tenant_members.total - 5).to_string());
|
||||
message.push_str(" others");
|
||||
}
|
||||
|
||||
return Err(error("Tenant has members", message.into()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1237,9 +1294,12 @@ impl ManageDirectory for Store {
|
|||
async fn list_principals(
|
||||
&self,
|
||||
filter: Option<&str>,
|
||||
typ: Option<Type>,
|
||||
tenant_id: Option<u32>,
|
||||
) -> trc::Result<Vec<String>> {
|
||||
types: &[Type],
|
||||
fields: &[PrincipalField],
|
||||
page: usize,
|
||||
limit: usize,
|
||||
) -> trc::Result<PrincipalList> {
|
||||
let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![])));
|
||||
let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![
|
||||
u8::MAX;
|
||||
|
@ -1252,9 +1312,10 @@ impl ManageDirectory for Store {
|
|||
|key, value| {
|
||||
let pt = PrincipalInfo::deserialize(value).caused_by(trc::location!())?;
|
||||
|
||||
if typ.map_or(true, |t| pt.typ == t) && pt.has_tenant_access(tenant_id) {
|
||||
results.push((
|
||||
pt.id,
|
||||
if (types.is_empty() || types.contains(&pt.typ)) && pt.has_tenant_access(tenant_id)
|
||||
{
|
||||
results.push(Principal::new(pt.id, pt.typ).with_field(
|
||||
PrincipalField::Name,
|
||||
String::from_utf8_lossy(key.get(1..).unwrap_or_default()).into_owned(),
|
||||
));
|
||||
}
|
||||
|
@ -1265,30 +1326,83 @@ impl ManageDirectory for Store {
|
|||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(filter) = filter {
|
||||
let mut filtered = Vec::new();
|
||||
if filter.is_none() && fields.iter().all(|f| matches!(f, PrincipalField::Name)) {
|
||||
return Ok(PrincipalList {
|
||||
total: results.len() as u64,
|
||||
items: results
|
||||
.into_iter()
|
||||
.skip(page.saturating_sub(1) * limit)
|
||||
.take(if limit > 0 { limit } else { usize::MAX })
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut result = PrincipalList::default();
|
||||
let filters = filter.and_then(|filter| {
|
||||
let filters = filter
|
||||
.split_whitespace()
|
||||
.map(|r| r.to_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
if !filters.is_empty() {
|
||||
Some(filters)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
for (principal_id, principal_name) in results {
|
||||
let principal = self
|
||||
let mut offset = limit * page;
|
||||
let mut is_done = false;
|
||||
let map_principals = fields.is_empty()
|
||||
|| fields.iter().any(|f| {
|
||||
matches!(
|
||||
f,
|
||||
PrincipalField::MemberOf
|
||||
| PrincipalField::Lists
|
||||
| PrincipalField::Roles
|
||||
| PrincipalField::EnabledPermissions
|
||||
| PrincipalField::DisabledPermissions
|
||||
| PrincipalField::Members
|
||||
| PrincipalField::UsedQuota
|
||||
)
|
||||
});
|
||||
|
||||
for mut principal in results {
|
||||
if !is_done || filters.is_some() {
|
||||
principal = self
|
||||
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::Principal(principal_id),
|
||||
DirectoryClass::Principal(principal.id),
|
||||
)))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or_else(|| not_found(principal_id.to_string()))?;
|
||||
if filters.iter().all(|f| principal.find_str(f)) {
|
||||
filtered.push(principal_name);
|
||||
}
|
||||
.ok_or_else(|| not_found(principal.name().to_string()))?;
|
||||
}
|
||||
|
||||
Ok(filtered)
|
||||
} else {
|
||||
Ok(results.into_iter().map(|(_, name)| name).collect())
|
||||
if filters.as_ref().map_or(true, |filters| {
|
||||
filters.iter().all(|f| principal.find_str(f))
|
||||
}) {
|
||||
result.total += 1;
|
||||
|
||||
if offset == 0 {
|
||||
if !is_done {
|
||||
if !fields.is_empty() {
|
||||
principal.fields.retain(|k, _| fields.contains(k));
|
||||
}
|
||||
|
||||
if map_principals {
|
||||
self.map_field_ids(&mut principal, fields)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
result.items.push(principal);
|
||||
is_done = result.items.len() >= limit;
|
||||
}
|
||||
} else {
|
||||
offset -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn count_principals(
|
||||
|
@ -1372,6 +1486,146 @@ impl ManageDirectory for Store {
|
|||
.caused_by(trc::location!())?;
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn map_field_ids(
|
||||
&self,
|
||||
principal: &mut Principal,
|
||||
fields: &[PrincipalField],
|
||||
) -> trc::Result<()> {
|
||||
// Map groups
|
||||
for field in [
|
||||
PrincipalField::MemberOf,
|
||||
PrincipalField::Lists,
|
||||
PrincipalField::Roles,
|
||||
] {
|
||||
if let Some(member_of) = principal
|
||||
.take_int_array(field)
|
||||
.filter(|_| fields.is_empty() || fields.contains(&field))
|
||||
{
|
||||
for principal_id in member_of {
|
||||
match principal_id as u32 {
|
||||
ROLE_ADMIN if field == PrincipalField::Roles => {
|
||||
principal.append_str(field, "admin");
|
||||
}
|
||||
ROLE_TENANT_ADMIN if field == PrincipalField::Roles => {
|
||||
principal.append_str(field, "tenant-admin");
|
||||
}
|
||||
ROLE_USER if field == PrincipalField::Roles => {
|
||||
principal.append_str(field, "user");
|
||||
}
|
||||
principal_id => {
|
||||
if let Some(name) = self
|
||||
.get_principal_name(principal_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
principal.append_str(field, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain member names
|
||||
if fields.is_empty() || fields.contains(&PrincipalField::Members) {
|
||||
match principal.typ {
|
||||
Type::Group | Type::List | Type::Role => {
|
||||
for member_id in self.get_members(principal.id).await? {
|
||||
if let Some(mut member_principal) =
|
||||
self.query(QueryBy::Id(member_id), false).await?
|
||||
{
|
||||
if let Some(name) = member_principal.take_str(PrincipalField::Name) {
|
||||
principal.append_str(PrincipalField::Members, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Type::Domain => {
|
||||
let from_key =
|
||||
ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId(vec![])));
|
||||
let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId(
|
||||
vec![u8::MAX; 10],
|
||||
)));
|
||||
let mut results = Vec::new();
|
||||
let domain_name = principal.name();
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key).no_values(),
|
||||
|key, _| {
|
||||
let email = std::str::from_utf8(key.get(1..).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
if email
|
||||
.rsplit_once('@')
|
||||
.map_or(false, |(_, domain)| domain == domain_name)
|
||||
{
|
||||
results.push(email.to_string());
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
principal.set(PrincipalField::Members, results);
|
||||
}
|
||||
Type::Tenant => {
|
||||
let from_key =
|
||||
ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![])));
|
||||
let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(
|
||||
vec![u8::MAX; 10],
|
||||
)));
|
||||
let mut results = Vec::new();
|
||||
self.iterate(IterateParams::new(from_key, to_key), |key, value| {
|
||||
let pinfo =
|
||||
PrincipalInfo::deserialize(value).caused_by(trc::location!())?;
|
||||
|
||||
if pinfo.typ == Type::Individual
|
||||
&& pinfo.has_tenant_access(Some(principal.id))
|
||||
{
|
||||
results.push(
|
||||
std::str::from_utf8(key.get(1..).unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(true)
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
principal.set(PrincipalField::Members, results);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain used quota
|
||||
if matches!(principal.typ, Type::Individual | Type::Group | Type::Tenant)
|
||||
&& (fields.is_empty() || fields.contains(&PrincipalField::UsedQuota))
|
||||
{
|
||||
let quota = self
|
||||
.get_counter(DirectoryClass::UsedQuota(principal.id))
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if quota > 0 {
|
||||
principal.set(PrincipalField::UsedQuota, quota as u64);
|
||||
}
|
||||
}
|
||||
|
||||
// Map permissions
|
||||
for field in [
|
||||
PrincipalField::EnabledPermissions,
|
||||
PrincipalField::DisabledPermissions,
|
||||
] {
|
||||
if let Some(permissions) = principal.take_int_array(field) {
|
||||
for permission in permissions {
|
||||
if let Some(name) = Permission::from_id(permission as usize) {
|
||||
principal.append_str(field, name.name().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PrincipalField {
|
||||
|
|
|
@ -40,32 +40,24 @@ pub struct Principal {
|
|||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Type {
|
||||
#[serde(rename = "individual")]
|
||||
#[default]
|
||||
Individual = 0,
|
||||
#[serde(rename = "group")]
|
||||
Group = 1,
|
||||
#[serde(rename = "resource")]
|
||||
Resource = 2,
|
||||
#[serde(rename = "location")]
|
||||
Location = 3,
|
||||
#[serde(rename = "list")]
|
||||
List = 5,
|
||||
#[serde(rename = "other")]
|
||||
Other = 6,
|
||||
#[serde(rename = "domain")]
|
||||
Domain = 7,
|
||||
#[serde(rename = "tenant")]
|
||||
Tenant = 8,
|
||||
#[serde(rename = "role")]
|
||||
Role = 9,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Permission {
|
||||
// Admin
|
||||
Impersonate,
|
||||
|
|
|
@ -351,6 +351,10 @@ impl<T: SessionStream> SessionData<T> {
|
|||
.shared_accounts(Collection::Mailbox)
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
let c = println!(
|
||||
"{} has_access_to: {:?}",
|
||||
access_token.primary_id, has_access_to
|
||||
);
|
||||
for account in mailboxes.drain(..) {
|
||||
if access_token.is_primary_id(account.account_id)
|
||||
|| has_access_to.contains(&account.account_id)
|
||||
|
|
|
@ -68,16 +68,19 @@ impl<T: SessionStream> SessionData<T> {
|
|||
op_start: Instant,
|
||||
) -> trc::Result<Vec<u8>> {
|
||||
// Resync messages if needed
|
||||
let c = println!("Checking mailbox acl 1 {:?}", mailbox.state.lock());
|
||||
let account_id = mailbox.id.account_id;
|
||||
self.synchronize_messages(&mailbox)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Convert IMAP ids to JMAP ids.
|
||||
let c = println!("Checking mailbox acl 2 {:?}", mailbox.state.lock());
|
||||
let mut ids = mailbox
|
||||
.sequence_to_ids(&arguments.sequence_set, is_uid)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let c = println!("Checking mailbox acl3 {:?}", arguments.sequence_set);
|
||||
if ids.is_empty() {
|
||||
return Ok(StatusResponse::completed(Command::Store(is_uid))
|
||||
.with_tag(arguments.tag)
|
||||
|
@ -85,6 +88,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
}
|
||||
|
||||
// Verify that the user can modify messages in this mailbox.
|
||||
let c = println!("Checking mailbox acl4");
|
||||
if !self
|
||||
.check_mailbox_acl(
|
||||
mailbox.id.account_id,
|
||||
|
|
|
@ -13,7 +13,7 @@ use directory::{
|
|||
manage::{self, not_found, ManageDirectory},
|
||||
PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets,
|
||||
},
|
||||
DirectoryInner, Permission, Principal, QueryBy, Type, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER,
|
||||
DirectoryInner, Permission, Principal, QueryBy, Type,
|
||||
};
|
||||
|
||||
use hyper::{header, Method};
|
||||
|
@ -89,54 +89,6 @@ impl JMAP {
|
|||
self.assert_supported_directory()?;
|
||||
}
|
||||
|
||||
// Validate tenant limits
|
||||
#[cfg(feature = "enterprise")]
|
||||
if self.core.is_enterprise_edition() {
|
||||
if let Some(tenant_info) = access_token.tenant {
|
||||
let tenant = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(tenant_info.id), false)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
trc::ManageEvent::NotFound
|
||||
.into_err()
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
|
||||
// Enforce tenant quotas
|
||||
if let Some(limit) = tenant
|
||||
.get_int_array(PrincipalField::Quota)
|
||||
.and_then(|quotas| quotas.get(principal.typ() as usize + 1))
|
||||
.copied()
|
||||
.filter(|q| *q > 0)
|
||||
{
|
||||
// Obtain number of principals
|
||||
let total = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.count_principals(
|
||||
None,
|
||||
principal.typ().into(),
|
||||
tenant_info.id.into(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if total >= limit {
|
||||
trc::bail!(trc::LimitEvent::TenantQuota
|
||||
.into_err()
|
||||
.details("Tenant principal quota exceeded")
|
||||
.ctx(trc::Key::Details, principal.typ().as_str())
|
||||
.ctx(trc::Key::Limit, limit)
|
||||
.ctx(trc::Key::Total, total));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create principal
|
||||
let result = self
|
||||
.core
|
||||
|
@ -154,20 +106,59 @@ impl JMAP {
|
|||
// List principal ids
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let filter = params.get("filter");
|
||||
let typ = params.parse("type").unwrap_or(Type::Individual);
|
||||
let page: usize = params.parse("page").unwrap_or(0);
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
|
||||
// Parse types
|
||||
let mut types = Vec::new();
|
||||
for typ in params
|
||||
.get("types")
|
||||
.or_else(|| params.get("type"))
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
{
|
||||
if let Some(typ) = Type::parse(typ) {
|
||||
if !types.contains(&typ) {
|
||||
types.push(typ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
let mut fields = Vec::new();
|
||||
for field in params.get("fields").unwrap_or_default().split(',') {
|
||||
if let Some(field) = PrincipalField::try_parse(field) {
|
||||
if !fields.contains(&field) {
|
||||
fields.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(match typ {
|
||||
Type::Individual => Permission::IndividualList,
|
||||
Type::Group => Permission::GroupList,
|
||||
Type::List => Permission::MailingListList,
|
||||
Type::Domain => Permission::DomainList,
|
||||
Type::Tenant => Permission::TenantList,
|
||||
Type::Role => Permission::RoleList,
|
||||
Type::Resource | Type::Location | Type::Other => Permission::PrincipalList,
|
||||
})?;
|
||||
let validate_types = if !types.is_empty() {
|
||||
types.as_slice()
|
||||
} else {
|
||||
&[
|
||||
Type::Individual,
|
||||
Type::Group,
|
||||
Type::List,
|
||||
Type::Domain,
|
||||
Type::Tenant,
|
||||
Type::Role,
|
||||
Type::Other,
|
||||
]
|
||||
};
|
||||
for typ in validate_types {
|
||||
access_token.assert_has_permission(match typ {
|
||||
Type::Individual => Permission::IndividualList,
|
||||
Type::Group => Permission::GroupList,
|
||||
Type::List => Permission::MailingListList,
|
||||
Type::Domain => Permission::DomainList,
|
||||
Type::Tenant => Permission::TenantList,
|
||||
Type::Role => Permission::RoleList,
|
||||
Type::Resource | Type::Location | Type::Other => Permission::PrincipalList,
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut tenant = access_token.tenant.map(|t| t.id);
|
||||
|
||||
|
@ -186,32 +177,19 @@ impl JMAP {
|
|||
.map(|p| p.id);
|
||||
}
|
||||
}
|
||||
} else if matches!(typ, Type::Tenant) {
|
||||
} else if types.contains(&Type::Tenant) {
|
||||
return Err(manage::enterprise());
|
||||
}
|
||||
|
||||
let accounts = self
|
||||
let principals = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.list_principals(filter, typ.into(), tenant)
|
||||
.list_principals(filter, tenant, &types, &fields, page, limit)
|
||||
.await?;
|
||||
|
||||
let (total, accounts) = if limit > 0 {
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
(
|
||||
accounts.len(),
|
||||
accounts.into_iter().skip(offset).take(limit).collect(),
|
||||
)
|
||||
} else {
|
||||
(accounts.len(), accounts)
|
||||
};
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": {
|
||||
"items": accounts,
|
||||
"total": total,
|
||||
},
|
||||
"data": principals,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
|
@ -256,64 +234,13 @@ impl JMAP {
|
|||
.await?
|
||||
.ok_or_else(|| trc::ManageEvent::NotFound.into_err())?;
|
||||
|
||||
// Map groups
|
||||
for field in [
|
||||
PrincipalField::MemberOf,
|
||||
PrincipalField::Lists,
|
||||
PrincipalField::Roles,
|
||||
] {
|
||||
if let Some(member_of) = principal.take_int_array(field) {
|
||||
for principal_id in member_of {
|
||||
match principal_id as u32 {
|
||||
ROLE_ADMIN if field == PrincipalField::Roles => {
|
||||
principal.append_str(field, "admin");
|
||||
}
|
||||
ROLE_TENANT_ADMIN if field == PrincipalField::Roles => {
|
||||
principal.append_str(field, "tenant-admin");
|
||||
}
|
||||
ROLE_USER if field == PrincipalField::Roles => {
|
||||
principal.append_str(field, "user");
|
||||
}
|
||||
principal_id => {
|
||||
if let Some(name) = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_principal_name(principal_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
principal.append_str(field, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain quota usage
|
||||
if matches!(typ, Type::Individual | Type::Group | Type::Tenant) {
|
||||
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? {
|
||||
if let Some(mut member_principal) = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(member_id), false)
|
||||
.await?
|
||||
{
|
||||
if let Some(name) = member_principal.take_str(PrincipalField::Name)
|
||||
{
|
||||
principal.append_str(PrincipalField::Members, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Map fields
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.map_field_ids(&mut principal, &[])
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": principal,
|
||||
|
@ -334,9 +261,6 @@ impl JMAP {
|
|||
}
|
||||
})?;
|
||||
|
||||
// Remove FTS index
|
||||
self.core.storage.fts.remove_all(account_id).await?;
|
||||
|
||||
// Delete account
|
||||
self.core
|
||||
.storage
|
||||
|
@ -344,6 +268,11 @@ impl JMAP {
|
|||
.delete_principal(QueryBy::Id(account_id))
|
||||
.await?;
|
||||
|
||||
// Remove FTS index
|
||||
if matches!(typ, Type::Individual | Type::Group) {
|
||||
self.core.storage.fts.remove_all(account_id).await?;
|
||||
}
|
||||
|
||||
// Remove entries from cache
|
||||
self.inner.sessions.retain(|_, id| id.item != account_id);
|
||||
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use common::auth::AccessToken;
|
||||
use directory::{backend::internal::manage::ManageDirectory, Permission, Type};
|
||||
use directory::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalField},
|
||||
Permission, Type,
|
||||
};
|
||||
use hyper::Method;
|
||||
use mail_auth::{
|
||||
dmarc::URI,
|
||||
|
@ -120,8 +123,22 @@ impl JMAP {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.list_principals(None, Type::Domain.into(), tenant.id.into())
|
||||
.list_principals(
|
||||
None,
|
||||
tenant.id.into(),
|
||||
&[Type::Domain],
|
||||
&[PrincipalField::Name],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.map(|principals| {
|
||||
principals
|
||||
.items
|
||||
.into_iter()
|
||||
.filter_map(|mut p| p.take_str(PrincipalField::Name))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.caused_by(trc::location!())?
|
||||
.into();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use directory::{backend::internal::manage::ManageDirectory, Permission, Type};
|
||||
use directory::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalField},
|
||||
Permission, Type,
|
||||
};
|
||||
use hyper::Method;
|
||||
use mail_auth::report::{
|
||||
tlsrpt::{FailureDetails, Policy, TlsReport},
|
||||
|
@ -48,8 +51,22 @@ impl JMAP {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.list_principals(None, Type::Domain.into(), tenant.id.into())
|
||||
.list_principals(
|
||||
None,
|
||||
tenant.id.into(),
|
||||
&[Type::Domain],
|
||||
&[PrincipalField::Name],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.map(|principals| {
|
||||
principals
|
||||
.items
|
||||
.into_iter()
|
||||
.filter_map(|mut p| p.take_str(PrincipalField::Name))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.caused_by(trc::location!())?
|
||||
.into();
|
||||
}
|
||||
|
|
|
@ -33,13 +33,13 @@ async fn internal_directory() {
|
|||
|
||||
// A principal without name should fail
|
||||
assert_eq!(
|
||||
store.create_account(Principal::default()).await,
|
||||
store.create_principal(Principal::default(), None).await,
|
||||
Err(manage::err_missing(PrincipalField::Name))
|
||||
);
|
||||
|
||||
// Basic account creation
|
||||
let john_id = store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "john".to_string(),
|
||||
description: Some("John Doe".to_string()),
|
||||
|
@ -47,6 +47,7 @@ async fn internal_directory() {
|
|||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -54,12 +55,13 @@ async fn internal_directory() {
|
|||
// Two accounts with the same name should fail
|
||||
assert_eq!(
|
||||
store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "john".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(manage::err_exists(PrincipalField::Name, "john".to_string()))
|
||||
|
@ -68,32 +70,45 @@ async fn internal_directory() {
|
|||
// An account using a non-existent domain should fail
|
||||
assert_eq!(
|
||||
store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "jane".to_string(),
|
||||
emails: vec!["jane@example.org".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(manage::not_found("example.org".to_string()))
|
||||
);
|
||||
|
||||
// Create a domain name
|
||||
assert_eq!(store.create_domain("example.org").await, Ok(()));
|
||||
store
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "example.org".to_string(),
|
||||
typ: Type::Domain,
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.is_local_domain("example.org").await.unwrap());
|
||||
assert!(!store.is_local_domain("otherdomain.org").await.unwrap());
|
||||
|
||||
// Add an email address
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john"),
|
||||
vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::String("john@example.org".to_string()),
|
||||
)],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
|
@ -107,12 +122,13 @@ async fn internal_directory() {
|
|||
// Using non-existent domain should fail
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john"),
|
||||
vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::String("john@otherdomain.org".to_string()),
|
||||
)],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(manage::not_found("otherdomain.org".to_string()))
|
||||
|
@ -120,7 +136,7 @@ async fn internal_directory() {
|
|||
|
||||
// Create an account with an email address
|
||||
let jane_id = store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "jane".to_string(),
|
||||
description: Some("Jane Doe".to_string()),
|
||||
|
@ -130,6 +146,7 @@ async fn internal_directory() {
|
|||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -180,14 +197,15 @@ async fn internal_directory() {
|
|||
// Duplicate email address should fail
|
||||
assert_eq!(
|
||||
store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "janeth".to_string(),
|
||||
description: Some("Janeth Doe".to_string()),
|
||||
emails: vec!["jane@example.org".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
.into(),
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(manage::err_exists(
|
||||
|
@ -198,7 +216,7 @@ async fn internal_directory() {
|
|||
|
||||
// Create a mailing list
|
||||
let list_id = store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "list".to_string(),
|
||||
typ: Type::List,
|
||||
|
@ -206,17 +224,19 @@ async fn internal_directory() {
|
|||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("list"),
|
||||
vec![PrincipalUpdate::set(
|
||||
PrincipalField::Members,
|
||||
PrincipalValue::StringList(vec!["john".to_string(), "jane".to_string()]),
|
||||
),],
|
||||
)],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
|
@ -261,7 +281,7 @@ async fn internal_directory() {
|
|||
|
||||
// Create groups
|
||||
store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "sales".to_string(),
|
||||
description: Some("Sales Team".to_string()),
|
||||
|
@ -269,11 +289,12 @@ async fn internal_directory() {
|
|||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.create_account(
|
||||
.create_principal(
|
||||
TestPrincipal {
|
||||
name: "support".to_string(),
|
||||
description: Some("Support Team".to_string()),
|
||||
|
@ -281,6 +302,7 @@ async fn internal_directory() {
|
|||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -288,7 +310,7 @@ async fn internal_directory() {
|
|||
// Add John to the Sales and Support groups
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john"),
|
||||
vec![
|
||||
PrincipalUpdate::add_item(
|
||||
|
@ -300,23 +322,19 @@ async fn internal_directory() {
|
|||
PrincipalValue::String("support".to_string()),
|
||||
)
|
||||
],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
);
|
||||
let mut principal = store
|
||||
.query(QueryBy::Name("john"), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
store.map_field_ids(&mut principal, &[]).await.unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.map_group_ids(
|
||||
store
|
||||
.query(QueryBy::Name("john"), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_test()
|
||||
.into_sorted(),
|
||||
principal.into_test().into_sorted(),
|
||||
TestPrincipal {
|
||||
id: john_id,
|
||||
name: "john".to_string(),
|
||||
|
@ -335,12 +353,13 @@ async fn internal_directory() {
|
|||
// Adding a non-existent user should fail
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john"),
|
||||
vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::MemberOf,
|
||||
PrincipalValue::String("accounting".to_string()),
|
||||
)],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(manage::not_found("accounting".to_string()))
|
||||
|
@ -349,29 +368,25 @@ async fn internal_directory() {
|
|||
// Remove a member from a group
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john"),
|
||||
vec![PrincipalUpdate::remove_item(
|
||||
PrincipalField::MemberOf,
|
||||
PrincipalValue::String("support".to_string()),
|
||||
)],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
);
|
||||
let mut principal = store
|
||||
.query(QueryBy::Name("john"), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
store.map_field_ids(&mut principal, &[]).await.unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.map_group_ids(
|
||||
store
|
||||
.query(QueryBy::Name("john"), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_test()
|
||||
.into_sorted(),
|
||||
principal.into_test().into_sorted(),
|
||||
TestPrincipal {
|
||||
id: john_id,
|
||||
name: "john".to_string(),
|
||||
|
@ -386,7 +401,7 @@ async fn internal_directory() {
|
|||
// Update multiple fields
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john"),
|
||||
vec![
|
||||
PrincipalUpdate::set(
|
||||
|
@ -411,23 +426,20 @@ async fn internal_directory() {
|
|||
PrincipalValue::String("john.doe@example.org".to_string()),
|
||||
)
|
||||
],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
);
|
||||
|
||||
let mut principal = store
|
||||
.query(QueryBy::Name("john.doe"), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
store.map_field_ids(&mut principal, &[]).await.unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.map_group_ids(
|
||||
store
|
||||
.query(QueryBy::Name("john.doe"), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_test()
|
||||
.into_sorted(),
|
||||
principal.into_test().into_sorted(),
|
||||
TestPrincipal {
|
||||
id: john_id,
|
||||
name: "john.doe".to_string(),
|
||||
|
@ -446,12 +458,13 @@ async fn internal_directory() {
|
|||
// Remove a member from a mailing list and then add it back
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("list"),
|
||||
vec![PrincipalUpdate::remove_item(
|
||||
PrincipalField::Members,
|
||||
PrincipalValue::String("john.doe".to_string()),
|
||||
)],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
|
@ -462,12 +475,13 @@ async fn internal_directory() {
|
|||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("list"),
|
||||
vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::Members,
|
||||
PrincipalValue::String("john.doe".to_string()),
|
||||
)],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
|
@ -485,24 +499,26 @@ async fn internal_directory() {
|
|||
// Field validation
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john.doe"),
|
||||
vec![PrincipalUpdate::set(
|
||||
PrincipalField::Name,
|
||||
PrincipalValue::String("jane".to_string())
|
||||
),],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(manage::err_exists(PrincipalField::Name, "jane".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.update_account(
|
||||
.update_principal(
|
||||
QueryBy::Name("john.doe"),
|
||||
vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::String("jane@example.org".to_string())
|
||||
),],
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(manage::err_exists(
|
||||
|
@ -514,10 +530,12 @@ async fn internal_directory() {
|
|||
// List accounts
|
||||
assert_eq!(
|
||||
store
|
||||
.list_accounts(None, None)
|
||||
.list_principals(None, None, &[], &[], 0, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<AHashSet<_>>(),
|
||||
["jane", "john.doe", "list", "sales", "support"]
|
||||
.into_iter()
|
||||
|
@ -525,15 +543,24 @@ async fn internal_directory() {
|
|||
.collect::<AHashSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
store.list_accounts("john".into(), None).await.unwrap(),
|
||||
store
|
||||
.list_principals("john".into(), None, &[], &[], 0, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["john.doe"]
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.list_accounts(None, Type::Individual.into())
|
||||
.list_principals(None, None, &[Type::Individual], &[], 0, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<AHashSet<_>>(),
|
||||
["jane", "john.doe"]
|
||||
.into_iter()
|
||||
|
@ -542,10 +569,12 @@ async fn internal_directory() {
|
|||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.list_accounts(None, Type::Group.into())
|
||||
.list_principals(None, None, &[Type::Group], &[], 0, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<AHashSet<_>>(),
|
||||
["sales", "support"]
|
||||
.into_iter()
|
||||
|
@ -553,7 +582,14 @@ async fn internal_directory() {
|
|||
.collect::<AHashSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
store.list_accounts(None, Type::List.into()).await.unwrap(),
|
||||
store
|
||||
.list_principals(None, None, &[Type::List], &[], 0, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["list"]
|
||||
);
|
||||
|
||||
|
@ -588,7 +624,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();
|
||||
store.delete_principal(QueryBy::Id(john_id)).await.unwrap();
|
||||
assert_eq!(store.get_principal_id("john.doe").await.unwrap(), None);
|
||||
assert_eq!(
|
||||
store.email_to_ids("john.doe@example.org").await.unwrap(),
|
||||
|
@ -597,10 +633,12 @@ async fn internal_directory() {
|
|||
assert!(!store.rcpt("john.doe@example.org").await.unwrap());
|
||||
assert_eq!(
|
||||
store
|
||||
.list_accounts(None, None)
|
||||
.list_principals(None, None, &[], &[], 0, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<AHashSet<_>>(),
|
||||
["jane", "list", "sales", "support"]
|
||||
.into_iter()
|
||||
|
|
|
@ -166,6 +166,7 @@ pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConn
|
|||
.await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
}
|
||||
let c = println!("----cococ");
|
||||
imap_john.send("UID STORE 1 +FLAGS (\\Deleted)").await;
|
||||
imap_john.assert_read(Type::Tagged, ResponseType::No).await;
|
||||
|
||||
|
|
|
@ -424,7 +424,10 @@ 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_principal_id("admin").await.unwrap();
|
||||
store
|
||||
.get_or_create_principal_id("admin", directory::Type::Individual)
|
||||
.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_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap()
|
||||
.into();
|
||||
|
@ -59,7 +59,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jane.smith@example.com")
|
||||
.get_or_create_principal_id("jane.smith@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap()
|
||||
.into();
|
||||
|
@ -67,7 +67,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("bill@example.com")
|
||||
.get_or_create_principal_id("bill@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap()
|
||||
.into();
|
||||
|
@ -75,7 +75,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("sales@example.com")
|
||||
.get_or_create_principal_id("sales@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap()
|
||||
.into();
|
||||
|
@ -671,7 +671,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.add_to_group(name, "sales@example.com")
|
||||
.await;
|
||||
}
|
||||
server.inner.access_tokens.clear();
|
||||
server.core.security.access_tokens.clear();
|
||||
john_client.refresh_session().await.unwrap();
|
||||
jane_client.refresh_session().await.unwrap();
|
||||
bill_client.refresh_session().await.unwrap();
|
||||
|
|
|
@ -42,7 +42,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
@ -268,5 +268,5 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
// Check webhook events
|
||||
params
|
||||
.webhook
|
||||
.assert_contains(&["auth.failed", "auth.success", "auth.banned"]);
|
||||
.assert_contains(&["auth.failed", "auth.success", "security.authentication-ban"]);
|
||||
}
|
||||
|
|
|
@ -26,9 +26,9 @@ use super::JMAPTest;
|
|||
#[derive(serde::Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct OAuthCodeResponse {
|
||||
code: String,
|
||||
is_admin: bool,
|
||||
is_enterprise: bool,
|
||||
pub code: String,
|
||||
#[serde(rename = "isEnterprise")]
|
||||
pub is_enterprise: bool,
|
||||
}
|
||||
|
||||
pub async fn test(params: &mut JMAPTest) {
|
||||
|
@ -45,7 +45,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -25,7 +25,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
|
|
@ -32,7 +32,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -41,7 +41,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
@ -51,7 +51,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jane@example.com")
|
||||
.get_or_create_principal_id("jane@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
@ -61,7 +61,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("bill@example.com")
|
||||
.get_or_create_principal_id("bill@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -89,7 +89,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -34,7 +34,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -4,13 +4,18 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use base64::{
|
||||
engine::general_purpose::{self, STANDARD},
|
||||
Engine,
|
||||
};
|
||||
use common::{
|
||||
auth::AccessToken,
|
||||
config::{
|
||||
server::{ServerProtocol, Servers},
|
||||
telemetry::Telemetry,
|
||||
|
@ -36,7 +41,7 @@ use store::{
|
|||
IterateParams, Stores, ValueKey, SUBSPACE_PROPERTY,
|
||||
};
|
||||
use tokio::sync::{mpsc, watch};
|
||||
use utils::{config::Config, BlobHash};
|
||||
use utils::{config::Config, map::ttl_dashmap::TtlMap, BlobHash};
|
||||
use webhooks::{spawn_mock_webhook_endpoint, MockWebhookEndpoint};
|
||||
|
||||
use crate::{add_test_certs, directory::DirectoryStore, store::TempDir, AssertConfig};
|
||||
|
@ -287,7 +292,7 @@ disabled-events = ["network.*"]
|
|||
|
||||
[webhook."test"]
|
||||
url = "http://127.0.0.1:8821/hook"
|
||||
events = ["auth.*", "delivery.dsn*", "message-ingest.*"]
|
||||
events = ["auth.*", "delivery.dsn*", "message-ingest.*", "security.authentication-ban"]
|
||||
signature-key = "ovos-moles"
|
||||
throttle = "100ms"
|
||||
|
||||
|
@ -311,7 +316,7 @@ pub async fn jmap_tests() {
|
|||
.await;
|
||||
|
||||
webhooks::test(&mut params).await;
|
||||
email_query::test(&mut params, delete).await;
|
||||
/*email_query::test(&mut params, delete).await;
|
||||
email_get::test(&mut params).await;
|
||||
email_set::test(&mut params).await;
|
||||
email_parse::test(&mut params).await;
|
||||
|
@ -324,7 +329,7 @@ pub async fn jmap_tests() {
|
|||
mailbox::test(&mut params).await;
|
||||
delivery::test(&mut params).await;
|
||||
auth_acl::test(&mut params).await;
|
||||
auth_limits::test(&mut params).await;
|
||||
auth_limits::test(&mut params).await;*/
|
||||
auth_oauth::test(&mut params).await;
|
||||
event_source::test(&mut params).await;
|
||||
push_subscription::test(&mut params).await;
|
||||
|
@ -469,7 +474,24 @@ pub async fn emails_purge_tombstoned(server: &JMAP) {
|
|||
.unwrap();
|
||||
|
||||
for account_id in account_ids {
|
||||
let do_add = server
|
||||
.core
|
||||
.security
|
||||
.access_tokens
|
||||
.get_with_ttl(&account_id)
|
||||
.is_none();
|
||||
|
||||
if do_add {
|
||||
server.core.security.access_tokens.insert_with_ttl(
|
||||
account_id,
|
||||
Arc::new(AccessToken::from_id(account_id)),
|
||||
Instant::now() + Duration::from_secs(3600),
|
||||
);
|
||||
}
|
||||
server.emails_purge_tombstoned(account_id).await.unwrap();
|
||||
if do_add {
|
||||
server.core.security.access_tokens.remove(&account_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.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_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
|
|
@ -34,7 +34,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
@ -43,7 +43,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("robert@example.com")
|
||||
.get_or_create_principal_id("robert@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
|
|
@ -42,7 +42,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -29,7 +29,7 @@ pub async fn test(server: Arc<JMAP>, mut client: Client) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("john")
|
||||
.get_or_create_principal_id("john", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap();
|
||||
client.set_default_account_id(Id::from(TEST_USER_ID).to_string());
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
jmap::{assert_is_empty, mailbox::destroy_all_mailboxes},
|
||||
store::deflate_test_resource,
|
||||
};
|
||||
use common::auth::AccessToken;
|
||||
use jmap::email::ingest::{IngestEmail, IngestSource};
|
||||
use jmap_client::{email, mailbox::Role};
|
||||
use jmap_proto::types::{collection::Collection, id::Id};
|
||||
|
@ -242,8 +243,7 @@ async fn test_multi_thread(params: &mut JMAPTest) {
|
|||
.email_ingest(IngestEmail {
|
||||
raw_message: message.contents(),
|
||||
message: MessageParser::new().parse(message.contents()),
|
||||
account_id: 0,
|
||||
account_quota: 0,
|
||||
resource: AccessToken::from_id(0).as_resource_token(),
|
||||
mailbox_ids: vec![mailbox_id],
|
||||
keywords: vec![],
|
||||
received_at: None,
|
||||
|
|
|
@ -36,7 +36,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_or_create_principal_id("jdoe@example.com")
|
||||
.get_or_create_principal_id("jdoe@example.com", directory::Type::Individual)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -203,12 +203,6 @@ pub async fn test(db: Store) {
|
|||
))),
|
||||
random_bytes(4),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryClass::Domain(random_bytes(
|
||||
4 + account_id as usize,
|
||||
))),
|
||||
random_bytes(4),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Static(
|
||||
account_id,
|
||||
|
|
Loading…
Reference in a new issue