Legal holds, step 1: the hold itself
inbuxa:LegalHold get/set places a hold on accounts, groups, domains, tenants or the whole server, with an optional date range. A hold's range and scope can only widen, a released hold is read-only, and none is ever deleted. Placing, changing and releasing each need a reason and are audited (LH-1, LH-3, LH-10, AU-12). Permissions 669-672 (see, place, widen or release, export held data) go to server administrators only; the tenant ceiling always strips them, as it does Impersonate (LH-13). Schema: Compliance > Legal Holds. What a hold keeps comes next, through the undelete hooks. Also moves the lock expiry helpers below the lock module's imports.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! `inbuxa:LegalHold/get` and `/set` under `urn:inbuxa:jmap`: legal holds
|
||||
//! (audit-hold-lock spec, LH-1 to LH-14). Creating one places the hold;
|
||||
//! updating renames it, widens its range or scope, or releases it with
|
||||
//! `released: true`. There is no destroy: a released hold stays listed. The
|
||||
//! set call's `reason` argument says why, for the audit log (AU-12);
|
||||
//! creating takes it as a property too.
|
||||
|
||||
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 LegalHold;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LegalHoldProperty {
|
||||
Id,
|
||||
/// The case name.
|
||||
Name,
|
||||
/// A matter or ticket number.
|
||||
Reference,
|
||||
Description,
|
||||
/// `{server, accounts, groups, domains, tenants}`.
|
||||
Scope,
|
||||
/// The range's start, a UTC date, or null.
|
||||
From,
|
||||
/// The range's end, a UTC date, or null.
|
||||
To,
|
||||
/// Why it was placed (create only; later reasons are the audit log's).
|
||||
Reason,
|
||||
PlacedAt,
|
||||
PlacedBy,
|
||||
/// Set to true to release it.
|
||||
Released,
|
||||
ReleasedAt,
|
||||
ReleasedBy,
|
||||
ReleaseReason,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LegalHoldValue {
|
||||
Id(Id),
|
||||
}
|
||||
|
||||
impl Property for LegalHoldProperty {
|
||||
fn try_parse(parent: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
// Keys inside the scope stay plain keys
|
||||
match parent {
|
||||
None => LegalHoldProperty::parse(value),
|
||||
Some(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
LegalHoldProperty::Id => "id",
|
||||
LegalHoldProperty::Name => "name",
|
||||
LegalHoldProperty::Reference => "reference",
|
||||
LegalHoldProperty::Description => "description",
|
||||
LegalHoldProperty::Scope => "scope",
|
||||
LegalHoldProperty::From => "from",
|
||||
LegalHoldProperty::To => "to",
|
||||
LegalHoldProperty::Reason => "reason",
|
||||
LegalHoldProperty::PlacedAt => "placedAt",
|
||||
LegalHoldProperty::PlacedBy => "placedBy",
|
||||
LegalHoldProperty::Released => "released",
|
||||
LegalHoldProperty::ReleasedAt => "releasedAt",
|
||||
LegalHoldProperty::ReleasedBy => "releasedBy",
|
||||
LegalHoldProperty::ReleaseReason => "releaseReason",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl LegalHoldProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => LegalHoldProperty::Id,
|
||||
b"name" => LegalHoldProperty::Name,
|
||||
b"reference" => LegalHoldProperty::Reference,
|
||||
b"description" => LegalHoldProperty::Description,
|
||||
b"scope" => LegalHoldProperty::Scope,
|
||||
b"from" => LegalHoldProperty::From,
|
||||
b"to" => LegalHoldProperty::To,
|
||||
b"reason" => LegalHoldProperty::Reason,
|
||||
b"placedAt" => LegalHoldProperty::PlacedAt,
|
||||
b"placedBy" => LegalHoldProperty::PlacedBy,
|
||||
b"released" => LegalHoldProperty::Released,
|
||||
b"releasedAt" => LegalHoldProperty::ReleasedAt,
|
||||
b"releasedBy" => LegalHoldProperty::ReleasedBy,
|
||||
b"releaseReason" => LegalHoldProperty::ReleaseReason,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LegalHoldProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
LegalHoldProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for LegalHoldValue {
|
||||
type Property = LegalHoldProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
match key {
|
||||
Key::Property(LegalHoldProperty::Id) => Id::from_str(value).ok().map(LegalHoldValue::Id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
LegalHoldValue::Id(id) => id.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The set call's own arguments: why (AU-12).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LegalHoldSetArguments {
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for LegalHoldSetArguments {
|
||||
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 LegalHold {
|
||||
type Property = LegalHoldProperty;
|
||||
|
||||
type Element = LegalHoldValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = LegalHoldSetArguments;
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = LegalHoldProperty::Id;
|
||||
}
|
||||
|
||||
impl From<Id> for LegalHoldValue {
|
||||
fn from(id: Id) -> Self {
|
||||
LegalHoldValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for LegalHoldValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
LegalHoldValue::Id(id) => Some(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
LegalHoldValue::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 = LegalHoldValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for LegalHoldProperty {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ pub mod fastmail_masked_email; // inbuxa: masked email
|
||||
pub mod inbuxa_account_lock; // inbuxa: account lock with delegation
|
||||
pub mod inbuxa_ai_limits; // inbuxa: AI spam classification
|
||||
pub mod inbuxa_audit; // inbuxa: the audit log
|
||||
pub mod inbuxa_legal_hold; // inbuxa: legal hold
|
||||
pub mod inbuxa_explanation; // inbuxa: "Explain this" with the local model
|
||||
pub mod inbuxa_protocol_policy; // inbuxa: legacy protocols off
|
||||
pub mod inbuxa_tenant_protocol_policy; // inbuxa: legacy protocols off, per tenant
|
||||
|
||||
@@ -70,6 +70,9 @@ impl Response<'_> {
|
||||
GetResponseMethod::AccountLock(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::LegalHold(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::ProtocolPolicy(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ impl Response<'_> {
|
||||
GetRequestMethod::AuditEvent(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AuditSettings(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AccountLock(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::LegalHold(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::ProtocolPolicy(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::TenantProtocolPolicy(request) => {
|
||||
request.resolve_references(self)?
|
||||
@@ -111,6 +112,9 @@ impl Response<'_> {
|
||||
SetRequestMethod::AccountLock(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::LegalHold(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::ProtocolPolicy(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ pub enum MethodObject {
|
||||
AuditVerification,
|
||||
// inbuxa: account lock with delegation
|
||||
AccountLock,
|
||||
// inbuxa: legal hold
|
||||
LegalHold,
|
||||
ProtocolPolicy,
|
||||
TenantProtocolPolicy,
|
||||
}
|
||||
@@ -91,7 +93,8 @@ impl MethodObject {
|
||||
| MethodObject::AuditSettings
|
||||
| MethodObject::AuditExport
|
||||
| MethodObject::AuditVerification
|
||||
| MethodObject::AccountLock => Capability::Inbuxa,
|
||||
| MethodObject::AccountLock
|
||||
| MethodObject::LegalHold => Capability::Inbuxa,
|
||||
MethodObject::ProtocolPolicy => Capability::Inbuxa,
|
||||
MethodObject::TenantProtocolPolicy => Capability::Inbuxa,
|
||||
}
|
||||
@@ -279,6 +282,8 @@ impl MethodName {
|
||||
(MethodFunction::Set, MethodObject::AuditExport) => "inbuxa:AuditExport/set",
|
||||
(MethodFunction::Get, MethodObject::AccountLock) => "inbuxa:AccountLock/get",
|
||||
(MethodFunction::Set, MethodObject::AccountLock) => "inbuxa:AccountLock/set",
|
||||
(MethodFunction::Get, MethodObject::LegalHold) => "inbuxa:LegalHold/get",
|
||||
(MethodFunction::Set, MethodObject::LegalHold) => "inbuxa:LegalHold/set",
|
||||
(MethodFunction::Set, MethodObject::AuditVerification) => {
|
||||
"inbuxa:AuditVerification/set"
|
||||
}
|
||||
@@ -423,6 +428,8 @@ impl MethodName {
|
||||
"inbuxa:AuditExport/set" => (MethodObject::AuditExport, MethodFunction::Set),
|
||||
"inbuxa:AccountLock/get" => (MethodObject::AccountLock, MethodFunction::Get),
|
||||
"inbuxa:AccountLock/set" => (MethodObject::AccountLock, MethodFunction::Set),
|
||||
"inbuxa:LegalHold/get" => (MethodObject::LegalHold, MethodFunction::Get),
|
||||
"inbuxa:LegalHold/set" => (MethodObject::LegalHold, MethodFunction::Set),
|
||||
"inbuxa:AuditVerification/set" => (MethodObject::AuditVerification, MethodFunction::Set),
|
||||
"inbuxa:ProtocolPolicy/get" => (MethodObject::ProtocolPolicy, MethodFunction::Get),
|
||||
"inbuxa:ProtocolPolicy/set" => (MethodObject::ProtocolPolicy, MethodFunction::Set),
|
||||
@@ -487,6 +494,7 @@ impl Display for MethodObject {
|
||||
MethodObject::AuditExport => "inbuxa:AuditExport",
|
||||
MethodObject::AuditVerification => "inbuxa:AuditVerification",
|
||||
MethodObject::AccountLock => "inbuxa:AccountLock",
|
||||
MethodObject::LegalHold => "inbuxa:LegalHold",
|
||||
MethodObject::ProtocolPolicy => "inbuxa:ProtocolPolicy",
|
||||
MethodObject::TenantProtocolPolicy => "inbuxa:TenantProtocolPolicy",
|
||||
MethodObject::Registry(obj) => {
|
||||
|
||||
@@ -119,6 +119,7 @@ pub enum GetRequestMethod {
|
||||
AuditEvent(Box<GetRequest<crate::object::inbuxa_audit::AuditEvent>>),
|
||||
AuditSettings(Box<GetRequest<crate::object::inbuxa_audit::AuditSettings>>),
|
||||
AccountLock(Box<GetRequest<crate::object::inbuxa_account_lock::AccountLock>>),
|
||||
LegalHold(Box<GetRequest<crate::object::inbuxa_legal_hold::LegalHold>>),
|
||||
ProtocolPolicy(Box<GetRequest<crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
||||
TenantProtocolPolicy(
|
||||
Box<GetRequest<crate::object::inbuxa_tenant_protocol_policy::TenantProtocolPolicy>>,
|
||||
@@ -151,6 +152,7 @@ pub enum SetRequestMethod<'x> {
|
||||
AuditExport(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditExport>>),
|
||||
AuditVerification(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditVerification>>),
|
||||
AccountLock(Box<SetRequest<'x, crate::object::inbuxa_account_lock::AccountLock>>),
|
||||
LegalHold(Box<SetRequest<'x, crate::object::inbuxa_legal_hold::LegalHold>>),
|
||||
ProtocolPolicy(Box<SetRequest<'x, crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
||||
TenantProtocolPolicy(
|
||||
Box<SetRequest<'x, crate::object::inbuxa_tenant_protocol_policy::TenantProtocolPolicy>>,
|
||||
|
||||
@@ -566,6 +566,21 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
// inbuxa: legal hold
|
||||
(MethodFunction::Get, MethodObject::LegalHold) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::LegalHold(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::LegalHold) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::LegalHold(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
// inbuxa: the audit log
|
||||
(MethodFunction::Get, MethodObject::AuditEvent) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::AuditEvent(value)),
|
||||
|
||||
@@ -106,6 +106,7 @@ pub enum GetResponseMethod {
|
||||
AuditEvent(GetResponse<crate::object::inbuxa_audit::AuditEvent>),
|
||||
AuditSettings(GetResponse<crate::object::inbuxa_audit::AuditSettings>),
|
||||
AccountLock(GetResponse<crate::object::inbuxa_account_lock::AccountLock>),
|
||||
LegalHold(GetResponse<crate::object::inbuxa_legal_hold::LegalHold>),
|
||||
ProtocolPolicy(GetResponse<crate::object::inbuxa_protocol_policy::ProtocolPolicy>),
|
||||
TenantProtocolPolicy(
|
||||
GetResponse<crate::object::inbuxa_tenant_protocol_policy::TenantProtocolPolicy>,
|
||||
@@ -138,6 +139,7 @@ pub enum SetResponseMethod {
|
||||
AuditExport(Box<SetResponse<crate::object::inbuxa_audit::AuditExport>>),
|
||||
AuditVerification(Box<SetResponse<crate::object::inbuxa_audit::AuditVerification>>),
|
||||
AccountLock(Box<SetResponse<crate::object::inbuxa_account_lock::AccountLock>>),
|
||||
LegalHold(Box<SetResponse<crate::object::inbuxa_legal_hold::LegalHold>>),
|
||||
Explanation(Box<SetResponse<crate::object::inbuxa_explanation::Explanation>>),
|
||||
ProtocolPolicy(Box<SetResponse<crate::object::inbuxa_protocol_policy::ProtocolPolicy>>),
|
||||
TenantProtocolPolicy(
|
||||
@@ -765,3 +767,16 @@ impl<'x> From<SetResponse<crate::object::inbuxa_account_lock::AccountLock>> for
|
||||
ResponseMethod::Set(SetResponseMethod::AccountLock(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
// inbuxa: legal hold
|
||||
impl<'x> From<GetResponse<crate::object::inbuxa_legal_hold::LegalHold>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<crate::object::inbuxa_legal_hold::LegalHold>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::LegalHold(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<crate::object::inbuxa_legal_hold::LegalHold>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<crate::object::inbuxa_legal_hold::LegalHold>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::LegalHold(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user