/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::registry::{ mapping::{RegistryGetResponse, RegistrySetResponse, map_bootstrap_error}, set::map_write_error, }; use common::{ DATABASE_SCHEMA_VERSION, Server, config::storage::Storage, network::acme::account::acme_create_account, psl, }; use directory::core::secret::hash_secret; use jmap_proto::error::set::{SetError, SetErrorType}; use jmap_tools::{JsonPointer, JsonPointerItem, Key}; use rand::{Rng, distr::Alphanumeric, rng}; use registry::{ jmap::{IntoValue, JmapValue, JsonPointerPatch, RegistryJsonPatch}, schema::{ enums::{AcmeChallengeType, DnsRecordType}, prelude::{Object, Property}, structs::{ Account, AcmeProvider, BlobStore, Bootstrap, CertificateManagement, CertificateManagementProperties, Credential, DataStore, Directory, DirectoryBootstrap, DkimManagement, DkimManagementProperties, DnsManagement, DnsManagementProperties, DnsServer, DnsServerBootstrap, Domain, InMemoryStore, PasswordCredential, RocksDbStore, SearchStore, SystemSettings, Task, TaskDnsManagement, TaskDomainManagement, TaskStatus, Tracer, TracerLog, UserAccount, UserRoles, }, }, types::{ObjectImpl, list::List, map::Map}, }; use store::{ RegistryStore, SUBSPACE_PROPERTY, Store, registry::write::{RegistryWrite, RegistryWriteResult}, write::{AnyKey, BatchBuilder}, }; use types::id::Id; use utils::is_valid_domain; pub(crate) async fn bootstrap_get( mut get: RegistryGetResponse<'_>, ) -> trc::Result> { if !get.server.registry().is_bootstrap_mode() { get.not_found(Id::singleton()); return Ok(get); } let mut ids = get .ids .take() .unwrap_or_else(|| vec![Id::singleton()]) .into_iter(); for id in ids.by_ref() { if id == Id::singleton() { get.insert( Id::singleton(), build_default_bootstrap(get.server).into_value(), ); break; } else { get.not_found(id); } } get.response.not_found.extend(ids); Ok(get) } pub(crate) async fn bootstrap_set( mut set: RegistrySetResponse<'_>, ) -> trc::Result> { if !set.server.registry().is_bootstrap_mode() { set.fail_all_create("This operation is only allowed bootstrap mode"); set.fail_all_update("This operation is only allowed bootstrap mode"); set.fail_all_destroy("This operation is only allowed bootstrap mode"); return Ok(set); } set.fail_all_create("Bootstrap objects can only be updated"); set.fail_all_destroy("Bootstrap objects cannot be deleted"); let mut bootstrap = build_default_bootstrap(set.server); 'outer: for (id, value) in set.update.drain(..) { if id != Id::singleton() { set.response.not_updated.append(id, SetError::not_found()); continue; } for (key, value) in value.into_expanded_object() { if let Key::Property(property) = key { let ptr = JsonPointer::new(vec![JsonPointerItem::Key(Key::Property(property))]); if let Err(err) = bootstrap.patch(JsonPointerPatch::new(&ptr).with_create(false), value) { set.response.not_updated.append(id, err.into()); break 'outer; } } else { set.response.not_updated.append( id, SetError::invalid_properties().with_property(key.into_owned()), ); break 'outer; } } let mut validation_errors = Vec::new(); if !bootstrap.validate(&mut validation_errors) { set.response.not_updated.append( id, SetError::new(SetErrorType::ValidationFailed) .with_validation_errors(validation_errors), ); break; } // Validate domain name and hostname let server_hostname = bootstrap.server_hostname.trim().to_lowercase(); let domain_name = bootstrap.default_domain.trim().to_lowercase(); if !is_valid_domain(&server_hostname) { set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::ServerHostname) .with_description("Invalid server hostname"), ); break; } if !is_valid_domain(&domain_name) { set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::DefaultDomain) .with_description("Invalid default domain"), ); break; } // Build store let store = match Store::build(bootstrap.data_store.clone()).await { Ok(store) => store, Err(err) => { set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::DataStore) .with_description(err), ); break; } }; // Create tables (SQL only) if let Err(err) = store.create_tables().await { set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::DataStore) .with_description(format!("Failed to initialize data store: {err}")), ); break; } // Make sure this is blank deployment match store .get_value::(AnyKey { subspace: SUBSPACE_PROPERTY, key: vec![0u8], }) .await { Ok(None) => {} Ok(Some(DATABASE_SCHEMA_VERSION)) => { set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::DataStore) .with_description("The selected data store has already been initialized."), ); break; } Ok(Some(_)) => { set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::DataStore) .with_description(concat!( "The selected data store contains information from an older version. ", "Please follow the upgrade instructions at ", "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" )), ); break; } Err(err) => { trc::error!(err.caused_by(trc::location!())); set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::DataStore) .with_description( "Failed to initialize data store, check logs for details.", ), ); break; } }; // Validate stores and registry let tmp_registry = set.server.registry(); for (property, object) in [ ( Property::BlobStore, Some(bootstrap.blob_store.clone().into()), ), ( Property::SearchStore, Some(bootstrap.search_store.clone().into()), ), ( Property::InMemoryStore, Some(bootstrap.in_memory_store.clone().into()), ), ( Property::Directory, map_directory(&bootstrap.directory).map(Into::into), ), ( Property::DnsServer, map_dns_server(&bootstrap.dns_server).map(Into::into), ), (Property::Tracer, Some(bootstrap.tracer.clone().into())), ] { if let Some(object) = object { match write_object(tmp_registry, &object).await { Ok(_) => {} Err(err) => { set.response .not_updated .append(id, err.with_property(property)); break 'outer; } } } } let mut bp_check = store::registry::bootstrap::Bootstrap::new_uninitialized(tmp_registry.clone()); let _ = Storage::parse(&mut bp_check).await; if !bp_check.errors.is_empty() { set.response .not_updated .append(id, map_bootstrap_error(bp_check.errors)); break 'outer; } // Create inner store let registry = RegistryStore::from_inner_bootstrapped(set.server.registry().initialize_inner(store)); // Save datastore if let Err(err) = registry.write_data_store(&bootstrap.data_store).await { let details = format!("Failed to save data store settings: {err}"); trc::error!(err.caused_by(trc::location!())); set.response.not_updated.append( id, SetError::invalid_properties() .with_property(Property::DataStore) .with_description(details), ); break; } // Write stores and traces to registry for (property, object) in [ (Property::BlobStore, bootstrap.blob_store.into()), (Property::SearchStore, bootstrap.search_store.into()), (Property::InMemoryStore, bootstrap.in_memory_store.into()), (Property::Tracer, bootstrap.tracer.into()), ] { match write_object(®istry, &object).await { Ok(_) => {} Err(err) => { set.response .not_updated .append(id, err.with_property(property)); break 'outer; } } } // Write directory and dns server to registry let mut directory_id = None; let mut dns_server_id = None; if let Some(directory) = map_directory(&bootstrap.directory) { match write_object(®istry, &directory.into()).await { Ok(id) => { directory_id = Some(id); } Err(err) => { set.response .not_updated .append(id, err.with_property(Property::Directory)); break 'outer; } } } if let Some(dns_server) = map_dns_server(&bootstrap.dns_server) { match write_object(®istry, &dns_server.into()).await { Ok(id) => { dns_server_id = Some(id); } Err(err) => { set.response .not_updated .append(id, err.with_property(Property::DnsServer)); break 'outer; } } } // Create ACME provider if needed let mut acme_provider_id = None; if bootstrap.request_tls_certificate { let mut acme_provider = AcmeProvider { challenge_type: if dns_server_id.is_some() { AcmeChallengeType::Dns01 } else { AcmeChallengeType::TlsAlpn01 }, contact: Map::new(vec![format!("postmaster@{domain_name}")]), #[cfg(not(feature = "dev_mode"))] directory: "https://acme-v02.api.letsencrypt.org/directory".to_string(), #[cfg(feature = "dev_mode")] directory: "https://localhost:14000/dir".to_string(), ..Default::default() }; if let Err(err) = acme_create_account(&mut acme_provider, None).await { trc::error!(trc::ResourceEvent::Error.into_err().reason(err)); } else { match write_object(®istry, &acme_provider.into()).await { Ok(id) => { acme_provider_id = Some(id); } Err(err) => { set.response .not_updated .append(id, err.with_property(Property::DataStore)); break 'outer; } } } } // Create domain let publish_records = Map::new(vec![ DnsRecordType::Dkim, DnsRecordType::Spf, DnsRecordType::Dmarc, DnsRecordType::Srv, DnsRecordType::MtaSts, DnsRecordType::TlsRpt, DnsRecordType::AutoConfig, DnsRecordType::AutoConfigLegacy, DnsRecordType::AutoDiscover, ]); let domain = Domain { name: domain_name.clone(), is_enabled: true, certificate_management: if let Some(acme_provider_id) = acme_provider_id { CertificateManagement::Automatic(CertificateManagementProperties { acme_provider_id, subject_alternative_names: Default::default(), }) } else { CertificateManagement::Manual }, dkim_management: if bootstrap.generate_dkim_keys { DkimManagement::Automatic(DkimManagementProperties::default()) } else { DkimManagement::Manual }, dns_management: if let Some(dns_server_id) = dns_server_id { DnsManagement::Automatic(DnsManagementProperties { dns_server_id, origin: None, publish_records: publish_records.clone(), }) } else { DnsManagement::Manual }, directory_id, ..Default::default() }; let domain_id = match write_object(®istry, &domain.into()).await { Ok(id) => id, Err(err) => { set.response .not_updated .append(id, err.with_property(Property::DefaultDomain)); break 'outer; } }; // Write system settings let system_settings = SystemSettings { default_hostname: bootstrap.server_hostname, default_domain_id: domain_id, ..Default::default() }; match write_object(®istry, &system_settings.into()).await { Ok(_) => {} Err(err) => { set.response .not_updated .append(id, err.with_property(Property::DefaultDomain)); break 'outer; } } // Create tasks let mut batch = BatchBuilder::new(); if dns_server_id.is_some() { batch.schedule_task(Task::DnsManagement(TaskDnsManagement { domain_id, update_records: publish_records, on_success_renew_certificate: acme_provider_id.is_some(), status: TaskStatus::now(), })); } else if acme_provider_id.is_some() { batch.schedule_task(Task::AcmeRenewal(TaskDomainManagement { domain_id, status: TaskStatus::now(), })); } if bootstrap.generate_dkim_keys { batch.schedule_task(Task::DkimManagement(TaskDomainManagement { domain_id, status: TaskStatus::now(), })); } if !batch.is_empty() { match registry.store().write(batch.build_all()).await { Ok(_) => {} Err(err) => { trc::error!(err.caused_by(trc::location!())); } } } // Create admin account let mut response = None; if directory_id.is_none() { let secret = rng() .sample_iter(Alphanumeric) .take(16) .map(char::from) .collect::(); let account = Account::User(UserAccount { name: "admin".to_string(), domain_id, credentials: List::from_iter([Credential::Password(PasswordCredential { credential_id: Id::new(0), secret: hash_secret( set.server.core.network.security.password_hash_algorithm, secret.clone().into_bytes(), ) .await .unwrap_or_default(), ..Default::default() })]), roles: UserRoles::Admin, description: "System administrator".to_string().into(), ..Default::default() }); match write_object(®istry, &account.into()).await { Ok(_) => { response = Some(JmapValue::Object(jmap_tools::Map::from_iter([ ( Key::Property(Property::Username), JmapValue::Str(format!("admin@{domain_name}").into()), ), ( Key::Property(Property::Secret), JmapValue::Str(secret.into()), ), ]))); } Err(err) => { set.response .not_updated .append(id, err.with_property(Property::DefaultDomain)); break 'outer; } } } set.response.updated.append(id, response); break; } Ok(set) } async fn write_object(registry: &RegistryStore, object: &Object) -> Result> { match registry.write(RegistryWrite::insert(object)).await { Ok(RegistryWriteResult::Success(id)) => Ok(id), Ok(err) => Err(map_write_error(err)), Err(err) => { let details = format!("Failed to save settings: {err}"); trc::error!(err.caused_by(trc::location!())); Err(SetError::invalid_properties().with_description(details)) } } } fn map_directory(directory: &DirectoryBootstrap) -> Option { match directory { DirectoryBootstrap::Internal => None, DirectoryBootstrap::Ldap(ldap_directory) => Directory::Ldap(ldap_directory.clone()).into(), DirectoryBootstrap::Sql(sql_directory) => Directory::Sql(sql_directory.clone()).into(), DirectoryBootstrap::Oidc(oidc_directory) => Directory::Oidc(oidc_directory.clone()).into(), } } fn map_dns_server(dns_server: &DnsServerBootstrap) -> Option { match dns_server { DnsServerBootstrap::Manual => None, DnsServerBootstrap::Tsig(dns_server_tsig) => { DnsServer::Tsig(dns_server_tsig.clone()).into() } DnsServerBootstrap::Sig0(dns_server_sig0) => { DnsServer::Sig0(dns_server_sig0.clone()).into() } DnsServerBootstrap::Cloudflare(dns_server_cloudflare) => { DnsServer::Cloudflare(dns_server_cloudflare.clone()).into() } DnsServerBootstrap::DigitalOcean(dns_server_cloud) => { DnsServer::DigitalOcean(dns_server_cloud.clone()).into() } DnsServerBootstrap::DeSEC(dns_server_cloud) => { DnsServer::DeSEC(dns_server_cloud.clone()).into() } DnsServerBootstrap::Ovh(dns_server_ovh) => DnsServer::Ovh(dns_server_ovh.clone()).into(), DnsServerBootstrap::Bunny(dns_server_cloud) => { DnsServer::Bunny(dns_server_cloud.clone()).into() } DnsServerBootstrap::Porkbun(dns_server_porkbun) => { DnsServer::Porkbun(dns_server_porkbun.clone()).into() } DnsServerBootstrap::Dnsimple(dns_server_dnsimple) => { DnsServer::Dnsimple(dns_server_dnsimple.clone()).into() } DnsServerBootstrap::Spaceship(dns_server_spaceship) => { DnsServer::Spaceship(dns_server_spaceship.clone()).into() } DnsServerBootstrap::Route53(dns_server_route53) => { DnsServer::Route53(dns_server_route53.clone()).into() } DnsServerBootstrap::GoogleCloudDns(dns_server_google_cloud_dns) => { DnsServer::GoogleCloudDns(dns_server_google_cloud_dns.clone()).into() } } } fn build_default_bootstrap(server: &Server) -> Bootstrap { let server_hostname = server.registry().local_hostname().to_string(); let default_domain = psl::domain_str(&server_hostname) .unwrap_or("example.org") .to_string(); Bootstrap { data_store: DataStore::RocksDb(RocksDbStore { path: "/var/lib/stalwart/".to_string(), ..Default::default() }), blob_store: BlobStore::Default, search_store: SearchStore::Default, in_memory_store: InMemoryStore::Default, directory: DirectoryBootstrap::Internal, tracer: Tracer::Log(TracerLog { path: "/var/log/stalwart/".to_string(), prefix: "stalwart".to_string(), ansi: true, enable: true, ..Default::default() }), server_hostname, default_domain, request_tls_certificate: true, generate_dkim_keys: true, dns_server: DnsServerBootstrap::Manual, } }