canic_backup/plan/preflight/
authority.rs1use crate::plan::{
8 BackupPlan, BackupPlanError, BackupTarget, ControlAuthorityReceipt,
9 SnapshotReadAuthorityReceipt,
10 validation::{
11 validate_control_authority, validate_nonempty, validate_optional_nonempty,
12 validate_preflight_id, validate_preflight_window, validate_principal,
13 },
14};
15
16use std::collections::{BTreeMap, BTreeSet};
17
18impl BackupPlan {
19 pub fn apply_authority_preflight_receipts(
21 &mut self,
22 preflight_id: &str,
23 control_receipts: &[ControlAuthorityReceipt],
24 snapshot_read_receipts: &[SnapshotReadAuthorityReceipt],
25 as_of: &str,
26 ) -> Result<(), BackupPlanError> {
27 self.apply_control_authority_receipts(preflight_id, control_receipts, as_of)?;
28 self.apply_snapshot_read_authority_receipts(preflight_id, snapshot_read_receipts, as_of)
29 }
30
31 pub fn apply_control_authority_receipts(
33 &mut self,
34 preflight_id: &str,
35 receipts: &[ControlAuthorityReceipt],
36 as_of: &str,
37 ) -> Result<(), BackupPlanError> {
38 let mut receipts =
39 control_receipt_map(&self.plan_id, preflight_id, as_of, &self.targets, receipts)?;
40 let mut updates = Vec::with_capacity(self.targets.len());
41 for target in &self.targets {
42 let receipt = receipts.remove(&target.canister_id).ok_or_else(|| {
43 BackupPlanError::MissingControlAuthorityReceipt(target.canister_id.clone())
44 })?;
45 if !receipt.authority.is_proven() {
46 return Err(BackupPlanError::UnprovenControlAuthority(
47 target.canister_id.clone(),
48 ));
49 }
50 if self.requires_root_controller
51 && target.canister_id != self.root_canister_id
52 && !receipt.authority.is_proven_root_controller()
53 {
54 return Err(BackupPlanError::MissingRootController(
55 target.canister_id.clone(),
56 ));
57 }
58 updates.push((target.canister_id.clone(), receipt.authority));
59 }
60
61 for (target_id, authority) in updates {
62 let target = self
63 .targets
64 .iter_mut()
65 .find(|target| target.canister_id == target_id)
66 .expect("validated update target should exist");
67 target.control_authority = authority;
68 }
69 Ok(())
70 }
71
72 pub fn apply_snapshot_read_authority_receipts(
74 &mut self,
75 preflight_id: &str,
76 receipts: &[SnapshotReadAuthorityReceipt],
77 as_of: &str,
78 ) -> Result<(), BackupPlanError> {
79 let mut receipts =
80 snapshot_read_receipt_map(&self.plan_id, preflight_id, as_of, &self.targets, receipts)?;
81 let mut updates = Vec::with_capacity(self.targets.len());
82 for target in &self.targets {
83 let receipt = receipts.remove(&target.canister_id).ok_or_else(|| {
84 BackupPlanError::MissingSnapshotReadAuthorityReceipt(target.canister_id.clone())
85 })?;
86 if !receipt.authority.is_proven() {
87 return Err(BackupPlanError::UnprovenTargetSnapshotReadAuthority(
88 target.canister_id.clone(),
89 ));
90 }
91 updates.push((target.canister_id.clone(), receipt.authority));
92 }
93
94 for (target_id, authority) in updates {
95 let target = self
96 .targets
97 .iter_mut()
98 .find(|target| target.canister_id == target_id)
99 .expect("validated update target should exist");
100 target.snapshot_read_authority = authority;
101 }
102 Ok(())
103 }
104}
105
106fn control_receipt_map(
107 plan_id: &str,
108 preflight_id: &str,
109 as_of: &str,
110 targets: &[BackupTarget],
111 receipts: &[ControlAuthorityReceipt],
112) -> Result<BTreeMap<String, ControlAuthorityReceipt>, BackupPlanError> {
113 let target_ids = targets
114 .iter()
115 .map(|target| target.canister_id.as_str())
116 .collect::<BTreeSet<_>>();
117 let mut receipt_map = BTreeMap::new();
118
119 for receipt in receipts {
120 validate_authority_receipt_header(AuthorityReceiptHeaderInput {
121 expected_plan_id: plan_id,
122 expected_preflight_id: preflight_id,
123 as_of,
124 target_ids: &target_ids,
125 actual_plan_id: &receipt.plan_id,
126 actual_preflight_id: &receipt.preflight_id,
127 target_canister_id: &receipt.target_canister_id,
128 validated_at: &receipt.validated_at,
129 expires_at: &receipt.expires_at,
130 message: receipt.message.as_deref(),
131 })?;
132 validate_control_authority(&receipt.authority)?;
133 if receipt_map
134 .insert(receipt.target_canister_id.clone(), receipt.clone())
135 .is_some()
136 {
137 return Err(BackupPlanError::DuplicateAuthorityReceipt(
138 receipt.target_canister_id.clone(),
139 ));
140 }
141 }
142
143 Ok(receipt_map)
144}
145
146fn snapshot_read_receipt_map(
147 plan_id: &str,
148 preflight_id: &str,
149 as_of: &str,
150 targets: &[BackupTarget],
151 receipts: &[SnapshotReadAuthorityReceipt],
152) -> Result<BTreeMap<String, SnapshotReadAuthorityReceipt>, BackupPlanError> {
153 let target_ids = targets
154 .iter()
155 .map(|target| target.canister_id.as_str())
156 .collect::<BTreeSet<_>>();
157 let mut receipt_map = BTreeMap::new();
158
159 for receipt in receipts {
160 validate_authority_receipt_header(AuthorityReceiptHeaderInput {
161 expected_plan_id: plan_id,
162 expected_preflight_id: preflight_id,
163 as_of,
164 target_ids: &target_ids,
165 actual_plan_id: &receipt.plan_id,
166 actual_preflight_id: &receipt.preflight_id,
167 target_canister_id: &receipt.target_canister_id,
168 validated_at: &receipt.validated_at,
169 expires_at: &receipt.expires_at,
170 message: receipt.message.as_deref(),
171 })?;
172 if receipt_map
173 .insert(receipt.target_canister_id.clone(), receipt.clone())
174 .is_some()
175 {
176 return Err(BackupPlanError::DuplicateAuthorityReceipt(
177 receipt.target_canister_id.clone(),
178 ));
179 }
180 }
181
182 Ok(receipt_map)
183}
184
185struct AuthorityReceiptHeaderInput<'a> {
186 expected_plan_id: &'a str,
187 expected_preflight_id: &'a str,
188 as_of: &'a str,
189 target_ids: &'a BTreeSet<&'a str>,
190 actual_plan_id: &'a str,
191 actual_preflight_id: &'a str,
192 target_canister_id: &'a str,
193 validated_at: &'a str,
194 expires_at: &'a str,
195 message: Option<&'a str>,
196}
197
198fn validate_authority_receipt_header(
199 input: AuthorityReceiptHeaderInput<'_>,
200) -> Result<(), BackupPlanError> {
201 validate_nonempty("authority_receipts[].plan_id", input.actual_plan_id)?;
202 validate_preflight_id(input.actual_preflight_id)?;
203 validate_principal(
204 "authority_receipts[].target_canister_id",
205 input.target_canister_id,
206 )?;
207 validate_optional_nonempty("authority_receipts[].message", input.message)?;
208 validate_preflight_window(
209 input.actual_preflight_id,
210 input.validated_at,
211 input.expires_at,
212 input.as_of,
213 )?;
214
215 if input.actual_plan_id != input.expected_plan_id {
216 return Err(BackupPlanError::AuthorityReceiptPlanMismatch {
217 expected: input.expected_plan_id.to_string(),
218 actual: input.actual_plan_id.to_string(),
219 });
220 }
221 if input.actual_preflight_id != input.expected_preflight_id {
222 return Err(BackupPlanError::AuthorityReceiptPreflightMismatch {
223 expected: input.expected_preflight_id.to_string(),
224 actual: input.actual_preflight_id.to_string(),
225 });
226 }
227 if !input.target_ids.contains(input.target_canister_id) {
228 return Err(BackupPlanError::UnknownAuthorityReceiptTarget(
229 input.target_canister_id.to_string(),
230 ));
231 }
232
233 Ok(())
234}