Permissions & multi-tenancy test suite

This commit is contained in:
mdecimus
2024-09-18 18:08:57 +02:00
parent d0303aefa8
commit e9d12aea44
12 changed files with 1186 additions and 99 deletions

View File

@@ -25,7 +25,7 @@ use crate::{
use super::JMAPTest;
#[derive(serde::Deserialize)]
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct OAuthCodeResponse {
pub code: String,

View File

@@ -5,6 +5,7 @@
*/
use std::{
fmt::Debug,
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
@@ -66,6 +67,7 @@ pub mod email_submission;
pub mod enterprise;
pub mod event_source;
pub mod mailbox;
pub mod permissions;
pub mod purge;
pub mod push_subscription;
pub mod quota;
@@ -310,7 +312,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;
@@ -318,8 +320,8 @@ pub async fn jmap_tests() {
email_changes::test(&mut params).await;
email_query_changes::test(&mut params).await;
email_copy::test(&mut params).await;
//thread_get::test(&mut params).await;
//thread_merge::test(&mut params).await;
thread_get::test(&mut params).await;
thread_merge::test(&mut params).await;
mailbox::test(&mut params).await;
delivery::test(&mut params).await;
auth_acl::test(&mut params).await;
@@ -333,7 +335,8 @@ pub async fn jmap_tests() {
websocket::test(&mut params).await;
quota::test(&mut params).await;
crypto::test(&mut params).await;
blob::test(&mut params).await;*/
blob::test(&mut params).await;
permissions::test(&params).await;
purge::test(&mut params).await;
enterprise::test(&mut params).await;
@@ -740,8 +743,15 @@ pub async fn test_account_login(login: &str, secret: &str) -> Client {
#[serde(untagged)]
pub enum Response<T> {
RequestError(RequestError<'static>),
Error { error: String, details: String },
Data { data: T },
Error {
error: String,
details: Option<String>,
item: Option<String>,
reason: Option<String>,
},
Data {
data: T,
},
}
pub struct ManagementApi {
@@ -786,6 +796,32 @@ impl ManagementApi {
})
}
pub async fn patch<T: DeserializeOwned>(
&self,
query: &str,
body: &impl Serialize,
) -> Result<Response<T>, String> {
self.request_raw(
Method::PATCH,
query,
Some(serde_json::to_string(body).unwrap()),
)
.await
.map(|result| {
serde_json::from_str::<Response<T>>(&result)
.unwrap_or_else(|err| panic!("{err}: {result}"))
})
}
pub async fn delete<T: DeserializeOwned>(&self, query: &str) -> Result<Response<T>, String> {
self.request_raw(Method::DELETE, query, None)
.await
.map(|result| {
serde_json::from_str::<Response<T>>(&result)
.unwrap_or_else(|err| panic!("{err}: {result}"))
})
}
pub async fn get<T: DeserializeOwned>(&self, query: &str) -> Result<Response<T>, String> {
self.request_raw(Method::GET, query, None)
.await
@@ -840,12 +876,17 @@ impl ManagementApi {
}
}
impl<T> Response<T> {
impl<T: Debug> Response<T> {
pub fn unwrap_data(self) -> T {
match self {
Response::Data { data } => data,
Response::Error { error, details } => {
panic!("Expected data, found error {error:?}: {details:?}")
Response::Error {
error,
details,
reason,
..
} => {
panic!("Expected data, found error {error:?}: {details:?} {reason:?}")
}
Response::RequestError(err) => {
panic!("Expected data, found error {err:?}")
@@ -857,8 +898,13 @@ impl<T> Response<T> {
match self {
Response::Data { data } => Some(data),
Response::RequestError(error) if error.status == 404 => None,
Response::Error { error, details } => {
panic!("Expected data, found error {error:?}: {details:?}")
Response::Error {
error,
details,
reason,
..
} => {
panic!("Expected data, found error {error:?}: {details:?} {reason:?}")
}
Response::RequestError(err) => {
panic!("Expected data, found error {err:?}")
@@ -866,13 +912,50 @@ impl<T> Response<T> {
}
}
pub fn unwrap_error(self) -> (String, String) {
pub fn unwrap_error(self) -> (String, Option<String>, Option<String>) {
match self {
Response::Error { error, details } => (error, details),
Response::Data { .. } => panic!("Expected error, found data."),
Response::Error {
error,
details,
reason,
..
} => (error, details, reason),
Response::Data { data } => panic!("Expected error, found data: {data:?}"),
Response::RequestError(err) => {
panic!("Expected error, found request error {err:?}")
}
}
}
pub fn unwrap_request_error(self) -> RequestError<'static> {
match self {
Response::Error {
error,
details,
reason,
..
} => {
panic!("Expected request error, found error {error:?}: {details:?} {reason:?}")
}
Response::Data { data } => panic!("Expected request error, found data: {data:?}"),
Response::RequestError(err) => err,
}
}
pub fn expect_request_error(self, value: &str) {
let err = self.unwrap_request_error();
if !err.detail.contains(value) && !err.title.as_ref().map_or(false, |t| t.contains(value)) {
panic!("Expected request error containing {value:?}, found {err:?}")
}
}
pub fn expect_error(self, value: &str) {
let (error, details, reason) = self.unwrap_error();
if !error.contains(value)
&& !details.as_ref().map_or(false, |d| d.contains(value))
&& !reason.as_ref().map_or(false, |r| r.contains(value))
{
panic!("Expected error containing {value:?}, found {error:?}: {details:?} {reason:?}")
}
}
}

View File

@@ -0,0 +1,862 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::AHashSet;
use common::{
auth::{AccessToken, TenantInfo},
DeliveryResult, IngestMessage,
};
use directory::{
backend::internal::{PrincipalField, PrincipalUpdate, PrincipalValue},
Permission, Principal, Type,
};
use hyper::header::TE;
use rayon::vec;
use utils::BlobHash;
use crate::jmap::assert_is_empty;
use super::{enterprise::List, JMAPTest, ManagementApi};
pub async fn test(params: &JMAPTest) {
let core = params.server.core.clone();
let server = params.server.clone();
// 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",
&Principal::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();
core.get_access_token(account_id)
.await
.unwrap()
.validate_permissions(
Permission::all().filter(|p| p.is_user_permission() && *p != Permission::Pop3Dele),
);
// 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",
&Principal::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();
core.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,
]);
// Query all principals
api.get::<List<Principal>>("/api/principal")
.await
.unwrap()
.unwrap_data()
.assert_count(6)
.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",
&Principal::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",
&Principal::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",
&Principal::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",
&Principal::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",
&Principal::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",
&Principal::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
core.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",
&Principal::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_error("notFound");
tenant_api
.get::<()>("/api/principal/foobar")
.await
.unwrap()
.expect_error("notFound");
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",
&Principal::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",
&Principal::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",
&Principal::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",
&Principal::new(u32::MAX, Type::Individual)
.with_field(PrincipalField::Name, "john@foobar.org")
.with_field(PrincipalField::Roles, vec!["admin".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
core.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",
&Principal::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",
&Principal::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
core.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<Principal>>("/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,
&["admin", "no-mail-for-you@foobar.com"][..],
),
(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 = BlobHash::from(TEST_MESSAGE.as_bytes());
core.storage
.blob
.put_blob(message_blob.as_ref(), TEST_MESSAGE.as_bytes())
.await
.unwrap();
assert_eq!(
server
.deliver_message(IngestMessage {
sender_address: "bill@foobar.org".to_string(),
recipients: vec!["john@foobar.org".to_string()],
message_blob: message_blob.clone(),
message_size: TEST_MESSAGE.len(),
session_id: 0,
})
.await,
vec![DeliveryResult::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();
core.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(),
recipients: vec!["john@foobar.org".to_string()],
message_blob: message_blob.clone(),
message_size: TEST_MESSAGE.len(),
session_id: 0,
})
.await,
vec![DeliveryResult::Success]
);
// Quota for the tenant and user should be updated
assert_eq!(
server.get_used_quota(tenant_id).await.unwrap(),
TEST_MESSAGE.len() as i64
);
assert_eq!(
server.get_used_quota(tenant_user_id).await.unwrap(),
TEST_MESSAGE.len() as i64
);
// Next delivery should fail due to tenant quota
assert_eq!(
server
.deliver_message(IngestMessage {
sender_address: "bill@foobar.org".to_string(),
recipients: vec!["john@foobar.org".to_string()],
message_blob,
message_size: TEST_MESSAGE.len(),
session_id: 0,
})
.await,
vec![DeliveryResult::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
);
// 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/foobar.org",
"/api/principal/foobar.com",
"/api/principal/admin@foobar.org",
] {
tenant_api.delete::<()>(query).await.unwrap().unwrap_data();
}
// Delete tenant
api.delete::<()>("/api/principal/foobar")
.await
.unwrap()
.unwrap_data();
assert_is_empty(server).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<Principal> {
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 Principal {
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 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
}
}

View File

@@ -56,7 +56,7 @@ reject-non-fqdn = false
relay = true
"#;
#[derive(serde::Deserialize)]
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
pub(super) struct List<T> {
pub items: Vec<T>,