JMAP Registry API implementation - part 11

This commit is contained in:
mdecimus
2026-03-07 21:08:23 +01:00
parent d15efc6fb9
commit 455d3e3beb
41 changed files with 661 additions and 577 deletions

View File

@@ -67,8 +67,7 @@ impl Server {
let username = UsernameParts::new(username);
// Try to authenticate as fallback admin if configured
if let Some((fallback_user, fallback_hash)) =
&self.core.network.security.fallback_admin
if let Some((fallback_user, fallback_hash)) = &self.registry().recovery_admin()
&& username.auth_as().address() == fallback_user
{
return if verify_secret_hash(fallback_hash, secret.as_bytes()).await? {

View File

@@ -231,7 +231,7 @@ impl Server {
.into_err()
.details("Account no longer exists")
})
} else if let Some((_, secret)) = &self.core.network.security.fallback_admin {
} else if let Some((_, secret)) = self.registry().recovery_admin() {
Ok(secret.into())
} else {
Err(trc::AuthEvent::Error

View File

@@ -197,186 +197,11 @@ impl PermissionsGroup {
}
pub fn user() -> Self {
let todo = "fix";
let mut permissions = PermissionsGroup::default();
for permission in [
Permission::Authenticate,
Permission::AuthenticateOauth,
Permission::EmailSend,
Permission::EmailReceive,
Permission::ManageEncryption,
Permission::ManagePasswords,
Permission::JmapEmailGet,
Permission::JmapMailboxGet,
Permission::JmapThreadGet,
Permission::JmapIdentityGet,
Permission::JmapEmailSubmissionGet,
Permission::JmapPushSubscriptionGet,
Permission::JmapSieveScriptGet,
Permission::JmapVacationResponseGet,
Permission::JmapQuotaGet,
Permission::JmapBlobGet,
Permission::JmapEmailSet,
Permission::JmapMailboxSet,
Permission::JmapIdentitySet,
Permission::JmapEmailSubmissionSet,
Permission::JmapPushSubscriptionSet,
Permission::JmapSieveScriptSet,
Permission::JmapVacationResponseSet,
Permission::JmapEmailChanges,
Permission::JmapMailboxChanges,
Permission::JmapThreadChanges,
Permission::JmapIdentityChanges,
Permission::JmapEmailSubmissionChanges,
Permission::JmapQuotaChanges,
Permission::JmapEmailCopy,
Permission::JmapBlobCopy,
Permission::JmapEmailImport,
Permission::JmapEmailParse,
Permission::JmapEmailQueryChanges,
Permission::JmapMailboxQueryChanges,
Permission::JmapEmailSubmissionQueryChanges,
Permission::JmapSieveScriptQueryChanges,
Permission::JmapQuotaQueryChanges,
Permission::JmapEmailQuery,
Permission::JmapMailboxQuery,
Permission::JmapEmailSubmissionQuery,
Permission::JmapSieveScriptQuery,
Permission::JmapQuotaQuery,
Permission::JmapSearchSnippet,
Permission::JmapSieveScriptValidate,
Permission::JmapBlobLookup,
Permission::JmapBlobUpload,
Permission::JmapEcho,
Permission::ImapAuthenticate,
Permission::ImapAclGet,
Permission::ImapAclSet,
Permission::ImapMyRights,
Permission::ImapListRights,
Permission::ImapAppend,
Permission::ImapCapability,
Permission::ImapId,
Permission::ImapCopy,
Permission::ImapMove,
Permission::ImapCreate,
Permission::ImapDelete,
Permission::ImapEnable,
Permission::ImapExpunge,
Permission::ImapFetch,
Permission::ImapIdle,
Permission::ImapList,
Permission::ImapLsub,
Permission::ImapNamespace,
Permission::ImapRename,
Permission::ImapSearch,
Permission::ImapSort,
Permission::ImapSelect,
Permission::ImapExamine,
Permission::ImapStatus,
Permission::ImapStore,
Permission::ImapSubscribe,
Permission::ImapThread,
Permission::Pop3Authenticate,
Permission::Pop3List,
Permission::Pop3Uidl,
Permission::Pop3Stat,
Permission::Pop3Retr,
Permission::Pop3Dele,
Permission::SieveAuthenticate,
Permission::SieveListScripts,
Permission::SieveSetActive,
Permission::SieveGetScript,
Permission::SievePutScript,
Permission::SieveDeleteScript,
Permission::SieveRenameScript,
Permission::SieveCheckScript,
Permission::SieveHaveSpace,
Permission::DavSyncCollection,
Permission::DavExpandProperty,
Permission::DavPrincipalAcl,
Permission::DavPrincipalList,
Permission::DavPrincipalSearch,
Permission::DavPrincipalMatch,
Permission::DavPrincipalSearchPropSet,
Permission::DavFilePropFind,
Permission::DavFilePropPatch,
Permission::DavFileGet,
Permission::DavFileMkCol,
Permission::DavFileDelete,
Permission::DavFilePut,
Permission::DavFileCopy,
Permission::DavFileMove,
Permission::DavFileLock,
Permission::DavFileAcl,
Permission::DavCardPropFind,
Permission::DavCardPropPatch,
Permission::DavCardGet,
Permission::DavCardMkCol,
Permission::DavCardDelete,
Permission::DavCardPut,
Permission::DavCardCopy,
Permission::DavCardMove,
Permission::DavCardLock,
Permission::DavCardAcl,
Permission::DavCardQuery,
Permission::DavCardMultiGet,
Permission::DavCalPropFind,
Permission::DavCalPropPatch,
Permission::DavCalGet,
Permission::DavCalMkCol,
Permission::DavCalDelete,
Permission::DavCalPut,
Permission::DavCalCopy,
Permission::DavCalMove,
Permission::DavCalLock,
Permission::DavCalAcl,
Permission::DavCalQuery,
Permission::DavCalMultiGet,
Permission::DavCalFreeBusyQuery,
Permission::CalendarAlarms,
Permission::CalendarSchedulingSend,
Permission::CalendarSchedulingReceive,
Permission::JmapAddressBookGet,
Permission::JmapAddressBookSet,
Permission::JmapAddressBookChanges,
Permission::JmapContactCardGet,
Permission::JmapContactCardChanges,
Permission::JmapContactCardQuery,
Permission::JmapContactCardQueryChanges,
Permission::JmapContactCardSet,
Permission::JmapContactCardCopy,
Permission::JmapContactCardParse,
Permission::JmapFileNodeGet,
Permission::JmapFileNodeSet,
Permission::JmapFileNodeChanges,
Permission::JmapFileNodeQuery,
Permission::JmapFileNodeQueryChanges,
Permission::JmapPrincipalGetAvailability,
Permission::JmapPrincipalChanges,
Permission::JmapPrincipalQuery,
Permission::JmapPrincipalGet,
Permission::JmapPrincipalQueryChanges,
Permission::JmapShareNotificationGet,
Permission::JmapShareNotificationSet,
Permission::JmapShareNotificationChanges,
Permission::JmapShareNotificationQuery,
Permission::JmapShareNotificationQueryChanges,
Permission::JmapCalendarGet,
Permission::JmapCalendarSet,
Permission::JmapCalendarChanges,
Permission::JmapCalendarEventGet,
Permission::JmapCalendarEventSet,
Permission::JmapCalendarEventChanges,
Permission::JmapCalendarEventQuery,
Permission::JmapCalendarEventQueryChanges,
Permission::JmapCalendarEventCopy,
Permission::JmapCalendarEventParse,
Permission::JmapCalendarEventNotificationGet,
Permission::JmapCalendarEventNotificationSet,
Permission::JmapCalendarEventNotificationChanges,
Permission::JmapCalendarEventNotificationQuery,
Permission::JmapCalendarEventNotificationQueryChanges,
Permission::JmapParticipantIdentityGet,
Permission::JmapParticipantIdentitySet,
Permission::JmapParticipantIdentityChanges,
] {
permissions.enabled.set(permission as usize);

View File

@@ -116,7 +116,7 @@ impl Server {
}
SubAddressing::Custom(custom) => {
flags |= DOMAIN_FLAG_SUB_ADDRESSING;
let mut bp = Bootstrap::new(self.registry().clone());
let mut bp = Bootstrap::new_uninitialized(self.registry().clone());
let custom = bp.compile_expr(
ObjectId::new(ObjectType::Domain, domain_id.into()),
&custom.ctx_custom_rule(),

View File

@@ -31,7 +31,7 @@ pub struct ReloadResult {
impl Server {
pub async fn reload_registry(&self, change: RegistryChange) -> trc::Result<ReloadResult> {
let mut bootstrap = Bootstrap::new(self.registry().clone());
let mut bootstrap = Bootstrap::new(self.registry().clone()).await;
let object = match change {
RegistryChange::Insert(id) => {
if matches!(id.object(), ObjectType::BlockedIp) {

View File

@@ -47,7 +47,7 @@ impl Data {
// Build and test snowflake id generator
let node_id = bp.node_id();
SnowflakeIdGenerator::set_node_id(node_id);
SnowflakeIdGenerator::set_node_id(node_id as u64);
let id_generator = SnowflakeIdGenerator::new();
if !id_generator.is_valid() {
panic!("Invalid system time, panicking to avoid data corruption");

View File

@@ -9,13 +9,10 @@ use crate::{
expr::if_block::{BootstrapExprExt, IfBlock},
network::security::Security,
};
use ahash::AHashMap;
use registry::{
schema::{
prelude::ObjectType,
structs::{self, Asn, HttpForm, Rate, SystemSettings, TaskManager},
},
types::EnumImpl,
use registry::schema::{
enums::{ClusterShardedTaskType, ClusterTaskType},
prelude::ObjectType,
structs::{self, Asn, ClusterTaskGroup, HttpForm, Rate, SystemSettings, TaskManager},
};
use std::{hash::Hasher, str::FromStr, time::Duration};
use xxhash_rust::xxh3::Xxh3Builder;
@@ -152,7 +149,7 @@ impl Network {
let system = bp.setting_infallible::<SystemSettings>().await;
let mut network = Network {
node_id: bp.node_id(),
node_id: bp.node_id() as u64,
server_name: system.default_hostname,
security: Security::parse(bp).await,
contact_form: ContactForm::parse(bp).await,
@@ -162,161 +159,146 @@ impl Network {
task_manager: bp.setting_infallible::<TaskManager>().await,
};
// Process ranges
let node_id = bp.node_id();
let ranges = bp.list_infallible::<NodeRole>().await;
if !ranges.is_empty() {
for network_role in network.roles.all_mut() {
network_role.set_uninit();
}
for range in ranges {
let is_success = match &range.object {
NodeRole::CalculateMetrics(_)
| NodeRole::PushMetrics(_)
| NodeRole::SpamClassifierTraining(_)
| NodeRole::TaskScheduler(_) => {
let (roles, role_obj) = match &range.object {
NodeRole::CalculateMetrics(role) => {
(&mut network.roles.calculate_metrics, role)
}
NodeRole::PushMetrics(role) => (&mut network.roles.push_metrics, role),
NodeRole::SpamClassifierTraining(role) => {
(&mut network.roles.spam_training, role)
}
NodeRole::TaskScheduler(role) => {
(&mut network.roles.task_scheduler, role)
}
_ => unreachable!(),
};
roles.set_role(role_obj.node_id == node_id)
if let Some(role) = &bp.role {
match &role.tasks {
ClusterTaskGroup::EnableAll => {}
ClusterTaskGroup::DisableAll => {
for network_role in network.roles.all_mut() {
network_role.set_role(false);
}
NodeRole::StoreMaintenance(_)
| NodeRole::AccountMaintenance(_)
| NodeRole::PushNotifications(_)
| NodeRole::SearchIndexing(_)
| NodeRole::ImipProcessing(_)
| NodeRole::CalendarAlerts(_)
| NodeRole::MergeThreads(_)
| NodeRole::DnsAndAcme(_)
| NodeRole::OutboundMta(_)
| NodeRole::TaskQueueProcessing(_) => {
let (roles, role_obj) = match &range.object {
NodeRole::StoreMaintenance(role) => {
(&mut network.roles.store_maintenance, role)
}
NodeRole::AccountMaintenance(role) => {
(&mut network.roles.account_maintenance, role)
}
NodeRole::PushNotifications(role) => {
(&mut network.roles.push_notifications, role)
}
NodeRole::SearchIndexing(role) => {
(&mut network.roles.search_indexing, role)
}
NodeRole::ImipProcessing(role) => {
(&mut network.roles.imip_processing, role)
}
NodeRole::CalendarAlerts(role) => {
(&mut network.roles.calendar_alerts, role)
}
NodeRole::MergeThreads(role) => {
(&mut network.roles.merge_threads, role)
}
NodeRole::OutboundMta(role) => (&mut network.roles.outbound_mta, role),
NodeRole::DnsAndAcme(role) => (&mut network.roles.dns_acme, role),
NodeRole::TaskQueueProcessing(role) => {
(&mut network.roles.task_manager, role)
}
_ => unreachable!(),
};
roles.set_role(
role_obj
.node_ranges
.values()
.any(|range| range.contains(node_id)),
)
}
ClusterTaskGroup::EnableSome(group) => {
for network_role in network.roles.all_mut() {
network_role.set_role(false);
}
};
if !is_success {
bp.build_warning(
range.id,
format!("Multiple role definitions found for node id {node_id}",),
);
}
}
for network_role in network.roles.all_mut() {
network_role.finalize();
}
// Node shards
let mut shards = AHashMap::new();
for shard in bp.list_infallible::<NodeShard>().await {
shards
.entry(shard.object.shard_type)
.or_insert_with(Vec::new)
.push(shard);
}
for (shard_type, shards) in shards {
if shards.len() == 1 {
bp.build_warning(shards[0].id, format!(
"Only one shard defined for shard type {:?}, ignoring shard configuration",
shard_type.as_str()
));
continue;
}
let roles = match shard_type {
NodeShardType::StoreMaintenance => &mut network.roles.store_maintenance,
NodeShardType::AccountMaintenance => &mut network.roles.account_maintenance,
NodeShardType::DnsAndAcme => &mut network.roles.dns_acme,
NodeShardType::PushNotifications => &mut network.roles.push_notifications,
NodeShardType::SearchIndexing => &mut network.roles.search_indexing,
NodeShardType::ImipProcessing => &mut network.roles.imip_processing,
NodeShardType::CalendarAlerts => &mut network.roles.calendar_alerts,
NodeShardType::MergeThreads => &mut network.roles.merge_threads,
};
if matches!(roles, ClusterRole::Disabled) {
continue;
}
for (shard_num, shard) in shards.iter().enumerate() {
if shard
.object
.node_ranges
.values()
.any(|range| range.contains(node_id))
{
if matches!(roles, ClusterRole::Enabled) {
*roles = ClusterRole::Sharded {
shard_id: shard_num as u32,
total_shards: shards.len() as u32,
};
} else {
bp.build_warning(
shard.id,
format!(
"Node id {node_id} matches multiple shards for shard type {:?}",
shard_type.as_str()
),
);
for task_type in group.task_types.iter() {
match task_type {
ClusterTaskType::StoreMaintenance => {
network.roles.store_maintenance.set_role(true);
}
ClusterTaskType::AccountMaintenance => {
network.roles.account_maintenance.set_role(true);
}
ClusterTaskType::DnsAndAcme => {
network.roles.dns_acme.set_role(true);
}
ClusterTaskType::CalculateMetrics => {
network.roles.calculate_metrics.set_role(true);
}
ClusterTaskType::PushMetrics => {
network.roles.push_metrics.set_role(true);
}
ClusterTaskType::PushNotifications => {
network.roles.push_notifications.set_role(true);
}
ClusterTaskType::SearchIndexing => {
network.roles.search_indexing.set_role(true);
}
ClusterTaskType::SpamClassifierTraining => {
network.roles.spam_training.set_role(true);
}
ClusterTaskType::ImipProcessing => {
network.roles.imip_processing.set_role(true);
}
ClusterTaskType::CalendarAlerts => {
network.roles.calendar_alerts.set_role(true);
}
ClusterTaskType::MergeThreads => {
network.roles.merge_threads.set_role(true);
}
ClusterTaskType::OutboundMta => {
network.roles.outbound_mta.set_role(true);
}
ClusterTaskType::TaskQueueProcessing => {
network.roles.task_manager.set_role(true);
}
ClusterTaskType::TaskScheduler => {
network.roles.task_scheduler.set_role(true);
}
}
}
}
ClusterTaskGroup::DisableSome(group) => {
for task_type in group.task_types.iter() {
match task_type {
ClusterTaskType::StoreMaintenance => {
network.roles.store_maintenance.set_role(true);
}
ClusterTaskType::AccountMaintenance => {
network.roles.account_maintenance.set_role(false);
}
ClusterTaskType::DnsAndAcme => {
network.roles.dns_acme.set_role(false);
}
ClusterTaskType::CalculateMetrics => {
network.roles.calculate_metrics.set_role(false);
}
ClusterTaskType::PushMetrics => {
network.roles.push_metrics.set_role(false);
}
ClusterTaskType::PushNotifications => {
network.roles.push_notifications.set_role(false);
}
ClusterTaskType::SearchIndexing => {
network.roles.search_indexing.set_role(false);
}
ClusterTaskType::SpamClassifierTraining => {
network.roles.spam_training.set_role(false);
}
ClusterTaskType::ImipProcessing => {
network.roles.imip_processing.set_role(false);
}
ClusterTaskType::CalendarAlerts => {
network.roles.calendar_alerts.set_role(false);
}
ClusterTaskType::MergeThreads => {
network.roles.merge_threads.set_role(false);
}
ClusterTaskType::OutboundMta => {
network.roles.outbound_mta.set_role(false);
}
ClusterTaskType::TaskQueueProcessing => {
network.roles.task_manager.set_role(false);
}
ClusterTaskType::TaskScheduler => {
network.roles.task_scheduler.set_role(false);
}
}
}
}
}
if matches!(roles, ClusterRole::Enabled) {
bp.build_warning(
shards[0].id,
format!(
"Node id {node_id} does not match any shards for shard type {:?}, defaulting to all shards",
shard_type.as_str()
),
);
if role.shard_size > 1 {
for task_type in role.shard_task_types.iter() {
let network_role = match task_type {
ClusterShardedTaskType::StoreMaintenance => {
&mut network.roles.store_maintenance
}
ClusterShardedTaskType::AccountMaintenance => {
&mut network.roles.account_maintenance
}
ClusterShardedTaskType::DnsAndAcme => &mut network.roles.dns_acme,
ClusterShardedTaskType::PushNotifications => {
&mut network.roles.push_notifications
}
ClusterShardedTaskType::SearchIndexing => {
&mut network.roles.search_indexing
}
ClusterShardedTaskType::ImipProcessing => {
&mut network.roles.imip_processing
}
ClusterShardedTaskType::CalendarAlerts => {
&mut network.roles.calendar_alerts
}
ClusterShardedTaskType::MergeThreads => &mut network.roles.merge_threads,
};
if network_role.is_enabled_or_sharded() {
*network_role = ClusterRole::Sharded {
shard_id: bp.registry.cluster_role_shard() as u32,
total_shards: role.shard_size as u32,
};
}
}
}
}

View File

@@ -14,7 +14,7 @@ use crate::{
};
use registry::schema::{
enums::{NetworkListenerProtocol, TlsCipherSuite, TlsVersion},
structs::{NetworkListener, SystemSettings},
structs::{ClusterListenerGroup, NetworkListener, SystemSettings},
};
use rustls::{
ALL_VERSIONS, ServerConfig, SupportedCipherSuite,
@@ -35,17 +35,19 @@ impl Listeners {
};
// Parse servers
let node_id = bp.node_id();
let system = bp.setting_infallible::<SystemSettings>().await;
for listener in bp.list_infallible::<NetworkListener>().await {
if listener.object.enable_for_nodes.is_empty()
|| listener
.object
.enable_for_nodes
.values()
.any(|n| n.contains(node_id))
{
if bp.role.as_ref().is_none_or(|r| match &r.listeners {
ClusterListenerGroup::EnableAll => true,
ClusterListenerGroup::DisableAll => false,
ClusterListenerGroup::EnableSome(group) => {
group.listener_ids.iter().any(|id| *id == listener.id.id())
}
ClusterListenerGroup::DisableSome(group) => {
!group.listener_ids.iter().any(|id| *id == listener.id.id())
}
}) {
servers.parse_server(bp, listener, &system);
}
}

View File

@@ -235,7 +235,8 @@ impl BootManager {
let registry = RegistryStore::init(PathBuf::from(config_path.unwrap()))
.await
.failed("⚠️ Startup failed");
let mut bootstrap = Bootstrap::new(registry);
let mut bootstrap = Bootstrap::new(registry).await;
let todo = "implement recovery mode";
// Start listeners
let mut servers = Listeners::parse(&mut bootstrap).await;
@@ -270,9 +271,23 @@ impl BootManager {
trc::event!(
Server(trc::ServerEvent::Startup),
Hostname = bootstrap.registry.local_hostname().to_string(),
Version = env!("CARGO_PKG_VERSION"),
);
if core.storage.coordinator.is_enabled() {
trc::event!(
Cluster(trc::ClusterEvent::Startup),
Id = bootstrap.registry.node_id(),
Type = bootstrap
.registry
.cluster_role()
.unwrap_or("[default]")
.to_string(),
Details = bootstrap.registry.cluster_role_shard()
);
}
// Build shared inner
let has_remote_asn = matches!(
core.network.asn_geo_lookup,

View File

@@ -14,7 +14,7 @@ use registry::{
schema::{
enums::{BlockReason, PasswordHashAlgorithm},
prelude::{Object, ObjectType},
structs::{self, AllowedIp, BlockedIp, Rate},
structs::{self, AllowedIp, BlockedIp, Rate, SystemSettings},
},
types::{datetime::UTCDateTime, ipmask::IpAddrOrMask},
};
@@ -32,8 +32,6 @@ use utils::glob::{GlobPattern, MatchType};
#[derive(Debug, Clone)]
pub struct Security {
pub fallback_admin: Option<(String, String)>,
pub allowed_ip_addresses: AHashSet<IpAddr>,
pub allowed_ip_networks: Vec<IpAddrOrMask>,
pub has_allowed_networks: bool,
@@ -75,7 +73,7 @@ impl Security {
if ip.expires_at.as_ref().is_none_or(|ip| ip.timestamp() > now) {
if let Some(ip) = ip.address.try_to_ip() {
allowed_ip_addresses.insert(ip);
} else {
} else if !allowed_ip_networks.contains(&ip.address) {
allowed_ip_networks.push(ip.address);
}
} else {
@@ -90,6 +88,16 @@ impl Security {
}
}
// Add proxy protocol IPs as allowed
let system = bp.setting_infallible::<SystemSettings>().await;
for ip in system.proxy_trusted_networks {
if let Some(ip) = ip.try_to_ip() {
allowed_ip_addresses.insert(ip);
} else if !allowed_ip_networks.contains(&ip) {
allowed_ip_networks.push(ip);
}
}
if !expired_allows.is_empty() {
for (id, _, object) in &expired_allows {
if let Err(err) = bp
@@ -123,12 +131,6 @@ impl Security {
let security = bp.setting_infallible::<structs::Security>().await;
let auth = bp.setting_infallible::<structs::Authentication>().await;
Security {
fallback_admin: local.fallback_admin_user.as_ref().and_then(|user| {
local
.fallback_admin_secret
.as_ref()
.map(|secret| (user.to_string(), secret.to_string()))
}),
has_allowed_networks: !allowed_ip_networks.is_empty(),
allowed_ip_addresses,
allowed_ip_networks,