Files
inbuxa-server/crates/jmap/src/registry/set.rs
T
jcoffey-dev 63adb4e2b8
ci / fork-checks (pull_request) Successful in 31s
ci / build (pull_request) Successful in 11m31s
Add the compliance permission and the Compliance Officer roles
Personal-data catalog spec, §7 (settled 2026-09-28).

sysComplianceGet (673) sees the data inventory and compliance
overview: superusers and, for their tenant's slice, tenant
administrators, by default and through the one-time grants on servers
that already have their roles stored.

A Compliance Officer role at server level holds it with reading and
exporting the audit log, placing, widening, releasing and exporting
legal holds, seeing account locks, and reading accounts, lists,
domains, tenants and roles. It changes no server setting, creates or
deletes no account, and can't shorten audit retention.

A tenant's accounts can hold only roles of their own tenant (MT-3), so
the tenant role is one "Compliance Officer" role per tenant, without
holds (LH-13): made once for every tenant a server has, and whenever a
tenant is created. While nobody holds it, it is removed with its tenant
so it doesn't block the delete, and put back if the delete is refused
for another reason. Both roles carry a user's own permissions too,
since roles given to a person replace the default user role, which a
tenant's accounts can't hold anyway.

Every server makes these once, new or existing -- the built-in roles
are only made on a server with none -- and records each under P c, so a
role an administrator deletes stays deleted.

Tested: unit tests (neither role changes a setting beyond a user's
own; holds for the server's officer only; per-place records); a new
compliance system test (one server-level role; an officer reads the
audit log, places and releases a hold, and is refused a setting, an
account and audit retention; a tenant gets its role, whose holder reads
the tenant's audit log and no holds; a tenant with an unused role is
deleted and the role goes with it); the system, audit, legal hold,
account lock and SCIM suites; fork checks. The directory suite needs
its LDAP container and wasn't run here.
2026-09-28 08:50:17 -07:00

1161 lines
50 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use crate::registry::{
EnterpriseRegistry,
mapping::{
ObjectResponse, RegistrySetResponse,
account::account_set,
action::action_set,
bootstrap::bootstrap_set,
dkim::validate_dkim_signature,
domain::{validate_dns_server, validate_domain},
map_bootstrap_error,
principal::{
AccountUpdate, schedule_account_destruction, validate_account, validate_role,
validate_tenant_quota,
},
public_key::validate_public_key,
queued_message::queued_message_set,
report::report_set,
sieve::validate_sieve_script,
spam_sample::spam_sample_set,
task::task_set,
tls::{validate_acme_provider, validate_certificate},
},
};
use common::{
Server, auth::AccessToken, cache::invalidate::CacheInvalidationBuilder,
expr::if_block::BootstrapExprExt, ipc::CacheInvalidation,
manager::application::WebApplicationManager,
};
use directory::core::secret::{hash_secret, is_password_hash};
use http_proto::HttpSessionData;
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{SetRequest, SetResponse, SettingsReload},
object::registry::Registry,
references::resolve::ResolveCreatedReference,
request::{IntoValid, MaybeInvalid},
};
use jmap_tools::{JsonPointer, JsonPointerItem, Key};
use registry::{
jmap::{JmapValue, JsonPointerPatch, MaybeUnpatched, RegistryValue},
schema::{
enums::{Permission, TenantStorageQuota},
prelude::{
OBJ_FILTER_ACCOUNT, OBJ_FILTER_TENANT, OBJ_SINGLETON, Object, ObjectInner, ObjectType,
Property,
},
structs::{
Certificate, DkimSignature, DnsServer, Domain, PublicKey, Role, SieveSystemScript,
SieveUserScript, Task,
},
},
types::id::ObjectId,
};
use std::borrow::Cow;
use store::{
registry::{
bootstrap::Bootstrap,
write::{RegistryWrite, RegistryWriteResult},
},
write::BatchBuilder,
};
use trc::AddContext;
use types::id::Id;
use utils::map::vec_map::VecMap;
pub trait RegistrySet: Sync + Send {
fn registry_set(
&self,
object_type: ObjectType,
request: SetRequest<'_, Registry>,
access_token: &AccessToken,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<SetResponse<Registry>>> + Send;
}
#[allow(clippy::large_enum_variant)]
enum Modification {
Create {
client_id: String,
object: Option<Object>,
},
Update {
id: Id,
object: Object,
},
}
impl RegistrySet for Server {
async fn registry_set(
&self,
object_type: ObjectType,
mut request: SetRequest<'_, Registry>,
access_token: &AccessToken,
session: &HttpSessionData,
) -> trc::Result<SetResponse<Registry>> {
// Initial assertions
if self.registry().is_bootstrap_mode() && !matches!(object_type, ObjectType::Bootstrap) {
return Err(trc::JmapEvent::Forbidden.into_err().details(concat!(
"The server is in bootstrap mode. Only the 'Bootstrap' object type ",
"can be modified until the bootstrap process is complete.",
)));
}
self.assert_enterprise_object(object_type)?;
let object_flags = object_type.flags();
let is_singleton = (object_flags & OBJ_SINGLETON) != 0;
let has_account_id = (object_flags & OBJ_FILTER_ACCOUNT) != 0;
let is_tenant_filtered =
(object_flags & OBJ_FILTER_TENANT) != 0 && access_token.tenant_id().is_some();
let can_set_tenant = access_token.tenant_id().is_none();
let can_set_account = access_token.has_permission(Permission::Impersonate);
let is_account_filtered = has_account_id && !can_set_account;
// Build response
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
// Initial create validation for singletons
let create = request.unwrap_create();
// Initial destroy validation for singletons
let mut destroy = request.unwrap_destroy().into_valid().collect::<Vec<_>>();
if is_singleton && !destroy.is_empty() {
response.not_destroyed.extend(
destroy
.drain(..)
.map(|id| (MaybeInvalid::Value(id), SetError::singleton())),
);
}
// Update validation for willDestroy
let update = request
.unwrap_update()
.into_valid()
.filter_map(|(id, value)| {
if is_singleton {
if id.is_singleton() {
Some((id, value))
} else {
response.not_updated.append(id, SetError::not_found());
None
}
} else if !destroy.contains(&id) {
Some((id, value))
} else {
response.not_updated.append(id, SetError::will_destroy());
None
}
})
.collect::<Vec<_>>();
let mut set = RegistrySetResponse {
access_token,
server: self,
remote_ip: session.remote_ip,
account_id: request.account_id.document_id(),
object_type,
response,
is_tenant_filtered,
is_account_filtered,
create,
update,
destroy,
};
let result = match object_type {
ObjectType::AddressBook
| ObjectType::Asn
| ObjectType::Authentication
| ObjectType::BlobStore
| ObjectType::Cache
| ObjectType::Calendar
| ObjectType::CalendarAlarm
| ObjectType::CalendarScheduling
| ObjectType::Coordinator
| ObjectType::DataRetention
| ObjectType::DataStore
| ObjectType::DkimReportSettings
| ObjectType::DmarcReportSettings
| ObjectType::DnsResolver
| ObjectType::Email
| ObjectType::Enterprise
| ObjectType::FileStorage
| ObjectType::Http
| ObjectType::HttpForm
| ObjectType::Imap
| ObjectType::InMemoryStore
| ObjectType::Jmap
| ObjectType::SystemSettings
| ObjectType::Metrics
| ObjectType::MetricsStore
| ObjectType::MtaConnectionStrategy
| ObjectType::MtaExtensions
| ObjectType::MtaInboundSession
| ObjectType::MtaOutboundStrategy
| ObjectType::MtaOutboundThrottle
| ObjectType::MtaStageAuth
| ObjectType::MtaStageConnect
| ObjectType::MtaStageData
| ObjectType::MtaStageEhlo
| ObjectType::MtaStageMail
| ObjectType::MtaStageRcpt
| ObjectType::MtaSts
| ObjectType::OidcProvider
| ObjectType::ReportSettings
| ObjectType::Search
| ObjectType::SearchStore
| ObjectType::Security
| ObjectType::SenderAuth
| ObjectType::Sharing
| ObjectType::SieveSystemInterpreter
| ObjectType::SieveUserInterpreter
| ObjectType::SpamClassifier
| ObjectType::SpamDnsblSettings
| ObjectType::SpamLlm
| ObjectType::SpamPyzor
| ObjectType::SpamSettings
| ObjectType::SpfReportSettings
| ObjectType::TaskManager
| ObjectType::TlsReportSettings
| ObjectType::TracingStore
| ObjectType::WebDav
| ObjectType::DsnReportSettings
| ObjectType::AcmeProvider
| ObjectType::AiModel
| ObjectType::Alert
| ObjectType::AllowedIp
| ObjectType::Application
| ObjectType::BlockedIp
| ObjectType::Certificate
| ObjectType::Directory
| ObjectType::DnsServer
| ObjectType::EventTracingLevel
| ObjectType::HttpLookup
| ObjectType::MemoryLookupKey
| ObjectType::MemoryLookupKeyValue
| ObjectType::MtaVirtualQueue
| ObjectType::MtaQueueQuota
| ObjectType::MtaRoute
| ObjectType::MtaDeliverySchedule
| ObjectType::MtaInboundThrottle
| ObjectType::MtaTlsStrategy
| ObjectType::MtaMilter
| ObjectType::MtaHook
| ObjectType::NetworkListener
| ObjectType::ClusterRole
| ObjectType::SieveSystemScript
| ObjectType::SieveUserScript
| ObjectType::SpamDnsblServer
| ObjectType::SpamFileExtension
| ObjectType::SpamRule
| ObjectType::SpamTag
| ObjectType::StoreLookup
| ObjectType::Tracer
| ObjectType::WebHook
| ObjectType::PublicKey
| ObjectType::DkimSignature
| ObjectType::MaskedEmail
| ObjectType::Account
| ObjectType::MailingList
| ObjectType::OAuthClient
| ObjectType::Role
| ObjectType::Tenant
| ObjectType::Domain => {
// Bundle modifications together
let mut modifications = Vec::with_capacity(set.create.len() + set.update.len());
for (id, value) in set.create.drain() {
if is_singleton
&& let Some(object) = self
.registry()
.get(ObjectId::new(object_type, Id::singleton()))
.await
.caused_by(trc::location!())?
{
modifications.push((
Modification::Create {
client_id: id,
object: Some(object),
},
value,
Object::from(set.object_type),
));
} else {
modifications.push((
Modification::Create {
client_id: id,
object: None,
},
value,
Object::from(set.object_type),
));
}
}
for (id, value) in set.update.drain(..) {
if let Some(object) = self
.registry()
.get(ObjectId::new(object_type, id))
.await
.caused_by(trc::location!())?
{
if (is_tenant_filtered
&& access_token.tenant_id().map(Id::from)
!= object.inner.member_tenant_id())
|| (is_account_filtered
&& object.inner.account_id() != Some(Id::from(set.account_id)))
{
set.response.not_updated.append(id, SetError::not_found());
continue;
}
modifications.push((
Modification::Update {
id,
object: object.clone(),
},
value,
object,
));
} else if is_singleton {
modifications.push((
Modification::Update {
id,
object: Object::from(set.object_type),
},
value,
Object::from(set.object_type),
));
} else {
set.response.not_updated.append(id, SetError::not_found());
}
}
// Process modifications
let mut cache_invalidator = CacheInvalidationBuilder::default();
'outer: for (modification, mut value, mut new_object) in modifications {
// Initial validations
let is_create = matches!(modification, Modification::Create { .. });
let mut unpatched_properties = VecMap::new();
if let Err(err) = set.response.resolve_self_references(&mut value, 0, true) {
set.failed(modification, err);
continue 'outer;
};
if is_create
|| value
.as_object()
.unwrap()
.get(&Key::Property(Property::Type))
.and_then(|v| v.as_str())
.is_some_and(|t| new_object.object_variant().is_some_and(|v| v != t))
{
// Patch object
match new_object.patch(
JsonPointerPatch::new(&JsonPointer::new(vec![]))
.with_create(true)
.with_can_set_tenant(can_set_tenant)
.with_can_set_account(can_set_account),
value,
) {
Ok(MaybeUnpatched::Patched) => {}
Ok(MaybeUnpatched::Unpatched { property, value }) => {
unpatched_properties.append(property, value);
}
Ok(MaybeUnpatched::UnpatchedMany { properties }) => {
unpatched_properties = properties;
}
Err(err) => {
set.failed(modification, err.into());
continue 'outer;
}
}
// Add tenantId for tenant filtered objects
if is_tenant_filtered && let Some(tenant_id) = set.access_token.tenant_id()
{
new_object.inner.set_member_tenant_id(tenant_id.into());
}
// inbuxa: MT-7: a principal takes its domain's tenant
if can_set_tenant {
inbuxa_features::tenancy::writes::default_tenant(
self.registry(),
&mut new_object,
)
.await?;
}
// Add accountId
if has_account_id {
new_object.inner.set_account_id(set.account_id.into());
}
} else {
for (key, value) in value.into_expanded_object() {
let ptr = match key {
Key::Property(Property::Type) => {
continue;
}
Key::Property(prop) => {
JsonPointer::new(vec![JsonPointerItem::Key(Key::Property(
prop,
))])
}
Key::Borrowed(other) => JsonPointer::parse(other),
Key::Owned(other) => JsonPointer::parse(&other),
};
// Patch object
match new_object.patch(
JsonPointerPatch::new(&ptr)
.with_create(false)
.with_can_set_tenant(can_set_tenant)
.with_can_set_account(can_set_account),
value,
) {
Ok(MaybeUnpatched::Patched) => {}
Ok(MaybeUnpatched::Unpatched { property, value }) => {
unpatched_properties.append(property, value);
}
Ok(MaybeUnpatched::UnpatchedMany { properties }) => {
if unpatched_properties.is_empty() {
unpatched_properties = properties;
} else {
unpatched_properties.extend(properties);
}
}
Err(err) => {
set.failed(modification, err.into());
continue 'outer;
}
}
}
}
// Validate objects
let mut tasks = Vec::new();
let result = match &mut new_object.inner {
ObjectInner::Account(account) => {
validate_account(self, access_token, account, modification.as_account())
.await?
}
ObjectInner::Role(role) => {
validate_role(self, access_token, role, modification.as_role()).await?
}
ObjectInner::PublicKey(key) => {
validate_public_key(&set, key, modification.as_public_key()).await?
}
ObjectInner::DkimSignature(key) => {
validate_dkim_signature(&set, key, modification.as_dkim_signature())
.await?
}
ObjectInner::Domain(domain) => {
validate_domain(&set, domain, modification.as_domain(), &mut tasks)
.await?
}
ObjectInner::DnsServer(dns) => {
validate_dns_server(&set, dns, modification.as_dns_server()).await?
}
ObjectInner::MailingList(_) if is_create => {
validate_tenant_quota(
self,
access_token,
TenantStorageQuota::MaxMailingLists,
)
.await?
}
ObjectInner::OAuthClient(client) => {
if let Some(secret) = client.secret.as_mut()
&& !secret.is_empty()
&& !(matches!(secret.as_bytes().first(), Some(&b'$' | &b'{'))
&& is_password_hash(secret))
{
*secret = hash_secret(
set.server.core.network.security.password_hash_algorithm,
std::mem::take(secret).into_bytes(),
)
.await
.caused_by(trc::location!())?;
}
if is_create {
validate_tenant_quota(
self,
access_token,
TenantStorageQuota::MaxOauthClients,
)
.await?
} else {
Ok(ObjectResponse::default())
}
}
ObjectInner::Directory(_) if is_create => {
validate_tenant_quota(
self,
access_token,
TenantStorageQuota::MaxDirectories,
)
.await?
}
// inbuxa: legacy-protocols LP-4
ObjectInner::NetworkListener(listener) => {
crate::inbuxa::protocol_policy::validate_listener(&set, listener)
.await?
}
// inbuxa: ME-12 to ME-17
ObjectInner::MaskedEmail(mask) => {
let old = match &modification {
Modification::Update { object, .. } => match &object.inner {
ObjectInner::MaskedEmail(old) => Some(old),
_ => None,
},
Modification::Create { .. } => None,
};
crate::inbuxa::masked_email::validate(
&set,
mask,
old,
unpatched_properties,
)
.await?
}
ObjectInner::AcmeProvider(provider) if is_create => {
validate_acme_provider(&set, provider, unpatched_properties).await?
}
ObjectInner::Certificate(cert) => {
validate_certificate(cert, modification.as_certificate()).await?
}
ObjectInner::SieveUserScript(SieveUserScript { contents, .. }) => {
validate_sieve_script(
set.server,
contents,
modification.as_sieve_script(),
false,
)
.await?
}
ObjectInner::SieveSystemScript(SieveSystemScript { contents, .. }) => {
validate_sieve_script(
set.server,
contents,
modification.as_sieve_script(),
true,
)
.await?
}
_ => Ok(ObjectResponse::default()),
};
let mut response = match result {
Ok(response) => response,
Err(err) => {
set.failed(modification, err);
continue 'outer;
}
};
// inbuxa: MT-3, MT-8, MT-17
let (stored_id, stored) = match &modification {
Modification::Update { id, object } => (Some(*id), Some(object)),
Modification::Create { .. } => (None, None),
};
let after_save = match inbuxa_features::tenancy::writes::check(
self.registry(),
stored_id,
stored,
&new_object,
)
.await?
{
Ok(after_save) => after_save,
Err(err) => {
set.failed(modification, err);
continue 'outer;
}
};
// inbuxa: BT-3, BT-15, BT-22: logos and templates follow their rules
let before = match &modification {
Modification::Update { object, .. }
| Modification::Create {
object: Some(object),
..
} => Some(object),
Modification::Create { object: None, .. } => None,
};
if let Err(err) =
inbuxa_features::branding::writes::check(before, &new_object)
{
set.failed(modification, err);
continue 'outer;
}
// inbuxa: AI-12, AI-18: the classifier and its models follow their rules
match inbuxa_features::ai::writes::check(self.registry(), &new_object).await? {
Ok(()) => {}
Err(err) => {
set.failed(modification, err);
continue 'outer;
}
}
// inbuxa: UD-16: a kept account's addresses stay its own
if let Some(err) =
crate::inbuxa::deleted_account::reserved(self, stored, &new_object).await?
{
set.failed(modification, err);
continue 'outer;
}
// Validate expressions
// inbuxa: MON-25: an alert condition may name metrics with underscores
let alert_condition = match &new_object.inner {
ObjectInner::Alert(alert) => Some(
common::telemetry::alerts::rewrite_condition(&alert.condition),
),
_ => None,
};
let expressions = match (&new_object.inner, &alert_condition) {
(ObjectInner::Alert(alert), Some(condition)) => {
Some(vec![registry::schema::prelude::ExpressionContext {
expr: condition,
..alert.ctx_condition()
}])
}
_ => new_object.inner.expression_ctxs(),
};
if let Some(expressions) = expressions {
let mut bp = Bootstrap::new_uninitialized(self.registry().clone());
for expression in expressions {
bp.compile_expr(ObjectId::new(object_type, 0u64.into()), &expression);
if !bp.errors.is_empty() {
set.failed(
modification,
map_bootstrap_error(bp.errors)
.with_object_id_opt(None)
.with_property(expression.property),
);
continue 'outer;
}
}
}
// Save object
let result = match &modification {
Modification::Create { client_id, object } => {
if let Some(object) = object {
if object.inner != new_object.inner {
self.registry()
.write(RegistryWrite::update(
Id::singleton(),
&new_object,
object,
))
.await?
} else {
set.response.created(client_id.to_string(), Id::singleton());
continue;
}
} else {
self.registry()
.write(RegistryWrite::Insert {
object: &new_object,
id: response.id,
})
.await?
}
}
Modification::Update { id, object } => {
if object.inner != new_object.inner {
if !(is_singleton && object.revision == 0) {
self.registry()
.write(RegistryWrite::update(*id, &new_object, object))
.await?
} else {
self.registry()
.write(RegistryWrite::insert(&new_object))
.await?
}
} else {
set.response.updated.append(*id, None);
continue;
}
}
};
let object_id = match (modification, result) {
(Modification::Update { id, object }, RegistryWriteResult::Success(_)) => {
cache_invalidator.process_update(id, &object, &new_object);
// inbuxa: AI-2: content leaving the network is flagged
if let ObjectInner::AiModel(model) = &new_object.inner {
self.ai_warn_if_remote(model).await;
}
// inbuxa: MT-8: what moves with a domain follows it
for (id, old, new) in inbuxa_features::tenancy::writes::after_save(
&self.core.storage.data,
self.registry(),
after_save,
)
.await?
{
cache_invalidator.process_update(id, &old, &new);
}
// inbuxa: ME-1, ME-2
if let (ObjectInner::MaskedEmail(old), ObjectInner::MaskedEmail(new)) =
(&object.inner, &new_object.inner)
{
crate::inbuxa::masked_email::updated(self, id, old, new).await?;
}
if let (
ObjectInner::Application(previous),
ObjectInner::Application(updated),
) = (&object.inner, &new_object.inner)
&& previous.resource_url != updated.resource_url
&& let Err(err) =
WebApplicationManager::delete_bundle(self, id).await
{
trc::error!(
err.details("Failed to delete cached application bundle")
);
}
set.response.updated.append(
id,
if !response.object.is_empty() {
Some(JmapValue::Object(response.object))
} else {
None
},
);
Some(id)
}
(
Modification::Create { client_id, .. },
RegistryWriteResult::Success(id),
) => {
cache_invalidator.process_create(&new_object);
// inbuxa: AI-2: content leaving the network is flagged
if let ObjectInner::AiModel(model) = &new_object.inner {
self.ai_warn_if_remote(model).await;
}
// inbuxa: ME-7a
if let ObjectInner::MaskedEmail(mask) = &new_object.inner {
crate::inbuxa::masked_email::created(self, id, mask).await?;
}
// inbuxa: personal-data catalog: a new tenant gets its
// Compliance Officer role
if matches!(new_object.inner, ObjectInner::Tenant(_)) {
common::manager::compliance_roles::tenant_created(
self.registry(),
&self.core.storage.data,
id,
)
.await?;
}
response.object.insert(Property::Id, RegistryValue::Id(id));
set.response
.created
.insert(client_id, JmapValue::Object(response.object));
Some(id)
}
(Modification::Update { id, .. }, err) => {
set.response.not_updated.append(id, map_write_error(err));
None
}
(Modification::Create { client_id, .. }, err) => {
set.response
.not_created
.append(client_id, map_write_error(err));
None
}
};
// Dispatch tasks
if !tasks.is_empty()
&& let Some(object_id) = object_id
{
let mut batch = BatchBuilder::new();
for mut task in tasks.drain(..) {
match &mut task {
Task::AcmeRenewal(task) => task.domain_id = object_id,
Task::DkimManagement(task) => task.domain_id = object_id,
Task::DnsManagement(task) => task.domain_id = object_id,
_ => unreachable!(),
}
batch.schedule_task(task);
}
set.server.store().write(batch.build_all()).await?;
set.server.notify_task_queue();
}
}
// Process destroy
for id in set.destroy.drain(..) {
let object_id = ObjectId::new(object_type, id);
if let Some(object) = self
.registry()
.get(object_id)
.await
.caused_by(trc::location!())?
.filter(|object| {
!((is_tenant_filtered
&& access_token.tenant_id().map(Id::from)
!= object.inner.member_tenant_id())
|| (is_account_filtered
&& object.inner.account_id() != Some(Id::from(set.account_id))))
})
{
// inbuxa: personal-data catalog: a tenant's compliance
// role, while nobody holds it, goes first
let role_released = matches!(object.inner, ObjectInner::Tenant(_))
&& common::manager::compliance_roles::tenant_deleting(
self.registry(),
&self.core.storage.data,
id,
)
.await?;
match self
.registry()
.write(RegistryWrite::Delete {
object_id,
object: Some(&object),
allowed_orphan_types: if object_type == ObjectType::Account {
&[ObjectType::PublicKey, ObjectType::MaskedEmail]
} else {
&[]
},
})
.await?
{
RegistryWriteResult::Success(_) => {
// inbuxa: UD-15, UD-17a: kept for its period, shares suspended
if let ObjectInner::Account(account) = &object.inner
&& let Some(others) =
crate::inbuxa::deleted_account::keep(self, id, account)
.await?
{
for other in others {
cache_invalidator
.invalidate(CacheInvalidation::AccessToken(other));
}
} else if let ObjectInner::Account(account) = &object.inner {
for sharee_id in self
.store()
.acl_revoke_all(id.document_id())
.await
.caused_by(trc::location!())?
{
cache_invalidator
.invalidate(CacheInvalidation::AccessToken(sharee_id));
}
schedule_account_destruction(set.server, id, account).await?;
}
if matches!(object.inner, ObjectInner::Application(_))
&& let Err(err) =
WebApplicationManager::delete_bundle(self, id).await
{
trc::error!(
err.details("Failed to delete cached application bundle")
);
}
// inbuxa: ME-3
if let ObjectInner::MaskedEmail(mask) = &object.inner {
crate::inbuxa::masked_email::destroyed(self, id, mask).await?;
}
// inbuxa: legacy-protocols, a tenant's switch goes with it
if matches!(object.inner, ObjectInner::Tenant(_)) {
inbuxa_features::security::tenant_protocol_policy::remove(
&self.core.storage.data,
id.document_id(),
)
.await?;
}
cache_invalidator.process_delete(id, &object);
set.response.destroyed.push(id);
}
err => {
// inbuxa: refused for another reason: the role comes back
if role_released {
common::manager::compliance_roles::tenant_kept(
self.registry(),
&self.core.storage.data,
id,
)
.await?;
}
set.response.not_destroyed.append(id, map_write_error(err));
}
}
} else {
set.response.not_destroyed.append(id, SetError::not_found());
}
}
// Finalize cache invalidation
self.invalidate_caches(cache_invalidator).await?;
Ok(set.into_response())
}
ObjectType::ArfExternalReport
| ObjectType::DmarcExternalReport
| ObjectType::TlsExternalReport
| ObjectType::DmarcInternalReport
| ObjectType::TlsInternalReport => report_set(set).await.map(|set| set.into_response()),
ObjectType::SpamTrainingSample => {
spam_sample_set(set).await.map(|set| set.into_response())
}
ObjectType::AccountSettings
| ObjectType::ApiKey
| ObjectType::AccountPassword
| ObjectType::AppPassword => Box::pin(account_set(set))
.await
.map(|set| set.into_response()),
ObjectType::QueuedMessage => {
queued_message_set(set).await.map(|set| set.into_response())
}
ObjectType::Task => task_set(set).await.map(|set| set.into_response()),
// inbuxa: undelete (UD-8, UD-12)
ObjectType::ArchivedItem => crate::inbuxa::undelete::set(set)
.await
.map(|set| set.into_response()),
ObjectType::Action => Box::pin(action_set(set))
.await
.map(|set| set.into_response()),
ObjectType::Bootstrap => Box::pin(bootstrap_set(set))
.await
.map(|set| set.into_response()),
// inbuxa: MON-32: a trace can be destroyed, never created or changed
ObjectType::Trace => crate::inbuxa::telemetry::trace_set(set)
.await
.map(|set| set.into_response()),
ObjectType::Log | ObjectType::Metric | ObjectType::ClusterNode => {
set.fail_all_create("Telemetry objects cannot be created");
set.fail_all_update("Telemetry objects cannot be modified");
set.fail_all_destroy("Telemetry objects cannot be deleted");
Ok(set.into_response())
}
#[allow(unreachable_patterns)] // inbuxa: ArchivedItem was the last one
_ => {
set.fail_all_create("Enterprise objects cannot be created");
set.fail_all_update("Enterprise objects cannot be modified");
set.fail_all_destroy("Enterprise objects cannot be deleted");
Ok(set.into_response())
}
};
// inbuxa: a write to an object the running settings are built from
// applies at once, here and on every node (DIR-17 did this for
// directories and the server default; now it covers every such object)
let mut result = result;
if let Ok(response) = &mut result
&& (!response.created.is_empty()
|| !response.updated.is_empty()
|| !response.destroyed.is_empty())
&& let Some(reload) = self.reload_after_write(object_type).await
{
response.settings_reload = Some(match reload {
Ok(()) => SettingsReload {
applied: true,
description: None,
},
Err(reason) => SettingsReload {
applied: false,
description: Some(format!(
"Saved, but the running settings were not reloaded. {reason}"
)),
},
});
}
result
}
}
impl RegistrySetResponse<'_> {
fn failed(&mut self, modification: Modification, error: SetError<Property>) {
match modification {
Modification::Create { client_id, .. } => {
self.response.not_created.append(client_id, error)
}
Modification::Update { id, .. } => self.response.not_updated.append(id, error),
}
}
pub fn fail_all(&mut self, error: SetError<Property>) {
for (client_id, _) in self.create.drain() {
self.response.not_created.append(client_id, error.clone());
}
for (id, _) in self.update.drain(..) {
self.response.not_updated.append(id, error.clone());
}
for id in self.destroy.drain(..) {
self.response.not_destroyed.append(id, error.clone());
}
}
pub fn fail_all_create(&mut self, error: impl Into<Cow<'static, str>>) {
let error = error.into();
for (client_id, _) in self.create.drain() {
self.response.not_created.append(
client_id,
SetError::forbidden().with_description(error.clone()),
);
}
}
pub fn fail_all_update(&mut self, error: impl Into<Cow<'static, str>>) {
let error = error.into();
for (id, _) in self.update.drain(..) {
self.response
.not_updated
.append(id, SetError::forbidden().with_description(error.clone()));
}
}
pub fn fail_all_destroy(&mut self, error: impl Into<Cow<'static, str>>) {
let error = error.into();
for id in self.destroy.drain(..) {
self.response
.not_destroyed
.append(id, SetError::forbidden().with_description(error.clone()));
}
}
fn into_response(self) -> SetResponse<Registry> {
self.response
}
}
impl Modification {
fn as_account(&self) -> AccountUpdate<'_> {
match self {
Modification::Create { client_id, .. } => AccountUpdate::Create(client_id),
Modification::Update { object, .. } => match &object.inner {
ObjectInner::Account(account) => AccountUpdate::Update(account),
_ => unreachable!(),
},
}
}
fn as_role(&self) -> Option<&Role> {
match self {
Modification::Create { .. } => None,
Modification::Update { object, .. } => match &object.inner {
ObjectInner::Role(role) => Some(role),
_ => None,
},
}
}
fn as_public_key(&self) -> Option<&PublicKey> {
match self {
Modification::Create { .. } => None,
Modification::Update { object, .. } => match &object.inner {
ObjectInner::PublicKey(key) => Some(key),
_ => None,
},
}
}
fn as_dkim_signature(&self) -> Option<&DkimSignature> {
match self {
Modification::Create { .. } => None,
Modification::Update { object, .. } => match &object.inner {
ObjectInner::DkimSignature(key) => Some(key),
_ => None,
},
}
}
fn as_domain(&self) -> Option<&Domain> {
match self {
Modification::Create { .. } => None,
Modification::Update { object, .. } => match &object.inner {
ObjectInner::Domain(domain) => Some(domain),
_ => None,
},
}
}
fn as_dns_server(&self) -> Option<&DnsServer> {
match self {
Modification::Create { .. } => None,
Modification::Update { object, .. } => match &object.inner {
ObjectInner::DnsServer(dns) => Some(dns),
_ => None,
},
}
}
fn as_certificate(&self) -> Option<&Certificate> {
match self {
Modification::Create { .. } => None,
Modification::Update { object, .. } => match &object.inner {
ObjectInner::Certificate(cert) => Some(cert),
_ => None,
},
}
}
fn as_sieve_script(&self) -> Option<&str> {
match self {
Modification::Create { .. } => None,
Modification::Update { object, .. } => match &object.inner {
ObjectInner::SieveUserScript(SieveUserScript { contents, .. })
| ObjectInner::SieveSystemScript(SieveSystemScript { contents, .. }) => {
Some(contents.as_str())
}
_ => None,
},
}
}
}
pub(crate) fn map_write_error(err: RegistryWriteResult) -> SetError<Property> {
match err {
RegistryWriteResult::CannotDeleteLinked {
object_id,
linked_objects,
} => SetError::new(SetErrorType::ObjectIsLinked)
.with_object_id(object_id)
.with_linked_objects(linked_objects),
RegistryWriteResult::InvalidSingletonId => SetError::invalid_properties()
.with_property(Property::Id)
.with_description("Invalid singleton id"),
RegistryWriteResult::CannotDeleteSingleton => {
SetError::forbidden().with_description("Singleton objects cannot be deleted")
}
RegistryWriteResult::InvalidForeignKey { object_id } => {
SetError::new(SetErrorType::InvalidForeignKey).with_object_id(object_id)
}
RegistryWriteResult::PrimaryKeyConflict {
property,
existing_id,
} => SetError::new(SetErrorType::PrimaryKeyViolation)
.with_property(property)
.with_object_id(existing_id),
RegistryWriteResult::ValidationError { errors } => {
SetError::new(SetErrorType::ValidationFailed).with_validation_errors(errors)
}
RegistryWriteResult::NotSupported => SetError::forbidden()
.with_description("The requested action is not supported by the registry store"),
RegistryWriteResult::NotFound { .. } => SetError::not_found(),
RegistryWriteResult::Success(_) => unreachable!(),
}
}