Skip to main content

canic_backup/plan/preflight/
authority.rs

1//! Module: plan::preflight::authority
2//!
3//! Responsibility: apply authority preflight receipts to backup plan targets.
4//! Does not own: authority probing, plan construction, or execution mutation.
5//! Boundary: validates receipt headers before upgrading target authority evidence.
6
7use 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    /// Apply proven authority receipts produced by execution preflights.
20    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    /// Apply proven control authority receipts for every selected target.
32    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    /// Apply proven snapshot read authority receipts for every selected target.
73    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}