write_reload_target sent AllowedIp writes to the blocked-IP reload, but
that reload rebuilds only BlockedIps. Allowed IPs are parsed into the
core's security settings (Security::parse), which only a full reload
rebuilds, so an AllowedIp write reported x:settingsReload applied: true
while the change wasn't live until the next full reload.
AllowedIp now maps to the full reload, like the other settings objects;
BlockedIp keeps its targeted reload.
system::auto_reload::settings_reload_tests now creates an allowed IP
over JMAP and checks that is_ip_allowed sees it with no ReloadSettings,
and that destroying it takes it out again. On main it fails ("allowed
IP not in the running settings").
221 lines
6.8 KiB
Rust
221 lines
6.8 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
// inbuxa: a registry write to an object the running settings are built from
|
|
// applies without an x:Action ReloadSettings, and the set response says so.
|
|
|
|
use crate::utils::{
|
|
jmap::JmapResponse,
|
|
server::{TestServer, TestServerBuilder},
|
|
};
|
|
use common::BuildServer;
|
|
use registry::{
|
|
schema::{
|
|
enums::TracingLevel,
|
|
prelude::ObjectType,
|
|
structs::{
|
|
AllowedIp, CertificateManagement, DkimManagement, DnsManagement, Domain, Expression,
|
|
MtaDeliverySchedule, MtaStageAuth, MtaVirtualQueue, Tracer, TracerStdout,
|
|
},
|
|
},
|
|
types::ipmask::IpAddrOrMask,
|
|
};
|
|
use serde_json::Value;
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
pub async fn settings_reload_tests() {
|
|
let mut test = TestServerBuilder::new("settings_reload_tests")
|
|
.await
|
|
.with_default_listeners()
|
|
.await
|
|
.with_object(MtaStageAuth {
|
|
require: Expression {
|
|
else_: "false".to_string(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.build()
|
|
.await;
|
|
|
|
let admin = test
|
|
.create_user_account(
|
|
"admin",
|
|
"[email protected]",
|
|
"these_pretzels_are_making_me_thirsty",
|
|
&[],
|
|
"Admin",
|
|
)
|
|
.await;
|
|
test.account("admin")
|
|
.assign_roles_to_account(admin.id(), &["user", "system"])
|
|
.await;
|
|
test.insert_account(admin);
|
|
|
|
test_write_applies(&test).await;
|
|
|
|
if test.is_reset() {
|
|
test.temp_dir.delete();
|
|
}
|
|
}
|
|
|
|
async fn test_write_applies(test: &TestServer) {
|
|
println!("Running settings reload after registry writes...");
|
|
let admin = test.account("[email protected]");
|
|
|
|
// A delivery schedule is in use as soon as it is saved
|
|
let response = admin
|
|
.registry_create([MtaVirtualQueue {
|
|
name: "autorld".into(),
|
|
threads_per_node: 2,
|
|
description: None,
|
|
}])
|
|
.await;
|
|
assert_applied(&response);
|
|
let queue_id = response.created_id(0);
|
|
assert!(!has_schedule(test, "autoreload-schedule"));
|
|
let response = admin
|
|
.registry_create([MtaDeliverySchedule {
|
|
name: "autoreload-schedule".into(),
|
|
queue_id,
|
|
..Default::default()
|
|
}])
|
|
.await;
|
|
assert_applied(&response);
|
|
assert!(has_schedule(test, "autoreload-schedule"));
|
|
|
|
// Destroyed, it's gone at once too
|
|
let schedule_id = response.created_id(0);
|
|
let response = admin
|
|
.registry_destroy(ObjectType::MtaDeliverySchedule, [schedule_id])
|
|
.await;
|
|
assert_applied(&response);
|
|
assert!(!has_schedule(test, "autoreload-schedule"));
|
|
|
|
// Concurrent writes all end up in the running settings
|
|
let names = (0..8)
|
|
.map(|i| format!("autoreload-{i}"))
|
|
.collect::<Vec<_>>();
|
|
let mut writes = Vec::new();
|
|
for name in &names {
|
|
writes.push(admin.registry_create([MtaDeliverySchedule {
|
|
name: name.clone(),
|
|
queue_id,
|
|
..Default::default()
|
|
}]));
|
|
}
|
|
let mut schedule_ids = Vec::new();
|
|
for response in futures::future::join_all(writes).await {
|
|
assert_applied(&response);
|
|
schedule_ids.push(response.created_id(0));
|
|
}
|
|
for name in &names {
|
|
assert!(has_schedule(test, name), "{name} missing");
|
|
}
|
|
// Several objects in one request: one reload
|
|
let response = admin
|
|
.registry_destroy(ObjectType::MtaDeliverySchedule, schedule_ids.iter())
|
|
.await;
|
|
assert_applied(&response);
|
|
for name in &names {
|
|
assert!(!has_schedule(test, name), "{name} still present");
|
|
}
|
|
|
|
// A write whose reload fails is stored, and the response says the
|
|
// settings weren't reloaded: only one console tracer is allowed.
|
|
let response = admin
|
|
.registry_create([
|
|
Tracer::Stdout(TracerStdout {
|
|
enable: true,
|
|
level: TracingLevel::Error,
|
|
..Default::default()
|
|
}),
|
|
Tracer::Stdout(TracerStdout {
|
|
enable: true,
|
|
level: TracingLevel::Error,
|
|
..Default::default()
|
|
}),
|
|
])
|
|
.await;
|
|
let reload = settings_reload(&response).expect("x:settingsReload missing");
|
|
assert_eq!(reload["applied"], Value::Bool(false), "{response:?}");
|
|
let description = reload["description"].as_str().unwrap_or_default();
|
|
assert!(
|
|
description.starts_with("Saved, but the running settings were not reloaded. ")
|
|
&& description.contains("Only one console tracer is allowed"),
|
|
"{description}"
|
|
);
|
|
let tracer_ids = [response.created_id(0), response.created_id(1)];
|
|
let response = admin
|
|
.registry_destroy(ObjectType::Tracer, tracer_ids.iter())
|
|
.await;
|
|
assert_applied(&response);
|
|
|
|
// An allowed IP is live as soon as it is saved, and gone once
|
|
// destroyed. It lives in the core's security settings, which the
|
|
// blocked-IP reload it used to get doesn't rebuild.
|
|
let ip: std::net::IpAddr = "198.51.100.7".parse().unwrap();
|
|
assert!(!is_allowed(test, ip));
|
|
let response = admin
|
|
.registry_create([AllowedIp {
|
|
address: IpAddrOrMask::from_ip(ip),
|
|
reason: Some("autoreload".into()),
|
|
..Default::default()
|
|
}])
|
|
.await;
|
|
assert_applied(&response);
|
|
assert!(
|
|
is_allowed(test, ip),
|
|
"allowed IP not in the running settings"
|
|
);
|
|
let allowed_id = response.created_id(0);
|
|
let response = admin
|
|
.registry_destroy(ObjectType::AllowedIp, [allowed_id])
|
|
.await;
|
|
assert_applied(&response);
|
|
assert!(!is_allowed(test, ip), "destroyed allowed IP still live");
|
|
|
|
// Data that isn't part of the running settings doesn't reload them
|
|
let response = admin
|
|
.registry_create([Domain {
|
|
name: "autoreload.example.org".into(),
|
|
certificate_management: CertificateManagement::Manual,
|
|
dns_management: DnsManagement::Manual,
|
|
dkim_management: DkimManagement::Manual,
|
|
..Default::default()
|
|
}])
|
|
.await;
|
|
assert!(settings_reload(&response).is_none(), "{response:?}");
|
|
}
|
|
|
|
fn settings_reload(response: &JmapResponse) -> Option<&Value> {
|
|
response.pointer("/methodResponses/0/1/x:settingsReload")
|
|
}
|
|
|
|
fn assert_applied(response: &JmapResponse) {
|
|
assert_eq!(
|
|
settings_reload(response),
|
|
Some(&serde_json::json!({"applied": true})),
|
|
"{response:?}"
|
|
);
|
|
}
|
|
|
|
fn has_schedule(test: &TestServer, name: &str) -> bool {
|
|
test.server
|
|
.inner
|
|
.build_server()
|
|
.core
|
|
.smtp
|
|
.queue
|
|
.queue_strategy
|
|
.contains_key(name)
|
|
}
|
|
|
|
fn is_allowed(test: &TestServer, ip: std::net::IpAddr) -> bool {
|
|
test.server.inner.build_server().is_ip_allowed(ip)
|
|
}
|