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_nonempty, validate_optional_nonempty, validate_preflight_id,
12        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 (index, target) in self.targets.iter().enumerate() {
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((index, receipt.authority));
59        }
60
61        for (index, authority) in updates {
62            self.targets[index].control_authority = authority;
63        }
64        Ok(())
65    }
66
67    /// Apply proven snapshot read authority receipts for every selected target.
68    pub fn apply_snapshot_read_authority_receipts(
69        &mut self,
70        preflight_id: &str,
71        receipts: &[SnapshotReadAuthorityReceipt],
72        as_of: &str,
73    ) -> Result<(), BackupPlanError> {
74        let mut receipts =
75            snapshot_read_receipt_map(&self.plan_id, preflight_id, as_of, &self.targets, receipts)?;
76        let mut updates = Vec::with_capacity(self.targets.len());
77        for (index, target) in self.targets.iter().enumerate() {
78            let receipt = receipts.remove(&target.canister_id).ok_or_else(|| {
79                BackupPlanError::MissingSnapshotReadAuthorityReceipt(target.canister_id.clone())
80            })?;
81            if !receipt.authority.is_proven() {
82                return Err(BackupPlanError::UnprovenTargetSnapshotReadAuthority(
83                    target.canister_id.clone(),
84                ));
85            }
86            updates.push((index, receipt.authority));
87        }
88
89        for (index, authority) in updates {
90            self.targets[index].snapshot_read_authority = authority;
91        }
92        Ok(())
93    }
94}
95
96fn control_receipt_map(
97    plan_id: &str,
98    preflight_id: &str,
99    as_of: &str,
100    targets: &[BackupTarget],
101    receipts: &[ControlAuthorityReceipt],
102) -> Result<BTreeMap<String, ControlAuthorityReceipt>, BackupPlanError> {
103    let target_ids = targets
104        .iter()
105        .map(|target| target.canister_id.as_str())
106        .collect::<BTreeSet<_>>();
107    let mut receipt_map = BTreeMap::new();
108
109    for receipt in receipts {
110        validate_authority_receipt_header(AuthorityReceiptHeaderInput {
111            expected_plan_id: plan_id,
112            expected_preflight_id: preflight_id,
113            as_of,
114            target_ids: &target_ids,
115            actual_plan_id: &receipt.plan_id,
116            actual_preflight_id: &receipt.preflight_id,
117            target_canister_id: &receipt.target_canister_id,
118            validated_at: &receipt.validated_at,
119            expires_at: &receipt.expires_at,
120            message: receipt.message.as_deref(),
121        })?;
122        if receipt_map
123            .insert(receipt.target_canister_id.clone(), receipt.clone())
124            .is_some()
125        {
126            return Err(BackupPlanError::DuplicateAuthorityReceipt(
127                receipt.target_canister_id.clone(),
128            ));
129        }
130    }
131
132    Ok(receipt_map)
133}
134
135fn snapshot_read_receipt_map(
136    plan_id: &str,
137    preflight_id: &str,
138    as_of: &str,
139    targets: &[BackupTarget],
140    receipts: &[SnapshotReadAuthorityReceipt],
141) -> Result<BTreeMap<String, SnapshotReadAuthorityReceipt>, BackupPlanError> {
142    let target_ids = targets
143        .iter()
144        .map(|target| target.canister_id.as_str())
145        .collect::<BTreeSet<_>>();
146    let mut receipt_map = BTreeMap::new();
147
148    for receipt in receipts {
149        validate_authority_receipt_header(AuthorityReceiptHeaderInput {
150            expected_plan_id: plan_id,
151            expected_preflight_id: preflight_id,
152            as_of,
153            target_ids: &target_ids,
154            actual_plan_id: &receipt.plan_id,
155            actual_preflight_id: &receipt.preflight_id,
156            target_canister_id: &receipt.target_canister_id,
157            validated_at: &receipt.validated_at,
158            expires_at: &receipt.expires_at,
159            message: receipt.message.as_deref(),
160        })?;
161        if receipt_map
162            .insert(receipt.target_canister_id.clone(), receipt.clone())
163            .is_some()
164        {
165            return Err(BackupPlanError::DuplicateAuthorityReceipt(
166                receipt.target_canister_id.clone(),
167            ));
168        }
169    }
170
171    Ok(receipt_map)
172}
173
174struct AuthorityReceiptHeaderInput<'a> {
175    expected_plan_id: &'a str,
176    expected_preflight_id: &'a str,
177    as_of: &'a str,
178    target_ids: &'a BTreeSet<&'a str>,
179    actual_plan_id: &'a str,
180    actual_preflight_id: &'a str,
181    target_canister_id: &'a str,
182    validated_at: &'a str,
183    expires_at: &'a str,
184    message: Option<&'a str>,
185}
186
187fn validate_authority_receipt_header(
188    input: AuthorityReceiptHeaderInput<'_>,
189) -> Result<(), BackupPlanError> {
190    validate_nonempty("authority_receipts[].plan_id", input.actual_plan_id)?;
191    validate_preflight_id(input.actual_preflight_id)?;
192    validate_principal(
193        "authority_receipts[].target_canister_id",
194        input.target_canister_id,
195    )?;
196    validate_optional_nonempty("authority_receipts[].message", input.message)?;
197    validate_preflight_window(
198        input.actual_preflight_id,
199        input.validated_at,
200        input.expires_at,
201        input.as_of,
202    )?;
203
204    if input.actual_plan_id != input.expected_plan_id {
205        return Err(BackupPlanError::AuthorityReceiptPlanMismatch {
206            expected: input.expected_plan_id.to_string(),
207            actual: input.actual_plan_id.to_string(),
208        });
209    }
210    if input.actual_preflight_id != input.expected_preflight_id {
211        return Err(BackupPlanError::AuthorityReceiptPreflightMismatch {
212            expected: input.expected_preflight_id.to_string(),
213            actual: input.actual_preflight_id.to_string(),
214        });
215    }
216    if !input.target_ids.contains(input.target_canister_id) {
217        return Err(BackupPlanError::UnknownAuthorityReceiptTarget(
218            input.target_canister_id.to_string(),
219        ));
220    }
221
222    Ok(())
223}