From e240fdb124675cf0b6ec99f57dc02bc8c32972b5 Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 6 Feb 2026 15:33:24 +0100 Subject: [PATCH 1/5] Make children sync with parents when resources have updated --- src/server/ca/certauth.rs | 11 +- src/server/mq.rs | 58 ++++++++- src/server/scheduler.rs | 29 +++++ tests/functional_grandchild_resources.rs | 156 +++++++++++++++++++++++ 4 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 tests/functional_grandchild_resources.rs diff --git a/src/server/ca/certauth.rs b/src/server/ca/certauth.rs index defe99691..021d9004a 100644 --- a/src/server/ca/certauth.rs +++ b/src/server/ca/certauth.rs @@ -1363,7 +1363,7 @@ impl CertAuth { ); events.push(CertAuthEvent::ChildCertificateIssued { - child: child_handle, + child: child_handle.clone(), resource_class_name: my_rcn.clone(), ki: issued.key_identifier(), }); @@ -1375,6 +1375,15 @@ impl CertAuth { updates: cert_updates }); + if let Ok(child) = self.get_child(&child_handle) { + if &child.resources != resources { + events.push(CertAuthEvent::ChildUpdatedResources { + child: child_handle.clone(), + resources: resources.clone() + }); + } + } + Ok(()) } diff --git a/src/server/mq.rs b/src/server/mq.rs index 22dcab4fb..1dc276d8b 100644 --- a/src/server/mq.rs +++ b/src/server/mq.rs @@ -56,6 +56,11 @@ pub enum Task { parent: ParentHandle, }, + SyncChildren { + ca_handle: CaHandle, + ca_version: u64 + }, + ResourceClassRemoved { ca_handle: CaHandle, ca_version: u64, @@ -120,6 +125,20 @@ impl Task { ).finish() ) } + Task::SyncChildren { + ca_handle: ca, + .. + } => { + Cow::Owned( + Ident::builder( + const { Ident::make("sync_") } + ).push_handle( + ca + ).push_ident( + const { Ident::make("_children") } + ).finish() + ) + }, Task::SuspendChildrenIfNeeded { ca_handle: ca } => { Cow::Owned( Ident::builder( @@ -234,6 +253,12 @@ impl fmt::Display for Task { } => { write!(f, "synchronize CA '{ca}' with parent '{parent}'") } + Task::SyncChildren { + ca_handle: ca, + .. + } => { + write!(f, "synchronize CA '{ca}' with children") + } Task::RenewTestbedTa => write!(f, "renew testbed TA"), Task::SyncTrustAnchorProxySignerIfPossible => { write!(f, "sync TA Proxy and Signer if both in this server.") @@ -445,6 +470,23 @@ impl TaskQueue { now(), ), + // CertAuthEvent::ChildUpdatedResources { .. } => { + // self.schedule( + // Task::SyncChildren { + // ca_handle: ca_handle.clone(), + // ca_version: 0, + // }, + // now() + // )?; + // self.schedule( + // Task::SyncRepo { + // ca_handle, + // ca_version, + // }, + // now(), + // ) + // } + CertAuthEvent::KeyRollActivated { resource_class_name, .. @@ -624,7 +666,21 @@ impl eventsourcing::PostSaveEventListener for TaskQueue { ); } } - + CertAuthEvent::ChildCertificateIssued { child, .. } => { + if let Err(e) = self.schedule( + Task::SyncChildren { + ca_handle: child.convert(), + ca_version: 0 + }, + now() + ) { + error!( + "Could not schedule sync from {}. Restart Krill or run 'krillc bulk parents'. Error was: {}", + child, + e + ); + } + } _ => { // nothing to do } diff --git a/src/server/scheduler.rs b/src/server/scheduler.rs index 564aa59a0..9c1d46368 100644 --- a/src/server/scheduler.rs +++ b/src/server/scheduler.rs @@ -147,6 +147,11 @@ impl Scheduler { parent, } => self.sync_parent(ca, ca_version, parent).await, + Task::SyncChildren { + ca_handle: ca, + ca_version, + } => self.sync_children(ca, ca_version).await, + Task::RenewTestbedTa => self.renew_testbed_ta().await, Task::SyncTrustAnchorProxySignerIfPossible => { @@ -419,6 +424,30 @@ impl Scheduler { } } + async fn sync_children( + &self, + ca: CaHandle, + ca_version: u64, + ) -> Result { + if let Ok(parent) = self.ca_manager.get_ca(&ca) { + for child in parent.children() { + if let Ok(child) = self.ca_manager.get_ca(&child.convert()) { + debug!( + "Syncing child '{}' for parent {}", + child.handle(), + parent.handle() + ); + self.tasks.schedule(Task::SyncParent { + ca_handle: child.handle().convert(), + ca_version: ca_version, + parent: parent.handle().convert() + }, now()).map_err(FatalError)?; + } + } + } + Ok(TaskResult::Done) + } + /// Resync the testbed TA signer and proxy async fn renew_testbed_ta(&self) -> Result { if let Err(e) = self.ca_manager.ta_renew_testbed_ta() { diff --git a/tests/functional_grandchild_resources.rs b/tests/functional_grandchild_resources.rs new file mode 100644 index 000000000..3489ad741 --- /dev/null +++ b/tests/functional_grandchild_resources.rs @@ -0,0 +1,156 @@ +//! Tests grandchild resources updated. + +use krill::api; +use rpki::repository::resources::ResourceSet; + +use crate::common::sleep_seconds; + +mod common; + + +//------------ Test Function ------------------------------------------------- + +/// Test Krill parent/child/grandchild interactions. +/// +/// The setup is: +/// +/// ```text +/// TA +/// | +/// parent +/// | +/// child +/// | +/// grandchild +/// ``` +/// +/// The test verifies that: +/// * Grandchild gets the resources from child, +/// * Parent can restrict the resources on child, +/// * Grandchild will learn about those resource changes and adjust the +/// effective resources accordingly + +#[tokio::test] +async fn functional_resource_updates() { + let (server, _tmpdir) + = common::KrillServer::start_with_file_storage_and_testbed().await; + + let testbed = common::ca_handle("testbed"); + + let ca_parent = common::ca_handle("parent"); + let ca_parent_res = common::resources("AS65000", "10.0.0.0/16", ""); + + let ca_child = common::ca_handle("child"); + let ca_child_res = common::resources("AS65000", "10.0.0.0/16", ""); + + let ca_grandchild = common::ca_handle("grandchild"); + let ca_grandchild_res = common::resources("AS65000", "10.0.0.0/23", ""); + + let rcn0 = common::rcn(0); + + // Wait for the *testbed* CA to get its certificate, this means + // that all CAs which are set up as part of krill_start under the + // testbed config have been set up. + assert!( + server.wait_for_ca_resources(&testbed, &ResourceSet::all()).await + ); + + eprintln!(">>>> Set up CA parent under testbed."); + server.create_ca_with_repo(&ca_parent).await; + server.register_ca_with_parent(&ca_parent, &testbed, &ca_parent_res).await; + + eprintln!(">>>> Set up CA child under parent."); + server.create_ca_with_repo(&ca_child).await; + server.register_ca_with_parent(&ca_child, &ca_parent, &ca_child_res).await; + + eprintln!(">>>> Set up CA grandchild under child."); + server.create_ca_with_repo(&ca_grandchild).await; + server.register_ca_with_parent(&ca_grandchild, &ca_child, &ca_grandchild_res).await; + + eprintln!(">>>> Expect that CA child publishes the certificate for CA grandchild."); + let mut files = server.expected_objects(&ca_child); + files.push_mft_and_crl(&rcn0).await; + files.push_cer(&ca_grandchild, &rcn0).await; + assert!(files.wait_for_published().await); + + sleep_seconds(3).await; + + eprintln!(">>>> Update resources given to the child."); + let ca_child_new_res = common::resources("65000", "10.0.0.0/24", ""); + let ca_grandchild_new_res = + ca_grandchild_res.clone().intersection(&ca_child_new_res); + eprintln!("New resources: {}", &ca_grandchild_new_res); + server.client().child_update( + &ca_parent, + &ca_child.convert(), + api::admin::UpdateChildRequest::resources(ca_child_new_res.clone()) + ).await.unwrap(); + assert!(server.wait_for_ca_resources(&ca_child, &ca_child_new_res).await); + + // Wait before the child gets aware of the new resources + sleep_seconds(3).await; + + eprintln!(">>>> Check new resources given to the child."); + assert_eq!( + server.client().ca_details(&ca_child).await.unwrap().resources, + ca_child_new_res.clone() + ); + // We want the 'parent' and 'child' to agree on the new resources. + // i.e. that the 'child' learnt that the new resource set it smaller. + assert_eq!( + server.client().ca_details(&ca_child).await.unwrap().resources, + server.client().child_details( + &ca_parent, &ca_child.convert() + ).await.unwrap().entitled_resources, + ); + + sleep_seconds(3).await; + + eprintln!(">>>> Check resources updated on grandchild."); + // We do not want the 'child' to update the entitled resources of the + // 'grandchild'. The resources of the grandchild will shrink automatically + // and grow again when the child gets more resources again. + // This checks the entitled resources have not changed. + assert_eq!( + server.client().child_details( + &ca_child, &ca_grandchild.convert() + ).await.unwrap().entitled_resources, + ca_grandchild_res.clone() + ); + // We do want 'grandchild' to learn of the effective resources after + // shrinking. + assert_ne!( + server.client().ca_details(&ca_grandchild).await.unwrap().resources, + ca_grandchild_res.clone() + ); + assert_eq!( + server.client().ca_details(&ca_grandchild).await.unwrap().resources, + ca_grandchild_new_res.clone() + ); + + eprintln!(">>>> Revert the resources given to the child."); + server.client().child_update( + &ca_parent, + &ca_child.convert(), + api::admin::UpdateChildRequest::resources(ca_child_res.clone()) + ).await.unwrap(); + assert!(server.wait_for_ca_resources(&ca_child, &ca_child_new_res).await); + + sleep_seconds(3).await; + + // Everything should be back to the original state again + assert_eq!( + server.client().ca_details(&ca_child).await.unwrap().resources, + ca_child_res.clone() + ); + assert_eq!( + server.client().child_details( + &ca_child, &ca_grandchild.convert() + ).await.unwrap().entitled_resources, + ca_grandchild_res.clone() + ); + assert_eq!( + server.client().ca_details(&ca_grandchild).await.unwrap().resources, + ca_grandchild_res.clone() + ); +} From 7161efc7273c4c5af36a1a9a5ddbd8977e9ae8fe Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 6 Feb 2026 15:35:54 +0100 Subject: [PATCH 2/5] Fix krillc command --- src/server/mq.rs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/server/mq.rs b/src/server/mq.rs index 1dc276d8b..e1e81a4e3 100644 --- a/src/server/mq.rs +++ b/src/server/mq.rs @@ -470,23 +470,6 @@ impl TaskQueue { now(), ), - // CertAuthEvent::ChildUpdatedResources { .. } => { - // self.schedule( - // Task::SyncChildren { - // ca_handle: ca_handle.clone(), - // ca_version: 0, - // }, - // now() - // )?; - // self.schedule( - // Task::SyncRepo { - // ca_handle, - // ca_version, - // }, - // now(), - // ) - // } - CertAuthEvent::KeyRollActivated { resource_class_name, .. @@ -675,7 +658,7 @@ impl eventsourcing::PostSaveEventListener for TaskQueue { now() ) { error!( - "Could not schedule sync from {}. Restart Krill or run 'krillc bulk parents'. Error was: {}", + "Could not schedule sync from {}. Restart Krill or run 'krillc bulk refresh'. Error was: {}", child, e ); From fcdbac568033141ca96ba563a9245ced99a7ef62 Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 6 Feb 2026 15:38:56 +0100 Subject: [PATCH 3/5] Do not add double ChildUpdatedResources --- src/server/ca/certauth.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/server/ca/certauth.rs b/src/server/ca/certauth.rs index 021d9004a..defe99691 100644 --- a/src/server/ca/certauth.rs +++ b/src/server/ca/certauth.rs @@ -1363,7 +1363,7 @@ impl CertAuth { ); events.push(CertAuthEvent::ChildCertificateIssued { - child: child_handle.clone(), + child: child_handle, resource_class_name: my_rcn.clone(), ki: issued.key_identifier(), }); @@ -1375,15 +1375,6 @@ impl CertAuth { updates: cert_updates }); - if let Ok(child) = self.get_child(&child_handle) { - if &child.resources != resources { - events.push(CertAuthEvent::ChildUpdatedResources { - child: child_handle.clone(), - resources: resources.clone() - }); - } - } - Ok(()) } From bfaa5bf7869960e97c7cab892825de52b71e40af Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 6 Feb 2026 15:39:54 +0100 Subject: [PATCH 4/5] Make Clippy happy --- src/server/scheduler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/scheduler.rs b/src/server/scheduler.rs index 9c1d46368..96c717d7a 100644 --- a/src/server/scheduler.rs +++ b/src/server/scheduler.rs @@ -439,7 +439,7 @@ impl Scheduler { ); self.tasks.schedule(Task::SyncParent { ca_handle: child.handle().convert(), - ca_version: ca_version, + ca_version, parent: parent.handle().convert() }, now()).map_err(FatalError)?; } From af55046cfd759e9c3b68be737d7910c4a194cfb9 Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 6 Feb 2026 16:21:49 +0100 Subject: [PATCH 5/5] Also add a grandgrandchild --- tests/functional_grandchild_resources.rs | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/functional_grandchild_resources.rs b/tests/functional_grandchild_resources.rs index 3489ad741..baec7869a 100644 --- a/tests/functional_grandchild_resources.rs +++ b/tests/functional_grandchild_resources.rs @@ -22,6 +22,8 @@ mod common; /// child /// | /// grandchild +/// | +/// grandgrandchild /// ``` /// /// The test verifies that: @@ -46,6 +48,9 @@ async fn functional_resource_updates() { let ca_grandchild = common::ca_handle("grandchild"); let ca_grandchild_res = common::resources("AS65000", "10.0.0.0/23", ""); + let ca_grandgrandchild = common::ca_handle("grandgrandchild"); + let ca_grandgrandchild_res = common::resources("AS65000", "10.0.0.0/23", ""); + let rcn0 = common::rcn(0); // Wait for the *testbed* CA to get its certificate, this means @@ -67,6 +72,10 @@ async fn functional_resource_updates() { server.create_ca_with_repo(&ca_grandchild).await; server.register_ca_with_parent(&ca_grandchild, &ca_child, &ca_grandchild_res).await; + eprintln!(">>>> Set up CA grandgrandchild under grandchild."); + server.create_ca_with_repo(&ca_grandgrandchild).await; + server.register_ca_with_parent(&ca_grandgrandchild, &ca_grandchild, &ca_grandgrandchild_res).await; + eprintln!(">>>> Expect that CA child publishes the certificate for CA grandchild."); let mut files = server.expected_objects(&ca_child); files.push_mft_and_crl(&rcn0).await; @@ -79,6 +88,8 @@ async fn functional_resource_updates() { let ca_child_new_res = common::resources("65000", "10.0.0.0/24", ""); let ca_grandchild_new_res = ca_grandchild_res.clone().intersection(&ca_child_new_res); + let ca_grandgrandchild_new_res = + ca_grandgrandchild_res.clone().intersection(&ca_grandchild_new_res); eprintln!("New resources: {}", &ca_grandchild_new_res); server.client().child_update( &ca_parent, @@ -127,6 +138,17 @@ async fn functional_resource_updates() { server.client().ca_details(&ca_grandchild).await.unwrap().resources, ca_grandchild_new_res.clone() ); + // We also want the grandgrandchild to learn of the new effective resources + assert_eq!( + server.client().child_details( + &ca_grandchild, &ca_grandgrandchild.convert() + ).await.unwrap().entitled_resources, + ca_grandgrandchild_res.clone() + ); + assert_eq!( + server.client().ca_details(&ca_grandgrandchild).await.unwrap().resources, + ca_grandgrandchild_new_res.clone() + ); eprintln!(">>>> Revert the resources given to the child."); server.client().child_update( @@ -153,4 +175,8 @@ async fn functional_resource_updates() { server.client().ca_details(&ca_grandchild).await.unwrap().resources, ca_grandchild_res.clone() ); + assert_eq!( + server.client().ca_details(&ca_grandgrandchild).await.unwrap().resources, + ca_grandgrandchild_res.clone() + ); }