diff --git a/crates/common/src/auth/authentication.rs b/crates/common/src/auth/authentication.rs index 460c2357..f028477b 100644 --- a/crates/common/src/auth/authentication.rs +++ b/crates/common/src/auth/authentication.rs @@ -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? { diff --git a/crates/common/src/auth/oauth/token.rs b/crates/common/src/auth/oauth/token.rs index 6a460b5d..ff74f9de 100644 --- a/crates/common/src/auth/oauth/token.rs +++ b/crates/common/src/auth/oauth/token.rs @@ -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 diff --git a/crates/common/src/auth/permissions.rs b/crates/common/src/auth/permissions.rs index da333a5c..8341fcd4 100644 --- a/crates/common/src/auth/permissions.rs +++ b/crates/common/src/auth/permissions.rs @@ -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); diff --git a/crates/common/src/cache/principals.rs b/crates/common/src/cache/principals.rs index c7260715..11575b67 100644 --- a/crates/common/src/cache/principals.rs +++ b/crates/common/src/cache/principals.rs @@ -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(), diff --git a/crates/common/src/cache/reload.rs b/crates/common/src/cache/reload.rs index c313812e..aa9748a5 100644 --- a/crates/common/src/cache/reload.rs +++ b/crates/common/src/cache/reload.rs @@ -31,7 +31,7 @@ pub struct ReloadResult { impl Server { pub async fn reload_registry(&self, change: RegistryChange) -> trc::Result { - 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) { diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index f06d0f6c..481e1ec5 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -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"); diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index 3d7c4e96..900bdff9 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -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::().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::().await, }; - // Process ranges - let node_id = bp.node_id(); - let ranges = bp.list_infallible::().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::().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, + }; + } } } } diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index 21a3d3af..fdd8185e 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -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::().await; for listener in bp.list_infallible::().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); } } diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index bc2108c2..a9fbd687 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -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, diff --git a/crates/common/src/network/security.rs b/crates/common/src/network/security.rs index 086f3820..ae79a370 100644 --- a/crates/common/src/network/security.rs +++ b/crates/common/src/network/security.rs @@ -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, pub allowed_ip_networks: Vec, 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::().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::().await; let auth = bp.setting_infallible::().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, diff --git a/crates/coordinator/src/bootstrap.rs b/crates/coordinator/src/bootstrap.rs index 4beb6e3e..832715e6 100644 --- a/crates/coordinator/src/bootstrap.rs +++ b/crates/coordinator/src/bootstrap.rs @@ -59,6 +59,10 @@ impl Coordinator { } } } + + pub fn is_enabled(&self) -> bool { + !matches!(self, Coordinator::None) + } } #[cfg(feature = "redis")] diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index e112de69..4df62ac6 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -200,7 +200,7 @@ impl PropFindRequestHandler for Server { } else if access_token.has_account_access(account_id) || (self.core.groupware.allow_directory_query && access_token.has_permission(Permission::DavPrincipalList)) - || access_token.has_permission(Permission::IndividualList) + || access_token.has_permission(Permission::AccountQuery) { self.prepare_principal_propfind_response( access_token, @@ -255,7 +255,7 @@ impl PropFindRequestHandler for Server { ) } else if (self.core.groupware.allow_directory_query && access_token.has_permission(Permission::DavPrincipalList)) - || access_token.has_permission(Permission::IndividualList) + || access_token.has_permission(Permission::AccountQuery) { // Return all principals self.registry() diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index 2c09a7ce..b4b7ad4f 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -179,7 +179,7 @@ impl DavRequestDispatcher for Server { Report::AclPrincipalPropSet(report) => { // Validate permissions if !self.core.groupware.allow_directory_query - && !access_token.has_permission(Permission::IndividualList) + && !access_token.has_permission(Permission::AccountQuery) { return Err(DavError::Condition( DavErrorCondition::new( @@ -198,7 +198,7 @@ impl DavRequestDispatcher for Server { Report::PrincipalMatch(report) => { // Validate permissions if !self.core.groupware.allow_directory_query - && !access_token.has_permission(Permission::IndividualList) + && !access_token.has_permission(Permission::AccountQuery) { return Err(DavError::Condition( DavErrorCondition::new( @@ -218,7 +218,7 @@ impl DavRequestDispatcher for Server { if resource == DavResourceName::Principal { // Validate permissions if !self.core.groupware.allow_directory_query - && !access_token.has_permission(Permission::IndividualList) + && !access_token.has_permission(Permission::AccountQuery) { return Err(DavError::Condition( DavErrorCondition::new( diff --git a/crates/http/src/auth/oauth/registration.rs b/crates/http/src/auth/oauth/registration.rs index 8c76a53e..23780d18 100644 --- a/crates/http/src/auth/oauth/registration.rs +++ b/crates/http/src/auth/oauth/registration.rs @@ -56,7 +56,7 @@ impl ClientRegistrationHandler for Server { let (_, access_token) = self.authenticate_headers(req, &session).await?; // Validate permissions - access_token.enforce_permission(Permission::OauthClientRegistration)?; + access_token.enforce_permission(Permission::OAuthClientRegistration)?; access_token.tenant_id() } else { self.is_http_anonymous_request_allowed(&session.remote_ip) @@ -164,7 +164,7 @@ impl ClientRegistrationHandler for Server { .await .caused_by(trc::location!())? .build() - .has_permission(Permission::OauthClientOverride) + .has_permission(Permission::OAuthClientOverride) { return Ok(None); } diff --git a/crates/http/src/management/mod.rs b/crates/http/src/management/mod.rs index d2dd335a..0edf91ee 100644 --- a/crates/http/src/management/mod.rs +++ b/crates/http/src/management/mod.rs @@ -60,7 +60,7 @@ impl ManagementApi for Server { #[cfg(feature = "enterprise")] Some("tracing") if self.core.is_enterprise_edition() => { // Validate the access token - access_token.enforce_permission(Permission::TracingLive)?; + access_token.enforce_permission(Permission::LiveTracing)?; // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ @@ -71,7 +71,7 @@ impl ManagementApi for Server { #[cfg(feature = "enterprise")] Some("metrics") if self.core.is_enterprise_edition() => { // Validate the access token - access_token.enforce_permission(Permission::MetricsLive)?; + access_token.enforce_permission(Permission::LiveMetrics)?; // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ @@ -82,7 +82,7 @@ impl ManagementApi for Server { // SPDX-SnippetEnd Some("delivery") => { // Validate the access token - access_token.enforce_permission(Permission::Troubleshoot)?; + access_token.enforce_permission(Permission::LiveDeliveryTest)?; // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ @@ -108,7 +108,7 @@ impl ManagementApi for Server { ) { ("delivery", Some(target), &Method::GET) => { // Validate the access token - access_token.enforce_permission(Permission::Troubleshoot)?; + access_token.enforce_permission(Permission::LiveDeliveryTest)?; let timeout = Duration::from_secs( params diff --git a/crates/http/src/management/telemetry.rs b/crates/http/src/management/telemetry.rs index f27e019a..f5f33ce5 100644 --- a/crates/http/src/management/telemetry.rs +++ b/crates/http/src/management/telemetry.rs @@ -49,7 +49,7 @@ impl TelemetryApi for Server { let params = UrlParams::new(req.uri().query()); if is_tracing { // Validate the access token - access_token.enforce_permission(Permission::TracingLive)?; + access_token.enforce_permission(Permission::LiveTracing)?; let mut key_filters = AHashMap::new(); let mut filter = None; @@ -175,7 +175,7 @@ impl TelemetryApi for Server { )))) } else { // Validate the access token - access_token.enforce_permission(Permission::MetricsLive)?; + access_token.enforce_permission(Permission::LiveMetrics)?; let interval = Duration::from_secs( params diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 3e2d3dca..69f59911 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -395,11 +395,11 @@ impl ParseHttp for Server { let path = req.uri().path(); let (grant_type, permissions) = if path.starts_with("/api/telemetry/traces") { - (GrantType::LiveTracing, Permission::TracingLive) + (GrantType::LiveTracing, Permission::LiveTracing) } else if path.starts_with("/api/telemetry/metrics") { - (GrantType::LiveMetrics, Permission::MetricsLive) + (GrantType::LiveMetrics, Permission::LiveMetrics) } else if path.starts_with("/api/diagnose") { - (GrantType::Diagnose, Permission::Troubleshoot) + (GrantType::Diagnose, Permission::LiveDeliveryTest) } else { return Err(trc::ResourceEvent::NotFound.into_err()); }; diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs index ee87f0f7..32ea8f22 100644 --- a/crates/jmap-proto/src/request/method.rs +++ b/crates/jmap-proto/src/request/method.rs @@ -5,7 +5,7 @@ */ use std::{borrow::Cow, fmt::Display}; -use registry::{schema::prelude::ObjectType, types::EnumImpl}; +use registry::{schema::prelude::{OBJ_SINGLETON, ObjectType}, types::EnumImpl}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MethodName { @@ -306,7 +306,12 @@ impl MethodName { "set" => MethodFunction::Set, "query" => MethodFunction::Query, )?; - (MethodObject::Registry(obj), fnc).into() + + if obj.flags() & OBJ_SINGLETON == 0 || fnc != MethodFunction::Query { + (MethodObject::Registry(obj), fnc).into() + } else { + None + } }).map(|(obj, fnc)| MethodName { obj, fnc }) } diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index 4625784c..bda8ef27 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -149,7 +149,6 @@ pub enum QueryChangesRequestMethod { Email(QueryChangesRequest), Mailbox(QueryChangesRequest), EmailSubmission(QueryChangesRequest), - Sieve(QueryChangesRequest), Principal(QueryChangesRequest), Quota(QueryChangesRequest), ContactCard(QueryChangesRequest), diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index 59a508a9..3e029519 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -490,15 +490,6 @@ impl<'de> Visitor<'de> for CallVisitor { } } } - (MethodFunction::QueryChanges, MethodObject::SieveScript) => match seq.next_element() { - Ok(Some(value)) => { - RequestMethod::QueryChanges(QueryChangesRequestMethod::Sieve(value)) - } - Err(err) => RequestMethod::invalid(err), - Ok(None) => { - return Err(de::Error::invalid_length(1, &self)); - } - }, (MethodFunction::QueryChanges, MethodObject::Principal) => match seq.next_element() { Ok(Some(value)) => { RequestMethod::QueryChanges(QueryChangesRequestMethod::Principal(value)) diff --git a/crates/jmap/src/api/auth.rs b/crates/jmap/src/api/auth.rs index 8c750320..3b945eaa 100644 --- a/crates/jmap/src/api/auth.rs +++ b/crates/jmap/src/api/auth.rs @@ -5,9 +5,14 @@ */ use common::auth::AccessToken; -use jmap_proto::request::{ - CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod, - QueryRequestMethod, RequestMethod, SetRequestMethod, method::MethodObject, +use jmap_proto::{ + method::set::SetRequest, + object::JmapObject, + request::{ + CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod, + QueryRequestMethod, RequestMethod, SetRequestMethod, method::MethodObject, + reference::MaybeResultReference, + }, }; use registry::schema::enums::Permission; use types::{collection::Collection, id::Id}; @@ -80,34 +85,135 @@ impl JmapAuthorization for AccessToken { } GetRequestMethod::ParticipantIdentity(_) => Permission::JmapParticipantIdentityGet, GetRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationGet, - GetRequestMethod::Registry(request) => { - let todo = "map registry objects to permissions"; - Permission::JmapEmailGet - } - }, - RequestMethod::Set(m) => match &m { - SetRequestMethod::Email(_) => Permission::JmapEmailSet, - SetRequestMethod::Mailbox(_) => Permission::JmapMailboxSet, - SetRequestMethod::Identity(_) => Permission::JmapIdentitySet, - SetRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionSet, - SetRequestMethod::PushSubscription(_) => Permission::JmapPushSubscriptionSet, - SetRequestMethod::Sieve(_) => Permission::JmapSieveScriptSet, - SetRequestMethod::VacationResponse(_) => Permission::JmapVacationResponseSet, - SetRequestMethod::AddressBook(_) => Permission::JmapAddressBookSet, - SetRequestMethod::ContactCard(_) => Permission::JmapContactCardSet, - SetRequestMethod::FileNode(_) => Permission::JmapFileNodeSet, - SetRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationSet, - SetRequestMethod::Calendar(_) => Permission::JmapCalendarSet, - SetRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventSet, - SetRequestMethod::CalendarEventNotification(_) => { - Permission::JmapCalendarEventNotificationSet - } - SetRequestMethod::ParticipantIdentity(_) => Permission::JmapParticipantIdentitySet, - SetRequestMethod::Registry(other) => { - let todo = "map registry objects to permissions"; - Permission::JmapEmailSet + GetRequestMethod::Registry(_) => { + let MethodObject::Registry(object_type) = object else { + unreachable!() + }; + object_type.get_permission() } }, + RequestMethod::Set(m) => { + return match &m { + SetRequestMethod::Email(s) => validate_set( + s, + self, + Permission::JmapEmailCreate, + Permission::JmapEmailUpdate, + Permission::JmapEmailDestroy, + ), + SetRequestMethod::Mailbox(s) => validate_set( + s, + self, + Permission::JmapMailboxCreate, + Permission::JmapMailboxUpdate, + Permission::JmapMailboxDestroy, + ), + SetRequestMethod::Identity(s) => validate_set( + s, + self, + Permission::JmapIdentityCreate, + Permission::JmapIdentityUpdate, + Permission::JmapIdentityDestroy, + ), + SetRequestMethod::EmailSubmission(s) => validate_set( + s, + self, + Permission::JmapEmailSubmissionCreate, + Permission::JmapEmailSubmissionUpdate, + Permission::JmapEmailSubmissionDestroy, + ), + SetRequestMethod::PushSubscription(s) => validate_set( + s, + self, + Permission::JmapPushSubscriptionCreate, + Permission::JmapPushSubscriptionUpdate, + Permission::JmapPushSubscriptionDestroy, + ), + SetRequestMethod::Sieve(s) => validate_set( + s, + self, + Permission::JmapSieveScriptCreate, + Permission::JmapSieveScriptUpdate, + Permission::JmapSieveScriptDestroy, + ), + SetRequestMethod::VacationResponse(s) => validate_set( + s, + self, + Permission::JmapVacationResponseCreate, + Permission::JmapVacationResponseUpdate, + Permission::JmapVacationResponseDestroy, + ), + SetRequestMethod::AddressBook(s) => validate_set( + s, + self, + Permission::JmapAddressBookCreate, + Permission::JmapAddressBookUpdate, + Permission::JmapAddressBookDestroy, + ), + SetRequestMethod::ContactCard(s) => validate_set( + s, + self, + Permission::JmapContactCardCreate, + Permission::JmapContactCardUpdate, + Permission::JmapContactCardDestroy, + ), + SetRequestMethod::FileNode(s) => validate_set( + s, + self, + Permission::JmapFileNodeCreate, + Permission::JmapFileNodeUpdate, + Permission::JmapFileNodeDestroy, + ), + SetRequestMethod::ShareNotification(s) => validate_set( + s, + self, + Permission::JmapShareNotificationCreate, + Permission::JmapShareNotificationUpdate, + Permission::JmapShareNotificationDestroy, + ), + SetRequestMethod::Calendar(s) => validate_set( + s, + self, + Permission::JmapCalendarCreate, + Permission::JmapCalendarUpdate, + Permission::JmapCalendarDestroy, + ), + SetRequestMethod::CalendarEvent(s) => validate_set( + s, + self, + Permission::JmapCalendarEventCreate, + Permission::JmapCalendarEventUpdate, + Permission::JmapCalendarEventDestroy, + ), + SetRequestMethod::CalendarEventNotification(s) => validate_set( + s, + self, + Permission::JmapCalendarEventNotificationCreate, + Permission::JmapCalendarEventNotificationUpdate, + Permission::JmapCalendarEventNotificationDestroy, + ), + SetRequestMethod::ParticipantIdentity(s) => validate_set( + s, + self, + Permission::JmapParticipantIdentityCreate, + Permission::JmapParticipantIdentityUpdate, + Permission::JmapParticipantIdentityDestroy, + ), + SetRequestMethod::Registry(s) => { + let MethodObject::Registry(object_type) = object else { + unreachable!() + }; + let set_permissions = object_type.set_permission(); + validate_set( + s, + self, + set_permissions[0], + set_permissions[1], + set_permissions[2], + ) + } + }; + } RequestMethod::Changes(_) => match object { MethodObject::Email => Permission::JmapEmailChanges, MethodObject::Mailbox => Permission::JmapMailboxChanges, @@ -125,13 +231,13 @@ impl JmapAuthorization for AccessToken { MethodObject::ParticipantIdentity => Permission::JmapParticipantIdentityChanges, MethodObject::ShareNotification => Permission::JmapShareNotificationChanges, MethodObject::Principal => Permission::JmapPrincipalChanges, + MethodObject::AddressBook => Permission::JmapAddressBookChanges, MethodObject::Core | MethodObject::Blob | MethodObject::PushSubscription | MethodObject::SearchSnippet | MethodObject::VacationResponse | MethodObject::SieveScript - | MethodObject::AddressBook | MethodObject::Registry(_) => Permission::JmapEmailChanges, }, RequestMethod::Copy(m) => match &m { @@ -152,7 +258,6 @@ impl JmapAuthorization for AccessToken { QueryChangesRequestMethod::EmailSubmission(_) => { Permission::JmapEmailSubmissionQueryChanges } - QueryChangesRequestMethod::Sieve(_) => Permission::JmapSieveScriptQueryChanges, QueryChangesRequestMethod::Principal(_) => Permission::JmapPrincipalQueryChanges, QueryChangesRequestMethod::Quota(_) => Permission::JmapQuotaQueryChanges, QueryChangesRequestMethod::ContactCard(_) => { @@ -183,16 +288,18 @@ impl JmapAuthorization for AccessToken { Permission::JmapCalendarEventNotificationQuery } QueryRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationQuery, - QueryRequestMethod::Registry(other) => { - let todo = "map registry objects to permissions"; - Permission::JmapShareNotificationQuery + QueryRequestMethod::Registry(_) => { + let MethodObject::Registry(object_type) = object else { + unreachable!() + }; + object_type.query_permission() } }, - RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, + RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippetGet, RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup, RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload, - RequestMethod::Echo(_) => Permission::JmapEcho, + RequestMethod::Echo(_) => Permission::JmapCoreEcho, RequestMethod::Error(_) => return Ok(()), }; @@ -205,3 +312,42 @@ impl JmapAuthorization for AccessToken { } } } + +fn validate_set( + set: &SetRequest<'_, T>, + access_token: &AccessToken, + create_permission: Permission, + update_permission: Permission, + destroy_permission: Permission, +) -> trc::Result<()> { + let can_create = access_token.has_permission(create_permission); + let can_update = access_token.has_permission(update_permission); + let can_destroy = access_token.has_permission(destroy_permission); + + if can_create && can_update && can_destroy { + Ok(()) + } else if !can_create && !can_update && !can_destroy { + Err(trc::JmapEvent::Forbidden + .into_err() + .details("You are not authorized to create, update or destroy objects of this type")) + } else if !can_create && set.create.as_ref().is_some_and(|objs| !objs.is_empty()) { + Err(trc::JmapEvent::Forbidden + .into_err() + .details("You are not authorized to create objects of this type")) + } else if !can_update && set.update.as_ref().is_some_and(|objs| !objs.is_empty()) { + Err(trc::JmapEvent::Forbidden + .into_err() + .details("You are not authorized to update objects of this type")) + } else if !can_destroy + && set.destroy.as_ref().is_some_and(|objs| match objs { + MaybeResultReference::Value(v) => !v.is_empty(), + MaybeResultReference::Reference(_) => true, + }) + { + Err(trc::JmapEvent::Forbidden + .into_err() + .details("You are not authorized to destroy objects of this type")) + } else { + Ok(()) + } +} diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index a04ae27e..242ed662 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -96,7 +96,7 @@ impl AccountCapabilities for AccessToken { .filter(move |capability| { let permission = match capability { Capability::Mail => Permission::JmapEmailGet, - Capability::Submission => Permission::JmapEmailSubmissionSet, + Capability::Submission => Permission::JmapEmailSubmissionCreate, Capability::VacationResponse => Permission::JmapVacationResponseGet, Capability::Contacts => Permission::JmapContactCardGet, Capability::ContactsParse => Permission::JmapContactCardParse, diff --git a/crates/jmap/src/blob/download.rs b/crates/jmap/src/blob/download.rs index 76448546..741aa9aa 100644 --- a/crates/jmap/src/blob/download.rs +++ b/crates/jmap/src/blob/download.rs @@ -102,7 +102,7 @@ impl BlobDownload for Server { access_token: &AccessToken, ) -> trc::Result { Ok( - (blob_id.class.is_superuser() && access_token.has_permission(Permission::BlobFetch)) + (blob_id.class.is_superuser() && access_token.has_permission(Permission::FetchAnyBlob)) || (self .store() .blob_has_access(&blob_id.hash, &blob_id.class) diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index edcbcbfa..899314c1 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -117,28 +117,6 @@ impl QueryChanges for Server { up_to_id = request.up_to_id; results = self.email_submission_query(request.into()).await?; } - QueryChangesRequestMethod::Sieve(mut request) => { - // Query changes - set_account_id_if_missing(&mut request.account_id, access_token); - changes = self - .changes( - build_changes_request(&request), - MethodObject::SieveScript, - access_token, - ) - .await? - .response; - let calculate_total = request.calculate_total.unwrap_or(false); - has_changes = changes.has_changes(); - response = build_query_changes_response(&request, &changes); - - if !has_changes && !calculate_total { - return Ok(response); - } - - up_to_id = request.up_to_id; - results = self.sieve_script_query(request.into()).await?; - } QueryChangesRequestMethod::ContactCard(mut request) => { // Query changes set_account_id_if_missing(&mut request.account_id, access_token); diff --git a/crates/jmap/src/principal/availability.rs b/crates/jmap/src/principal/availability.rs index f1e5afc8..1ebb6a84 100644 --- a/crates/jmap/src/principal/availability.rs +++ b/crates/jmap/src/principal/availability.rs @@ -62,7 +62,7 @@ impl PrincipalGetAvailability for Server { access_token: &AccessToken, ) -> trc::Result { if !self.core.groupware.allow_directory_query - && !access_token.has_permission(Permission::IndividualList) + && !access_token.has_permission(Permission::AccountQuery) { return Err(trc::JmapEvent::Forbidden .into_err() diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 52c7c3b3..091fed9d 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -32,7 +32,7 @@ impl PrincipalGet for Server { access_token: &AccessToken, ) -> trc::Result> { if !self.core.groupware.allow_directory_query - && !access_token.has_permission(Permission::IndividualList) + && !access_token.has_permission(Permission::AccountQuery) { return Err(trc::JmapEvent::Forbidden .into_err() diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index 744949f5..acd1aa96 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -42,7 +42,7 @@ impl PrincipalQuery for Server { access_token: &AccessToken, ) -> trc::Result { if !self.core.groupware.allow_directory_query - && !access_token.has_permission(Permission::IndividualList) + && !access_token.has_permission(Permission::AccountQuery) { return Err(trc::JmapEvent::Forbidden .into_err() diff --git a/crates/jmap/src/registry/get.rs b/crates/jmap/src/registry/get.rs index cccf659e..877a5798 100644 --- a/crates/jmap/src/registry/get.rs +++ b/crates/jmap/src/registry/get.rs @@ -121,7 +121,7 @@ impl RegistryGet for Server { | ObjectType::Imap | ObjectType::InMemoryStore | ObjectType::Jmap - | ObjectType::LocalSettings + | ObjectType::SystemSettings | ObjectType::MemoryLookupKey | ObjectType::MemoryLookupKeyValue | ObjectType::Metrics @@ -147,9 +147,7 @@ impl RegistryGet for Server { | ObjectType::MtaTlsStrategy | ObjectType::MtaVirtualQueue | ObjectType::NetworkListener - | ObjectType::Node - | ObjectType::NodeRole - | ObjectType::NodeShard + | ObjectType::ClusterRole | ObjectType::OidcProvider | ObjectType::RegistryBundle | ObjectType::ReportSettings diff --git a/crates/jmap/src/registry/mapping/mod.rs b/crates/jmap/src/registry/mapping/mod.rs index 82855ecb..9708812f 100644 --- a/crates/jmap/src/registry/mapping/mod.rs +++ b/crates/jmap/src/registry/mapping/mod.rs @@ -57,7 +57,6 @@ pub(crate) struct RegistrySetResponse<'x> { pub destroy: Vec, pub response: SetResponse, pub object_type: ObjectType, - pub object_flags: u64, pub is_tenant_filtered: bool, pub is_account_filtered: bool, } diff --git a/crates/jmap/src/registry/set.rs b/crates/jmap/src/registry/set.rs index de4801b4..1082a644 100644 --- a/crates/jmap/src/registry/set.rs +++ b/crates/jmap/src/registry/set.rs @@ -30,7 +30,7 @@ use jmap_proto::{ object::registry::Registry, request::IntoValid, }; -use jmap_tools::{JsonPointer, JsonPointerItem, Key, Map}; +use jmap_tools::{JsonPointer, JsonPointerItem, Key}; use registry::{ jmap::{JmapValue, JsonPointerPatch, MaybeUnpatched, RegistryValue}, schema::{ @@ -75,8 +75,6 @@ impl RegistrySet for Server { let todo = "list"; // locks for expensive tasks should be longer or renewed // Validate expressions - // Fallback admin password from env or files - // Individual permissions for each object + create/update/destroy let object_flags = object_type.flags(); let is_singleton = (object_flags & OBJ_SINGLETON) != 0; let has_account_id = (object_flags & OBJ_FILTER_ACCOUNT) != 0; @@ -132,7 +130,6 @@ impl RegistrySet for Server { account_id: request.account_id.document_id(), object_type, response, - object_flags, is_tenant_filtered, is_account_filtered, create, @@ -162,7 +159,7 @@ impl RegistrySet for Server { | ObjectType::Imap | ObjectType::InMemoryStore | ObjectType::Jmap - | ObjectType::LocalSettings + | ObjectType::SystemSettings | ObjectType::Metrics | ObjectType::MetricsStore | ObjectType::MtaConnectionStrategy @@ -219,9 +216,7 @@ impl RegistrySet for Server { | ObjectType::MtaMilter | ObjectType::MtaHook | ObjectType::NetworkListener - | ObjectType::Node - | ObjectType::NodeRole - | ObjectType::NodeShard + | ObjectType::ClusterRole | ObjectType::RegistryBundle | ObjectType::SieveSystemScript | ObjectType::SieveUserScript @@ -564,44 +559,6 @@ impl RegistrySetResponse<'_> { } } - fn create( - &mut self, - client_id: String, - result: RegistryWriteResult, - mut object: Map<'static, Property, RegistryValue>, - ) { - match result { - RegistryWriteResult::Success(id) => { - object.insert(Key::Property(Property::Id), RegistryValue::Id(id)); - self.response - .created - .insert(client_id, JmapValue::Object(object)); - } - RegistryWriteResult::NotFound { .. } => { - self.response - .not_created - .append(client_id, SetError::not_found()); - } - err => { - self.response - .not_created - .append(client_id, map_write_error(err)); - } - } - } - - fn update(&mut self, id: Id, result: RegistryWriteResult) { - match result { - RegistryWriteResult::Success(_) => self.response.updated.append(id, None), - RegistryWriteResult::NotFound { .. } => { - self.response.not_updated.append(id, SetError::not_found()); - } - err => { - self.response.not_updated.append(id, map_write_error(err)); - } - } - } - pub fn fail_all(&mut self, error: SetError) { for (client_id, _) in self.create.drain() { self.response.not_created.append(client_id, error.clone()); diff --git a/crates/services/src/task_manager/alarm.rs b/crates/services/src/task_manager/alarm.rs index 3573d60a..c2040238 100644 --- a/crates/services/src/task_manager/alarm.rs +++ b/crates/services/src/task_manager/alarm.rs @@ -109,7 +109,7 @@ async fn send_email_alarm( .caused_by(trc::location!())? .build(); - if !access_token.has_permission(Permission::CalendarAlarms) { + if !access_token.has_permission(Permission::CalendarAlarmsSend) { trc::event!( Calendar(trc::CalendarEvent::AlarmSkipped), Reason = "Account does not have permission to send calendar alarms", diff --git a/crates/services/src/task_manager/maintenance.rs b/crates/services/src/task_manager/maintenance.rs index 2f293f62..f8c4b393 100644 --- a/crates/services/src/task_manager/maintenance.rs +++ b/crates/services/src/task_manager/maintenance.rs @@ -127,7 +127,7 @@ async fn store_maintenance( let todo = "make sure spam samples with their indexes and undelete items are purged as well"; - let mut started = Instant::now(); + let started = Instant::now(); server .store() diff --git a/crates/services/src/task_manager/scheduler.rs b/crates/services/src/task_manager/scheduler.rs index dcbd5d1f..c260e8de 100644 --- a/crates/services/src/task_manager/scheduler.rs +++ b/crates/services/src/task_manager/scheduler.rs @@ -24,7 +24,7 @@ use registry::{ types::EnumImpl, }; use store::write::{BatchBuilder, now}; -use trc::{Collector, MetricType, TaskManagerEvent, TelemetryEvent}; +use trc::{ClusterEvent, Collector, MetricType, TaskManagerEvent, TelemetryEvent}; #[derive(PartialEq, Eq)] struct Action { @@ -40,6 +40,7 @@ enum Event { OtelMetrics, CalculateMetrics, TrainSpamClassifier, + RenewNodeIdLease, // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL @@ -88,6 +89,14 @@ pub fn spawn_task_scheduler(inner: Arc) { Event::PurgeBlobStore, ); + // Node ID lease renewal + if server.core.storage.coordinator.is_enabled() { + queue.schedule( + Instant::now() + server.registry().refresh_node_id_interval(), + Event::RenewNodeIdLease, + ); + } + // Spam classifier training if let Some(train_frequency) = server .core @@ -218,6 +227,24 @@ pub fn spawn_task_scheduler(inner: Arc) { })); } } + Event::RenewNodeIdLease => { + queue.schedule( + Instant::now() + server.registry().refresh_node_id_interval(), + Event::RenewNodeIdLease, + ); + + trc::event!( + Cluster(ClusterEvent::NodeIdRenewed), + Id = server.registry().node_id() + ); + + let server = server.clone(); + tokio::spawn(async move { + if let Err(err) = server.registry().refresh_node_id_lease().await { + trc::error!(err.details("Failed to renew node ID lease")); + } + }); + } Event::OtelMetrics => { if let Some(otel) = &server.core.metrics.otel { queue.schedule(Instant::now() + otel.interval, Event::OtelMetrics); @@ -546,7 +573,7 @@ impl Event { Event::OtelMetrics => "otelMetrics", Event::CalculateMetrics => "calculateMetrics", Event::TrainSpamClassifier => "trainSpamClassifier", - + Event::RenewNodeIdLease => "renewNodeIdLease", // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL diff --git a/crates/store/src/build/registry.rs b/crates/store/src/build/registry.rs index 9963773a..f6285b7a 100644 --- a/crates/store/src/build/registry.rs +++ b/crates/store/src/build/registry.rs @@ -4,47 +4,176 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{RegistryStore, RegistryStoreInner, Store}; -use ahash::AHashSet; -use registry::schema::enums::NodeRole; -use std::path::PathBuf; +use crate::{ + IterateParams, RegistryStore, RegistryStoreInner, Store, U16_LEN, U32_LEN, U64_LEN, ValueKey, + write::{ + BatchBuilder, ValueClass, + assert::AssertValue, + key::{DeserializeBigEndian, KeySerializer}, + now, + }, +}; +use std::{path::PathBuf, time::Duration}; +use trc::AddContext; use utils::snowflake::SnowflakeIdGenerator; +const STALE_NODE_TIMEOUT: u64 = 60 * 60; // 1 hour +const DEAD_NODE_TIMEOUT: u64 = 60 * 60 * 24; // 24 hours + impl RegistryStore { pub async fn init(local: PathBuf) -> Result { - const ERROR_MSG: &str = "Failed to initialize registry"; - + // Create inner store let mut inner = RegistryStoreInner::new(local); // Build store let store = Store::build(inner.read_data_store().await?).await?; - let todo = "obtain node id"; - inner.store = store; - inner.node_id = 0; + // Obtain node id + let mut retry_count = 0; + loop { + let mut found_node_id = false; + let mut batch = BatchBuilder::new(); + let now = now(); + let mut node_ids = Vec::new(); + store + .iterate( + IterateParams::new( + ValueKey::from(ValueClass::NodeId(0)), + ValueKey::from(ValueClass::NodeId(u16::MAX)), + ) + .ascending(), + |key, value| { + if key.len() == U16_LEN * 3 { + let node_id = key.deserialize_be_u16(U32_LEN)?; + let last_renewal = now.saturating_sub(value.deserialize_be_u64(0)?); + let hostname = value + .get(U64_LEN..) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .filter(|text| !text.is_empty()) + .ok_or_else(|| trc::StoreEvent::DataCorruption.into_err())?; + let hash = xxhash_rust::xxh3::xxh3_64(value); - if inner.node_id == 0 { - return Err(format!( - "{ERROR_MSG}: \"LocalSettings\" object has invalid nodeId of 0." - )); + if hostname == inner.env_hostname || last_renewal > STALE_NODE_TIMEOUT { + if !found_node_id { + inner.node_id = node_id; + batch + .assert_value( + ValueClass::NodeId(node_id), + AssertValue::Hash(hash), + ) + .set( + ValueClass::NodeId(node_id), + KeySerializer::new(hostname.len() + U64_LEN) + .write(now) + .write(hostname) + .finalize(), + ); + found_node_id = true; + } else if last_renewal > DEAD_NODE_TIMEOUT { + batch + .assert_value( + ValueClass::NodeId(node_id), + AssertValue::Hash(hash), + ) + .clear(ValueClass::NodeId(node_id)); + } + } else { + node_ids.push(node_id); + } + } + Ok(true) + }, + ) + .await + .map_err(|err| format!("Failed to iterate store: {err}"))?; + + if !found_node_id { + if !node_ids.is_empty() { + node_ids.sort_unstable(); + let mut last_node_id = 0; + for node_id in node_ids { + if node_id > last_node_id { + break; + } + last_node_id = node_id + 1; + } + inner.node_id = last_node_id; + } else { + inner.node_id = 0; + } + + batch + .assert_value(ValueClass::NodeId(inner.node_id), ()) + .set( + ValueClass::NodeId(inner.node_id), + KeySerializer::new(inner.env_hostname.len() + U64_LEN) + .write(now) + .write(&inner.env_hostname) + .finalize(), + ); + } + + match store.write(batch.build_all()).await { + Ok(_) => break, + Err(err) => { + if err.is_assertion_failure() && retry_count < 5 { + retry_count += 1; + continue; + } else { + return Err(format!("Failed to write node id to store: {err}")); + } + } + } } + inner.id_generator = SnowflakeIdGenerator::new(); Ok(Self(inner.into())) } + pub fn node_id(&self) -> u16 { + self.0.node_id + } + + pub fn refresh_node_id_interval(&self) -> Duration { + Duration::from_secs(STALE_NODE_TIMEOUT / 2) + } + + pub async fn refresh_node_id_lease(&self) -> trc::Result<()> { + let mut batch = BatchBuilder::new(); + batch + .assert_value(ValueClass::NodeId(self.0.node_id), ()) + .set( + ValueClass::NodeId(self.0.node_id), + KeySerializer::new(self.0.env_hostname.len() + U64_LEN) + .write(now()) + .write(&self.0.env_hostname) + .finalize(), + ); + self.0 + .store + .write(batch.build_all()) + .await + .caused_by(trc::location!()) + .map(|_| ()) + } + pub fn recovery_admin(&self) -> Option<&(String, String)> { self.0.env_recovery_admin.as_ref() } - pub fn node_roles(&self) -> &AHashSet { - &self.0.env_node_roles + pub fn cluster_role(&self) -> Option<&str> { + self.0.env_cluster_role.as_deref() } - pub fn node_roles_shard(&self) -> u64 { - self.0.env_node_roles_shard_id + pub fn cluster_role_shard(&self) -> u64 { + self.0.env_cluster_role_shard_id } pub fn local_hostname(&self) -> &str { &self.0.env_hostname } + + pub fn is_recovery_mode(&self) -> bool { + self.0.env_recovery_mode + } } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index f74cc140..4061e20d 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -12,7 +12,7 @@ pub mod registry; pub mod search; pub mod write; -use ::registry::schema::enums::{CompressionAlgo, NodeRole}; +use ::registry::schema::enums::CompressionAlgo; pub use ahash; pub use blake3; pub use parking_lot; @@ -23,7 +23,7 @@ use utils::snowflake::SnowflakeIdGenerator; pub use xxhash_rust; use crate::backend::{elastic::ElasticSearchStore, meili::MeiliSearchStore}; -use ahash::{AHashMap, AHashSet}; +use ahash::AHashMap; use backend::{fs::FsStore, http::HttpStore, memory::StaticMemoryStore}; use std::{borrow::Cow, path::PathBuf, sync::Arc}; use write::ValueClass; @@ -207,10 +207,11 @@ pub struct RegistryStore(pub(crate) Arc); pub struct RegistryStoreInner { pub(crate) local_path: PathBuf, pub(crate) store: Store, - pub(crate) node_id: u64, + pub(crate) node_id: u16, + pub(crate) env_recovery_mode: bool, pub(crate) env_recovery_admin: Option<(String, String)>, - pub(crate) env_node_roles: AHashSet, - pub(crate) env_node_roles_shard_id: u64, + pub(crate) env_cluster_role: Option, + pub(crate) env_cluster_role_shard_id: u64, pub(crate) env_hostname: String, pub(crate) id_generator: SnowflakeIdGenerator, } diff --git a/crates/store/src/registry/bootstrap.rs b/crates/store/src/registry/bootstrap.rs index 7ec3444a..fe1e3758 100644 --- a/crates/store/src/registry/bootstrap.rs +++ b/crates/store/src/registry/bootstrap.rs @@ -6,7 +6,10 @@ use crate::{RegistryStore, Store, registry::RegistryObject}; use registry::{ - schema::prelude::{Object, Property}, + schema::{ + prelude::{Object, ObjectType, Property}, + structs::ClusterRole, + }, types::{ ObjectImpl, error::{Error, ValidationError, Warning}, @@ -21,16 +24,50 @@ pub struct Bootstrap { pub errors: Vec, pub warnings: Vec, pub has_fatal_errors: bool, + pub role: Option, } impl Bootstrap { - pub fn new(registry: RegistryStore) -> Self { + pub async fn new(registry: RegistryStore) -> Self { + let mut bp = Self::new_uninitialized(registry); + + let Some(role_name) = bp.registry.cluster_role().map(|r| r.to_string()) else { + return bp; + }; + + for role in bp.list_infallible::().await { + if role.object.name == role_name { + if bp.registry.cluster_role_shard() >= role.object.shard_size { + bp.build_error( + ObjectType::ClusterRole.singleton(), + format!( + "Cluster role \"{role_name}\" has shard size of {}, which is smaller than the configured shard id {}.", + role.object.shard_size, + bp.registry.cluster_role_shard() + ), + ); + } + bp.role = Some(role.object); + return bp; + } + } + + bp.build_error( + ObjectType::ClusterRole.singleton(), + format!("Cluster role \"{role_name}\" not found in registry"), + ); + + bp + } + + pub fn new_uninitialized(registry: RegistryStore) -> Self { Self { data_store: registry.0.store.clone(), registry, errors: Vec::new(), warnings: Vec::new(), has_fatal_errors: false, + role: None, } } @@ -156,7 +193,7 @@ impl Bootstrap { } } - pub fn node_id(&self) -> u64 { + pub fn node_id(&self) -> u16 { self.registry.0.node_id } diff --git a/crates/store/src/registry/local.rs b/crates/store/src/registry/local.rs index a63685f9..6b9e49fc 100644 --- a/crates/store/src/registry/local.rs +++ b/crates/store/src/registry/local.rs @@ -5,10 +5,7 @@ */ use crate::{RegistryStore, RegistryStoreInner, Store}; -use registry::{ - schema::{enums::NodeRole, structs::DataStore}, - types::EnumImpl, -}; +use registry::schema::structs::DataStore; use std::path::PathBuf; use utils::snowflake::SnowflakeIdGenerator; @@ -19,38 +16,26 @@ impl RegistryStoreInner { store: Store::None, id_generator: SnowflakeIdGenerator::new(), node_id: 0, - env_recovery_admin: std::env::var("STALWART_RECOVERY_ACCOUNT") + env_recovery_mode: std::env::var("STALWART_RECOVERY_MODE") + .ok() + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false), + env_recovery_admin: std::env::var("STALWART_ADMIN_ACCOUNT") .ok() .filter(|a| !a.is_empty()) .and_then(|a| { - std::env::var("STALWART_RECOVERY_PASS") + std::env::var("STALWART_ADMIN_PASS") .ok() .filter(|p| !p.is_empty()) .map(|p| (a, p)) }), - env_node_roles: std::env::var("STALWART_ROLES") + env_cluster_role: std::env::var("STALWART_ROLE") .ok() - .map(|roles| { - roles - .split(',') - .map(|r| r.trim()) - .filter(|r| !r.is_empty()) - .filter_map(|r| { - let role = NodeRole::parse(r); - if role.is_none() { - eprintln!( - "Invalid node role specified in STALWART_NODE_ROLES: {r}" - ); - } - role - }) - .collect() - }) - .unwrap_or_default(), - env_node_roles_shard_id: std::env::var("STALWART_ROLES_SHARD") + .filter(|r| !r.is_empty()), + env_cluster_role_shard_id: std::env::var("STALWART_ROLE_SHARD") .ok() - .and_then(|id| id.parse::().ok()) - .unwrap_or(1), + .and_then(|id| id.parse::().ok().and_then(|v| v.checked_sub(1))) + .unwrap_or(0), env_hostname: std::env::var("STALWART_HOSTNAME") .ok() .filter(|h| !h.is_empty()) diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 14ba4cde..c0a017ee 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -354,6 +354,7 @@ impl ValueClass { ValueClass::ChangeId => serializer.write(account_id), ValueClass::Quota => serializer.write(account_id).write(u8::MAX), ValueClass::TenantQuota(tenant_id) => serializer.write(*tenant_id).write(u8::MAX - 1), + ValueClass::NodeId(node_id) => serializer.write(u32::MAX).write(*node_id), ValueClass::ShareNotification { notification_id, notify_account_id, @@ -527,6 +528,7 @@ impl ValueClass { ValueClass::DocumentId | ValueClass::Quota | ValueClass::TenantQuota(_) => U32_LEN + 1, ValueClass::ChangeId => U32_LEN, ValueClass::ShareNotification { .. } => U32_LEN + U64_LEN + 1, + ValueClass::NodeId(_) => (U16_LEN * 3) + 1, ValueClass::SearchIndex(v) => match &v.typ { SearchIndexType::Term { hash, .. } => U64_LEN + hash.len() + 2, SearchIndexType::Index { field, .. } => 1 + field.data.len() + U64_LEN, @@ -576,6 +578,7 @@ impl ValueClass { } RegistryClass::IdCounter { .. } => SUBSPACE_COUNTER, }, + ValueClass::NodeId(_) => SUBSPACE_REGISTRY_PK, ValueClass::InMemory(lookup) => match lookup { InMemoryClass::Key(_) => SUBSPACE_IN_MEMORY_VALUE, InMemoryClass::Counter(_) => SUBSPACE_IN_MEMORY_COUNTER, diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 0df4c785..8d3aa777 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -183,6 +183,7 @@ pub enum ValueClass { ChangeId, Quota, TenantQuota(u32), + NodeId(u16), } #[derive(Debug, PartialEq, Clone, Eq, Hash)] diff --git a/crates/trc/src/event/enums.rs b/crates/trc/src/event/enums.rs index 7b56fc9f..8c8abc07 100644 --- a/crates/trc/src/event/enums.rs +++ b/crates/trc/src/event/enums.rs @@ -129,6 +129,7 @@ pub enum CalendarEvent { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u16)] pub enum ClusterEvent { + Startup = 278, SubscriberStart = 39, SubscriberStop = 40, SubscriberError = 41, @@ -139,6 +140,7 @@ pub enum ClusterEvent { MessageReceived = 46, MessageSkipped = 47, MessageInvalid = 49, + NodeIdRenewed = 275, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -399,8 +401,6 @@ pub enum MailAuthEvent { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u16)] pub enum ManageEvent { - Reserved1 = 278, - Reserved2 = 275, Reserved3 = 276, Reserved4 = 279, Reserved5 = 280, diff --git a/crates/trc/src/event/enums_impl.rs b/crates/trc/src/event/enums_impl.rs index 82cfdae2..f397b50f 100644 --- a/crates/trc/src/event/enums_impl.rs +++ b/crates/trc/src/event/enums_impl.rs @@ -57,6 +57,7 @@ impl EventType { b"calendar.itip-message-sent" => EventType::Calendar(CalendarEvent::ItipMessageSent), b"calendar.itip-message-received" => EventType::Calendar(CalendarEvent::ItipMessageReceived), b"calendar.itip-message-error" => EventType::Calendar(CalendarEvent::ItipMessageError), + b"cluster.startup" => EventType::Cluster(ClusterEvent::Startup), b"cluster.subscriber-start" => EventType::Cluster(ClusterEvent::SubscriberStart), b"cluster.subscriber-stop" => EventType::Cluster(ClusterEvent::SubscriberStop), b"cluster.subscriber-error" => EventType::Cluster(ClusterEvent::SubscriberError), @@ -67,6 +68,7 @@ impl EventType { b"cluster.message-received" => EventType::Cluster(ClusterEvent::MessageReceived), b"cluster.message-skipped" => EventType::Cluster(ClusterEvent::MessageSkipped), b"cluster.message-invalid" => EventType::Cluster(ClusterEvent::MessageInvalid), + b"cluster.node-id-renewed" => EventType::Cluster(ClusterEvent::NodeIdRenewed), b"dane.authentication-success" => EventType::Dane(DaneEvent::AuthenticationSuccess), b"dane.authentication-failure" => EventType::Dane(DaneEvent::AuthenticationFailure), b"dane.no-certificates-found" => EventType::Dane(DaneEvent::NoCertificatesFound), @@ -257,8 +259,6 @@ impl EventType { b"mail-auth.dns-record-not-found" => EventType::MailAuth(MailAuthEvent::DnsRecordNotFound), b"mail-auth.dns-invalid-record-type" => EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType), b"mail-auth.policy-not-aligned" => EventType::MailAuth(MailAuthEvent::PolicyNotAligned), - b"manage.reserved1" => EventType::Manage(ManageEvent::Reserved1), - b"manage.reserved2" => EventType::Manage(ManageEvent::Reserved2), b"manage.reserved3" => EventType::Manage(ManageEvent::Reserved3), b"manage.reserved4" => EventType::Manage(ManageEvent::Reserved4), b"manage.reserved5" => EventType::Manage(ManageEvent::Reserved5), @@ -665,6 +665,7 @@ impl EventType { "calendar.itip-message-received" } EventType::Calendar(CalendarEvent::ItipMessageError) => "calendar.itip-message-error", + EventType::Cluster(ClusterEvent::Startup) => "cluster.startup", EventType::Cluster(ClusterEvent::SubscriberStart) => "cluster.subscriber-start", EventType::Cluster(ClusterEvent::SubscriberStop) => "cluster.subscriber-stop", EventType::Cluster(ClusterEvent::SubscriberError) => "cluster.subscriber-error", @@ -677,6 +678,7 @@ impl EventType { EventType::Cluster(ClusterEvent::MessageReceived) => "cluster.message-received", EventType::Cluster(ClusterEvent::MessageSkipped) => "cluster.message-skipped", EventType::Cluster(ClusterEvent::MessageInvalid) => "cluster.message-invalid", + EventType::Cluster(ClusterEvent::NodeIdRenewed) => "cluster.node-id-renewed", EventType::Dane(DaneEvent::AuthenticationSuccess) => "dane.authentication-success", EventType::Dane(DaneEvent::AuthenticationFailure) => "dane.authentication-failure", EventType::Dane(DaneEvent::NoCertificatesFound) => "dane.no-certificates-found", @@ -915,8 +917,6 @@ impl EventType { "mail-auth.dns-invalid-record-type" } EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => "mail-auth.policy-not-aligned", - EventType::Manage(ManageEvent::Reserved1) => "manage.reserved1", - EventType::Manage(ManageEvent::Reserved2) => "manage.reserved2", EventType::Manage(ManageEvent::Reserved3) => "manage.reserved3", EventType::Manage(ManageEvent::Reserved4) => "manage.reserved4", EventType::Manage(ManageEvent::Reserved5) => "manage.reserved5", @@ -1386,6 +1386,7 @@ impl EventType { EventType::Calendar(CalendarEvent::ItipMessageSent) => 583, EventType::Calendar(CalendarEvent::ItipMessageReceived) => 584, EventType::Calendar(CalendarEvent::ItipMessageError) => 585, + EventType::Cluster(ClusterEvent::Startup) => 278, EventType::Cluster(ClusterEvent::SubscriberStart) => 39, EventType::Cluster(ClusterEvent::SubscriberStop) => 40, EventType::Cluster(ClusterEvent::SubscriberError) => 41, @@ -1396,6 +1397,7 @@ impl EventType { EventType::Cluster(ClusterEvent::MessageReceived) => 46, EventType::Cluster(ClusterEvent::MessageSkipped) => 47, EventType::Cluster(ClusterEvent::MessageInvalid) => 49, + EventType::Cluster(ClusterEvent::NodeIdRenewed) => 275, EventType::Dane(DaneEvent::AuthenticationSuccess) => 67, EventType::Dane(DaneEvent::AuthenticationFailure) => 66, EventType::Dane(DaneEvent::NoCertificatesFound) => 69, @@ -1586,8 +1588,6 @@ impl EventType { EventType::MailAuth(MailAuthEvent::DnsRecordNotFound) => 250, EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType) => 249, EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => 255, - EventType::Manage(ManageEvent::Reserved1) => 278, - EventType::Manage(ManageEvent::Reserved2) => 275, EventType::Manage(ManageEvent::Reserved3) => 276, EventType::Manage(ManageEvent::Reserved4) => 279, EventType::Manage(ManageEvent::Reserved5) => 280, @@ -1987,6 +1987,7 @@ impl EventType { 583 => Some(EventType::Calendar(CalendarEvent::ItipMessageSent)), 584 => Some(EventType::Calendar(CalendarEvent::ItipMessageReceived)), 585 => Some(EventType::Calendar(CalendarEvent::ItipMessageError)), + 278 => Some(EventType::Cluster(ClusterEvent::Startup)), 39 => Some(EventType::Cluster(ClusterEvent::SubscriberStart)), 40 => Some(EventType::Cluster(ClusterEvent::SubscriberStop)), 41 => Some(EventType::Cluster(ClusterEvent::SubscriberError)), @@ -1997,6 +1998,7 @@ impl EventType { 46 => Some(EventType::Cluster(ClusterEvent::MessageReceived)), 47 => Some(EventType::Cluster(ClusterEvent::MessageSkipped)), 49 => Some(EventType::Cluster(ClusterEvent::MessageInvalid)), + 275 => Some(EventType::Cluster(ClusterEvent::NodeIdRenewed)), 67 => Some(EventType::Dane(DaneEvent::AuthenticationSuccess)), 66 => Some(EventType::Dane(DaneEvent::AuthenticationFailure)), 69 => Some(EventType::Dane(DaneEvent::NoCertificatesFound)), @@ -2205,8 +2207,6 @@ impl EventType { 250 => Some(EventType::MailAuth(MailAuthEvent::DnsRecordNotFound)), 249 => Some(EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType)), 255 => Some(EventType::MailAuth(MailAuthEvent::PolicyNotAligned)), - 278 => Some(EventType::Manage(ManageEvent::Reserved1)), - 275 => Some(EventType::Manage(ManageEvent::Reserved2)), 276 => Some(EventType::Manage(ManageEvent::Reserved3)), 279 => Some(EventType::Manage(ManageEvent::Reserved4)), 280 => Some(EventType::Manage(ManageEvent::Reserved5)), @@ -2650,6 +2650,7 @@ impl EventType { EventType::Calendar(CalendarEvent::AlarmSent) => Level::Info, EventType::Calendar(CalendarEvent::ItipMessageSent) => Level::Info, EventType::Calendar(CalendarEvent::ItipMessageReceived) => Level::Info, + EventType::Cluster(ClusterEvent::Startup) => Level::Info, EventType::Cluster(ClusterEvent::SubscriberStart) => Level::Info, EventType::Cluster(ClusterEvent::SubscriberStop) => Level::Info, EventType::Cluster(ClusterEvent::PublisherStart) => Level::Info, @@ -2973,6 +2974,7 @@ impl EventType { "Calendar iTIP message received" } EventType::Calendar(CalendarEvent::ItipMessageError) => "iTIP message error", + EventType::Cluster(ClusterEvent::Startup) => "Clustering enabled", EventType::Cluster(ClusterEvent::SubscriberStart) => "PubSub subscriber started", EventType::Cluster(ClusterEvent::SubscriberStop) => "PubSub subscriber stopped", EventType::Cluster(ClusterEvent::SubscriberError) => "PubSub subscriber error", @@ -2985,6 +2987,7 @@ impl EventType { EventType::Cluster(ClusterEvent::MessageReceived) => "PubSub message received", EventType::Cluster(ClusterEvent::MessageSkipped) => "PubSub message skipped", EventType::Cluster(ClusterEvent::MessageInvalid) => "Invalid PubSub message", + EventType::Cluster(ClusterEvent::NodeIdRenewed) => "Node ID renewed", EventType::Dane(DaneEvent::AuthenticationSuccess) => "DANE authentication successful", EventType::Dane(DaneEvent::AuthenticationFailure) => "DANE authentication failed", EventType::Dane(DaneEvent::NoCertificatesFound) => "No certificates found for DANE", @@ -3221,8 +3224,6 @@ impl EventType { EventType::MailAuth(MailAuthEvent::DnsRecordNotFound) => "DNS record not found", EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType) => "Invalid DNS record type", EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => "Policy not aligned", - EventType::Manage(ManageEvent::Reserved1) => "Missing parameter", - EventType::Manage(ManageEvent::Reserved2) => "Record already exists", EventType::Manage(ManageEvent::Reserved3) => "Assertion failed", EventType::Manage(ManageEvent::Reserved4) => "Resource not found", EventType::Manage(ManageEvent::Reserved5) => "Management operation not supported", @@ -3728,6 +3729,9 @@ impl EventType { EventType::Calendar(CalendarEvent::ItipMessageError) => { "An error occurred while processing an iTIP/iMIP message" } + EventType::Cluster(ClusterEvent::Startup) => { + "The clustering subsystem has been initialized" + } EventType::Cluster(ClusterEvent::SubscriberStart) => { "The PubSub subscriber has started" } @@ -3752,6 +3756,7 @@ impl EventType { EventType::Cluster(ClusterEvent::MessageInvalid) => { "An invalid message was received from the PubSub server" } + EventType::Cluster(ClusterEvent::NodeIdRenewed) => "The node ID lease has been renewed", EventType::Dane(DaneEvent::AuthenticationSuccess) => "Successful DANE authentication", EventType::Dane(DaneEvent::AuthenticationFailure) => "Failed DANE authentication", EventType::Dane(DaneEvent::NoCertificatesFound) => { @@ -4086,10 +4091,6 @@ impl EventType { "The DNS record type is invalid" } EventType::MailAuth(MailAuthEvent::PolicyNotAligned) => "The policy is not aligned", - EventType::Manage(ManageEvent::Reserved1) => "A parameter is missing", - EventType::Manage(ManageEvent::Reserved2) => { - "A record with the same name already exists" - } EventType::Manage(ManageEvent::Reserved3) => "A management assertion has failed", EventType::Manage(ManageEvent::Reserved4) => "The managed resource was not found", EventType::Manage(ManageEvent::Reserved5) => { @@ -4855,8 +4856,6 @@ impl EventType { EventType::Limit(LimitEvent::BlobQuota) => "Blob quota exceeded", EventType::Limit(LimitEvent::TenantQuota) => "Tenant quota exceeded", EventType::Limit(LimitEvent::TooManyRequests) => "Too many requests", - EventType::Manage(ManageEvent::Reserved1) => "Missing parameter", - EventType::Manage(ManageEvent::Reserved2) => "Already exists", EventType::Manage(ManageEvent::Reserved3) => "Assertion failed", EventType::Manage(ManageEvent::Reserved4) => "Not found", EventType::Manage(ManageEvent::Reserved5) => "Operation not supported", @@ -5092,6 +5091,7 @@ impl EventType { EventType::Calendar(CalendarEvent::ItipMessageSent), EventType::Calendar(CalendarEvent::ItipMessageReceived), EventType::Calendar(CalendarEvent::ItipMessageError), + EventType::Cluster(ClusterEvent::Startup), EventType::Cluster(ClusterEvent::SubscriberStart), EventType::Cluster(ClusterEvent::SubscriberStop), EventType::Cluster(ClusterEvent::SubscriberError), @@ -5102,6 +5102,7 @@ impl EventType { EventType::Cluster(ClusterEvent::MessageReceived), EventType::Cluster(ClusterEvent::MessageSkipped), EventType::Cluster(ClusterEvent::MessageInvalid), + EventType::Cluster(ClusterEvent::NodeIdRenewed), EventType::Dane(DaneEvent::AuthenticationSuccess), EventType::Dane(DaneEvent::AuthenticationFailure), EventType::Dane(DaneEvent::NoCertificatesFound), @@ -5292,8 +5293,6 @@ impl EventType { EventType::MailAuth(MailAuthEvent::DnsRecordNotFound), EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType), EventType::MailAuth(MailAuthEvent::PolicyNotAligned), - EventType::Manage(ManageEvent::Reserved1), - EventType::Manage(ManageEvent::Reserved2), EventType::Manage(ManageEvent::Reserved3), EventType::Manage(ManageEvent::Reserved4), EventType::Manage(ManageEvent::Reserved5),