mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-03-17 14:34:03 +00:00
Registry testing - part 6
This commit is contained in:
@@ -17,7 +17,7 @@ use ahash::AHasher;
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::Permission,
|
||||
structs::{self, Account, Roles},
|
||||
structs::{self, Account, Roles, UserRoles},
|
||||
},
|
||||
types::EnumImpl,
|
||||
};
|
||||
@@ -46,10 +46,18 @@ impl Server {
|
||||
let permissions = self
|
||||
.effective_permissions(
|
||||
&account.permissions,
|
||||
account
|
||||
.roles
|
||||
.role_ids()
|
||||
.unwrap_or(self.core.network.security.default_role_ids_user.as_slice()),
|
||||
match &account.roles {
|
||||
UserRoles::User => {
|
||||
self.core.network.security.default_role_ids_user.as_slice()
|
||||
}
|
||||
UserRoles::TenantAdmin => self
|
||||
.core
|
||||
.network
|
||||
.security
|
||||
.default_role_ids_tenant
|
||||
.as_slice(),
|
||||
UserRoles::Custom(custom_roles) => custom_roles.role_ids.as_slice(),
|
||||
},
|
||||
tenant_id,
|
||||
)
|
||||
.await?;
|
||||
@@ -731,8 +739,14 @@ fn hash_account(account: &Account) -> u64 {
|
||||
Account::User(account) => {
|
||||
account.member_tenant_id.hash(&mut s);
|
||||
match &account.roles {
|
||||
Roles::Default => {}
|
||||
Roles::Custom(custom_roles) => {
|
||||
UserRoles::User => {
|
||||
0u8.hash(&mut s);
|
||||
}
|
||||
UserRoles::TenantAdmin => {
|
||||
1u8.hash(&mut s);
|
||||
}
|
||||
UserRoles::Custom(custom_roles) => {
|
||||
2u8.hash(&mut s);
|
||||
custom_roles.role_ids.as_slice().hash(&mut s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use ahash::AHashSet;
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::Permission,
|
||||
structs::{self, Account, PermissionsList},
|
||||
structs::{self, Account, PermissionsList, UserRoles},
|
||||
},
|
||||
types::EnumImpl,
|
||||
};
|
||||
@@ -112,10 +112,16 @@ impl Server {
|
||||
let (permissions, role_ids, tenant_id) = match account {
|
||||
Account::User(account) => (
|
||||
&account.permissions,
|
||||
account
|
||||
.roles
|
||||
.role_ids()
|
||||
.unwrap_or(self.core.network.security.default_role_ids_user.as_slice()),
|
||||
match &account.roles {
|
||||
UserRoles::User => self.core.network.security.default_role_ids_user.as_slice(),
|
||||
UserRoles::TenantAdmin => self
|
||||
.core
|
||||
.network
|
||||
.security
|
||||
.default_role_ids_tenant
|
||||
.as_slice(),
|
||||
UserRoles::Custom(custom_roles) => custom_roles.role_ids.as_slice(),
|
||||
},
|
||||
account.member_tenant_id.map(|t| t.document_id()),
|
||||
),
|
||||
Account::Group(account) => (
|
||||
|
||||
3
crates/common/src/cache/directory.rs
vendored
3
crates/common/src/cache/directory.rs
vendored
@@ -12,6 +12,7 @@ use registry::{
|
||||
prelude::{Object, ObjectType},
|
||||
structs::{
|
||||
Account, Credential, EmailAlias, GroupAccount, PasswordCredential, Roles, UserAccount,
|
||||
UserRoles,
|
||||
},
|
||||
},
|
||||
types::{datetime::UTCDateTime, id::ObjectId, list::List},
|
||||
@@ -180,7 +181,7 @@ impl Server {
|
||||
description: account.description,
|
||||
member_group_ids: member_group_ids.into(),
|
||||
member_tenant_id: domain.id_tenant.map(Id::from),
|
||||
roles: Roles::Default,
|
||||
roles: UserRoles::User,
|
||||
credentials: List::from_iter([Credential::Password(PasswordCredential {
|
||||
secret: account.secret.unwrap_or_default(),
|
||||
..Default::default()
|
||||
|
||||
2
crates/common/src/cache/invalidate.rs
vendored
2
crates/common/src/cache/invalidate.rs
vendored
@@ -197,10 +197,10 @@ impl CacheInvalidationBuilder {
|
||||
impl Server {
|
||||
pub async fn invalidate_caches(&self, changes: CacheInvalidationBuilder) -> trc::Result<()> {
|
||||
let mut changes = changes.changes;
|
||||
let c = println!("Invalidating caches for changes: {:?}", changes);
|
||||
if changes.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let c = println!("Invalidating caches for changes: {:?}", changes);
|
||||
|
||||
// Invalidate objects linking roles
|
||||
let mut role_ids = changes
|
||||
|
||||
@@ -370,8 +370,6 @@ impl OAuthApiHandler for Server {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let c = println!("Expires in: {}", self.core.oauth.oauth_expiry_user_code);
|
||||
|
||||
// Insert user code
|
||||
self.in_memory_store()
|
||||
.key_set(
|
||||
|
||||
@@ -260,12 +260,20 @@ impl RegistryGet for Server {
|
||||
|| get.properties.contains(&Property::DnsZoneFile) =>
|
||||
{
|
||||
let todo = "domain dns zone file";
|
||||
todo!()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
get.insert(id, object.into_value());
|
||||
let mut object = object.into_value();
|
||||
if !extra_properties.is_empty()
|
||||
&& let JmapValue::Object(obj) = &mut object
|
||||
{
|
||||
for (key, value) in extra_properties {
|
||||
obj.insert_unchecked(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
get.insert(id, object);
|
||||
}
|
||||
|
||||
Ok(get.into_response())
|
||||
@@ -300,30 +308,9 @@ impl RegistryGetResponse<'_> {
|
||||
pub fn insert(&mut self, id: Id, mut object: JmapValue<'static>) {
|
||||
let object_map = object.as_object_mut().unwrap();
|
||||
|
||||
if self.is_tenant_filtered
|
||||
&& let Some(tenant_id) = self.access_token.tenant_id()
|
||||
{
|
||||
let expected_value = JmapValue::Element(RegistryValue::Id(Id::from(tenant_id)));
|
||||
for (key, value) in object_map.iter() {
|
||||
if matches!(key, Key::Property(Property::MemberTenantId))
|
||||
&& (value != &expected_value
|
||||
|| value
|
||||
.as_array()
|
||||
.is_none_or(|arr| !arr.contains(&expected_value)))
|
||||
{
|
||||
self.not_found(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if self.is_tenant_filtered && self.access_token.tenant_id().is_some() {
|
||||
object_map.remove(&Key::Property(Property::MemberTenantId));
|
||||
} else if self.is_account_filtered {
|
||||
let expected_value = JmapValue::Element(RegistryValue::Id(self.account_id.into()));
|
||||
for (key, value) in object_map.iter() {
|
||||
if matches!(key, Key::Property(Property::AccountId)) && value != &expected_value {
|
||||
self.not_found(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
object_map.remove(&Key::Property(Property::AccountId));
|
||||
}
|
||||
|
||||
|
||||
@@ -796,7 +796,7 @@ pub(crate) async fn credential_query(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn validate_credential_permissions(
|
||||
pub(crate) fn validate_credential_permissions(
|
||||
access_token: &AccessToken,
|
||||
credential: &SecondaryCredential,
|
||||
) -> Result<(), SetError<Property>> {
|
||||
|
||||
@@ -322,6 +322,7 @@ pub(crate) async fn validate_tenant_quota(
|
||||
(ObjectType::OAuthClient, None, "OAuth clients")
|
||||
}
|
||||
TenantStorageQuota::MaxDkimKeys => (ObjectType::DkimSignature, None, "DKIM keys"),
|
||||
TenantStorageQuota::MaxDnsServers => (ObjectType::DnsServer, None, "DNS servers"),
|
||||
TenantStorageQuota::MaxDiskQuota => unreachable!(),
|
||||
};
|
||||
let query = RegistryQuery::new(object_type).with_tenant(tenant_id.into());
|
||||
|
||||
@@ -420,6 +420,9 @@ impl RegistrySet for Server {
|
||||
ObjectInner::OAuthClient(_) if is_create => {
|
||||
validate_tenant_quota(&set, TenantStorageQuota::MaxOauthClients).await?
|
||||
}
|
||||
ObjectInner::DnsServer(_) if is_create => {
|
||||
validate_tenant_quota(&set, TenantStorageQuota::MaxDnsServers).await?
|
||||
}
|
||||
_ => Ok(ObjectResponse::default()),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,908 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
directory::internal::TestInternalDirectory,
|
||||
jmap::{JMAPTest, ManagementApi, server::List},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use email::message::delivery::{IngestMessage, IngestRecipient, LocalDeliveryStatus, MailDelivery};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn test(params: &JMAPTest) {
|
||||
println!("Running permissions tests...");
|
||||
let server = params.server.clone();
|
||||
|
||||
// Disable spam filtering to avoid adding extra headers
|
||||
let old_core = params.server.core.clone();
|
||||
let mut new_core = old_core.as_ref().clone();
|
||||
new_core.spam.enabled = false;
|
||||
new_core.smtp.session.data.add_delivered_to = false;
|
||||
params.server.inner.shared_core.store(Arc::new(new_core));
|
||||
|
||||
// Remove unlimited requests permission
|
||||
for &account in params.accounts.keys() {
|
||||
params
|
||||
.server
|
||||
.store()
|
||||
.remove_permissions(account, [Permission::UnlimitedRequests])
|
||||
.await;
|
||||
}
|
||||
|
||||
// Prepare management API
|
||||
let api = ManagementApi::new(8899, "admin", "secret");
|
||||
|
||||
// Create a user with the default 'user' role
|
||||
let account_id = api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Individual)
|
||||
.with_field(PrincipalField::Name, "role_player")
|
||||
.with_field(PrincipalField::Roles, vec!["user".to_string()])
|
||||
.with_field(
|
||||
PrincipalField::DisabledPermissions,
|
||||
vec![Permission::Pop3Dele.name().to_string()],
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
let revision = server
|
||||
.get_access_token(account_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(
|
||||
Permission::all().filter(|p| p.is_user_permission() && *p != Permission::Pop3Dele),
|
||||
)
|
||||
.revision;
|
||||
|
||||
// Create multiple roles
|
||||
for (role, permissions, parent_role) in &[
|
||||
(
|
||||
"pop3_user",
|
||||
vec![Permission::Pop3Authenticate, Permission::Pop3List],
|
||||
vec![],
|
||||
),
|
||||
(
|
||||
"imap_user",
|
||||
vec![Permission::ImapAuthenticate, Permission::ImapList],
|
||||
vec![],
|
||||
),
|
||||
(
|
||||
"jmap_user",
|
||||
vec![
|
||||
Permission::JmapEmailQuery,
|
||||
Permission::AuthenticateOauth,
|
||||
Permission::ManageEncryption,
|
||||
],
|
||||
vec![],
|
||||
),
|
||||
(
|
||||
"email_user",
|
||||
vec![Permission::EmailSend, Permission::EmailReceive],
|
||||
vec!["pop3_user", "imap_user", "jmap_user"],
|
||||
),
|
||||
] {
|
||||
api.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Role)
|
||||
.with_field(PrincipalField::Name, role.to_string())
|
||||
.with_field(
|
||||
PrincipalField::EnabledPermissions,
|
||||
permissions
|
||||
.iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
parent_role
|
||||
.iter()
|
||||
.map(|r| r.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
}
|
||||
|
||||
// Update email_user role
|
||||
api.patch::<()>(
|
||||
"/api/principal/email_user",
|
||||
&vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::DisabledPermissions,
|
||||
PrincipalValue::String(Permission::ManageEncryption.name().to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Update the user role to the nested 'email_user' role
|
||||
api.patch::<()>(
|
||||
"/api/principal/role_player",
|
||||
&vec![PrincipalUpdate::set(
|
||||
PrincipalField::Roles,
|
||||
PrincipalValue::StringList(vec!["email_user".to_string()]),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
assert_ne!(
|
||||
server
|
||||
.get_access_token(account_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions([
|
||||
Permission::EmailSend,
|
||||
Permission::EmailReceive,
|
||||
Permission::JmapEmailQuery,
|
||||
Permission::AuthenticateOauth,
|
||||
Permission::ImapAuthenticate,
|
||||
Permission::ImapList,
|
||||
Permission::Pop3Authenticate,
|
||||
Permission::Pop3List,
|
||||
])
|
||||
.revision,
|
||||
revision
|
||||
);
|
||||
|
||||
// Query all principals
|
||||
api.get::<List<PrincipalSet>>("/api/principal")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
.assert_count(12)
|
||||
.assert_exists(
|
||||
"admin",
|
||||
Type::Individual,
|
||||
[
|
||||
(PrincipalField::Roles, &["admin"][..]),
|
||||
(PrincipalField::Members, &[][..]),
|
||||
(PrincipalField::EnabledPermissions, &[][..]),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"role_player",
|
||||
Type::Individual,
|
||||
[
|
||||
(PrincipalField::Roles, &["email_user"][..]),
|
||||
(PrincipalField::Members, &[][..]),
|
||||
(PrincipalField::EnabledPermissions, &[][..]),
|
||||
(
|
||||
PrincipalField::DisabledPermissions,
|
||||
&[Permission::Pop3Dele.name()][..],
|
||||
),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"email_user",
|
||||
Type::Role,
|
||||
[
|
||||
(
|
||||
PrincipalField::Roles,
|
||||
&["pop3_user", "imap_user", "jmap_user"][..],
|
||||
),
|
||||
(PrincipalField::Members, &["role_player"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::EmailReceive.name(),
|
||||
Permission::EmailSend.name(),
|
||||
][..],
|
||||
),
|
||||
(
|
||||
PrincipalField::DisabledPermissions,
|
||||
&[Permission::ManageEncryption.name()][..],
|
||||
),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"pop3_user",
|
||||
Type::Role,
|
||||
[
|
||||
(PrincipalField::Roles, &[][..]),
|
||||
(PrincipalField::Members, &["email_user"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::Pop3Authenticate.name(),
|
||||
Permission::Pop3List.name(),
|
||||
][..],
|
||||
),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"imap_user",
|
||||
Type::Role,
|
||||
[
|
||||
(PrincipalField::Roles, &[][..]),
|
||||
(PrincipalField::Members, &["email_user"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::ImapAuthenticate.name(),
|
||||
Permission::ImapList.name(),
|
||||
][..],
|
||||
),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"jmap_user",
|
||||
Type::Role,
|
||||
[
|
||||
(PrincipalField::Roles, &[][..]),
|
||||
(PrincipalField::Members, &["email_user"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::JmapEmailQuery.name(),
|
||||
Permission::AuthenticateOauth.name(),
|
||||
Permission::ManageEncryption.name(),
|
||||
][..],
|
||||
),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
);
|
||||
|
||||
// Create new tenants
|
||||
let tenant_id = api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Tenant)
|
||||
.with_field(PrincipalField::Name, "foobar")
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
vec!["tenant-admin".to_string(), "user".to_string()],
|
||||
)
|
||||
.with_field(
|
||||
PrincipalField::Quota,
|
||||
PrincipalValue::IntegerList(vec![TENANT_QUOTA, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
let other_tenant_id = api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Tenant)
|
||||
.with_field(PrincipalField::Name, "xanadu")
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
vec!["tenant-admin".to_string(), "user".to_string()],
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Creating a tenant without a valid domain should fail
|
||||
api.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Individual)
|
||||
.with_field(PrincipalField::Name, "admin-foobar")
|
||||
.with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()])
|
||||
.with_field(
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::String("mytenantpass".to_string()),
|
||||
)
|
||||
.with_field(
|
||||
PrincipalField::Tenant,
|
||||
PrincipalValue::String("foobar".to_string()),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_error("Principal name must include a valid domain assigned to the tenant");
|
||||
|
||||
// Create domain for the tenant and one outside the tenant
|
||||
api.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Domain)
|
||||
.with_field(PrincipalField::Name, "foobar.org")
|
||||
.with_field(
|
||||
PrincipalField::Tenant,
|
||||
PrincipalValue::String("foobar".to_string()),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
api.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Domain).with_field(PrincipalField::Name, "example.org"),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Create tenant admin
|
||||
let tenant_admin_id = api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Individual)
|
||||
.with_field(PrincipalField::Name, "admin@foobar.org")
|
||||
.with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()])
|
||||
.with_field(
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::String("mytenantpass".to_string()),
|
||||
)
|
||||
.with_field(
|
||||
PrincipalField::Tenant,
|
||||
PrincipalValue::String("foobar".to_string()),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Verify permissions
|
||||
server
|
||||
.get_access_token(tenant_admin_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(Permission::all().filter(|p| p.is_tenant_admin_permission()))
|
||||
.validate_tenant(tenant_id, TENANT_QUOTA);
|
||||
|
||||
// Prepare tenant admin API
|
||||
let tenant_api = ManagementApi::new(8899, "admin@foobar.org", "mytenantpass");
|
||||
|
||||
// Tenant should not be able to create other tenants or modify its tenant id
|
||||
tenant_api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Tenant)
|
||||
.with_field(PrincipalField::Name, "subfoobar"),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_request_error("Forbidden");
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/foobar",
|
||||
&vec![PrincipalUpdate::set(
|
||||
PrincipalField::Tenant,
|
||||
PrincipalValue::String("subfoobar".to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_request_error("Forbidden");
|
||||
tenant_api
|
||||
.get::<()>("/api/principal/foobar")
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_request_error("Forbidden");
|
||||
tenant_api
|
||||
.get::<()>("/api/principal?type=tenant")
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_request_error("Forbidden");
|
||||
|
||||
// Create a second domain for the tenant
|
||||
tenant_api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Domain)
|
||||
.with_field(PrincipalField::Name, "foobar.com"),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Creating a third domain should be limited by quota
|
||||
tenant_api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Domain)
|
||||
.with_field(PrincipalField::Name, "foobar.net"),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_request_error("Tenant quota exceeded");
|
||||
|
||||
// Creating a tenant user without a valid domain or with a domain outside the tenant should fail
|
||||
for user in ["mytenantuser", "john@example.org"] {
|
||||
tenant_api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Individual)
|
||||
.with_field(PrincipalField::Name, user.to_string())
|
||||
.with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()]),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_error("Principal name must include a valid domain assigned to the tenant");
|
||||
}
|
||||
|
||||
// Create an account
|
||||
let tenant_user_id = tenant_api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Individual)
|
||||
.with_field(PrincipalField::Name, "john@foobar.org")
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
vec!["tenant-admin".to_string(), "user".to_string()],
|
||||
)
|
||||
.with_field(
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::String("tenantpass".to_string()),
|
||||
)
|
||||
.with_field(
|
||||
PrincipalField::Tenant,
|
||||
PrincipalValue::String("xanadu".to_string()),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Although super user privileges were used and a different tenant name was provided, this should be ignored
|
||||
server
|
||||
.get_access_token(tenant_user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(
|
||||
Permission::all().filter(|p| p.is_tenant_admin_permission() || p.is_user_permission()),
|
||||
)
|
||||
.validate_tenant(tenant_id, TENANT_QUOTA);
|
||||
|
||||
// Create a second account should be limited by quota
|
||||
tenant_api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Individual)
|
||||
.with_field(PrincipalField::Name, "jane@foobar.org")
|
||||
.with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()]),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_request_error("Tenant quota exceeded");
|
||||
|
||||
// Create an tenant role
|
||||
tenant_api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Role)
|
||||
.with_field(PrincipalField::Name, "no-mail-for-you@foobar.com")
|
||||
.with_field(
|
||||
PrincipalField::DisabledPermissions,
|
||||
vec![Permission::EmailReceive.name().to_string()],
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Assigning a role that does not belong to the tenant should fail
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/john@foobar.org",
|
||||
&vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::Roles,
|
||||
PrincipalValue::String("imap_user".to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_error("notFound");
|
||||
|
||||
// Add tenant defined role
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/john@foobar.org",
|
||||
&vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::Roles,
|
||||
PrincipalValue::String("no-mail-for-you@foobar.com".to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Check updated permissions
|
||||
server
|
||||
.get_access_token(tenant_user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(Permission::all().filter(|p| {
|
||||
(p.is_tenant_admin_permission() || p.is_user_permission())
|
||||
&& *p != Permission::EmailReceive
|
||||
}));
|
||||
|
||||
// Changing the tenant of a user should fail
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/john@foobar.org",
|
||||
&vec![PrincipalUpdate::set(
|
||||
PrincipalField::Tenant,
|
||||
PrincipalValue::String("xanadu".to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_request_error("Forbidden");
|
||||
|
||||
// Renaming a tenant account without a valid domain should fail
|
||||
for user in ["john", "john@example.org"] {
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/john@foobar.org",
|
||||
&vec![PrincipalUpdate::set(
|
||||
PrincipalField::Name,
|
||||
PrincipalValue::String(user.to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_error("Principal name must include a valid domain assigned to the tenant");
|
||||
}
|
||||
|
||||
// Rename the tenant account and add an email address
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/john@foobar.org",
|
||||
&vec![
|
||||
PrincipalUpdate::set(
|
||||
PrincipalField::Name,
|
||||
PrincipalValue::String("john.doe@foobar.org".to_string()),
|
||||
),
|
||||
PrincipalUpdate::add_item(
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::String("john@foobar.org".to_string()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Tenants should only see their own principals
|
||||
tenant_api
|
||||
.get::<List<PrincipalSet>>("/api/principal?types=individual,group,role,list")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
.assert_count(3)
|
||||
.assert_exists(
|
||||
"admin@foobar.org",
|
||||
Type::Individual,
|
||||
[
|
||||
(PrincipalField::Roles, &["tenant-admin"][..]),
|
||||
(PrincipalField::Members, &[][..]),
|
||||
(PrincipalField::EnabledPermissions, &[][..]),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"john.doe@foobar.org",
|
||||
Type::Individual,
|
||||
[
|
||||
(
|
||||
PrincipalField::Roles,
|
||||
&["tenant-admin", "no-mail-for-you@foobar.com", "user"][..],
|
||||
),
|
||||
(PrincipalField::Members, &[][..]),
|
||||
(PrincipalField::EnabledPermissions, &[][..]),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"no-mail-for-you@foobar.com",
|
||||
Type::Role,
|
||||
[
|
||||
(PrincipalField::Roles, &[][..]),
|
||||
(PrincipalField::Members, &["john.doe@foobar.org"][..]),
|
||||
(PrincipalField::EnabledPermissions, &[][..]),
|
||||
(
|
||||
PrincipalField::DisabledPermissions,
|
||||
&[Permission::EmailReceive.name()][..],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// John should not be allowed to receive email
|
||||
let (message_blob, _) = server
|
||||
.put_temporary_blob(tenant_user_id, TEST_MESSAGE.as_bytes(), 60)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
server
|
||||
.deliver_message(IngestMessage {
|
||||
sender_address: "bill@foobar.org".to_string(),
|
||||
sender_authenticated: true,
|
||||
recipients: vec![IngestRecipient {
|
||||
address: "john@foobar.org".to_string(),
|
||||
is_spam: false
|
||||
}],
|
||||
message_blob: message_blob.clone(),
|
||||
message_size: TEST_MESSAGE.len() as u64,
|
||||
session_id: 0,
|
||||
})
|
||||
.await
|
||||
.status,
|
||||
vec![LocalDeliveryStatus::PermanentFailure {
|
||||
code: [5, 5, 0],
|
||||
reason: "This account is not authorized to receive email.".into()
|
||||
}]
|
||||
);
|
||||
|
||||
// Remove the restriction
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/john.doe@foobar.org",
|
||||
&vec![PrincipalUpdate::remove_item(
|
||||
PrincipalField::Roles,
|
||||
PrincipalValue::String("no-mail-for-you@foobar.com".to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
server
|
||||
.get_access_token(tenant_user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(
|
||||
Permission::all().filter(|p| p.is_tenant_admin_permission() || p.is_user_permission()),
|
||||
);
|
||||
|
||||
// Delivery should now succeed
|
||||
assert_eq!(
|
||||
server
|
||||
.deliver_message(IngestMessage {
|
||||
sender_address: "bill@foobar.org".to_string(),
|
||||
sender_authenticated: true,
|
||||
recipients: vec![IngestRecipient {
|
||||
address: "john@foobar.org".to_string(),
|
||||
is_spam: false
|
||||
}],
|
||||
message_blob: message_blob.clone(),
|
||||
message_size: TEST_MESSAGE.len() as u64,
|
||||
session_id: 0,
|
||||
})
|
||||
.await
|
||||
.status,
|
||||
vec![LocalDeliveryStatus::Success]
|
||||
);
|
||||
|
||||
// Quota for the tenant and user should be updated
|
||||
const EXTRA_BYTES: i64 = 19; // Storage overhead
|
||||
assert_eq!(
|
||||
server.get_used_quota(tenant_id).await.unwrap(),
|
||||
TEST_MESSAGE.len() as i64 + EXTRA_BYTES
|
||||
);
|
||||
assert_eq!(
|
||||
server.get_used_quota(tenant_user_id).await.unwrap(),
|
||||
TEST_MESSAGE.len() as i64 + EXTRA_BYTES
|
||||
);
|
||||
|
||||
// Next delivery should fail due to tenant quota
|
||||
assert_eq!(
|
||||
server
|
||||
.deliver_message(IngestMessage {
|
||||
sender_address: "bill@foobar.org".to_string(),
|
||||
sender_authenticated: true,
|
||||
recipients: vec![IngestRecipient {
|
||||
address: "john@foobar.org".to_string(),
|
||||
is_spam: false
|
||||
}],
|
||||
message_blob,
|
||||
message_size: TEST_MESSAGE.len() as u64,
|
||||
session_id: 0,
|
||||
})
|
||||
.await
|
||||
.status,
|
||||
vec![LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Organization over quota.".into()
|
||||
}]
|
||||
);
|
||||
|
||||
// Moving a user to another tenant should move its quota too
|
||||
api.patch::<()>(
|
||||
"/api/principal/john.doe@foobar.org",
|
||||
&vec![PrincipalUpdate::set(
|
||||
PrincipalField::Tenant,
|
||||
PrincipalValue::String("xanadu".to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
assert_eq!(server.get_used_quota(tenant_id).await.unwrap(), 0);
|
||||
assert_eq!(
|
||||
server.get_used_quota(other_tenant_id).await.unwrap(),
|
||||
TEST_MESSAGE.len() as i64 + EXTRA_BYTES
|
||||
);
|
||||
|
||||
// Deleting tenants with data should fail
|
||||
api.delete::<()>("/api/principal/xanadu")
|
||||
.await
|
||||
.unwrap()
|
||||
.expect_error("Tenant has members");
|
||||
|
||||
// Delete user
|
||||
api.delete::<()>("/api/principal/john.doe@foobar.org")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Quota usage for tenant should be updated
|
||||
assert_eq!(server.get_used_quota(other_tenant_id).await.unwrap(), 0);
|
||||
|
||||
// Delete tenant
|
||||
api.delete::<()>("/api/principal/xanadu")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Delete tenant information
|
||||
for query in [
|
||||
"/api/principal/no-mail-for-you@foobar.com",
|
||||
"/api/principal/admin@foobar.org",
|
||||
"/api/principal/foobar.org",
|
||||
"/api/principal/foobar.com",
|
||||
] {
|
||||
api.delete::<()>(query).await.unwrap().unwrap_data();
|
||||
}
|
||||
|
||||
// Delete tenant
|
||||
api.delete::<()>("/api/principal/foobar")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
server
|
||||
.core
|
||||
.storage
|
||||
.config
|
||||
.clear("report.domain")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
params.assert_is_empty().await;
|
||||
}
|
||||
|
||||
const TENANT_QUOTA: u64 = TEST_MESSAGE.len() as u64;
|
||||
const TEST_MESSAGE: &str = concat!(
|
||||
"From: bill@foobar.org\r\n",
|
||||
"To: jdoe@foobar.com\r\n",
|
||||
"Subject: TPS Report\r\n",
|
||||
"\r\n",
|
||||
"I'm going to need those TPS reports ASAP. ",
|
||||
"So, if you could do that, that'd be great."
|
||||
);
|
||||
|
||||
trait ValidatePrincipalList {
|
||||
fn assert_exists<'x>(
|
||||
self,
|
||||
name: &str,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
) -> Self;
|
||||
fn assert_count(self, count: usize) -> Self;
|
||||
}
|
||||
|
||||
impl ValidatePrincipalList for List<PrincipalSet> {
|
||||
fn assert_exists<'x>(
|
||||
self,
|
||||
name: &str,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
) -> Self {
|
||||
for item in &self.items {
|
||||
if item.name() == name {
|
||||
item.validate(typ, items);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Principal not found: {}", name);
|
||||
}
|
||||
|
||||
fn assert_count(self, count: usize) -> Self {
|
||||
assert_eq!(self.items.len(), count, "Principal count failed validation");
|
||||
assert_eq!(self.total, count, "Principal total failed validation");
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
trait ValidatePrincipal {
|
||||
fn validate<'x>(
|
||||
&self,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
);
|
||||
}
|
||||
|
||||
impl ValidatePrincipal for PrincipalSet {
|
||||
fn validate<'x>(
|
||||
&self,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
) {
|
||||
assert_eq!(self.typ(), typ, "Type failed validation");
|
||||
|
||||
for (field, values) in items {
|
||||
match (
|
||||
self.get_str_array(field).filter(|v| !v.is_empty()),
|
||||
(!values.is_empty()).then_some(values),
|
||||
) {
|
||||
(Some(values), Some(expected)) => {
|
||||
assert_eq!(
|
||||
values.iter().map(|s| s.as_str()).collect::<AHashSet<_>>(),
|
||||
expected.iter().copied().collect::<AHashSet<_>>(),
|
||||
"Field {field:?} failed validation: {values:?} != {expected:?}"
|
||||
);
|
||||
}
|
||||
(None, None) => {}
|
||||
(values, expected) => {
|
||||
panic!("Field {field:?} failed validation: {values:?} != {expected:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ValidatePermissions {
|
||||
fn validate_permissions(
|
||||
self,
|
||||
expected_permissions: impl IntoIterator<Item = Permission>,
|
||||
) -> Self;
|
||||
fn validate_tenant(self, tenant_id: u32, tenant_quota: u64) -> Self;
|
||||
}
|
||||
|
||||
impl ValidatePermissions for Arc<AccessToken> {
|
||||
fn validate_permissions(
|
||||
self,
|
||||
expected_permissions: impl IntoIterator<Item = Permission>,
|
||||
) -> Self {
|
||||
let expected_permissions: AHashSet<_> = expected_permissions.into_iter().collect();
|
||||
|
||||
let permissions = self.permissions();
|
||||
for permission in &permissions {
|
||||
assert!(
|
||||
expected_permissions.contains(permission),
|
||||
"Permission {:?} failed validation",
|
||||
permission
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
permissions.into_iter().collect::<AHashSet<_>>(),
|
||||
expected_permissions
|
||||
);
|
||||
|
||||
for permission in Permission::all() {
|
||||
if self.has_permission(permission) {
|
||||
assert!(
|
||||
expected_permissions.contains(&permission),
|
||||
"Permission {:?} failed validation",
|
||||
permission
|
||||
);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn validate_tenant(self, tenant_id: u32, tenant_quota: u64) -> Self {
|
||||
assert_eq!(
|
||||
self.tenant,
|
||||
Some(TenantInfo {
|
||||
id: tenant_id,
|
||||
quota: tenant_quota
|
||||
})
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use registry::{
|
||||
Account, Credential, CredentialPermissions, CredentialPermissionsList, CustomRoles,
|
||||
Domain, EmailAlias, EncryptionAtRest, EncryptionSettings, GroupAccount, MailingList,
|
||||
PasswordCredential, Permissions, PermissionsList, PublicKey, Roles,
|
||||
SecondaryCredential, UserAccount,
|
||||
SecondaryCredential, UserAccount, UserRoles,
|
||||
},
|
||||
},
|
||||
types::{
|
||||
@@ -99,7 +99,7 @@ pub async fn test(test: &TestServer) {
|
||||
(StorageQuota::MaxDiskQuota, 1024u64),
|
||||
(StorageQuota::MaxApiKeys, 3u64),
|
||||
]),
|
||||
roles: Roles::Custom(CustomRoles {
|
||||
roles: UserRoles::Custom(CustomRoles {
|
||||
role_ids: Map::new(vec![5000u64.into()]),
|
||||
}),
|
||||
});
|
||||
|
||||
448
tests/src/system/authorization.rs
Normal file
448
tests/src/system/authorization.rs
Normal file
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::utils::server::TestServer;
|
||||
|
||||
pub async fn test(test: &mut TestServer) {
|
||||
println!("Running authorization tests...");
|
||||
|
||||
/*
|
||||
|
||||
pub async fn test(params: &JMAPTest) {
|
||||
println!("Running permissions tests...");
|
||||
let server = params.server.clone();
|
||||
|
||||
// Disable spam filtering to avoid adding extra headers
|
||||
let old_core = params.server.core.clone();
|
||||
let mut new_core = old_core.as_ref().clone();
|
||||
new_core.spam.enabled = false;
|
||||
new_core.smtp.session.data.add_delivered_to = false;
|
||||
params.server.inner.shared_core.store(Arc::new(new_core));
|
||||
|
||||
// Remove unlimited requests permission
|
||||
for &account in params.accounts.keys() {
|
||||
params
|
||||
.server
|
||||
.store()
|
||||
.remove_permissions(account, [Permission::UnlimitedRequests])
|
||||
.await;
|
||||
}
|
||||
|
||||
// Prepare management API
|
||||
let api = ManagementApi::new(8899, "admin", "secret");
|
||||
|
||||
// Create a user with the default 'user' role
|
||||
let account_id = api
|
||||
.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Individual)
|
||||
.with_field(PrincipalField::Name, "role_player")
|
||||
.with_field(PrincipalField::Roles, vec!["user".to_string()])
|
||||
.with_field(
|
||||
PrincipalField::DisabledPermissions,
|
||||
vec![Permission::Pop3Dele.name().to_string()],
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
let revision = server
|
||||
.get_access_token(account_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(
|
||||
Permission::all().filter(|p| p.is_user_permission() && *p != Permission::Pop3Dele),
|
||||
)
|
||||
.revision;
|
||||
|
||||
// Create multiple roles
|
||||
for (role, permissions, parent_role) in &[
|
||||
(
|
||||
"pop3_user",
|
||||
vec![Permission::Pop3Authenticate, Permission::Pop3List],
|
||||
vec![],
|
||||
),
|
||||
(
|
||||
"imap_user",
|
||||
vec![Permission::ImapAuthenticate, Permission::ImapList],
|
||||
vec![],
|
||||
),
|
||||
(
|
||||
"jmap_user",
|
||||
vec![
|
||||
Permission::JmapEmailQuery,
|
||||
Permission::AuthenticateOauth,
|
||||
Permission::ManageEncryption,
|
||||
],
|
||||
vec![],
|
||||
),
|
||||
(
|
||||
"email_user",
|
||||
vec![Permission::EmailSend, Permission::EmailReceive],
|
||||
vec!["pop3_user", "imap_user", "jmap_user"],
|
||||
),
|
||||
] {
|
||||
api.post::<u32>(
|
||||
"/api/principal",
|
||||
&PrincipalSet::new(u32::MAX, Type::Role)
|
||||
.with_field(PrincipalField::Name, role.to_string())
|
||||
.with_field(
|
||||
PrincipalField::EnabledPermissions,
|
||||
permissions
|
||||
.iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
parent_role
|
||||
.iter()
|
||||
.map(|r| r.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
}
|
||||
|
||||
// Update email_user role
|
||||
api.patch::<()>(
|
||||
"/api/principal/email_user",
|
||||
&vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::DisabledPermissions,
|
||||
PrincipalValue::String(Permission::ManageEncryption.name().to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
||||
// Update the user role to the nested 'email_user' role
|
||||
api.patch::<()>(
|
||||
"/api/principal/role_player",
|
||||
&vec![PrincipalUpdate::set(
|
||||
PrincipalField::Roles,
|
||||
PrincipalValue::StringList(vec!["email_user".to_string()]),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
assert_ne!(
|
||||
server
|
||||
.get_access_token(account_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions([
|
||||
Permission::EmailSend,
|
||||
Permission::EmailReceive,
|
||||
Permission::JmapEmailQuery,
|
||||
Permission::AuthenticateOauth,
|
||||
Permission::ImapAuthenticate,
|
||||
Permission::ImapList,
|
||||
Permission::Pop3Authenticate,
|
||||
Permission::Pop3List,
|
||||
])
|
||||
.revision,
|
||||
revision
|
||||
);
|
||||
|
||||
// Query all principals
|
||||
api.get::<List<PrincipalSet>>("/api/principal")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
.assert_count(12)
|
||||
.assert_exists(
|
||||
"admin",
|
||||
Type::Individual,
|
||||
[
|
||||
(PrincipalField::Roles, &["admin"][..]),
|
||||
(PrincipalField::Members, &[][..]),
|
||||
(PrincipalField::EnabledPermissions, &[][..]),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"role_player",
|
||||
Type::Individual,
|
||||
[
|
||||
(PrincipalField::Roles, &["email_user"][..]),
|
||||
(PrincipalField::Members, &[][..]),
|
||||
(PrincipalField::EnabledPermissions, &[][..]),
|
||||
(
|
||||
PrincipalField::DisabledPermissions,
|
||||
&[Permission::Pop3Dele.name()][..],
|
||||
),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"email_user",
|
||||
Type::Role,
|
||||
[
|
||||
(
|
||||
PrincipalField::Roles,
|
||||
&["pop3_user", "imap_user", "jmap_user"][..],
|
||||
),
|
||||
(PrincipalField::Members, &["role_player"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::EmailReceive.name(),
|
||||
Permission::EmailSend.name(),
|
||||
][..],
|
||||
),
|
||||
(
|
||||
PrincipalField::DisabledPermissions,
|
||||
&[Permission::ManageEncryption.name()][..],
|
||||
),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"pop3_user",
|
||||
Type::Role,
|
||||
[
|
||||
(PrincipalField::Roles, &[][..]),
|
||||
(PrincipalField::Members, &["email_user"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::Pop3Authenticate.name(),
|
||||
Permission::Pop3List.name(),
|
||||
][..],
|
||||
),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"imap_user",
|
||||
Type::Role,
|
||||
[
|
||||
(PrincipalField::Roles, &[][..]),
|
||||
(PrincipalField::Members, &["email_user"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::ImapAuthenticate.name(),
|
||||
Permission::ImapList.name(),
|
||||
][..],
|
||||
),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
)
|
||||
.assert_exists(
|
||||
"jmap_user",
|
||||
Type::Role,
|
||||
[
|
||||
(PrincipalField::Roles, &[][..]),
|
||||
(PrincipalField::Members, &["email_user"][..]),
|
||||
(
|
||||
PrincipalField::EnabledPermissions,
|
||||
&[
|
||||
Permission::JmapEmailQuery.name(),
|
||||
Permission::AuthenticateOauth.name(),
|
||||
Permission::ManageEncryption.name(),
|
||||
][..],
|
||||
),
|
||||
(PrincipalField::DisabledPermissions, &[][..]),
|
||||
],
|
||||
);
|
||||
|
||||
// Verify permissions
|
||||
server
|
||||
.get_access_token(tenant_admin_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(Permission::all().filter(|p| p.is_tenant_admin_permission()))
|
||||
.validate_tenant(tenant_id, TENANT_QUOTA);
|
||||
|
||||
// Prepare tenant admin API
|
||||
let tenant_api = ManagementApi::new(8899, "admin@foobar.org", "mytenantpass");
|
||||
|
||||
// John should not be allowed to receive email
|
||||
let (message_blob, _) = server
|
||||
.put_temporary_blob(tenant_user_id, TEST_MESSAGE.as_bytes(), 60)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
server
|
||||
.deliver_message(IngestMessage {
|
||||
sender_address: "bill@foobar.org".to_string(),
|
||||
sender_authenticated: true,
|
||||
recipients: vec![IngestRecipient {
|
||||
address: "john@foobar.org".to_string(),
|
||||
is_spam: false
|
||||
}],
|
||||
message_blob: message_blob.clone(),
|
||||
message_size: TEST_MESSAGE.len() as u64,
|
||||
session_id: 0,
|
||||
})
|
||||
.await
|
||||
.status,
|
||||
vec![LocalDeliveryStatus::PermanentFailure {
|
||||
code: [5, 5, 0],
|
||||
reason: "This account is not authorized to receive email.".into()
|
||||
}]
|
||||
);
|
||||
|
||||
// Remove the restriction
|
||||
tenant_api
|
||||
.patch::<()>(
|
||||
"/api/principal/john.doe@foobar.org",
|
||||
&vec![PrincipalUpdate::remove_item(
|
||||
PrincipalField::Roles,
|
||||
PrincipalValue::String("no-mail-for-you@foobar.com".to_string()),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
server
|
||||
.get_access_token(tenant_user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate_permissions(
|
||||
Permission::all().filter(|p| p.is_tenant_admin_permission() || p.is_user_permission()),
|
||||
);
|
||||
}
|
||||
|
||||
const TENANT_QUOTA: u64 = TEST_MESSAGE.len() as u64;
|
||||
const TEST_MESSAGE: &str = concat!(
|
||||
"From: bill@foobar.org\r\n",
|
||||
"To: jdoe@foobar.com\r\n",
|
||||
"Subject: TPS Report\r\n",
|
||||
"\r\n",
|
||||
"I'm going to need those TPS reports ASAP. ",
|
||||
"So, if you could do that, that'd be great."
|
||||
);
|
||||
|
||||
trait ValidatePrincipalList {
|
||||
fn assert_exists<'x>(
|
||||
self,
|
||||
name: &str,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
) -> Self;
|
||||
fn assert_count(self, count: usize) -> Self;
|
||||
}
|
||||
|
||||
impl ValidatePrincipalList for List<PrincipalSet> {
|
||||
fn assert_exists<'x>(
|
||||
self,
|
||||
name: &str,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
) -> Self {
|
||||
for item in &self.items {
|
||||
if item.name() == name {
|
||||
item.validate(typ, items);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Principal not found: {}", name);
|
||||
}
|
||||
|
||||
fn assert_count(self, count: usize) -> Self {
|
||||
assert_eq!(self.items.len(), count, "Principal count failed validation");
|
||||
assert_eq!(self.total, count, "Principal total failed validation");
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
trait ValidatePrincipal {
|
||||
fn validate<'x>(
|
||||
&self,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
);
|
||||
}
|
||||
|
||||
impl ValidatePrincipal for PrincipalSet {
|
||||
fn validate<'x>(
|
||||
&self,
|
||||
typ: Type,
|
||||
items: impl IntoIterator<Item = (PrincipalField, &'x [&'x str])>,
|
||||
) {
|
||||
assert_eq!(self.typ(), typ, "Type failed validation");
|
||||
|
||||
for (field, values) in items {
|
||||
match (
|
||||
self.get_str_array(field).filter(|v| !v.is_empty()),
|
||||
(!values.is_empty()).then_some(values),
|
||||
) {
|
||||
(Some(values), Some(expected)) => {
|
||||
assert_eq!(
|
||||
values.iter().map(|s| s.as_str()).collect::<AHashSet<_>>(),
|
||||
expected.iter().copied().collect::<AHashSet<_>>(),
|
||||
"Field {field:?} failed validation: {values:?} != {expected:?}"
|
||||
);
|
||||
}
|
||||
(None, None) => {}
|
||||
(values, expected) => {
|
||||
panic!("Field {field:?} failed validation: {values:?} != {expected:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ValidatePermissions {
|
||||
fn validate_permissions(
|
||||
self,
|
||||
expected_permissions: impl IntoIterator<Item = Permission>,
|
||||
) -> Self;
|
||||
fn validate_tenant(self, tenant_id: u32, tenant_quota: u64) -> Self;
|
||||
}
|
||||
|
||||
impl ValidatePermissions for Arc<AccessToken> {
|
||||
fn validate_permissions(
|
||||
self,
|
||||
expected_permissions: impl IntoIterator<Item = Permission>,
|
||||
) -> Self {
|
||||
let expected_permissions: AHashSet<_> = expected_permissions.into_iter().collect();
|
||||
|
||||
let permissions = self.permissions();
|
||||
for permission in &permissions {
|
||||
assert!(
|
||||
expected_permissions.contains(permission),
|
||||
"Permission {:?} failed validation",
|
||||
permission
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
permissions.into_iter().collect::<AHashSet<_>>(),
|
||||
expected_permissions
|
||||
);
|
||||
|
||||
for permission in Permission::all() {
|
||||
if self.has_permission(permission) {
|
||||
assert!(
|
||||
expected_permissions.contains(&permission),
|
||||
"Permission {:?} failed validation",
|
||||
permission
|
||||
);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn validate_tenant(self, tenant_id: u32, tenant_quota: u64) -> Self {
|
||||
assert_eq!(
|
||||
self.tenant,
|
||||
Some(TenantInfo {
|
||||
id: tenant_id,
|
||||
quota: tenant_quota
|
||||
})
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
}
|
||||
@@ -5,8 +5,10 @@
|
||||
*/
|
||||
|
||||
pub mod authentication;
|
||||
pub mod authorization;
|
||||
pub mod directory;
|
||||
pub mod oidc;
|
||||
pub mod tenant;
|
||||
|
||||
use crate::utils::server::TestServerBuilder;
|
||||
|
||||
@@ -34,5 +36,6 @@ pub async fn system_tests() {
|
||||
|
||||
//directory::test(&test).await;
|
||||
//authentication::test(&test).await;
|
||||
oidc::test(&mut test).await;
|
||||
//oidc::test(&mut test).await;
|
||||
tenant::test(&mut test).await;
|
||||
}
|
||||
|
||||
678
tests/src/system/tenant.rs
Normal file
678
tests/src/system/tenant.rs
Normal file
@@ -0,0 +1,678 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::utils::{jmap::JmapUtils, server::TestServer};
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use common::auth::BuildAccessToken;
|
||||
use email::message::delivery::{IngestMessage, IngestRecipient, LocalDeliveryStatus, MailDelivery};
|
||||
use jmap_proto::error::set::SetErrorType;
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::{AccountType, Permission, TenantStorageQuota},
|
||||
prelude::{ObjectType, Property},
|
||||
structs::{
|
||||
Account, Credential, Dkim1Signature, DkimPrivateKey, DkimSignature, DnsServer,
|
||||
DnsServerCloudflare, Domain, GroupAccount, MailingList, OAuthClient,
|
||||
PasswordCredential, Permissions, PermissionsList, Role, SecretKey, SecretKeyValue,
|
||||
SecretTextValue, Tenant, UserAccount, UserRoles,
|
||||
},
|
||||
},
|
||||
types::{EnumImpl, ObjectImpl, list::List, map::Map},
|
||||
};
|
||||
use serde_json::json;
|
||||
use types::id::Id;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub async fn test(test: &mut TestServer) {
|
||||
println!("Running multi-tenancy tests...");
|
||||
let account = test.account("admin@example.org");
|
||||
|
||||
// Create tenants
|
||||
let mut tenant_x_ids = AHashMap::new();
|
||||
let mut tenant_y_ids = AHashMap::new();
|
||||
for (tenant_ids, name) in [(&mut tenant_x_ids, "x"), (&mut tenant_y_ids, "y")] {
|
||||
let tenant_id = account
|
||||
.registry_create_object(Tenant {
|
||||
name: format!("Tenant {}", name),
|
||||
quotas: VecMap::from_iter([
|
||||
(TenantStorageQuota::MaxAccounts, 2),
|
||||
(TenantStorageQuota::MaxGroups, 1),
|
||||
(TenantStorageQuota::MaxDomains, 1),
|
||||
(TenantStorageQuota::MaxMailingLists, 1),
|
||||
(TenantStorageQuota::MaxRoles, 1),
|
||||
(TenantStorageQuota::MaxOauthClients, 1),
|
||||
(TenantStorageQuota::MaxDkimKeys, 1),
|
||||
(TenantStorageQuota::MaxDnsServers, 1),
|
||||
(TenantStorageQuota::MaxDiskQuota, TENANT_QUOTA),
|
||||
]),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
let domain_id = account
|
||||
.registry_create_object(Domain {
|
||||
name: format!("tenant{name}.org"),
|
||||
member_tenant_id: tenant_id.into(),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
let tenant_admin_id = account
|
||||
.registry_create_object(Account::User(UserAccount {
|
||||
name: "admin".to_string(),
|
||||
domain_id,
|
||||
member_tenant_id: tenant_id.into(),
|
||||
roles: UserRoles::TenantAdmin,
|
||||
description: format!("Tenant {name} Admin").into(),
|
||||
credentials: List::from_iter([Credential::Password(PasswordCredential {
|
||||
secret: format!("tenant {name} secret"),
|
||||
..Default::default()
|
||||
})]),
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
tenant_ids.insert(ObjectType::Tenant, tenant_id);
|
||||
tenant_ids.insert(ObjectType::Domain, domain_id);
|
||||
tenant_ids.insert(ObjectType::TaskManager, tenant_admin_id);
|
||||
}
|
||||
|
||||
// Tenants should not be allowed to create new tenants or modify their own tenant records
|
||||
let admin_x = crate::utils::account::Account::new(
|
||||
"admin@tenantx.org",
|
||||
"tenant x secret",
|
||||
&[],
|
||||
tenant_x_ids[&ObjectType::TaskManager],
|
||||
)
|
||||
.await;
|
||||
let admin_y = crate::utils::account::Account::new(
|
||||
"admin@tenanty.org",
|
||||
"tenant y secret",
|
||||
&[],
|
||||
tenant_y_ids[&ObjectType::TaskManager],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
admin_x
|
||||
.registry_create([Tenant {
|
||||
name: "Tenant Z".to_string(),
|
||||
..Default::default()
|
||||
}])
|
||||
.await
|
||||
.method_response()
|
||||
.text_field("type"),
|
||||
"forbidden"
|
||||
);
|
||||
assert_eq!(
|
||||
admin_x
|
||||
.registry_update(
|
||||
ObjectType::Tenant,
|
||||
[(
|
||||
tenant_x_ids[&ObjectType::Tenant],
|
||||
json!({
|
||||
"name": "New Tenant X Name"
|
||||
}),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
.method_response()
|
||||
.text_field("type"),
|
||||
"forbidden"
|
||||
);
|
||||
|
||||
// Tenants should not be able to create accounts without a domain
|
||||
admin_x
|
||||
.registry_create_object_expect_err(Account::User(UserAccount {
|
||||
name: "user".to_string(),
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.assert_type(SetErrorType::ValidationFailed);
|
||||
|
||||
// Test wrong tenant assignment, invalid domain, exceeding tenant quotas, and successful creation for each object type
|
||||
let mut tenant_ids = [tenant_x_ids, tenant_y_ids];
|
||||
let mut tests_run = AHashSet::new();
|
||||
for (admin, tenant_id_pos, other_tenant_id_pos) in [(&admin_x, 0, 1), (&admin_y, 1, 0)] {
|
||||
for result in [
|
||||
ExpectedResult::WrongTenant,
|
||||
ExpectedResult::InvalidDomain,
|
||||
ExpectedResult::Success,
|
||||
ExpectedResult::QuotaExceeded,
|
||||
] {
|
||||
if result != ExpectedResult::Success && tenant_id_pos == 1 {
|
||||
// Skip testing wrong tenant and invalid domain for tenant y, as they would be the same as tenant x
|
||||
continue;
|
||||
}
|
||||
|
||||
tests_run.insert(result);
|
||||
|
||||
let member_tenant_id = if result == ExpectedResult::WrongTenant {
|
||||
Some(tenant_ids[other_tenant_id_pos][&ObjectType::Tenant])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let domain_id = if result == ExpectedResult::InvalidDomain {
|
||||
tenant_ids[other_tenant_id_pos][&ObjectType::Domain]
|
||||
} else {
|
||||
tenant_ids[tenant_id_pos][&ObjectType::Domain]
|
||||
};
|
||||
|
||||
if matches!(
|
||||
result,
|
||||
ExpectedResult::WrongTenant | ExpectedResult::QuotaExceeded
|
||||
) {
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::Domain,
|
||||
Domain {
|
||||
name: format!("tenant{tenant_id_pos}.org"),
|
||||
member_tenant_id,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::Account,
|
||||
Account::User(UserAccount {
|
||||
name: "user".to_string(),
|
||||
domain_id,
|
||||
member_tenant_id,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::AccountSettings,
|
||||
Account::Group(GroupAccount {
|
||||
name: "group".to_string(),
|
||||
domain_id,
|
||||
member_tenant_id,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::MailingList,
|
||||
MailingList {
|
||||
name: "list".to_string(),
|
||||
domain_id,
|
||||
member_tenant_id,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::DkimSignature,
|
||||
DkimSignature::Dkim1Ed25519Sha256(Dkim1Signature {
|
||||
domain_id,
|
||||
member_tenant_id,
|
||||
selector: "ed-key".to_string(),
|
||||
private_key: DkimPrivateKey::Value(SecretTextValue {
|
||||
secret: DKIM_KEY.to_string(),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
if result != ExpectedResult::InvalidDomain {
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::Role,
|
||||
Role {
|
||||
description: "role".to_string(),
|
||||
member_tenant_id,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::OAuthClient,
|
||||
OAuthClient {
|
||||
client_id: format!("oauth-{tenant_id_pos}"),
|
||||
member_tenant_id,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
result
|
||||
.assert(
|
||||
admin,
|
||||
&mut tenant_ids[tenant_id_pos],
|
||||
ObjectType::DnsServer,
|
||||
DnsServer::Cloudflare(DnsServerCloudflare {
|
||||
member_tenant_id,
|
||||
secret: SecretKey::Value(SecretKeyValue {
|
||||
secret: "abc".to_string(),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(tests_run.len(), 4, "Not all expected results were tested");
|
||||
let tenant_x_ids = &tenant_ids[0];
|
||||
let tenant_y_ids = &tenant_ids[1];
|
||||
|
||||
// Assigning permissions not assigned to the tenant should be removed
|
||||
let user_x_id = tenant_x_ids[&ObjectType::Account];
|
||||
let user_x_at = test
|
||||
.server
|
||||
.access_token(user_x_id.document_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.build();
|
||||
assert!(!user_x_at.has_permission(Permission::FetchAnyBlob));
|
||||
assert!(!user_x_at.has_permission(Permission::Impersonate));
|
||||
admin_x
|
||||
.registry_update_object(
|
||||
ObjectType::Account,
|
||||
user_x_id,
|
||||
json!({
|
||||
Property::Permissions: Permissions::Merge(
|
||||
PermissionsList { disabled_permissions: Map::default(),
|
||||
enabled_permissions: Map::new(vec![Permission::FetchAnyBlob, Permission::Impersonate])
|
||||
})
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let user_x_at = test
|
||||
.server
|
||||
.access_token(user_x_id.document_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.build();
|
||||
assert!(user_x_at.has_permission(Permission::FetchAnyBlob));
|
||||
assert!(!user_x_at.has_permission(Permission::Impersonate));
|
||||
|
||||
// Tenants should only see their own objects
|
||||
for object_type in [
|
||||
ObjectType::Domain,
|
||||
ObjectType::Account,
|
||||
ObjectType::AccountSettings,
|
||||
ObjectType::MailingList,
|
||||
ObjectType::Role,
|
||||
ObjectType::OAuthClient,
|
||||
ObjectType::DnsServer,
|
||||
ObjectType::DkimSignature,
|
||||
] {
|
||||
let expected_id = tenant_y_ids[&object_type];
|
||||
match object_type {
|
||||
ObjectType::Account => {
|
||||
let expected = vec![tenant_y_ids[&ObjectType::TaskManager], expected_id];
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_query(
|
||||
ObjectType::Account,
|
||||
[(Property::Type, AccountType::User.as_str())],
|
||||
Vec::<&str>::new()
|
||||
)
|
||||
.await,
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_get_many(ObjectType::Account, expected.iter())
|
||||
.await
|
||||
.list()
|
||||
.len(),
|
||||
expected.len()
|
||||
);
|
||||
}
|
||||
ObjectType::AccountSettings => {
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_query(
|
||||
ObjectType::Account,
|
||||
[(Property::Type, AccountType::Group.as_str())],
|
||||
Vec::<&str>::new()
|
||||
)
|
||||
.await,
|
||||
vec![expected_id]
|
||||
);
|
||||
|
||||
// Fetch a single id and make sure member tenant id is not included in the response
|
||||
let account = admin_y.registry_get::<Account>(expected_id).await;
|
||||
assert_eq!(account.into_group().unwrap().member_tenant_id, None);
|
||||
|
||||
// Fetch all account types
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_get_many(ObjectType::Account, Vec::<&str>::new())
|
||||
.await
|
||||
.list()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_query(object_type, Vec::<(&str, &str)>::new(), Vec::<&str>::new())
|
||||
.await,
|
||||
vec![expected_id]
|
||||
);
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_get_many(object_type, vec![expected_id])
|
||||
.await
|
||||
.list()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_get_many(object_type, Vec::<&str>::new())
|
||||
.await
|
||||
.list()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tenants should not see, modify or destroy objects from other tenants, even if they have the id
|
||||
for object_type in [
|
||||
ObjectType::Domain,
|
||||
ObjectType::Account,
|
||||
ObjectType::AccountSettings,
|
||||
ObjectType::MailingList,
|
||||
ObjectType::Role,
|
||||
ObjectType::OAuthClient,
|
||||
ObjectType::DnsServer,
|
||||
ObjectType::DkimSignature,
|
||||
] {
|
||||
let foreign_id = tenant_x_ids[&object_type];
|
||||
match object_type {
|
||||
ObjectType::Account => {
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_get_many(
|
||||
ObjectType::Account,
|
||||
vec![tenant_x_ids[&ObjectType::TaskManager], foreign_id]
|
||||
)
|
||||
.await
|
||||
.list()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
admin_y
|
||||
.registry_update_object_expect_err(
|
||||
ObjectType::Account,
|
||||
foreign_id,
|
||||
json!({
|
||||
"name": "hacked"
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.assert_type(SetErrorType::NotFound);
|
||||
|
||||
admin_y
|
||||
.registry_destroy_object_expect_err(ObjectType::Account, foreign_id)
|
||||
.await
|
||||
.assert_type(SetErrorType::NotFound);
|
||||
}
|
||||
ObjectType::AccountSettings => {
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_get_many(ObjectType::Account, vec![foreign_id])
|
||||
.await
|
||||
.list()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
admin_y
|
||||
.registry_update_object_expect_err(
|
||||
ObjectType::Account,
|
||||
foreign_id,
|
||||
json!({
|
||||
"name": "hacked"
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.assert_type(SetErrorType::NotFound);
|
||||
|
||||
admin_y
|
||||
.registry_destroy_object_expect_err(ObjectType::Account, foreign_id)
|
||||
.await
|
||||
.assert_type(SetErrorType::NotFound);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(
|
||||
admin_y
|
||||
.registry_get_many(object_type, vec![foreign_id])
|
||||
.await
|
||||
.list()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
admin_y
|
||||
.registry_update_object_expect_err(
|
||||
object_type,
|
||||
foreign_id,
|
||||
json!({
|
||||
"name": "hacked"
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.assert_type(SetErrorType::NotFound);
|
||||
|
||||
admin_y
|
||||
.registry_destroy_object_expect_err(object_type, foreign_id)
|
||||
.await
|
||||
.assert_type(SetErrorType::NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test tenant quotas
|
||||
let (message_blob, _) = test
|
||||
.server
|
||||
.put_temporary_blob(user_x_id.document_id(), TEST_MESSAGE.as_bytes(), 60)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
test.server
|
||||
.deliver_message(IngestMessage {
|
||||
sender_address: "bill@foobar.org".to_string(),
|
||||
sender_authenticated: true,
|
||||
recipients: vec![IngestRecipient {
|
||||
address: "user@tenantx.org".to_string(),
|
||||
is_spam: false
|
||||
}],
|
||||
message_blob: message_blob.clone(),
|
||||
message_size: TEST_MESSAGE.len() as u64,
|
||||
session_id: 0,
|
||||
})
|
||||
.await
|
||||
.status,
|
||||
vec![LocalDeliveryStatus::Success]
|
||||
);
|
||||
|
||||
// Quota for the tenant and user should be updated
|
||||
const EXTRA_BYTES: i64 = 51; // Storage overhead
|
||||
assert_eq!(
|
||||
test.server
|
||||
.get_used_quota_account(user_x_id.document_id())
|
||||
.await
|
||||
.unwrap(),
|
||||
TEST_MESSAGE.len() as i64 + EXTRA_BYTES
|
||||
);
|
||||
assert_eq!(
|
||||
test.server
|
||||
.get_used_quota_tenant(tenant_x_ids[&ObjectType::Tenant].document_id())
|
||||
.await
|
||||
.unwrap(),
|
||||
TEST_MESSAGE.len() as i64 + EXTRA_BYTES
|
||||
);
|
||||
|
||||
// Next delivery should fail due to tenant quota
|
||||
assert_eq!(
|
||||
test.server
|
||||
.deliver_message(IngestMessage {
|
||||
sender_address: "bill@foobar.org".to_string(),
|
||||
sender_authenticated: true,
|
||||
recipients: vec![IngestRecipient {
|
||||
address: "user@tenantx.org".to_string(),
|
||||
is_spam: false
|
||||
}],
|
||||
message_blob,
|
||||
message_size: TEST_MESSAGE.len() as u64,
|
||||
session_id: 0,
|
||||
})
|
||||
.await
|
||||
.status,
|
||||
vec![LocalDeliveryStatus::TemporaryFailure {
|
||||
reason: "Organization over quota.".into()
|
||||
}]
|
||||
);
|
||||
|
||||
// Delete everything created during the test
|
||||
for (admin, tenant_id_pos) in [(&admin_x, 0), (&admin_y, 1)] {
|
||||
for object_type in [
|
||||
ObjectType::Account,
|
||||
ObjectType::AccountSettings,
|
||||
ObjectType::MailingList,
|
||||
ObjectType::Role,
|
||||
ObjectType::OAuthClient,
|
||||
ObjectType::DnsServer,
|
||||
ObjectType::DkimSignature,
|
||||
] {
|
||||
let id = tenant_ids[tenant_id_pos][&object_type];
|
||||
let object_type = if object_type == ObjectType::AccountSettings {
|
||||
ObjectType::Account
|
||||
} else {
|
||||
object_type
|
||||
};
|
||||
assert_eq!(
|
||||
admin
|
||||
.registry_destroy(object_type, [id])
|
||||
.await
|
||||
.destroyed_ids()
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for tenant_id_pos in [0, 1] {
|
||||
for object_type in [
|
||||
ObjectType::TaskManager,
|
||||
ObjectType::Domain,
|
||||
ObjectType::Tenant,
|
||||
] {
|
||||
let id = tenant_ids[tenant_id_pos][&object_type];
|
||||
let object_type = if object_type == ObjectType::TaskManager {
|
||||
ObjectType::Account
|
||||
} else {
|
||||
object_type
|
||||
};
|
||||
assert_eq!(
|
||||
account
|
||||
.registry_destroy(object_type, [id])
|
||||
.await
|
||||
.destroyed_ids()
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
enum ExpectedResult {
|
||||
Success,
|
||||
WrongTenant,
|
||||
QuotaExceeded,
|
||||
InvalidDomain,
|
||||
}
|
||||
|
||||
impl ExpectedResult {
|
||||
async fn assert<T: ObjectImpl>(
|
||||
&self,
|
||||
account: &crate::utils::account::Account,
|
||||
ids: &mut AHashMap<ObjectType, Id>,
|
||||
item_type: ObjectType,
|
||||
item: T,
|
||||
) {
|
||||
match self {
|
||||
ExpectedResult::Success => {
|
||||
assert!(
|
||||
ids.insert(item_type, account.registry_create_object(item).await)
|
||||
.is_none(),
|
||||
"Expected to create object successfully, but it already exists"
|
||||
)
|
||||
}
|
||||
ExpectedResult::WrongTenant => {
|
||||
account
|
||||
.registry_create_object_expect_err(item)
|
||||
.await
|
||||
.assert_type(SetErrorType::InvalidPatch)
|
||||
.assert_description_contains("Cannot modify memberTenantId property");
|
||||
}
|
||||
ExpectedResult::QuotaExceeded => {
|
||||
account
|
||||
.registry_create_object_expect_err(item)
|
||||
.await
|
||||
.assert_type(SetErrorType::OverQuota);
|
||||
}
|
||||
ExpectedResult::InvalidDomain => {
|
||||
account
|
||||
.registry_create_object_expect_err(item)
|
||||
.await
|
||||
.assert_type(SetErrorType::InvalidForeignKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TENANT_QUOTA: u64 = TEST_MESSAGE.len() as u64;
|
||||
const TEST_MESSAGE: &str = concat!(
|
||||
"From: bill@foobar.org\r\n",
|
||||
"To: jdoe@foobar.com\r\n",
|
||||
"Subject: TPS Report\r\n",
|
||||
"\r\n",
|
||||
"I'm going to need those TPS reports ASAP. ",
|
||||
"So, if you could do that, that'd be great."
|
||||
);
|
||||
|
||||
const DKIM_KEY: &str = "-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIAO3hAf144lTAVjTkht3ZwBTK0CMCCd1bI0alggneN3B
|
||||
-----END PRIVATE KEY-----
|
||||
";
|
||||
@@ -41,6 +41,31 @@ impl Account {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn registry_get<T: ObjectImpl>(&self, id: Id) -> T {
|
||||
let name = T::OBJECT.as_str();
|
||||
|
||||
let value = self
|
||||
.jmap_get_account(self, format!("x:{name}"), Vec::<&str>::new(), vec![id])
|
||||
.await
|
||||
.list()[0]
|
||||
.to_string();
|
||||
serde_json::from_str(&value).expect("Failed to deserialize item")
|
||||
}
|
||||
|
||||
pub async fn registry_get_many(
|
||||
&self,
|
||||
object_type: ObjectType,
|
||||
ids: impl IntoIterator<Item = impl Display>,
|
||||
) -> JmapResponse {
|
||||
self.jmap_get_account(
|
||||
self,
|
||||
format!("x:{}", object_type.as_str()),
|
||||
Vec::<&str>::new(),
|
||||
ids,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn registry_update(
|
||||
&self,
|
||||
object: ObjectType,
|
||||
@@ -206,8 +231,10 @@ fn remove_server_set_props(value: &mut serde_json::Value) {
|
||||
.get("@type")
|
||||
.and_then(|v| v.as_str())
|
||||
.is_some_and(|t| ["AppPassword", "ApiKey"].contains(&t));
|
||||
obj.retain(|k, _| {
|
||||
!(["createdAt", "credentialId"].contains(&k.as_str()) || (is_app_pass && k == "secret"))
|
||||
obj.retain(|k, v| {
|
||||
!(["createdAt", "credentialId", "retireAt"].contains(&k.as_str())
|
||||
|| (is_app_pass && k == "secret")
|
||||
|| (k == "memberTenantId" && v.is_null()))
|
||||
});
|
||||
for v in obj.values_mut() {
|
||||
remove_server_set_props(v);
|
||||
|
||||
Reference in New Issue
Block a user