mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-10 12:35:54 +08:00
Permissions & multi-tenancy test suite
This commit is contained in:
parent
d0303aefa8
commit
e9d12aea44
12 changed files with 1186 additions and 99 deletions
|
@ -23,7 +23,7 @@ pub struct AccessToken {
|
||||||
pub tenant: Option<TenantInfo>,
|
pub tenant: Option<TenantInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
pub struct TenantInfo {
|
pub struct TenantInfo {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub quota: u64,
|
pub quota: u64,
|
||||||
|
|
|
@ -119,7 +119,7 @@ pub struct IngestMessage {
|
||||||
pub session_id: u64,
|
pub session_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum DeliveryResult {
|
pub enum DeliveryResult {
|
||||||
Success,
|
Success,
|
||||||
TemporaryFailure {
|
TemporaryFailure {
|
||||||
|
|
|
@ -197,8 +197,51 @@ impl ManageDirectory for Store {
|
||||||
.ctx(trc::Key::Total, total));
|
.ctx(trc::Key::Total, total));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure new name is not taken
|
||||||
|
if self
|
||||||
|
.get_principal_id(&name)
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(err_exists(PrincipalField::Name, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain tenant id, only if no default tenant is provided
|
||||||
|
if let (Some(tenant_name), None) = (principal.take_str(PrincipalField::Tenant), tenant_id) {
|
||||||
|
tenant_id = self
|
||||||
|
.get_principal_info(&tenant_name)
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?
|
||||||
|
.filter(|v| v.typ == Type::Tenant)
|
||||||
|
.ok_or_else(|| not_found(tenant_name.clone()))?
|
||||||
|
.id
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
// Tenants must provide principal names including a valid domain
|
// Tenants must provide principal names including a valid domain
|
||||||
|
if let Some(tenant_id) = tenant_id {
|
||||||
|
if matches!(principal.typ, Type::Tenant) {
|
||||||
|
return Err(error(
|
||||||
|
"Invalid field",
|
||||||
|
"Tenants cannot contain a tenant field".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
principal.set(PrincipalField::Tenant, tenant_id);
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
principal.typ,
|
||||||
|
Type::Individual
|
||||||
|
| Type::Group
|
||||||
|
| Type::List
|
||||||
|
| Type::Role
|
||||||
|
| Type::Location
|
||||||
|
| Type::Resource
|
||||||
|
| Type::Other
|
||||||
|
) {
|
||||||
if let Some(domain) = name.split('@').nth(1) {
|
if let Some(domain) = name.split('@').nth(1) {
|
||||||
if self
|
if self
|
||||||
.get_principal_info(domain)
|
.get_principal_info(domain)
|
||||||
|
@ -214,19 +257,10 @@ impl ManageDirectory for Store {
|
||||||
if valid_domains.is_empty() {
|
if valid_domains.is_empty() {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"Invalid principal name",
|
"Invalid principal name",
|
||||||
"Principal name must include a valid domain".into(),
|
"Principal name must include a valid domain assigned to the tenant".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure new name is not taken
|
|
||||||
if self
|
|
||||||
.get_principal_id(&name)
|
|
||||||
.await
|
|
||||||
.caused_by(trc::location!())?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return Err(err_exists(PrincipalField::Name, name));
|
|
||||||
}
|
}
|
||||||
principal.set(PrincipalField::Name, name);
|
principal.set(PrincipalField::Name, name);
|
||||||
|
|
||||||
|
@ -307,20 +341,6 @@ impl ManageDirectory for Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain tenant id
|
|
||||||
if let Some(tenant_id) = tenant_id {
|
|
||||||
principal.set(PrincipalField::Tenant, tenant_id);
|
|
||||||
} else if let Some(tenant_name) = principal.take_str(PrincipalField::Tenant) {
|
|
||||||
tenant_id = self
|
|
||||||
.get_principal_info(&tenant_name)
|
|
||||||
.await
|
|
||||||
.caused_by(trc::location!())?
|
|
||||||
.filter(|v| v.typ == Type::Tenant)
|
|
||||||
.ok_or_else(|| not_found(tenant_name.clone()))?
|
|
||||||
.id
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write principal
|
// Write principal
|
||||||
let mut batch = BatchBuilder::new();
|
let mut batch = BatchBuilder::new();
|
||||||
let pinfo_name = DynamicPrincipalInfo::new(principal.typ, tenant_id);
|
let pinfo_name = DynamicPrincipalInfo::new(principal.typ, tenant_id);
|
||||||
|
@ -648,7 +668,18 @@ impl ManageDirectory for Store {
|
||||||
// Make sure new name is not taken
|
// Make sure new name is not taken
|
||||||
let new_name = new_name.to_lowercase();
|
let new_name = new_name.to_lowercase();
|
||||||
if principal.inner.name() != new_name {
|
if principal.inner.name() != new_name {
|
||||||
if tenant_id.is_some() {
|
if tenant_id.is_some()
|
||||||
|
&& matches!(
|
||||||
|
principal.inner.typ,
|
||||||
|
Type::Individual
|
||||||
|
| Type::Group
|
||||||
|
| Type::List
|
||||||
|
| Type::Role
|
||||||
|
| Type::Location
|
||||||
|
| Type::Resource
|
||||||
|
| Type::Other
|
||||||
|
)
|
||||||
|
{
|
||||||
if let Some(domain) = new_name.split('@').nth(1) {
|
if let Some(domain) = new_name.split('@').nth(1) {
|
||||||
if self
|
if self
|
||||||
.get_principal_info(domain)
|
.get_principal_info(domain)
|
||||||
|
@ -666,7 +697,7 @@ impl ManageDirectory for Store {
|
||||||
if valid_domains.is_empty() {
|
if valid_domains.is_empty() {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"Invalid principal name",
|
"Invalid principal name",
|
||||||
"Principal name must include a valid domain".into(),
|
"Principal name must include a valid domain assigned to the tenant".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1340,7 +1371,8 @@ impl ManageDirectory for Store {
|
||||||
|| fields.iter().any(|f| {
|
|| fields.iter().any(|f| {
|
||||||
matches!(
|
matches!(
|
||||||
f,
|
f,
|
||||||
PrincipalField::MemberOf
|
PrincipalField::Tenant
|
||||||
|
| PrincipalField::MemberOf
|
||||||
| PrincipalField::Lists
|
| PrincipalField::Lists
|
||||||
| PrincipalField::Roles
|
| PrincipalField::Roles
|
||||||
| PrincipalField::EnabledPermissions
|
| PrincipalField::EnabledPermissions
|
||||||
|
@ -1353,9 +1385,7 @@ impl ManageDirectory for Store {
|
||||||
for mut principal in results {
|
for mut principal in results {
|
||||||
if !is_done || filters.is_some() {
|
if !is_done || filters.is_some() {
|
||||||
principal = self
|
principal = self
|
||||||
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
|
.query(QueryBy::Id(principal.id), map_principals)
|
||||||
DirectoryClass::Principal(principal.id),
|
|
||||||
)))
|
|
||||||
.await
|
.await
|
||||||
.caused_by(trc::location!())?
|
.caused_by(trc::location!())?
|
||||||
.ok_or_else(|| not_found(principal.name().to_string()))?;
|
.ok_or_else(|| not_found(principal.name().to_string()))?;
|
||||||
|
@ -1581,6 +1611,19 @@ impl ManageDirectory for Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map tenant name
|
||||||
|
if let Some(tenant_id) = principal.take_int(PrincipalField::Tenant) {
|
||||||
|
if fields.is_empty() || fields.contains(&PrincipalField::Tenant) {
|
||||||
|
if let Some(name) = self
|
||||||
|
.get_principal_name(tenant_id as u32)
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?
|
||||||
|
{
|
||||||
|
principal.set(PrincipalField::Tenant, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Obtain used quota
|
// Obtain used quota
|
||||||
if matches!(principal.typ, Type::Individual | Type::Group | Type::Tenant)
|
if matches!(principal.typ, Type::Individual | Type::Group | Type::Tenant)
|
||||||
&& (fields.is_empty() || fields.contains(&PrincipalField::UsedQuota))
|
&& (fields.is_empty() || fields.contains(&PrincipalField::UsedQuota))
|
||||||
|
@ -1659,6 +1702,7 @@ fn validate_member_of(
|
||||||
if expected_types.is_empty() || !expected_types.contains(&member_type) {
|
if expected_types.is_empty() || !expected_types.contains(&member_type) {
|
||||||
Err(error(
|
Err(error(
|
||||||
format!("Invalid {} value", field.as_str()),
|
format!("Invalid {} value", field.as_str()),
|
||||||
|
if !expected_types.is_empty() {
|
||||||
format!(
|
format!(
|
||||||
"Principal {member_name:?} is not a {}.",
|
"Principal {member_name:?} is not a {}.",
|
||||||
expected_types
|
expected_types
|
||||||
|
@ -1667,7 +1711,10 @@ fn validate_member_of(
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
)
|
)
|
||||||
.into(),
|
.into()
|
||||||
|
} else {
|
||||||
|
format!("Principal {member_name:?} cannot be added as a member.").into()
|
||||||
|
},
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -92,6 +92,15 @@ impl Principal {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn take_int(&mut self, key: PrincipalField) -> Option<u64> {
|
||||||
|
self.take(key).and_then(|v| match v {
|
||||||
|
PrincipalValue::Integer(i) => Some(i),
|
||||||
|
PrincipalValue::IntegerList(l) => l.into_iter().next(),
|
||||||
|
PrincipalValue::String(s) => s.parse().ok(),
|
||||||
|
PrincipalValue::StringList(l) => l.into_iter().next().and_then(|s| s.parse().ok()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn take_str_array(&mut self, key: PrincipalField) -> Option<Vec<String>> {
|
pub fn take_str_array(&mut self, key: PrincipalField) -> Option<Vec<String>> {
|
||||||
self.take(key).map(|v| v.into_str_array())
|
self.take(key).map(|v| v.into_str_array())
|
||||||
}
|
}
|
||||||
|
@ -697,9 +706,19 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
||||||
let mut principal = Principal::default();
|
let mut principal = Principal::default();
|
||||||
|
|
||||||
while let Some(key) = map.next_key::<&str>()? {
|
while let Some(key) = map.next_key::<&str>()? {
|
||||||
let key = PrincipalField::try_parse(key).ok_or_else(|| {
|
let key = PrincipalField::try_parse(key)
|
||||||
|
.or_else(|| {
|
||||||
|
if key == "id" {
|
||||||
|
// Ignored
|
||||||
|
Some(PrincipalField::UsedQuota)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
serde::de::Error::custom(format!("invalid principal field: {}", key))
|
serde::de::Error::custom(format!("invalid principal field: {}", key))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let value = match key {
|
let value = match key {
|
||||||
PrincipalField::Name => PrincipalValue::String(map.next_value()?),
|
PrincipalField::Name => PrincipalValue::String(map.next_value()?),
|
||||||
PrincipalField::Description
|
PrincipalField::Description
|
||||||
|
@ -711,7 +730,6 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PrincipalField::Type => {
|
PrincipalField::Type => {
|
||||||
principal.typ = Type::parse(map.next_value()?).ok_or_else(|| {
|
principal.typ = Type::parse(map.next_value()?).ok_or_else(|| {
|
||||||
serde::de::Error::custom("invalid principal type")
|
serde::de::Error::custom("invalid principal type")
|
||||||
|
@ -719,7 +737,6 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PrincipalField::Quota => map.next_value::<PrincipalValue>()?,
|
PrincipalField::Quota => map.next_value::<PrincipalValue>()?,
|
||||||
|
|
||||||
PrincipalField::Secrets
|
PrincipalField::Secrets
|
||||||
| PrincipalField::Emails
|
| PrincipalField::Emails
|
||||||
| PrincipalField::MemberOf
|
| PrincipalField::MemberOf
|
||||||
|
@ -728,7 +745,16 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
||||||
| PrincipalField::Lists
|
| PrincipalField::Lists
|
||||||
| PrincipalField::EnabledPermissions
|
| PrincipalField::EnabledPermissions
|
||||||
| PrincipalField::DisabledPermissions => {
|
| PrincipalField::DisabledPermissions => {
|
||||||
PrincipalValue::StringList(map.next_value()?)
|
match map.next_value::<StringOrMany>()? {
|
||||||
|
StringOrMany::One(v) => PrincipalValue::StringList(vec![v]),
|
||||||
|
StringOrMany::Many(v) => {
|
||||||
|
if !v.is_empty() {
|
||||||
|
PrincipalValue::StringList(v)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PrincipalField::UsedQuota => {
|
PrincipalField::UsedQuota => {
|
||||||
// consume and ignore
|
// consume and ignore
|
||||||
|
@ -787,7 +813,56 @@ impl<'de> serde::Deserialize<'de> for StringOrU64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum StringOrMany {
|
||||||
|
One(String),
|
||||||
|
Many(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for StringOrMany {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct StringOrManyVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for StringOrManyVisitor {
|
||||||
|
type Value = StringOrMany;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a string or a sequence of strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(StringOrMany::One(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: de::SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
|
||||||
|
while let Some(value) = seq.next_element::<String>()? {
|
||||||
|
vec.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(StringOrMany::Many(vec))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_any(StringOrManyVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Permission {
|
impl Permission {
|
||||||
|
pub fn all() -> impl Iterator<Item = Permission> {
|
||||||
|
(0..Permission::COUNT).filter_map(Permission::from_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn is_user_permission(&self) -> bool {
|
pub const fn is_user_permission(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -818,11 +818,6 @@ impl ToHttpResponse for &trc::Error {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
match self.as_ref() {
|
match self.as_ref() {
|
||||||
trc::EventType::Manage(cause) => {
|
trc::EventType::Manage(cause) => {
|
||||||
let details_or_reason = self
|
|
||||||
.value(trc::Key::Details)
|
|
||||||
.or_else(|| self.value(trc::Key::Reason))
|
|
||||||
.and_then(|v| v.as_str());
|
|
||||||
|
|
||||||
match cause {
|
match cause {
|
||||||
trc::ManageEvent::MissingParameter => ManagementApiError::FieldMissing {
|
trc::ManageEvent::MissingParameter => ManagementApiError::FieldMissing {
|
||||||
field: self.value_as_str(trc::Key::Key).unwrap_or_default(),
|
field: self.value_as_str(trc::Key::Key).unwrap_or_default(),
|
||||||
|
@ -835,11 +830,18 @@ impl ToHttpResponse for &trc::Error {
|
||||||
item: self.value_as_str(trc::Key::Key).unwrap_or_default(),
|
item: self.value_as_str(trc::Key::Key).unwrap_or_default(),
|
||||||
},
|
},
|
||||||
trc::ManageEvent::NotSupported => ManagementApiError::Unsupported {
|
trc::ManageEvent::NotSupported => ManagementApiError::Unsupported {
|
||||||
details: details_or_reason.unwrap_or("Requested action is unsupported"),
|
details: self
|
||||||
|
.value(trc::Key::Details)
|
||||||
|
.or_else(|| self.value(trc::Key::Reason))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Requested action is unsupported"),
|
||||||
},
|
},
|
||||||
trc::ManageEvent::AssertFailed => ManagementApiError::AssertFailed,
|
trc::ManageEvent::AssertFailed => ManagementApiError::AssertFailed,
|
||||||
trc::ManageEvent::Error => ManagementApiError::Other {
|
trc::ManageEvent::Error => ManagementApiError::Other {
|
||||||
details: details_or_reason.unwrap_or("An error occurred."),
|
reason: self.value_as_str(trc::Key::Reason),
|
||||||
|
details: self
|
||||||
|
.value_as_str(trc::Key::Details)
|
||||||
|
.unwrap_or("Unknown error"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,12 +33,24 @@ use crate::JMAP;
|
||||||
#[serde(tag = "error")]
|
#[serde(tag = "error")]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum ManagementApiError<'x> {
|
pub enum ManagementApiError<'x> {
|
||||||
FieldAlreadyExists { field: &'x str, value: &'x str },
|
FieldAlreadyExists {
|
||||||
FieldMissing { field: &'x str },
|
field: &'x str,
|
||||||
NotFound { item: &'x str },
|
value: &'x str,
|
||||||
Unsupported { details: &'x str },
|
},
|
||||||
|
FieldMissing {
|
||||||
|
field: &'x str,
|
||||||
|
},
|
||||||
|
NotFound {
|
||||||
|
item: &'x str,
|
||||||
|
},
|
||||||
|
Unsupported {
|
||||||
|
details: &'x str,
|
||||||
|
},
|
||||||
AssertFailed,
|
AssertFailed,
|
||||||
Other { details: &'x str },
|
Other {
|
||||||
|
details: &'x str,
|
||||||
|
reason: Option<&'x str>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JMAP {
|
impl JMAP {
|
||||||
|
|
|
@ -85,7 +85,7 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the current directory supports updates
|
// Make sure the current directory supports updates
|
||||||
if matches!(principal.typ(), Type::Individual | Type::Group | Type::List) {
|
if matches!(principal.typ(), Type::Individual) {
|
||||||
self.assert_supported_directory()?;
|
self.assert_supported_directory()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,27 +315,27 @@ impl JMAP {
|
||||||
|
|
||||||
// Validate changes
|
// Validate changes
|
||||||
let mut needs_assert = false;
|
let mut needs_assert = false;
|
||||||
let mut is_password_change = false;
|
let mut expire_session = false;
|
||||||
|
let mut expire_token = false;
|
||||||
let mut is_role_change = false;
|
let mut is_role_change = false;
|
||||||
|
|
||||||
for change in &changes {
|
for change in &changes {
|
||||||
match change.field {
|
match change.field {
|
||||||
PrincipalField::Name
|
PrincipalField::Name | PrincipalField::Emails => {
|
||||||
| PrincipalField::Emails
|
needs_assert = true;
|
||||||
| PrincipalField::MemberOf
|
}
|
||||||
| PrincipalField::Members
|
PrincipalField::Secrets => {
|
||||||
| PrincipalField::Lists => {
|
expire_session = true;
|
||||||
needs_assert = true;
|
needs_assert = true;
|
||||||
}
|
}
|
||||||
PrincipalField::Quota
|
PrincipalField::Quota
|
||||||
| PrincipalField::UsedQuota
|
| PrincipalField::UsedQuota
|
||||||
| PrincipalField::Description
|
| PrincipalField::Description
|
||||||
| PrincipalField::Type
|
| PrincipalField::Type
|
||||||
| PrincipalField::Picture => (),
|
| PrincipalField::Picture
|
||||||
PrincipalField::Secrets => {
|
| PrincipalField::MemberOf
|
||||||
is_password_change = true;
|
| PrincipalField::Members
|
||||||
needs_assert = true;
|
| PrincipalField::Lists => (),
|
||||||
}
|
|
||||||
PrincipalField::Tenant => {
|
PrincipalField::Tenant => {
|
||||||
// Tenants are not allowed to change their tenantId
|
// Tenants are not allowed to change their tenantId
|
||||||
if access_token.tenant.is_some() {
|
if access_token.tenant.is_some() {
|
||||||
|
@ -353,6 +353,8 @@ impl JMAP {
|
||||||
| PrincipalField::DisabledPermissions => {
|
| PrincipalField::DisabledPermissions => {
|
||||||
if matches!(typ, Type::Role | Type::Tenant) {
|
if matches!(typ, Type::Role | Type::Tenant) {
|
||||||
is_role_change = true;
|
is_role_change = true;
|
||||||
|
} else {
|
||||||
|
expire_token = true;
|
||||||
}
|
}
|
||||||
if change.field == PrincipalField::Roles {
|
if change.field == PrincipalField::Roles {
|
||||||
needs_assert = true;
|
needs_assert = true;
|
||||||
|
@ -376,7 +378,7 @@ impl JMAP {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if is_password_change {
|
if expire_session {
|
||||||
// Remove entries from cache
|
// Remove entries from cache
|
||||||
self.inner.sessions.retain(|_, id| id.item != account_id);
|
self.inner.sessions.retain(|_, id| id.item != account_id);
|
||||||
}
|
}
|
||||||
|
@ -390,6 +392,10 @@ impl JMAP {
|
||||||
.fetch_add(1, Ordering::Relaxed);
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if expire_token {
|
||||||
|
self.core.security.access_tokens.remove(&account_id);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(JsonResponse::new(json!({
|
Ok(JsonResponse::new(json!({
|
||||||
"data": (),
|
"data": (),
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -345,7 +345,7 @@ impl EventType {
|
||||||
SpamEvent::ListUpdated => Level::Info,
|
SpamEvent::ListUpdated => Level::Info,
|
||||||
},
|
},
|
||||||
EventType::Http(event) => match event {
|
EventType::Http(event) => match event {
|
||||||
HttpEvent::ConnectionStart | HttpEvent::ConnectionEnd => Level::Info,
|
HttpEvent::ConnectionStart | HttpEvent::ConnectionEnd => Level::Debug,
|
||||||
HttpEvent::XForwardedMissing => Level::Warn,
|
HttpEvent::XForwardedMissing => Level::Warn,
|
||||||
HttpEvent::Error | HttpEvent::RequestUrl => Level::Debug,
|
HttpEvent::Error | HttpEvent::RequestUrl => Level::Debug,
|
||||||
HttpEvent::RequestBody | HttpEvent::ResponseBody => Level::Trace,
|
HttpEvent::RequestBody | HttpEvent::ResponseBody => Level::Trace,
|
||||||
|
|
|
@ -25,7 +25,7 @@ use crate::{
|
||||||
|
|
||||||
use super::JMAPTest;
|
use super::JMAPTest;
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct OAuthCodeResponse {
|
struct OAuthCodeResponse {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
|
@ -66,6 +67,7 @@ pub mod email_submission;
|
||||||
pub mod enterprise;
|
pub mod enterprise;
|
||||||
pub mod event_source;
|
pub mod event_source;
|
||||||
pub mod mailbox;
|
pub mod mailbox;
|
||||||
|
pub mod permissions;
|
||||||
pub mod purge;
|
pub mod purge;
|
||||||
pub mod push_subscription;
|
pub mod push_subscription;
|
||||||
pub mod quota;
|
pub mod quota;
|
||||||
|
@ -310,7 +312,7 @@ pub async fn jmap_tests() {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
webhooks::test(&mut params).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_get::test(&mut params).await;
|
||||||
email_set::test(&mut params).await;
|
email_set::test(&mut params).await;
|
||||||
email_parse::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_changes::test(&mut params).await;
|
||||||
email_query_changes::test(&mut params).await;
|
email_query_changes::test(&mut params).await;
|
||||||
email_copy::test(&mut params).await;
|
email_copy::test(&mut params).await;
|
||||||
//thread_get::test(&mut params).await;
|
thread_get::test(&mut params).await;
|
||||||
//thread_merge::test(&mut params).await;
|
thread_merge::test(&mut params).await;
|
||||||
mailbox::test(&mut params).await;
|
mailbox::test(&mut params).await;
|
||||||
delivery::test(&mut params).await;
|
delivery::test(&mut params).await;
|
||||||
auth_acl::test(&mut params).await;
|
auth_acl::test(&mut params).await;
|
||||||
|
@ -333,7 +335,8 @@ pub async fn jmap_tests() {
|
||||||
websocket::test(&mut params).await;
|
websocket::test(&mut params).await;
|
||||||
quota::test(&mut params).await;
|
quota::test(&mut params).await;
|
||||||
crypto::test(&mut params).await;
|
crypto::test(&mut params).await;
|
||||||
blob::test(&mut params).await;*/
|
blob::test(&mut params).await;
|
||||||
|
permissions::test(¶ms).await;
|
||||||
purge::test(&mut params).await;
|
purge::test(&mut params).await;
|
||||||
enterprise::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)]
|
#[serde(untagged)]
|
||||||
pub enum Response<T> {
|
pub enum Response<T> {
|
||||||
RequestError(RequestError<'static>),
|
RequestError(RequestError<'static>),
|
||||||
Error { error: String, details: String },
|
Error {
|
||||||
Data { data: T },
|
error: String,
|
||||||
|
details: Option<String>,
|
||||||
|
item: Option<String>,
|
||||||
|
reason: Option<String>,
|
||||||
|
},
|
||||||
|
Data {
|
||||||
|
data: T,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ManagementApi {
|
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> {
|
pub async fn get<T: DeserializeOwned>(&self, query: &str) -> Result<Response<T>, String> {
|
||||||
self.request_raw(Method::GET, query, None)
|
self.request_raw(Method::GET, query, None)
|
||||||
.await
|
.await
|
||||||
|
@ -840,12 +876,17 @@ impl ManagementApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Response<T> {
|
impl<T: Debug> Response<T> {
|
||||||
pub fn unwrap_data(self) -> T {
|
pub fn unwrap_data(self) -> T {
|
||||||
match self {
|
match self {
|
||||||
Response::Data { data } => data,
|
Response::Data { data } => data,
|
||||||
Response::Error { error, details } => {
|
Response::Error {
|
||||||
panic!("Expected data, found error {error:?}: {details:?}")
|
error,
|
||||||
|
details,
|
||||||
|
reason,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
panic!("Expected data, found error {error:?}: {details:?} {reason:?}")
|
||||||
}
|
}
|
||||||
Response::RequestError(err) => {
|
Response::RequestError(err) => {
|
||||||
panic!("Expected data, found error {err:?}")
|
panic!("Expected data, found error {err:?}")
|
||||||
|
@ -857,8 +898,13 @@ impl<T> Response<T> {
|
||||||
match self {
|
match self {
|
||||||
Response::Data { data } => Some(data),
|
Response::Data { data } => Some(data),
|
||||||
Response::RequestError(error) if error.status == 404 => None,
|
Response::RequestError(error) if error.status == 404 => None,
|
||||||
Response::Error { error, details } => {
|
Response::Error {
|
||||||
panic!("Expected data, found error {error:?}: {details:?}")
|
error,
|
||||||
|
details,
|
||||||
|
reason,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
panic!("Expected data, found error {error:?}: {details:?} {reason:?}")
|
||||||
}
|
}
|
||||||
Response::RequestError(err) => {
|
Response::RequestError(err) => {
|
||||||
panic!("Expected data, found error {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 {
|
match self {
|
||||||
Response::Error { error, details } => (error, details),
|
Response::Error {
|
||||||
Response::Data { .. } => panic!("Expected error, found data."),
|
error,
|
||||||
|
details,
|
||||||
|
reason,
|
||||||
|
..
|
||||||
|
} => (error, details, reason),
|
||||||
|
Response::Data { data } => panic!("Expected error, found data: {data:?}"),
|
||||||
Response::RequestError(err) => {
|
Response::RequestError(err) => {
|
||||||
panic!("Expected error, found request error {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:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
862
tests/src/jmap/permissions.rs
Normal file
862
tests/src/jmap/permissions.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ reject-non-fqdn = false
|
||||||
relay = true
|
relay = true
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(super) struct List<T> {
|
pub(super) struct List<T> {
|
||||||
pub items: Vec<T>,
|
pub items: Vec<T>,
|
||||||
|
|
Loading…
Add table
Reference in a new issue