Upstream commit: 3f657330c0f49a015a3a372fb59669b5cccbca6d Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 118 in 50 files Dangling module declarations removed: 5 Edits turning enterprise off: 25 Third-party code: 14 files, 0 not in THIRD-PARTY.md Renamed identifiers: 67 in 19 files Verification: clean The same Enterprise footprint as v0.16.24. The build check is clean apart from the expected errors in the rebuilt-feature tests. A bug-fix release: DKIM rotation, IMAP failed-login answers, DNSBL multi-code scoring and negative TTLs, Pyzor on short messages, queue quotas with an empty match, RocksDB info-log rotation, and Autodiscover schema handling.
229 lines
7.6 KiB
Rust
229 lines
7.6 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::registry::mapping::{
|
|
ObjectResponse, RegistrySetResponse, ValidationResult, principal::validate_tenant_quota,
|
|
};
|
|
use common::network::{dkim::generate_dkim_selector, dns::update::DnsUpdater};
|
|
use jmap_proto::error::set::{SetError, SetErrorType};
|
|
use registry::{
|
|
schema::{
|
|
enums::{AcmeChallengeType, DkimSignatureType, DnsRecordType, TenantStorageQuota},
|
|
prelude::{ObjectType, Property},
|
|
structs::{
|
|
AcmeProvider, CertificateManagement, DkimManagement, DkimManagementProperties,
|
|
DnsManagement, DnsServer, Domain, Task, TaskDnsManagement, TaskDomainManagement,
|
|
TaskStatus,
|
|
},
|
|
},
|
|
types::map::Map,
|
|
};
|
|
use types::id::Id;
|
|
|
|
pub(crate) async fn validate_domain(
|
|
set: &RegistrySetResponse<'_>,
|
|
domain: &mut Domain,
|
|
old_domain: Option<&Domain>,
|
|
tasks: &mut Vec<Task>,
|
|
) -> ValidationResult {
|
|
let response = if old_domain.is_none() {
|
|
match validate_tenant_quota(set.server, set.access_token, TenantStorageQuota::MaxDomains)
|
|
.await?
|
|
{
|
|
Ok(response) => response,
|
|
Err(err) => {
|
|
return Ok(Err(err));
|
|
}
|
|
}
|
|
} else {
|
|
ObjectResponse::default()
|
|
};
|
|
|
|
// Validate DKIM selector template
|
|
if let DkimManagement::Automatic(DkimManagementProperties {
|
|
selector_template, ..
|
|
}) = &domain.dkim_management
|
|
&& old_domain.is_none_or(|old| {
|
|
matches!(
|
|
&old.dkim_management,
|
|
DkimManagement::Automatic(DkimManagementProperties {
|
|
selector_template: old_selector_template,
|
|
..
|
|
}) if old_selector_template != selector_template
|
|
)
|
|
})
|
|
&& let Err(err) =
|
|
generate_dkim_selector(selector_template, DkimSignatureType::Dkim1RsaSha256)
|
|
{
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(Property::SelectorTemplate)
|
|
.with_description(err)));
|
|
}
|
|
|
|
// Validate that names and aliases do not collide with another domain
|
|
let registry = set.server.registry();
|
|
if old_domain.is_none_or(|old| old.name != domain.name)
|
|
&& let Some(existing) = registry
|
|
.primary_key(
|
|
ObjectType::Domain.into(),
|
|
Property::Aliases,
|
|
domain.name.as_bytes().to_vec(),
|
|
)
|
|
.await?
|
|
{
|
|
return Ok(Err(SetError::new(SetErrorType::PrimaryKeyViolation)
|
|
.with_property(Property::Name)
|
|
.with_object_id(existing)));
|
|
}
|
|
|
|
for alias in domain.aliases.iter() {
|
|
if alias == &domain.name
|
|
|| old_domain.is_some_and(|old| old.aliases.contains(alias) || &old.name == alias)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for index in [Property::Name, Property::Aliases] {
|
|
if let Some(existing) = registry
|
|
.primary_key(ObjectType::Domain.into(), index, alias.as_bytes().to_vec())
|
|
.await?
|
|
{
|
|
return Ok(Err(SetError::new(SetErrorType::PrimaryKeyViolation)
|
|
.with_property(Property::Aliases)
|
|
.with_object_id(existing)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Schedule DNS update task
|
|
let will_trigger_dkim = matches!(domain.dkim_management, DkimManagement::Automatic(_))
|
|
&& old_domain
|
|
.is_none_or(|old| !matches!(old.dkim_management, DkimManagement::Automatic(_)));
|
|
let will_schedule_dkim = !will_trigger_dkim
|
|
&& matches!(domain.dkim_management, DkimManagement::Automatic(_))
|
|
&& publishes_dkim(domain)
|
|
&& old_domain.is_some_and(|old| !publishes_dkim(old));
|
|
let will_trigger_acme = if let DnsManagement::Automatic(details) = &domain.dns_management
|
|
&& old_domain.is_none_or(|old| !matches!(old.dns_management, DnsManagement::Automatic(_)))
|
|
{
|
|
let on_success_renew_certificate = old_domain.is_none()
|
|
&& matches!(
|
|
domain.certificate_management,
|
|
CertificateManagement::Automatic(_)
|
|
);
|
|
tasks.push(Task::DnsManagement(TaskDnsManagement {
|
|
domain_id: Id::default(),
|
|
update_records: Map::new(
|
|
details
|
|
.publish_records
|
|
.iter()
|
|
.filter(|&&r| r != DnsRecordType::Dkim || !will_trigger_dkim)
|
|
.copied()
|
|
.collect(),
|
|
),
|
|
on_success_renew_certificate,
|
|
status: TaskStatus::now(),
|
|
}));
|
|
on_success_renew_certificate
|
|
} else {
|
|
if will_schedule_dkim {
|
|
tasks.push(Task::DnsManagement(TaskDnsManagement {
|
|
domain_id: Id::default(),
|
|
update_records: Map::new(vec![DnsRecordType::Dkim]),
|
|
on_success_renew_certificate: false,
|
|
status: TaskStatus::now(),
|
|
}));
|
|
}
|
|
false
|
|
};
|
|
|
|
// Schedule DKIM key rotation task
|
|
if will_trigger_dkim || will_schedule_dkim {
|
|
tasks.push(Task::DkimManagement(TaskDomainManagement {
|
|
domain_id: Id::default(),
|
|
status: TaskStatus::now(),
|
|
}));
|
|
}
|
|
|
|
// Schedule ACME renewal task if needed
|
|
if !will_trigger_acme
|
|
&& let CertificateManagement::Automatic(details) = &domain.certificate_management
|
|
&& old_domain.is_none_or(|old| {
|
|
!matches!(
|
|
old.certificate_management,
|
|
CertificateManagement::Automatic(_)
|
|
)
|
|
})
|
|
{
|
|
let Some(provider) = set
|
|
.server
|
|
.registry()
|
|
.object::<AcmeProvider>(details.acme_provider_id)
|
|
.await?
|
|
else {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(Property::AcmeProviderId)
|
|
.with_description("ACME provider not found")));
|
|
};
|
|
|
|
if matches!(provider.challenge_type, AcmeChallengeType::Dns01)
|
|
&& !matches!(domain.dns_management, DnsManagement::Automatic(_))
|
|
{
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(Property::AcmeProviderId)
|
|
.with_description(
|
|
"ACME provider requires automatic DNS management",
|
|
)));
|
|
}
|
|
|
|
tasks.push(Task::AcmeRenewal(TaskDomainManagement {
|
|
domain_id: Id::default(),
|
|
status: TaskStatus::now(),
|
|
}));
|
|
}
|
|
|
|
Ok(Ok(response))
|
|
}
|
|
|
|
fn publishes_dkim(domain: &Domain) -> bool {
|
|
matches!(
|
|
&domain.dns_management,
|
|
DnsManagement::Automatic(details) if details.publish_records.contains(&DnsRecordType::Dkim)
|
|
)
|
|
}
|
|
|
|
pub(crate) async fn validate_dns_server(
|
|
set: &RegistrySetResponse<'_>,
|
|
dns: &mut DnsServer,
|
|
old_dns: Option<&DnsServer>,
|
|
) -> ValidationResult {
|
|
let response = if old_dns.is_none() {
|
|
match validate_tenant_quota(
|
|
set.server,
|
|
set.access_token,
|
|
TenantStorageQuota::MaxDnsServers,
|
|
)
|
|
.await?
|
|
{
|
|
Ok(response) => response,
|
|
Err(err) => {
|
|
return Ok(Err(err));
|
|
}
|
|
}
|
|
} else {
|
|
ObjectResponse::default()
|
|
};
|
|
|
|
if old_dns.is_none_or(|old_dns| old_dns != dns)
|
|
&& let Err(err) = DnsUpdater::build(dns.clone(), set.server.core.clone()).await
|
|
{
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_description(format!("Failed to build DNS server: {err}"))));
|
|
}
|
|
|
|
Ok(Ok(response))
|
|
}
|