Test fixes - part 1

This commit is contained in:
mdecimus 2024-09-15 18:59:36 +02:00
parent 49bce9a3de
commit 1e08e56672
31 changed files with 579 additions and 302 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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