DLP and mail flow rules: rules, engine, and inbuxa:MailRule over JMAP #103
@@ -296,6 +296,17 @@ impl Default for DefaultPermissions {
|
|||||||
default.superuser.push(permission);
|
default.superuser.push(permission);
|
||||||
default.tenant.push(permission);
|
default.tenant.push(permission);
|
||||||
}
|
}
|
||||||
|
// inbuxa: DLP and mail flow rules, and held mail, are the
|
||||||
|
// server's: never a tenant's (dlp-and-mail-flow-rules spec,
|
||||||
|
// settled answer 3)
|
||||||
|
Permission::SysMailRuleGet
|
||||||
|
| Permission::SysMailRuleUpdate
|
||||||
|
| Permission::SysDlpPolicyGet
|
||||||
|
| Permission::SysDlpPolicyUpdate
|
||||||
|
| Permission::SysDlpReviewGet
|
||||||
|
| Permission::SysDlpReviewUpdate => {
|
||||||
|
default.superuser.push(permission);
|
||||||
|
}
|
||||||
// inbuxa: AL-12: tenant administrators lock and delegate
|
// inbuxa: AL-12: tenant administrators lock and delegate
|
||||||
// within their tenant
|
// within their tenant
|
||||||
Permission::SysAccountLockGet
|
Permission::SysAccountLockGet
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ const OFFICER: &[Permission] = &[
|
|||||||
Permission::SysLegalHoldUpdate,
|
Permission::SysLegalHoldUpdate,
|
||||||
Permission::SysLegalHoldExport,
|
Permission::SysLegalHoldExport,
|
||||||
Permission::SysAccountLockGet,
|
Permission::SysAccountLockGet,
|
||||||
|
// dlp-and-mail-flow-rules spec, §2.8: see DLP rules, review held mail
|
||||||
|
Permission::SysDlpPolicyGet,
|
||||||
|
Permission::SysDlpReviewGet,
|
||||||
|
Permission::SysDlpReviewUpdate,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// What a tenant's officer holds besides [`READS`].
|
/// What a tenant's officer holds besides [`READS`].
|
||||||
@@ -113,6 +117,11 @@ fn created_key(tenant: Option<Id>) -> ValueClass {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The server-level Compliance Officer role the server made, if it has.
|
||||||
|
pub async fn server_role(data: &Store) -> trc::Result<Option<Id>> {
|
||||||
|
recorded(data, None).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn recorded(data: &Store, tenant: Option<Id>) -> trc::Result<Option<Id>> {
|
async fn recorded(data: &Store, tenant: Option<Id>) -> trc::Result<Option<Id>> {
|
||||||
Ok(data
|
Ok(data
|
||||||
.get_value::<u64>(ValueKey::from(created_key(tenant)))
|
.get_value::<u64>(ValueKey::from(created_key(tenant)))
|
||||||
@@ -172,13 +181,19 @@ pub async fn ensure_compliance_roles(registry: &RegistryStore, data: &Store) ->
|
|||||||
|
|
||||||
/// A new tenant gets its Compliance Officer role.
|
/// A new tenant gets its Compliance Officer role.
|
||||||
pub async fn tenant_created(registry: &RegistryStore, data: &Store, tenant: Id) -> trc::Result<()> {
|
pub async fn tenant_created(registry: &RegistryStore, data: &Store, tenant: Id) -> trc::Result<()> {
|
||||||
create_once(registry, data, Some(tenant), tenant_role(tenant)).await.map(|_| ())
|
create_once(registry, data, Some(tenant), tenant_role(tenant))
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Before a tenant is deleted: removes its Compliance Officer role if nobody
|
/// Before a tenant is deleted: removes its Compliance Officer role if nobody
|
||||||
/// holds it, so the role doesn't block the delete. Returns whether it did,
|
/// holds it, so the role doesn't block the delete. Returns whether it did,
|
||||||
/// so a delete refused for another reason can put it back.
|
/// so a delete refused for another reason can put it back.
|
||||||
pub async fn tenant_deleting(registry: &RegistryStore, data: &Store, tenant: Id) -> trc::Result<bool> {
|
pub async fn tenant_deleting(
|
||||||
|
registry: &RegistryStore,
|
||||||
|
data: &Store,
|
||||||
|
tenant: Id,
|
||||||
|
) -> trc::Result<bool> {
|
||||||
let Some(role) = recorded(data, Some(tenant)).await? else {
|
let Some(role) = recorded(data, Some(tenant)).await? else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
@@ -220,7 +235,9 @@ mod tests {
|
|||||||
// Beyond what any user holds for their own account
|
// Beyond what any user holds for their own account
|
||||||
for permission in all.into_iter().filter(|p| !user.contains(p)) {
|
for permission in all.into_iter().filter(|p| !user.contains(p)) {
|
||||||
let name = permission.as_str();
|
let name = permission.as_str();
|
||||||
let holds = name.starts_with("sysLegalHold");
|
// Placing holds and reviewing held mail are the officer's
|
||||||
|
// job, not settings (settled answers 2 and 4)
|
||||||
|
let holds = name.starts_with("sysLegalHold") || name.starts_with("sysDlpReview");
|
||||||
assert!(
|
assert!(
|
||||||
!(name.ends_with("Update") && !holds)
|
!(name.ends_with("Update") && !holds)
|
||||||
&& !(name.ends_with("Create") && !holds)
|
&& !(name.ends_with("Create") && !holds)
|
||||||
@@ -249,7 +266,11 @@ mod tests {
|
|||||||
assert!(officer.contains(&hold));
|
assert!(officer.contains(&hold));
|
||||||
assert!(!tenant.contains(&hold));
|
assert!(!tenant.contains(&hold));
|
||||||
}
|
}
|
||||||
for both in [Permission::SysComplianceGet, Permission::SysAuditGet, Permission::SysAccountGet] {
|
for both in [
|
||||||
|
Permission::SysComplianceGet,
|
||||||
|
Permission::SysAuditGet,
|
||||||
|
Permission::SysAccountGet,
|
||||||
|
] {
|
||||||
assert!(officer.contains(&both) && tenant.contains(&both));
|
assert!(officer.contains(&both) && tenant.contains(&both));
|
||||||
}
|
}
|
||||||
assert!(!officer.contains(&Permission::SysAuditSettingsUpdate));
|
assert!(!officer.contains(&Permission::SysAuditSettingsUpdate));
|
||||||
@@ -257,9 +278,15 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn records_are_per_place() {
|
fn records_are_per_place() {
|
||||||
let ValueClass::Any(server) = created_key(None) else { panic!() };
|
let ValueClass::Any(server) = created_key(None) else {
|
||||||
let ValueClass::Any(a) = created_key(Some(Id::from(1u64))) else { panic!() };
|
panic!()
|
||||||
let ValueClass::Any(b) = created_key(Some(Id::from(2u64))) else { panic!() };
|
};
|
||||||
|
let ValueClass::Any(a) = created_key(Some(Id::from(1u64))) else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let ValueClass::Any(b) = created_key(Some(Id::from(2u64))) else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
assert_eq!(server.key, b"Pc");
|
assert_eq!(server.key, b"Pc");
|
||||||
assert_ne!(a.key, b.key);
|
assert_ne!(a.key, b.key);
|
||||||
assert!(a.key.starts_with(b"Pc"));
|
assert!(a.key.starts_with(b"Pc"));
|
||||||
|
|||||||
@@ -46,6 +46,21 @@ const ADMIN_GRANTS: &[Permission] = &[
|
|||||||
Permission::SysLegalHoldUpdate,
|
Permission::SysLegalHoldUpdate,
|
||||||
Permission::SysLegalHoldExport,
|
Permission::SysLegalHoldExport,
|
||||||
Permission::SysComplianceGet,
|
Permission::SysComplianceGet,
|
||||||
|
Permission::SysMailRuleGet,
|
||||||
|
Permission::SysMailRuleUpdate,
|
||||||
|
Permission::SysDlpPolicyGet,
|
||||||
|
Permission::SysDlpPolicyUpdate,
|
||||||
|
Permission::SysDlpReviewGet,
|
||||||
|
Permission::SysDlpReviewUpdate,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Granted to the server-level Compliance Officer role once it exists:
|
||||||
|
/// seeing DLP rules and reviewing held mail (dlp-and-mail-flow-rules spec,
|
||||||
|
/// §2.8, settled answer 4). A new install's role has them from the start.
|
||||||
|
const OFFICER_GRANTS: &[Permission] = &[
|
||||||
|
Permission::SysDlpPolicyGet,
|
||||||
|
Permission::SysDlpReviewGet,
|
||||||
|
Permission::SysDlpReviewUpdate,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Granted to the default tenant administrator roles: reading and exporting
|
/// Granted to the default tenant administrator roles: reading and exporting
|
||||||
@@ -65,13 +80,16 @@ const TENANT_GRANTS: &[Permission] = &[
|
|||||||
enum Audience {
|
enum Audience {
|
||||||
Admin,
|
Admin,
|
||||||
Tenant,
|
Tenant,
|
||||||
|
Officer,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn granted_key(permission: Permission, audience: Audience) -> ValueClass {
|
fn granted_key(permission: Permission, audience: Audience) -> ValueClass {
|
||||||
let mut key = b"Pg".to_vec();
|
let mut key = b"Pg".to_vec();
|
||||||
// Admin grants keep the key they were first recorded under
|
// Admin grants keep the key they were first recorded under
|
||||||
if audience == Audience::Tenant {
|
match audience {
|
||||||
key.extend_from_slice(b"tenant:");
|
Audience::Admin => {}
|
||||||
|
Audience::Tenant => key.extend_from_slice(b"tenant:"),
|
||||||
|
Audience::Officer => key.extend_from_slice(b"officer:"),
|
||||||
}
|
}
|
||||||
key.extend_from_slice(permission.as_str().as_bytes());
|
key.extend_from_slice(permission.as_str().as_bytes());
|
||||||
ValueClass::Any(AnyClass {
|
ValueClass::Any(AnyClass {
|
||||||
@@ -82,7 +100,8 @@ fn granted_key(permission: Permission, audience: Audience) -> ValueClass {
|
|||||||
|
|
||||||
pub(crate) async fn grant_new_admin_permissions(bp: &mut Bootstrap) -> trc::Result<()> {
|
pub(crate) async fn grant_new_admin_permissions(bp: &mut Bootstrap) -> trc::Result<()> {
|
||||||
grant(bp, Audience::Admin, ADMIN_GRANTS).await?;
|
grant(bp, Audience::Admin, ADMIN_GRANTS).await?;
|
||||||
grant(bp, Audience::Tenant, TENANT_GRANTS).await
|
grant(bp, Audience::Tenant, TENANT_GRANTS).await?;
|
||||||
|
grant(bp, Audience::Officer, OFFICER_GRANTS).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn grant(bp: &mut Bootstrap, audience: Audience, grants: &[Permission]) -> trc::Result<()> {
|
async fn grant(bp: &mut Bootstrap, audience: Audience, grants: &[Permission]) -> trc::Result<()> {
|
||||||
@@ -101,39 +120,47 @@ async fn grant(bp: &mut Bootstrap, audience: Audience, grants: &[Permission]) ->
|
|||||||
if pending.is_empty() {
|
if pending.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// An administrator's default roles include the plain User role, which
|
// The officer role is the one the server made, if it has made it yet: a
|
||||||
// every user also holds; only roles that are the audience's alone get it
|
// new install makes it after this, with the permissions already in it
|
||||||
let admin_roles: Vec<Id> = bp
|
let admin_roles: Vec<Id> = if audience == Audience::Officer {
|
||||||
.registry
|
super::compliance_roles::server_role(&bp.data_store)
|
||||||
.object::<Authentication>(Id::singleton())
|
.await?
|
||||||
.await?
|
.into_iter()
|
||||||
.map(|auth| {
|
.collect()
|
||||||
let (own, shared) = match audience {
|
} else {
|
||||||
Audience::Admin => (
|
// An administrator's default roles include the plain User role, which
|
||||||
auth.default_admin_role_ids.as_slice(),
|
// every user also holds; only roles that are the audience's alone get it
|
||||||
[
|
bp.registry
|
||||||
auth.default_user_role_ids.as_slice(),
|
.object::<Authentication>(Id::singleton())
|
||||||
auth.default_group_role_ids.as_slice(),
|
.await?
|
||||||
auth.default_tenant_role_ids.as_slice(),
|
.map(|auth| {
|
||||||
]
|
let (own, shared) = match audience {
|
||||||
.concat(),
|
Audience::Admin => (
|
||||||
),
|
|
||||||
Audience::Tenant => (
|
|
||||||
auth.default_tenant_role_ids.as_slice(),
|
|
||||||
[
|
|
||||||
auth.default_user_role_ids.as_slice(),
|
|
||||||
auth.default_group_role_ids.as_slice(),
|
|
||||||
auth.default_admin_role_ids.as_slice(),
|
auth.default_admin_role_ids.as_slice(),
|
||||||
]
|
[
|
||||||
.concat(),
|
auth.default_user_role_ids.as_slice(),
|
||||||
),
|
auth.default_group_role_ids.as_slice(),
|
||||||
};
|
auth.default_tenant_role_ids.as_slice(),
|
||||||
own.iter()
|
]
|
||||||
.filter(|id| !shared.contains(id))
|
.concat(),
|
||||||
.copied()
|
),
|
||||||
.collect()
|
Audience::Tenant | Audience::Officer => (
|
||||||
})
|
auth.default_tenant_role_ids.as_slice(),
|
||||||
.unwrap_or_default();
|
[
|
||||||
|
auth.default_user_role_ids.as_slice(),
|
||||||
|
auth.default_group_role_ids.as_slice(),
|
||||||
|
auth.default_admin_role_ids.as_slice(),
|
||||||
|
]
|
||||||
|
.concat(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
own.iter()
|
||||||
|
.filter(|id| !shared.contains(id))
|
||||||
|
.copied()
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
// Fetched by id: the registry's listing doesn't reach stored roles
|
// Fetched by id: the registry's listing doesn't reach stored roles
|
||||||
for role_id in admin_roles {
|
for role_id in admin_roles {
|
||||||
let Some(stored) = bp
|
let Some(stored) = bp
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! `inbuxa:MailRule/get` and `/set` under `urn:inbuxa:jmap`: mail flow rules
|
||||||
|
//! and DLP rules (dlp-and-mail-flow-rules spec, §2.2). `kind` says which,
|
||||||
|
//! and which permissions reach it. The set call's `reason` argument, if
|
||||||
|
//! given, goes into the audit log with the change.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
object::{AnyId, JmapObject, JmapObjectId},
|
||||||
|
request::deserialize::DeserializeArguments,
|
||||||
|
};
|
||||||
|
use jmap_tools::{Element, Key, Property};
|
||||||
|
use std::{borrow::Cow, str::FromStr};
|
||||||
|
use types::id::Id;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct MailRule;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum MailRuleProperty {
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
Description,
|
||||||
|
/// `dlp` or `transport`.
|
||||||
|
Kind,
|
||||||
|
Enabled,
|
||||||
|
/// Lower runs first.
|
||||||
|
Priority,
|
||||||
|
/// `outgoing`, `incoming` or `any`.
|
||||||
|
Direction,
|
||||||
|
Conditions,
|
||||||
|
Exceptions,
|
||||||
|
Actions,
|
||||||
|
StopProcessing,
|
||||||
|
CreatedBy,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum MailRuleValue {
|
||||||
|
Id(Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Property for MailRuleProperty {
|
||||||
|
fn try_parse(parent: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||||
|
// Keys inside conditions and actions stay plain keys
|
||||||
|
match parent {
|
||||||
|
None => MailRuleProperty::parse(value),
|
||||||
|
Some(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_cow(&self) -> Cow<'static, str> {
|
||||||
|
match self {
|
||||||
|
MailRuleProperty::Id => "id",
|
||||||
|
MailRuleProperty::Name => "name",
|
||||||
|
MailRuleProperty::Description => "description",
|
||||||
|
MailRuleProperty::Kind => "kind",
|
||||||
|
MailRuleProperty::Enabled => "enabled",
|
||||||
|
MailRuleProperty::Priority => "priority",
|
||||||
|
MailRuleProperty::Direction => "direction",
|
||||||
|
MailRuleProperty::Conditions => "conditions",
|
||||||
|
MailRuleProperty::Exceptions => "exceptions",
|
||||||
|
MailRuleProperty::Actions => "actions",
|
||||||
|
MailRuleProperty::StopProcessing => "stopProcessing",
|
||||||
|
MailRuleProperty::CreatedBy => "createdBy",
|
||||||
|
MailRuleProperty::CreatedAt => "createdAt",
|
||||||
|
MailRuleProperty::UpdatedAt => "updatedAt",
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailRuleProperty {
|
||||||
|
fn parse(value: &str) -> Option<Self> {
|
||||||
|
hashify::tiny_map!(value.as_bytes(),
|
||||||
|
b"id" => MailRuleProperty::Id,
|
||||||
|
b"name" => MailRuleProperty::Name,
|
||||||
|
b"description" => MailRuleProperty::Description,
|
||||||
|
b"kind" => MailRuleProperty::Kind,
|
||||||
|
b"enabled" => MailRuleProperty::Enabled,
|
||||||
|
b"priority" => MailRuleProperty::Priority,
|
||||||
|
b"direction" => MailRuleProperty::Direction,
|
||||||
|
b"conditions" => MailRuleProperty::Conditions,
|
||||||
|
b"exceptions" => MailRuleProperty::Exceptions,
|
||||||
|
b"actions" => MailRuleProperty::Actions,
|
||||||
|
b"stopProcessing" => MailRuleProperty::StopProcessing,
|
||||||
|
b"createdBy" => MailRuleProperty::CreatedBy,
|
||||||
|
b"createdAt" => MailRuleProperty::CreatedAt,
|
||||||
|
b"updatedAt" => MailRuleProperty::UpdatedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MailRuleProperty {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
MailRuleProperty::parse(s).ok_or(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for MailRuleValue {
|
||||||
|
type Property = MailRuleProperty;
|
||||||
|
|
||||||
|
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||||
|
match key {
|
||||||
|
Key::Property(MailRuleProperty::Id) => Id::from_str(value).ok().map(MailRuleValue::Id),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_cow(&self) -> Cow<'static, str> {
|
||||||
|
match self {
|
||||||
|
MailRuleValue::Id(id) => id.to_string().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The set call's own argument: why, for the audit log.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct MailRuleSetArguments {
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> DeserializeArguments<'de> for MailRuleSetArguments {
|
||||||
|
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||||
|
where
|
||||||
|
A: serde::de::MapAccess<'de>,
|
||||||
|
{
|
||||||
|
if key == "reason" {
|
||||||
|
self.reason = map.next_value()?;
|
||||||
|
} else {
|
||||||
|
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JmapObject for MailRule {
|
||||||
|
type Property = MailRuleProperty;
|
||||||
|
|
||||||
|
type Element = MailRuleValue;
|
||||||
|
|
||||||
|
type Id = Id;
|
||||||
|
|
||||||
|
type Filter = ();
|
||||||
|
|
||||||
|
type Comparator = ();
|
||||||
|
|
||||||
|
type GetArguments = ();
|
||||||
|
|
||||||
|
type SetArguments<'de> = MailRuleSetArguments;
|
||||||
|
|
||||||
|
type QueryArguments = ();
|
||||||
|
|
||||||
|
type CopyArguments = ();
|
||||||
|
|
||||||
|
type ParseArguments = ();
|
||||||
|
|
||||||
|
const ID_PROPERTY: Self::Property = MailRuleProperty::Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Id> for MailRuleValue {
|
||||||
|
fn from(id: Id) -> Self {
|
||||||
|
MailRuleValue::Id(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JmapObjectId for MailRuleValue {
|
||||||
|
fn as_id(&self) -> Option<Id> {
|
||||||
|
match self {
|
||||||
|
MailRuleValue::Id(id) => Some(*id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_id(&self) -> Option<AnyId> {
|
||||||
|
match self {
|
||||||
|
MailRuleValue::Id(id) => Some(AnyId::Id(*id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_id_ref(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||||
|
if let AnyId::Id(id) = new_id {
|
||||||
|
*self = MailRuleValue::Id(id);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JmapObjectId for MailRuleProperty {
|
||||||
|
fn as_id(&self) -> Option<Id> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_id(&self) -> Option<AnyId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_id_ref(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ pub mod inbuxa_data_inventory; // inbuxa: personal-data catalog
|
|||||||
pub mod inbuxa_inventory_snapshot; // inbuxa: personal-data catalog
|
pub mod inbuxa_inventory_snapshot; // inbuxa: personal-data catalog
|
||||||
pub mod inbuxa_audit; // inbuxa: the audit log
|
pub mod inbuxa_audit; // inbuxa: the audit log
|
||||||
pub mod inbuxa_legal_hold; // inbuxa: legal hold
|
pub mod inbuxa_legal_hold; // inbuxa: legal hold
|
||||||
|
pub mod inbuxa_mail_rule; // inbuxa: DLP and mail flow rules
|
||||||
pub mod inbuxa_hold_export; // inbuxa: legal hold exports
|
pub mod inbuxa_hold_export; // inbuxa: legal hold exports
|
||||||
pub mod inbuxa_explanation; // inbuxa: "Explain this" with the local model
|
pub mod inbuxa_explanation; // inbuxa: "Explain this" with the local model
|
||||||
pub mod inbuxa_protocol_policy; // inbuxa: legacy protocols off
|
pub mod inbuxa_protocol_policy; // inbuxa: legacy protocols off
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ impl Response<'_> {
|
|||||||
GetResponseMethod::LegalHold(response) => {
|
GetResponseMethod::LegalHold(response) => {
|
||||||
response.eval_jptr(path, &mut results)
|
response.eval_jptr(path, &mut results)
|
||||||
}
|
}
|
||||||
|
GetResponseMethod::MailRule(response) => {
|
||||||
|
response.eval_jptr(path, &mut results)
|
||||||
|
}
|
||||||
GetResponseMethod::HoldExport(response) => {
|
GetResponseMethod::HoldExport(response) => {
|
||||||
response.eval_jptr(path, &mut results)
|
response.eval_jptr(path, &mut results)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ impl Response<'_> {
|
|||||||
GetRequestMethod::AuditSettings(request) => request.resolve_references(self)?,
|
GetRequestMethod::AuditSettings(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::AccountLock(request) => request.resolve_references(self)?,
|
GetRequestMethod::AccountLock(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::LegalHold(request) => request.resolve_references(self)?,
|
GetRequestMethod::LegalHold(request) => request.resolve_references(self)?,
|
||||||
|
GetRequestMethod::MailRule(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::HoldExport(request) => request.resolve_references(self)?,
|
GetRequestMethod::HoldExport(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::ProtocolPolicy(request) => request.resolve_references(self)?,
|
GetRequestMethod::ProtocolPolicy(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::TenantProtocolPolicy(request) => {
|
GetRequestMethod::TenantProtocolPolicy(request) => {
|
||||||
@@ -122,6 +123,9 @@ impl Response<'_> {
|
|||||||
SetRequestMethod::LegalHold(request) => {
|
SetRequestMethod::LegalHold(request) => {
|
||||||
request.resolve_references(self, 1, false)?
|
request.resolve_references(self, 1, false)?
|
||||||
}
|
}
|
||||||
|
SetRequestMethod::MailRule(request) => {
|
||||||
|
request.resolve_references(self, 1, false)?
|
||||||
|
}
|
||||||
SetRequestMethod::HoldExport(request) => {
|
SetRequestMethod::HoldExport(request) => {
|
||||||
request.resolve_references(self, 1, false)?
|
request.resolve_references(self, 1, false)?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ pub enum MethodObject {
|
|||||||
LegalHold,
|
LegalHold,
|
||||||
HoldExport,
|
HoldExport,
|
||||||
ProtocolPolicy,
|
ProtocolPolicy,
|
||||||
|
// inbuxa: DLP and mail flow rules
|
||||||
|
MailRule,
|
||||||
TenantProtocolPolicy,
|
TenantProtocolPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +104,8 @@ impl MethodObject {
|
|||||||
| MethodObject::AuditVerification
|
| MethodObject::AuditVerification
|
||||||
| MethodObject::AccountLock
|
| MethodObject::AccountLock
|
||||||
| MethodObject::LegalHold
|
| MethodObject::LegalHold
|
||||||
| MethodObject::HoldExport => Capability::Inbuxa,
|
| MethodObject::HoldExport
|
||||||
|
| MethodObject::MailRule => Capability::Inbuxa,
|
||||||
MethodObject::ProtocolPolicy => Capability::Inbuxa,
|
MethodObject::ProtocolPolicy => Capability::Inbuxa,
|
||||||
MethodObject::TenantProtocolPolicy => Capability::Inbuxa,
|
MethodObject::TenantProtocolPolicy => Capability::Inbuxa,
|
||||||
}
|
}
|
||||||
@@ -296,6 +299,8 @@ impl MethodName {
|
|||||||
(MethodFunction::Set, MethodObject::AccountLock) => "inbuxa:AccountLock/set",
|
(MethodFunction::Set, MethodObject::AccountLock) => "inbuxa:AccountLock/set",
|
||||||
(MethodFunction::Get, MethodObject::LegalHold) => "inbuxa:LegalHold/get",
|
(MethodFunction::Get, MethodObject::LegalHold) => "inbuxa:LegalHold/get",
|
||||||
(MethodFunction::Set, MethodObject::LegalHold) => "inbuxa:LegalHold/set",
|
(MethodFunction::Set, MethodObject::LegalHold) => "inbuxa:LegalHold/set",
|
||||||
|
(MethodFunction::Get, MethodObject::MailRule) => "inbuxa:MailRule/get",
|
||||||
|
(MethodFunction::Set, MethodObject::MailRule) => "inbuxa:MailRule/set",
|
||||||
(MethodFunction::Get, MethodObject::HoldExport) => "inbuxa:HoldExport/get",
|
(MethodFunction::Get, MethodObject::HoldExport) => "inbuxa:HoldExport/get",
|
||||||
(MethodFunction::Set, MethodObject::HoldExport) => "inbuxa:HoldExport/set",
|
(MethodFunction::Set, MethodObject::HoldExport) => "inbuxa:HoldExport/set",
|
||||||
(MethodFunction::Set, MethodObject::AuditVerification) => {
|
(MethodFunction::Set, MethodObject::AuditVerification) => {
|
||||||
@@ -448,6 +453,8 @@ impl MethodName {
|
|||||||
"inbuxa:AccountLock/set" => (MethodObject::AccountLock, MethodFunction::Set),
|
"inbuxa:AccountLock/set" => (MethodObject::AccountLock, MethodFunction::Set),
|
||||||
"inbuxa:LegalHold/get" => (MethodObject::LegalHold, MethodFunction::Get),
|
"inbuxa:LegalHold/get" => (MethodObject::LegalHold, MethodFunction::Get),
|
||||||
"inbuxa:LegalHold/set" => (MethodObject::LegalHold, MethodFunction::Set),
|
"inbuxa:LegalHold/set" => (MethodObject::LegalHold, MethodFunction::Set),
|
||||||
|
"inbuxa:MailRule/get" => (MethodObject::MailRule, MethodFunction::Get),
|
||||||
|
"inbuxa:MailRule/set" => (MethodObject::MailRule, MethodFunction::Set),
|
||||||
"inbuxa:HoldExport/get" => (MethodObject::HoldExport, MethodFunction::Get),
|
"inbuxa:HoldExport/get" => (MethodObject::HoldExport, MethodFunction::Get),
|
||||||
"inbuxa:HoldExport/set" => (MethodObject::HoldExport, MethodFunction::Set),
|
"inbuxa:HoldExport/set" => (MethodObject::HoldExport, MethodFunction::Set),
|
||||||
"inbuxa:AuditVerification/set" => (MethodObject::AuditVerification, MethodFunction::Set),
|
"inbuxa:AuditVerification/set" => (MethodObject::AuditVerification, MethodFunction::Set),
|
||||||
@@ -518,6 +525,7 @@ impl Display for MethodObject {
|
|||||||
MethodObject::AuditVerification => "inbuxa:AuditVerification",
|
MethodObject::AuditVerification => "inbuxa:AuditVerification",
|
||||||
MethodObject::AccountLock => "inbuxa:AccountLock",
|
MethodObject::AccountLock => "inbuxa:AccountLock",
|
||||||
MethodObject::LegalHold => "inbuxa:LegalHold",
|
MethodObject::LegalHold => "inbuxa:LegalHold",
|
||||||
|
MethodObject::MailRule => "inbuxa:MailRule",
|
||||||
MethodObject::HoldExport => "inbuxa:HoldExport",
|
MethodObject::HoldExport => "inbuxa:HoldExport",
|
||||||
MethodObject::ProtocolPolicy => "inbuxa:ProtocolPolicy",
|
MethodObject::ProtocolPolicy => "inbuxa:ProtocolPolicy",
|
||||||
MethodObject::TenantProtocolPolicy => "inbuxa:TenantProtocolPolicy",
|
MethodObject::TenantProtocolPolicy => "inbuxa:TenantProtocolPolicy",
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ pub enum GetRequestMethod {
|
|||||||
AuditSettings(Box<GetRequest<crate::object::inbuxa_audit::AuditSettings>>),
|
AuditSettings(Box<GetRequest<crate::object::inbuxa_audit::AuditSettings>>),
|
||||||
AccountLock(Box<GetRequest<crate::object::inbuxa_account_lock::AccountLock>>),
|
AccountLock(Box<GetRequest<crate::object::inbuxa_account_lock::AccountLock>>),
|
||||||
LegalHold(Box<GetRequest<crate::object::inbuxa_legal_hold::LegalHold>>),
|
LegalHold(Box<GetRequest<crate::object::inbuxa_legal_hold::LegalHold>>),
|
||||||
|
MailRule(Box<GetRequest<crate::object::inbuxa_mail_rule::MailRule>>),
|
||||||
HoldExport(Box<GetRequest<crate::object::inbuxa_hold_export::HoldExport>>),
|
HoldExport(Box<GetRequest<crate::object::inbuxa_hold_export::HoldExport>>),
|
||||||
ProtocolPolicy(Box<GetRequest<crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
ProtocolPolicy(Box<GetRequest<crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
||||||
TenantProtocolPolicy(
|
TenantProtocolPolicy(
|
||||||
@@ -158,6 +159,7 @@ pub enum SetRequestMethod<'x> {
|
|||||||
AuditVerification(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditVerification>>),
|
AuditVerification(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditVerification>>),
|
||||||
AccountLock(Box<SetRequest<'x, crate::object::inbuxa_account_lock::AccountLock>>),
|
AccountLock(Box<SetRequest<'x, crate::object::inbuxa_account_lock::AccountLock>>),
|
||||||
LegalHold(Box<SetRequest<'x, crate::object::inbuxa_legal_hold::LegalHold>>),
|
LegalHold(Box<SetRequest<'x, crate::object::inbuxa_legal_hold::LegalHold>>),
|
||||||
|
MailRule(Box<SetRequest<'x, crate::object::inbuxa_mail_rule::MailRule>>),
|
||||||
HoldExport(Box<SetRequest<'x, crate::object::inbuxa_hold_export::HoldExport>>),
|
HoldExport(Box<SetRequest<'x, crate::object::inbuxa_hold_export::HoldExport>>),
|
||||||
ProtocolPolicy(Box<SetRequest<'x, crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
ProtocolPolicy(Box<SetRequest<'x, crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
||||||
TenantProtocolPolicy(
|
TenantProtocolPolicy(
|
||||||
|
|||||||
@@ -609,6 +609,21 @@ impl<'de> Visitor<'de> for CallVisitor {
|
|||||||
return Err(de::Error::invalid_length(1, &self));
|
return Err(de::Error::invalid_length(1, &self));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// inbuxa: DLP and mail flow rules
|
||||||
|
(MethodFunction::Get, MethodObject::MailRule) => match seq.next_element() {
|
||||||
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::MailRule(value)),
|
||||||
|
Err(err) => RequestMethod::invalid(err),
|
||||||
|
Ok(None) => {
|
||||||
|
return Err(de::Error::invalid_length(1, &self));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(MethodFunction::Set, MethodObject::MailRule) => match seq.next_element() {
|
||||||
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::MailRule(value)),
|
||||||
|
Err(err) => RequestMethod::invalid(err),
|
||||||
|
Ok(None) => {
|
||||||
|
return Err(de::Error::invalid_length(1, &self));
|
||||||
|
}
|
||||||
|
},
|
||||||
// inbuxa: legal hold
|
// inbuxa: legal hold
|
||||||
(MethodFunction::Get, MethodObject::LegalHold) => match seq.next_element() {
|
(MethodFunction::Get, MethodObject::LegalHold) => match seq.next_element() {
|
||||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::LegalHold(value)),
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::LegalHold(value)),
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ pub enum GetResponseMethod {
|
|||||||
AuditSettings(GetResponse<crate::object::inbuxa_audit::AuditSettings>),
|
AuditSettings(GetResponse<crate::object::inbuxa_audit::AuditSettings>),
|
||||||
AccountLock(GetResponse<crate::object::inbuxa_account_lock::AccountLock>),
|
AccountLock(GetResponse<crate::object::inbuxa_account_lock::AccountLock>),
|
||||||
LegalHold(GetResponse<crate::object::inbuxa_legal_hold::LegalHold>),
|
LegalHold(GetResponse<crate::object::inbuxa_legal_hold::LegalHold>),
|
||||||
|
MailRule(GetResponse<crate::object::inbuxa_mail_rule::MailRule>),
|
||||||
HoldExport(GetResponse<crate::object::inbuxa_hold_export::HoldExport>),
|
HoldExport(GetResponse<crate::object::inbuxa_hold_export::HoldExport>),
|
||||||
ProtocolPolicy(GetResponse<crate::object::inbuxa_protocol_policy::ProtocolPolicy>),
|
ProtocolPolicy(GetResponse<crate::object::inbuxa_protocol_policy::ProtocolPolicy>),
|
||||||
TenantProtocolPolicy(
|
TenantProtocolPolicy(
|
||||||
@@ -145,6 +146,7 @@ pub enum SetResponseMethod {
|
|||||||
AuditVerification(Box<SetResponse<crate::object::inbuxa_audit::AuditVerification>>),
|
AuditVerification(Box<SetResponse<crate::object::inbuxa_audit::AuditVerification>>),
|
||||||
AccountLock(Box<SetResponse<crate::object::inbuxa_account_lock::AccountLock>>),
|
AccountLock(Box<SetResponse<crate::object::inbuxa_account_lock::AccountLock>>),
|
||||||
LegalHold(Box<SetResponse<crate::object::inbuxa_legal_hold::LegalHold>>),
|
LegalHold(Box<SetResponse<crate::object::inbuxa_legal_hold::LegalHold>>),
|
||||||
|
MailRule(Box<SetResponse<crate::object::inbuxa_mail_rule::MailRule>>),
|
||||||
HoldExport(Box<SetResponse<crate::object::inbuxa_hold_export::HoldExport>>),
|
HoldExport(Box<SetResponse<crate::object::inbuxa_hold_export::HoldExport>>),
|
||||||
Explanation(Box<SetResponse<crate::object::inbuxa_explanation::Explanation>>),
|
Explanation(Box<SetResponse<crate::object::inbuxa_explanation::Explanation>>),
|
||||||
ProtocolPolicy(Box<SetResponse<crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
ProtocolPolicy(Box<SetResponse<crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
||||||
@@ -799,6 +801,18 @@ impl<'x> From<SetResponse<crate::object::inbuxa_account_lock::AccountLock>> for
|
|||||||
}
|
}
|
||||||
|
|
||||||
// inbuxa: legal hold
|
// inbuxa: legal hold
|
||||||
|
impl<'x> From<GetResponse<crate::object::inbuxa_mail_rule::MailRule>> for ResponseMethod<'x> {
|
||||||
|
fn from(value: GetResponse<crate::object::inbuxa_mail_rule::MailRule>) -> Self {
|
||||||
|
ResponseMethod::Get(GetResponseMethod::MailRule(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'x> From<SetResponse<crate::object::inbuxa_mail_rule::MailRule>> for ResponseMethod<'x> {
|
||||||
|
fn from(value: SetResponse<crate::object::inbuxa_mail_rule::MailRule>) -> Self {
|
||||||
|
ResponseMethod::Set(SetResponseMethod::MailRule(Box::new(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'x> From<GetResponse<crate::object::inbuxa_legal_hold::LegalHold>> for ResponseMethod<'x> {
|
impl<'x> From<GetResponse<crate::object::inbuxa_legal_hold::LegalHold>> for ResponseMethod<'x> {
|
||||||
fn from(value: GetResponse<crate::object::inbuxa_legal_hold::LegalHold>) -> Self {
|
fn from(value: GetResponse<crate::object::inbuxa_legal_hold::LegalHold>) -> Self {
|
||||||
ResponseMethod::Get(GetResponseMethod::LegalHold(value))
|
ResponseMethod::Get(GetResponseMethod::LegalHold(value))
|
||||||
|
|||||||
@@ -103,6 +103,16 @@ impl JmapAuthorization for AccessToken {
|
|||||||
// inbuxa: account lock (AL-12)
|
// inbuxa: account lock (AL-12)
|
||||||
GetRequestMethod::AccountLock(_) => Permission::SysAccountLockGet,
|
GetRequestMethod::AccountLock(_) => Permission::SysAccountLockGet,
|
||||||
GetRequestMethod::LegalHold(_) => Permission::SysLegalHoldGet,
|
GetRequestMethod::LegalHold(_) => Permission::SysLegalHoldGet,
|
||||||
|
// inbuxa: DLP and mail flow rules share an object; either
|
||||||
|
// permission reaches it, and the handler shows each kind
|
||||||
|
// only to those who may see it
|
||||||
|
GetRequestMethod::MailRule(_) => {
|
||||||
|
if self.has_permission(Permission::SysMailRuleGet) {
|
||||||
|
Permission::SysMailRuleGet
|
||||||
|
} else {
|
||||||
|
Permission::SysDlpPolicyGet
|
||||||
|
}
|
||||||
|
}
|
||||||
GetRequestMethod::HoldExport(_) => Permission::SysLegalHoldExport,
|
GetRequestMethod::HoldExport(_) => Permission::SysLegalHoldExport,
|
||||||
// inbuxa: legacy protocols off. It takes listeners away and
|
// inbuxa: legacy protocols off. It takes listeners away and
|
||||||
// puts them back, so it takes the listener's permissions
|
// puts them back, so it takes the listener's permissions
|
||||||
@@ -247,6 +257,19 @@ impl JmapAuthorization for AccessToken {
|
|||||||
Permission::SysLegalHoldUpdate,
|
Permission::SysLegalHoldUpdate,
|
||||||
Permission::SysLegalHoldUpdate,
|
Permission::SysLegalHoldUpdate,
|
||||||
),
|
),
|
||||||
|
// inbuxa: DLP and mail flow rules: either change
|
||||||
|
// permission gets in; the handler checks each rule's kind
|
||||||
|
SetRequestMethod::MailRule(_) => {
|
||||||
|
if self.has_permission(Permission::SysMailRuleUpdate)
|
||||||
|
|| self.has_permission(Permission::SysDlpPolicyUpdate)
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(trc::JmapEvent::Forbidden
|
||||||
|
.into_err()
|
||||||
|
.details("You are not authorized to change mail rules"))
|
||||||
|
}
|
||||||
|
}
|
||||||
// inbuxa: LH-12, exporting held data
|
// inbuxa: LH-12, exporting held data
|
||||||
SetRequestMethod::HoldExport(s) => validate_set(
|
SetRequestMethod::HoldExport(s) => validate_set(
|
||||||
s,
|
s,
|
||||||
@@ -407,6 +430,7 @@ impl JmapAuthorization for AccessToken {
|
|||||||
| MethodObject::AccountLock
|
| MethodObject::AccountLock
|
||||||
| MethodObject::LegalHold
|
| MethodObject::LegalHold
|
||||||
| MethodObject::HoldExport
|
| MethodObject::HoldExport
|
||||||
|
| MethodObject::MailRule
|
||||||
| MethodObject::ProtocolPolicy
|
| MethodObject::ProtocolPolicy
|
||||||
| MethodObject::TenantProtocolPolicy => Permission::JmapEmailChanges,
|
| MethodObject::TenantProtocolPolicy => Permission::JmapEmailChanges,
|
||||||
// inbuxa: x:MaskedEmail/changes reads what /get reads
|
// inbuxa: x:MaskedEmail/changes reads what /get reads
|
||||||
|
|||||||
@@ -279,6 +279,9 @@ impl RequestHandler for Server {
|
|||||||
SetResponseMethod::LegalHold(set_response) => {
|
SetResponseMethod::LegalHold(set_response) => {
|
||||||
set_response.update_created_ids(&mut response);
|
set_response.update_created_ids(&mut response);
|
||||||
}
|
}
|
||||||
|
SetResponseMethod::MailRule(set_response) => {
|
||||||
|
set_response.update_created_ids(&mut response);
|
||||||
|
}
|
||||||
SetResponseMethod::HoldExport(set_response) => {
|
SetResponseMethod::HoldExport(set_response) => {
|
||||||
set_response.update_created_ids(&mut response);
|
set_response.update_created_ids(&mut response);
|
||||||
}
|
}
|
||||||
@@ -486,6 +489,11 @@ impl RequestHandler for Server {
|
|||||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
crate::inbuxa::legal_hold::get(self, *req).await?.into()
|
crate::inbuxa::legal_hold::get(self, *req).await?.into()
|
||||||
}
|
}
|
||||||
|
// inbuxa: DLP and mail flow rules
|
||||||
|
GetRequestMethod::MailRule(mut req) => {
|
||||||
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
|
crate::inbuxa::mail_rule::get(self, access_token, *req).await?.into()
|
||||||
|
}
|
||||||
// inbuxa: the audit log (AU-9)
|
// inbuxa: the audit log (AU-9)
|
||||||
GetRequestMethod::AuditEvent(mut req) => {
|
GetRequestMethod::AuditEvent(mut req) => {
|
||||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
@@ -910,6 +918,22 @@ impl RequestHandler for Server {
|
|||||||
.await?
|
.await?
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
SetRequestMethod::MailRule(mut req) => {
|
||||||
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
|
let reason = req.arguments.reason.clone();
|
||||||
|
crate::inbuxa::audit::recorded(
|
||||||
|
self,
|
||||||
|
access_token,
|
||||||
|
session,
|
||||||
|
&method_name.obj.to_string(),
|
||||||
|
None,
|
||||||
|
reason,
|
||||||
|
*req,
|
||||||
|
|req| Box::pin(crate::inbuxa::mail_rule::set(self, access_token, req)),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into()
|
||||||
|
}
|
||||||
SetRequestMethod::AuditExport(mut req) => {
|
SetRequestMethod::AuditExport(mut req) => {
|
||||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
crate::inbuxa::audit_log::export_set(self, access_token, session, *req)
|
crate::inbuxa::audit_log::export_set(self, access_token, session, *req)
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ impl IntermediateChangesResponse {
|
|||||||
| MethodObject::AccountLock
|
| MethodObject::AccountLock
|
||||||
| MethodObject::LegalHold
|
| MethodObject::LegalHold
|
||||||
| MethodObject::HoldExport
|
| MethodObject::HoldExport
|
||||||
|
| MethodObject::MailRule
|
||||||
| MethodObject::ProtocolPolicy
|
| MethodObject::ProtocolPolicy
|
||||||
| MethodObject::TenantProtocolPolicy
|
| MethodObject::TenantProtocolPolicy
|
||||||
| MethodObject::Registry(_) => unreachable!(),
|
| MethodObject::Registry(_) => unreachable!(),
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! `inbuxa:MailRule` (dlp-and-mail-flow-rules spec, §2.2, §2.8): mail flow
|
||||||
|
//! rules and DLP rules. One object, two kinds, each with its own
|
||||||
|
//! permissions: `sysMailRuleGet`/`Update` for transport rules,
|
||||||
|
//! `sysDlpPolicyGet`/`Update` for DLP rules. Rules are the server's: nobody
|
||||||
|
//! in a tenant reaches them (settled answer 3). The request layer records
|
||||||
|
//! every change in the audit log.
|
||||||
|
|
||||||
|
use common::{Server, auth::AccessToken};
|
||||||
|
use inbuxa_features::mailflow::rules::{self, Kind, Rule};
|
||||||
|
use jmap_proto::{
|
||||||
|
error::set::SetError,
|
||||||
|
method::{
|
||||||
|
get::{GetRequest, GetResponse},
|
||||||
|
set::{SetRequest, SetResponse},
|
||||||
|
},
|
||||||
|
object::inbuxa_mail_rule::{MailRule, MailRuleProperty as P, MailRuleValue},
|
||||||
|
request::IntoValid,
|
||||||
|
types::date::UTCDate,
|
||||||
|
};
|
||||||
|
use jmap_tools::{Key, Map, Property, Value};
|
||||||
|
use registry::schema::enums::Permission;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use store::write::now;
|
||||||
|
use types::id::Id;
|
||||||
|
|
||||||
|
type RValue = Value<'static, P, MailRuleValue>;
|
||||||
|
|
||||||
|
const ALL: &[P] = &[
|
||||||
|
P::Id,
|
||||||
|
P::Name,
|
||||||
|
P::Description,
|
||||||
|
P::Kind,
|
||||||
|
P::Enabled,
|
||||||
|
P::Priority,
|
||||||
|
P::Direction,
|
||||||
|
P::Conditions,
|
||||||
|
P::Exceptions,
|
||||||
|
P::Actions,
|
||||||
|
P::StopProcessing,
|
||||||
|
P::CreatedBy,
|
||||||
|
P::CreatedAt,
|
||||||
|
P::UpdatedAt,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Properties the server sets; a client that sends them is refused.
|
||||||
|
const SERVER_SET: &[P] = &[P::Id, P::CreatedBy, P::CreatedAt, P::UpdatedAt];
|
||||||
|
|
||||||
|
fn can_see(access_token: &AccessToken, kind: Kind) -> bool {
|
||||||
|
access_token.has_permission(match kind {
|
||||||
|
Kind::Dlp => Permission::SysDlpPolicyGet,
|
||||||
|
Kind::Transport => Permission::SysMailRuleGet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_change(access_token: &AccessToken, kind: Kind) -> bool {
|
||||||
|
access_token.has_permission(match kind {
|
||||||
|
Kind::Dlp => Permission::SysDlpPolicyUpdate,
|
||||||
|
Kind::Transport => Permission::SysMailRuleUpdate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_level(access_token: &AccessToken) -> trc::Result<()> {
|
||||||
|
if access_token.tenant_id().is_some() {
|
||||||
|
Err(trc::JmapEvent::Forbidden
|
||||||
|
.into_err()
|
||||||
|
.details("Mail rules are the server's."))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_to_value(json: serde_json::Value) -> RValue {
|
||||||
|
match json {
|
||||||
|
serde_json::Value::Null => Value::Null,
|
||||||
|
serde_json::Value::Bool(b) => Value::Bool(b),
|
||||||
|
serde_json::Value::Number(n) => {
|
||||||
|
if let Some(n) = n.as_u64() {
|
||||||
|
Value::Number(n.into())
|
||||||
|
} else if let Some(n) = n.as_i64() {
|
||||||
|
Value::Number(n.into())
|
||||||
|
} else {
|
||||||
|
Value::Number(n.as_f64().unwrap_or_default().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::Value::String(s) => Value::Str(Cow::Owned(s)),
|
||||||
|
serde_json::Value::Array(items) => {
|
||||||
|
Value::Array(items.into_iter().map(json_to_value).collect())
|
||||||
|
}
|
||||||
|
serde_json::Value::Object(map) => {
|
||||||
|
let mut out = Map::with_capacity(map.len());
|
||||||
|
for (key, value) in map {
|
||||||
|
out.insert_unchecked(Key::Owned(key), json_to_value(value));
|
||||||
|
}
|
||||||
|
Value::Object(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn date(seconds: u64) -> RValue {
|
||||||
|
Value::Str(UTCDate::from_timestamp(seconds as i64).to_string().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_value(rule: &Rule, properties: &[P]) -> RValue {
|
||||||
|
let json = serde_json::to_value(rule).unwrap_or_default();
|
||||||
|
let mut out = Map::with_capacity(properties.len());
|
||||||
|
for property in properties {
|
||||||
|
let value = match property {
|
||||||
|
P::Id => Value::Element(MailRuleValue::Id(Id::from(rule.id))),
|
||||||
|
P::CreatedAt => date(rule.created_at),
|
||||||
|
P::UpdatedAt => date(rule.updated_at),
|
||||||
|
other => json
|
||||||
|
.get(other.to_cow().as_ref())
|
||||||
|
.cloned()
|
||||||
|
.map_or(Value::Null, json_to_value),
|
||||||
|
};
|
||||||
|
out.insert_unchecked(Key::Property(property.clone()), value);
|
||||||
|
}
|
||||||
|
Value::Object(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rule as sent: its JSON object, top-level keys only those a client may
|
||||||
|
/// set.
|
||||||
|
fn client_json(value: Value<'_, P, MailRuleValue>) -> Result<serde_json::Map<String, serde_json::Value>, SetError<P>> {
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
for (key, value) in value.into_expanded_object() {
|
||||||
|
match &key {
|
||||||
|
Key::Property(p) if SERVER_SET.contains(p) => {
|
||||||
|
return Err(SetError::invalid_properties()
|
||||||
|
.with_property(p.clone())
|
||||||
|
.with_description("The server sets this."));
|
||||||
|
}
|
||||||
|
Key::Property(p) => {
|
||||||
|
map.insert(p.to_cow().into_owned(), value.into());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(SetError::invalid_properties().with_property(key.clone().into_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(json: serde_json::Map<String, serde_json::Value>) -> Result<Rule, SetError<P>> {
|
||||||
|
let rule: Rule = serde_json::from_value(serde_json::Value::Object(json)).map_err(|err| {
|
||||||
|
SetError::invalid_properties().with_description(format!("Not a valid rule: {err}"))
|
||||||
|
})?;
|
||||||
|
rule.validate().map_err(|invalid| {
|
||||||
|
let property = invalid.property.parse::<P>().unwrap_or(P::Name);
|
||||||
|
SetError::invalid_properties()
|
||||||
|
.with_property(property)
|
||||||
|
.with_description(invalid.reason)
|
||||||
|
})?;
|
||||||
|
Ok(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forbidden(kind: Kind) -> SetError<P> {
|
||||||
|
SetError::forbidden().with_description(match kind {
|
||||||
|
Kind::Dlp => "Changing DLP rules needs the permission to change DLP rules.",
|
||||||
|
Kind::Transport => "Changing mail flow rules needs the permission to change them.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rule_id(id: Id) -> Option<u32> {
|
||||||
|
u32::try_from(id.id()).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `inbuxa:MailRule/get`: the rules the caller may see, in the order they
|
||||||
|
/// run.
|
||||||
|
pub async fn get(
|
||||||
|
server: &Server,
|
||||||
|
access_token: &AccessToken,
|
||||||
|
mut request: GetRequest<MailRule>,
|
||||||
|
) -> trc::Result<GetResponse<MailRule>> {
|
||||||
|
server_level(access_token)?;
|
||||||
|
let properties = request.unwrap_properties(ALL);
|
||||||
|
let (ids, not_found) = request.unwrap_ids(server.core.jmap.get_max_objects)?;
|
||||||
|
let mut response = GetResponse {
|
||||||
|
account_id: request.account_id.into(),
|
||||||
|
state: None,
|
||||||
|
list: Vec::new(),
|
||||||
|
not_found,
|
||||||
|
};
|
||||||
|
let visible: Vec<Rule> = rules::all(server.store())
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|rule| can_see(access_token, rule.kind))
|
||||||
|
.collect();
|
||||||
|
match ids {
|
||||||
|
None => {
|
||||||
|
response.list = visible
|
||||||
|
.iter()
|
||||||
|
.map(|rule| to_value(rule, &properties))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
Some(ids) => {
|
||||||
|
for id in ids {
|
||||||
|
match rule_id(id).and_then(|id| visible.iter().find(|r| r.id == id)) {
|
||||||
|
Some(rule) => response.list.push(to_value(rule, &properties)),
|
||||||
|
None => response.push_not_found(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `inbuxa:MailRule/set`: create, change or delete rules, each checked
|
||||||
|
/// against the permissions for its kind (and, on a change of kind, both).
|
||||||
|
pub async fn set(
|
||||||
|
server: &Server,
|
||||||
|
access_token: &AccessToken,
|
||||||
|
mut request: SetRequest<'_, MailRule>,
|
||||||
|
) -> trc::Result<SetResponse<MailRule>> {
|
||||||
|
server_level(access_token)?;
|
||||||
|
let mut response = SetResponse::from_request(&request, server.core.jmap.set_max_objects)?;
|
||||||
|
let data = server.store();
|
||||||
|
let actor = server.audit_actor(access_token).await;
|
||||||
|
|
||||||
|
for (client_id, value) in request.unwrap_create() {
|
||||||
|
let rule = match client_json(value).and_then(parse) {
|
||||||
|
Ok(rule) => rule,
|
||||||
|
Err(error) => {
|
||||||
|
response.not_created.append(client_id, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !can_change(access_token, rule.kind) {
|
||||||
|
response.not_created.append(client_id, forbidden(rule.kind));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let at = now();
|
||||||
|
let rule = Rule {
|
||||||
|
created_by: actor.name.clone(),
|
||||||
|
created_at: at,
|
||||||
|
updated_at: at,
|
||||||
|
..rule
|
||||||
|
};
|
||||||
|
let id = rules::create(data, &rule).await?;
|
||||||
|
let mut out = Map::with_capacity(1);
|
||||||
|
out.insert_unchecked(
|
||||||
|
Key::Property(P::Id),
|
||||||
|
Value::Element(MailRuleValue::Id(Id::from(id))),
|
||||||
|
);
|
||||||
|
response.created.insert(client_id, Value::Object(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (id, value) in request.unwrap_update().into_valid() {
|
||||||
|
let Some(current) = (match rule_id(id) {
|
||||||
|
Some(rule_id) => rules::get(data, rule_id).await?,
|
||||||
|
None => None,
|
||||||
|
}) else {
|
||||||
|
response.not_updated.append(id, SetError::not_found());
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !can_see(access_token, current.kind) {
|
||||||
|
response.not_updated.append(id, SetError::not_found());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !can_change(access_token, current.kind) {
|
||||||
|
response.not_updated.append(id, forbidden(current.kind));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The stored rule, with each property sent replacing its own
|
||||||
|
let mut json = match serde_json::to_value(¤t) {
|
||||||
|
Ok(serde_json::Value::Object(map)) => map,
|
||||||
|
_ => serde_json::Map::new(),
|
||||||
|
};
|
||||||
|
let changes = match client_json(value) {
|
||||||
|
Ok(changes) => changes,
|
||||||
|
Err(error) => {
|
||||||
|
response.not_updated.append(id, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
json.extend(changes);
|
||||||
|
let next = match parse(json) {
|
||||||
|
Ok(next) => next,
|
||||||
|
Err(error) => {
|
||||||
|
response.not_updated.append(id, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if next.kind != current.kind && !can_change(access_token, next.kind) {
|
||||||
|
response.not_updated.append(id, forbidden(next.kind));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let next = Rule {
|
||||||
|
id: current.id,
|
||||||
|
created_by: current.created_by.clone(),
|
||||||
|
created_at: current.created_at,
|
||||||
|
updated_at: now(),
|
||||||
|
..next
|
||||||
|
};
|
||||||
|
if next != current {
|
||||||
|
rules::update(data, &next).await?;
|
||||||
|
}
|
||||||
|
response.updated.append(id, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
for id in request.unwrap_destroy().into_valid() {
|
||||||
|
let Some(current) = (match rule_id(id) {
|
||||||
|
Some(rule_id) => rules::get(data, rule_id).await?,
|
||||||
|
None => None,
|
||||||
|
}) else {
|
||||||
|
response.not_destroyed.append(id, SetError::not_found());
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !can_see(access_token, current.kind) {
|
||||||
|
response.not_destroyed.append(id, SetError::not_found());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !can_change(access_token, current.kind) {
|
||||||
|
response.not_destroyed.append(id, forbidden(current.kind));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rules::delete(data, current.id).await?;
|
||||||
|
response.destroyed.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
pub mod access;
|
pub mod access;
|
||||||
pub mod account_lock;
|
pub mod account_lock;
|
||||||
pub mod legal_hold;
|
pub mod legal_hold;
|
||||||
|
pub mod mail_rule;
|
||||||
pub mod hold_export;
|
pub mod hold_export;
|
||||||
pub mod hold_export_api;
|
pub mod hold_export_api;
|
||||||
pub mod audit;
|
pub mod audit;
|
||||||
|
|||||||
@@ -1748,6 +1748,13 @@ pub enum Permission {
|
|||||||
SysLegalHoldExport = 672,
|
SysLegalHoldExport = 672,
|
||||||
// inbuxa: personal-data catalog, the data inventory and compliance overview
|
// inbuxa: personal-data catalog, the data inventory and compliance overview
|
||||||
SysComplianceGet = 673,
|
SysComplianceGet = 673,
|
||||||
|
// inbuxa: DLP and mail flow rules
|
||||||
|
SysMailRuleGet = 674,
|
||||||
|
SysMailRuleUpdate = 675,
|
||||||
|
SysDlpPolicyGet = 676,
|
||||||
|
SysDlpPolicyUpdate = 677,
|
||||||
|
SysDlpReviewGet = 678,
|
||||||
|
SysDlpReviewUpdate = 679,
|
||||||
SysAccountGet = 219,
|
SysAccountGet = 219,
|
||||||
SysAccountCreate = 220,
|
SysAccountCreate = 220,
|
||||||
SysAccountUpdate = 221,
|
SysAccountUpdate = 221,
|
||||||
|
|||||||
@@ -7091,6 +7091,12 @@ impl EnumImpl for Permission {
|
|||||||
b"sysLegalHoldUpdate" => Permission::SysLegalHoldUpdate,
|
b"sysLegalHoldUpdate" => Permission::SysLegalHoldUpdate,
|
||||||
b"sysLegalHoldExport" => Permission::SysLegalHoldExport,
|
b"sysLegalHoldExport" => Permission::SysLegalHoldExport,
|
||||||
b"sysComplianceGet" => Permission::SysComplianceGet,
|
b"sysComplianceGet" => Permission::SysComplianceGet,
|
||||||
|
b"sysMailRuleGet" => Permission::SysMailRuleGet,
|
||||||
|
b"sysMailRuleUpdate" => Permission::SysMailRuleUpdate,
|
||||||
|
b"sysDlpPolicyGet" => Permission::SysDlpPolicyGet,
|
||||||
|
b"sysDlpPolicyUpdate" => Permission::SysDlpPolicyUpdate,
|
||||||
|
b"sysDlpReviewGet" => Permission::SysDlpReviewGet,
|
||||||
|
b"sysDlpReviewUpdate" => Permission::SysDlpReviewUpdate,
|
||||||
b"sysAccountGet" => Permission::SysAccountGet,
|
b"sysAccountGet" => Permission::SysAccountGet,
|
||||||
b"sysAccountCreate" => Permission::SysAccountCreate,
|
b"sysAccountCreate" => Permission::SysAccountCreate,
|
||||||
b"sysAccountUpdate" => Permission::SysAccountUpdate,
|
b"sysAccountUpdate" => Permission::SysAccountUpdate,
|
||||||
@@ -7781,6 +7787,12 @@ impl EnumImpl for Permission {
|
|||||||
Permission::SysLegalHoldUpdate => "sysLegalHoldUpdate",
|
Permission::SysLegalHoldUpdate => "sysLegalHoldUpdate",
|
||||||
Permission::SysLegalHoldExport => "sysLegalHoldExport",
|
Permission::SysLegalHoldExport => "sysLegalHoldExport",
|
||||||
Permission::SysComplianceGet => "sysComplianceGet",
|
Permission::SysComplianceGet => "sysComplianceGet",
|
||||||
|
Permission::SysMailRuleGet => "sysMailRuleGet",
|
||||||
|
Permission::SysMailRuleUpdate => "sysMailRuleUpdate",
|
||||||
|
Permission::SysDlpPolicyGet => "sysDlpPolicyGet",
|
||||||
|
Permission::SysDlpPolicyUpdate => "sysDlpPolicyUpdate",
|
||||||
|
Permission::SysDlpReviewGet => "sysDlpReviewGet",
|
||||||
|
Permission::SysDlpReviewUpdate => "sysDlpReviewUpdate",
|
||||||
Permission::SysAccountGet => "sysAccountGet",
|
Permission::SysAccountGet => "sysAccountGet",
|
||||||
Permission::SysAccountCreate => "sysAccountCreate",
|
Permission::SysAccountCreate => "sysAccountCreate",
|
||||||
Permission::SysAccountUpdate => "sysAccountUpdate",
|
Permission::SysAccountUpdate => "sysAccountUpdate",
|
||||||
@@ -8464,6 +8476,12 @@ impl EnumImpl for Permission {
|
|||||||
671 => Some(Permission::SysLegalHoldUpdate),
|
671 => Some(Permission::SysLegalHoldUpdate),
|
||||||
672 => Some(Permission::SysLegalHoldExport),
|
672 => Some(Permission::SysLegalHoldExport),
|
||||||
673 => Some(Permission::SysComplianceGet),
|
673 => Some(Permission::SysComplianceGet),
|
||||||
|
674 => Some(Permission::SysMailRuleGet),
|
||||||
|
675 => Some(Permission::SysMailRuleUpdate),
|
||||||
|
676 => Some(Permission::SysDlpPolicyGet),
|
||||||
|
677 => Some(Permission::SysDlpPolicyUpdate),
|
||||||
|
678 => Some(Permission::SysDlpReviewGet),
|
||||||
|
679 => Some(Permission::SysDlpReviewUpdate),
|
||||||
219 => Some(Permission::SysAccountGet),
|
219 => Some(Permission::SysAccountGet),
|
||||||
220 => Some(Permission::SysAccountCreate),
|
220 => Some(Permission::SysAccountCreate),
|
||||||
221 => Some(Permission::SysAccountUpdate),
|
221 => Some(Permission::SysAccountUpdate),
|
||||||
@@ -8908,7 +8926,7 @@ impl EnumImpl for Permission {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const COUNT: usize = 674;
|
const COUNT: usize = 680;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl serde::Serialize for Permission {
|
impl serde::Serialize for Permission {
|
||||||
|
|||||||
@@ -79,6 +79,21 @@ lockedAt = ["metadata"]
|
|||||||
lockedBy = ["identifier"]
|
lockedBy = ["identifier"]
|
||||||
delegates = ["identifier"]
|
delegates = ["identifier"]
|
||||||
|
|
||||||
|
[object."inbuxa:MailRule"]
|
||||||
|
file = "inbuxa_mail_rule.rs"
|
||||||
|
default = "none"
|
||||||
|
whose = ["administrator", "holder", "correspondent"]
|
||||||
|
where = ["data-store"]
|
||||||
|
scope = "server"
|
||||||
|
retention = "unbounded"
|
||||||
|
[object."inbuxa:MailRule".properties]
|
||||||
|
name = ["content"]
|
||||||
|
description = ["content"]
|
||||||
|
conditions = ["contact", "content"]
|
||||||
|
exceptions = ["contact", "content"]
|
||||||
|
actions = ["contact", "content"]
|
||||||
|
createdBy = ["identifier"]
|
||||||
|
|
||||||
[object."inbuxa:LegalHold"]
|
[object."inbuxa:LegalHold"]
|
||||||
file = "inbuxa_legal_hold.rs"
|
file = "inbuxa_legal_hold.rs"
|
||||||
default = "none"
|
default = "none"
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
k496pjVWlQ2p4bkZCh8agzaCKMLD4c3Z9WtoxpckYDU
|
yF7PlBR3UBqxlabhW5zZ5WacQWsynG1wNYEiJVAl6w4
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! `inbuxa:MailRule` (dlp-and-mail-flow-rules spec, §2.2, §2.8): rules are
|
||||||
|
//! stored, listed in the order they run, checked when written, kept apart
|
||||||
|
//! by kind for permissions, and every change is audited.
|
||||||
|
|
||||||
|
use crate::utils::{
|
||||||
|
account::Account,
|
||||||
|
server::{TestServer, TestServerBuilder},
|
||||||
|
};
|
||||||
|
use registry::schema::{
|
||||||
|
prelude::{ObjectType, Property},
|
||||||
|
structs::{CustomRoles, Role, UserRoles},
|
||||||
|
};
|
||||||
|
use registry::types::map::Map;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
const USING: &[&str] = &[
|
||||||
|
"urn:ietf:params:jmap:core",
|
||||||
|
"urn:inbuxa:jmap",
|
||||||
|
"urn:inbuxa:jmap:registry",
|
||||||
|
];
|
||||||
|
|
||||||
|
async fn call(account: &Account, method: &str, mut arguments: Value) -> (String, Value) {
|
||||||
|
if arguments.get("accountId").is_none() {
|
||||||
|
arguments["accountId"] = account.id_string().into();
|
||||||
|
}
|
||||||
|
let response = account.jmap_request(USING, json!([[method, arguments, "0"]])).await;
|
||||||
|
let call = response
|
||||||
|
.0
|
||||||
|
.pointer("/methodResponses/0")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| panic!("{method}: {}", response.0));
|
||||||
|
(call[0].as_str().unwrap_or_default().to_string(), call[1].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dlp_rule() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "Cards leaving",
|
||||||
|
"kind": "dlp",
|
||||||
|
"direction": "outgoing",
|
||||||
|
"priority": 10,
|
||||||
|
"conditions": [
|
||||||
|
{"type": "recipientOutside"},
|
||||||
|
{"type": "detected", "detectors": [{"id": "payment-card", "atLeast": 5}]}
|
||||||
|
],
|
||||||
|
"actions": [{"type": "hold", "notice": "Held for review", "notifySender": true}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transport_rule() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "Disclaimer",
|
||||||
|
"kind": "transport",
|
||||||
|
"direction": "outgoing",
|
||||||
|
"priority": 1,
|
||||||
|
"conditions": [{"type": "recipientOutside"}],
|
||||||
|
"actions": [{"type": "addDisclaimer", "text": "Sent by Example Co.", "position": "bottom"}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn names(account: &Account) -> Vec<String> {
|
||||||
|
let (name, response) = call(account, "inbuxa:MailRule/get", json!({"ids": null})).await;
|
||||||
|
assert_eq!(name, "inbuxa:MailRule/get", "{response}");
|
||||||
|
response["list"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|r| r["name"].as_str().unwrap().to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test(test: &mut TestServer) {
|
||||||
|
println!("Running mail rule tests...");
|
||||||
|
let admin = test.account("[email protected]");
|
||||||
|
|
||||||
|
// Created, then listed in the order they run
|
||||||
|
let (_, response) = call(
|
||||||
|
&admin,
|
||||||
|
"inbuxa:MailRule/set",
|
||||||
|
json!({"create": {"d": dlp_rule(), "t": transport_rule()}}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let dlp_id = response["created"]["d"]["id"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_else(|| panic!("DLP rule created: {response}"))
|
||||||
|
.to_string();
|
||||||
|
let transport_id = response["created"]["t"]["id"].as_str().unwrap().to_string();
|
||||||
|
assert_eq!(names(&admin).await, vec!["Disclaimer", "Cards leaving"]);
|
||||||
|
|
||||||
|
let (_, response) = call(&admin, "inbuxa:MailRule/get", json!({"ids": [dlp_id]})).await;
|
||||||
|
let rule = &response["list"][0];
|
||||||
|
assert_eq!(rule["conditions"][1]["detectors"][0]["atLeast"], 5, "{rule}");
|
||||||
|
assert_eq!(rule["actions"][0]["notifySender"], true);
|
||||||
|
assert_eq!(rule["createdBy"], "[email protected]");
|
||||||
|
assert!(rule["createdAt"].as_str().is_some_and(|d| d.ends_with('Z')), "{rule}");
|
||||||
|
|
||||||
|
// Checked when written
|
||||||
|
let mut inbound = dlp_rule();
|
||||||
|
inbound["direction"] = "incoming".into();
|
||||||
|
let mut unknown = dlp_rule();
|
||||||
|
unknown["conditions"][1]["detectors"][0]["id"] = "no-such-detector".into();
|
||||||
|
let (_, response) = call(
|
||||||
|
&admin,
|
||||||
|
"inbuxa:MailRule/set",
|
||||||
|
json!({"create": {"a": inbound, "b": unknown, "c": {"name": "x", "kind": "dlp"}}}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response["notCreated"]["a"]["properties"][0], "direction", "{response}");
|
||||||
|
assert!(
|
||||||
|
response["notCreated"]["b"]["description"].as_str().unwrap().contains("no-such-detector"),
|
||||||
|
"{response}"
|
||||||
|
);
|
||||||
|
assert!(response["notCreated"].get("c").is_some(), "{response}");
|
||||||
|
|
||||||
|
// Changed in place; what the server sets can't be sent
|
||||||
|
let (_, response) = call(
|
||||||
|
&admin,
|
||||||
|
"inbuxa:MailRule/set",
|
||||||
|
json!({"update": {
|
||||||
|
transport_id.as_str(): {"name": "Footer", "priority": 50},
|
||||||
|
dlp_id.as_str(): {"createdBy": "someone else"}
|
||||||
|
}}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(response["updated"].get(transport_id.as_str()).is_some(), "{response}");
|
||||||
|
assert_eq!(response["notUpdated"][dlp_id.as_str()]["properties"][0], "createdBy", "{response}");
|
||||||
|
assert_eq!(names(&admin).await, vec!["Cards leaving", "Footer"]);
|
||||||
|
|
||||||
|
// A compliance officer sees DLP rules, not mail flow rules, and changes
|
||||||
|
// neither (settled answer 4)
|
||||||
|
let mut officer_role = None;
|
||||||
|
for id in admin
|
||||||
|
.registry_query_ids(ObjectType::Role, Vec::<(&str, &str)>::new(), Vec::<&str>::new())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let role = admin.registry_get::<Role>(id).await;
|
||||||
|
if role.description == "Compliance Officer" && role.member_tenant_id.is_none() {
|
||||||
|
officer_role = Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let officer = admin
|
||||||
|
.create_user_account("[email protected]", "officer-secret-7731", "Officer", &[], vec![])
|
||||||
|
.await;
|
||||||
|
admin
|
||||||
|
.registry_update_object(
|
||||||
|
ObjectType::Account,
|
||||||
|
officer.id(),
|
||||||
|
json!({Property::Roles: UserRoles::Custom(CustomRoles {
|
||||||
|
role_ids: Map::new(vec![officer_role.expect("the officer role")]),
|
||||||
|
})}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(names(&officer).await, vec!["Cards leaving"]);
|
||||||
|
let (name, response) =
|
||||||
|
call(&officer, "inbuxa:MailRule/set", json!({"create": {"d": dlp_rule()}})).await;
|
||||||
|
assert_eq!(name, "error", "the officer created a DLP rule: {response}");
|
||||||
|
|
||||||
|
// Deleted
|
||||||
|
let (_, response) = call(
|
||||||
|
&admin,
|
||||||
|
"inbuxa:MailRule/set",
|
||||||
|
json!({"destroy": [transport_id]}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response["destroyed"][0], transport_id.as_str(), "{response}");
|
||||||
|
assert_eq!(names(&admin).await, vec!["Cards leaving"]);
|
||||||
|
|
||||||
|
// Every change is in the audit log
|
||||||
|
let (_, response) = call(
|
||||||
|
&admin,
|
||||||
|
"inbuxa:AuditEvent/query",
|
||||||
|
json!({"filter": {"targetKind": "inbuxa:MailRule"}, "calculateTotal": true}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
response["total"].as_u64().unwrap_or(0) >= 4,
|
||||||
|
"creates, update and destroy audited: {response}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
pub async fn mail_rules_tests() {
|
||||||
|
let mut test = TestServerBuilder::new("mail_rules_tests")
|
||||||
|
.await
|
||||||
|
.with_default_listeners()
|
||||||
|
.await
|
||||||
|
.build()
|
||||||
|
.await;
|
||||||
|
let admin = test.create_admin_account("[email protected]").await;
|
||||||
|
test.insert_account(admin);
|
||||||
|
self::test(&mut test).await;
|
||||||
|
if test.is_reset() {
|
||||||
|
test.temp_dir.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ pub mod ai_explain;
|
|||||||
pub mod account_lock; // inbuxa: account lock with delegation
|
pub mod account_lock; // inbuxa: account lock with delegation
|
||||||
pub mod legal_hold; // inbuxa: legal hold
|
pub mod legal_hold; // inbuxa: legal hold
|
||||||
pub mod compliance; // inbuxa: the compliance roles
|
pub mod compliance; // inbuxa: the compliance roles
|
||||||
|
pub mod mail_rules; // inbuxa: DLP and mail flow rules
|
||||||
pub mod audit; // inbuxa: the audit log
|
pub mod audit; // inbuxa: the audit log
|
||||||
pub mod authorization;
|
pub mod authorization;
|
||||||
pub mod auto_reload; // inbuxa: registry writes apply at once
|
pub mod auto_reload; // inbuxa: registry writes apply at once
|
||||||
|
|||||||
Reference in New Issue
Block a user