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 (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        validate_control_authority(&receipt.authority)?;
123        if receipt_map
124            .insert(receipt.target_canister_id.clone(), receipt.clone())
125            .is_some()
126        {
127            return Err(BackupPlanError::DuplicateAuthorityReceipt(
128                receipt.target_canister_id.clone(),
129            ));
130        }
131    }
132
133    Ok(receipt_map)
134}
135
136fn snapshot_read_receipt_map(
137    plan_id: &str,
138    preflight_id: &str,
139    as_of: &str,
140    targets: &[BackupTarget],
141    receipts: &[SnapshotReadAuthorityReceipt],
142) -> Result<BTreeMap<String, SnapshotReadAuthorityReceipt>, BackupPlanError> {
143    let target_ids = targets
144        .iter()
145        .map(|target| target.canister_id.as_str())
146        .collect::<BTreeSet<_>>();
147    let mut receipt_map = BTreeMap::new();
148
149    for receipt in receipts {
150        validate_authority_receipt_header(AuthorityReceiptHeaderInput {
151            expected_plan_id: plan_id,
152            expected_preflight_id: preflight_id,
153            as_of,
154            target_ids: &target_ids,
155            actual_plan_id: &receipt.plan_id,
156            actual_preflight_id: &receipt.preflight_id,
157            target_canister_id: &receipt.target_canister_id,
158            validated_at: &receipt.validated_at,
159            expires_at: &receipt.expires_at,
160            message: receipt.message.as_deref(),
161        })?;
162        if receipt_map
163            .insert(receipt.target_canister_id.clone(), receipt.clone())
164            .is_some()
165        {
166            return Err(BackupPlanError::DuplicateAuthorityReceipt(
167                receipt.target_canister_id.clone(),
168            ));
169        }
170    }
171
172    Ok(receipt_map)
173}
174
175struct AuthorityReceiptHeaderInput<'a> {
176    expected_plan_id: &'a str,
177    expected_preflight_id: &'a str,
178    as_of: &'a str,
179    target_ids: &'a BTreeSet<&'a str>,
180    actual_plan_id: &'a str,
181    actual_preflight_id: &'a str,
182    target_canister_id: &'a str,
183    validated_at: &'a str,
184    expires_at: &'a str,
185    message: Option<&'a str>,
186}
187
188fn validate_authority_receipt_header(
189    input: AuthorityReceiptHeaderInput<'_>,
190) -> Result<(), BackupPlanError> {
191    validate_nonempty("authority_receipts[].plan_id", input.actual_plan_id)?;
192    validate_preflight_id(input.actual_preflight_id)?;
193    validate_principal(
194        "authority_receipts[].target_canister_id",
195        input.target_canister_id,
196    )?;
197    validate_optional_nonempty("authority_receipts[].message", input.message)?;
198    validate_preflight_window(
199        input.actual_preflight_id,
200        input.validated_at,
201        input.expires_at,
202        input.as_of,
203    )?;
204
205    if input.actual_plan_id != input.expected_plan_id {
206        return Err(BackupPlanError::AuthorityReceiptPlanMismatch {
207            expected: input.expected_plan_id.to_string(),
208            actual: input.actual_plan_id.to_string(),
209        });
210    }
211    if input.actual_preflight_id != input.expected_preflight_id {
212        return Err(BackupPlanError::AuthorityReceiptPreflightMismatch {
213            expected: input.expected_preflight_id.to_string(),
214            actual: input.actual_preflight_id.to_string(),
215        });
216    }
217    if !input.target_ids.contains(input.target_canister_id) {
218        return Err(BackupPlanError::UnknownAuthorityReceiptTarget(
219            input.target_canister_id.to_string(),
220        ));
221    }
222
223    Ok(())
224}