ic_backup/policy/restore_safety/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 consistency::ApplicationFenceState,
6 ic_request::IcManagementMethodRecord,
7 restore_safety::{
8 RestoreFenceEvidence, RestoreSafetyEvidence, RestoreSafetyLaneRecord,
9 RestoreSafetyObservation, RestoreSafetyRequest, RestoreSafetyRequirementRecord,
10 TargetRestoreEvidence,
11 },
12};
13use ic_management_canister_types::CanisterStatusType;
14use thiserror::Error;
15
16#[derive(Clone, Debug)]
18pub struct RestoreSafetyView<'a> {
19 request: ArtifactChecksumRecord,
20 requirement: ArtifactChecksumRecord,
21 observation: &'a RestoreSafetyObservation,
22}
23impl RestoreSafetyView<'_> {
24 #[must_use]
26 pub const fn request(&self) -> &ArtifactChecksumRecord {
27 &self.request
28 }
29 #[must_use]
31 pub const fn requirement(&self) -> &ArtifactChecksumRecord {
32 &self.requirement
33 }
34 #[must_use]
36 pub fn targets(&self) -> &[TargetRestoreEvidence] {
37 self.observation.targets()
38 }
39 #[must_use]
41 pub fn fence(&self) -> Option<&RestoreFenceEvidence> {
42 match self.observation.safety() {
43 RestoreSafetyEvidence::ApplicationFenced(fence) => Some(fence),
44 RestoreSafetyEvidence::NoIrreversibleEffects(_)
45 | RestoreSafetyEvidence::Unresolved(_) => None,
46 }
47 }
48 #[must_use]
50 pub const fn evidence(&self) -> &ArtifactChecksumRecord {
51 self.observation.evidence()
52 }
53 #[must_use]
55 pub const fn remote_observations(&self) -> u32 {
56 self.observation.remote_observations()
57 }
58}
59pub fn validate<'a>(
76 request: &RestoreSafetyRequest<'_>,
77 observation: &'a RestoreSafetyObservation,
78) -> Result<RestoreSafetyView<'a>, RestoreSafetyError> {
79 let digest = request.digest();
80 if *observation.request() != digest {
81 return Err(RestoreSafetyError::RequestMismatch);
82 }
83 let binding = request.binding();
84 for (field, expected, actual) in [
85 (
86 "network",
87 binding.network(),
88 observation.context().network(),
89 ),
90 ("caller", binding.caller(), observation.context().caller()),
91 (
92 "release",
93 binding.release(),
94 observation.context().release(),
95 ),
96 ] {
97 if actual != expected {
98 return Err(RestoreSafetyError::ContextMismatch(field));
99 }
100 }
101 if observation.inventory() != request.inventory() {
102 return Err(RestoreSafetyError::InventoryMismatch);
103 }
104 if !observation
105 .targets()
106 .iter()
107 .map(|target| &target.target)
108 .eq(request.selected_targets())
109 {
110 return Err(RestoreSafetyError::SelectionMismatch);
111 }
112 let requirement = request.requirement();
113 if observation.source_plan_intent().hash() != requirement.source_plan_intent() {
114 return Err(RestoreSafetyError::SourcePlanMismatch);
115 }
116 if observation.source_artifacts() != requirement.source_artifacts() {
117 return Err(RestoreSafetyError::SourceArtifactsMismatch);
118 }
119 if observation.remote_observations() > request.max_remote_observations() {
120 return Err(RestoreSafetyError::ObservationLimitExceeded {
121 limit: request.max_remote_observations(),
122 reported: observation.remote_observations(),
123 });
124 }
125 let before_start = request.wire().method() == IcManagementMethodRecord::StartCanister;
126 let lifecycle_matches = observation.targets().iter().all(|target| {
127 if before_start && target.target != binding.target() {
128 target.state != CanisterStatusType::Stopping
129 } else {
130 target.state == CanisterStatusType::Stopped
131 }
132 });
133 if !lifecycle_matches {
134 return Err(RestoreSafetyError::TargetNotStopped);
135 }
136 if before_start
137 && observation
138 .targets()
139 .iter()
140 .any(|target| target.restored_acceptance.is_none())
141 {
142 return Err(RestoreSafetyError::RestoredAcceptanceRequired);
143 }
144 validate_safety(requirement, observation.safety(), before_start)?;
145 Ok(RestoreSafetyView {
146 request: digest,
147 requirement: requirement.digest(),
148 observation,
149 })
150}
151fn validate_safety(
152 requirement: &RestoreSafetyRequirementRecord,
153 evidence: &RestoreSafetyEvidence,
154 before_start: bool,
155) -> Result<(), RestoreSafetyError> {
156 match (requirement.safety(), evidence) {
157 (_, RestoreSafetyEvidence::Unresolved(_)) => {
158 return Err(RestoreSafetyError::UnresolvedExternalObligations);
159 }
160 (
161 RestoreSafetyLaneRecord::NoIrreversibleEffects,
162 RestoreSafetyEvidence::NoIrreversibleEffects(_),
163 ) => {}
164 (
165 RestoreSafetyLaneRecord::ApplicationFenced,
166 RestoreSafetyEvidence::ApplicationFenced(fence),
167 ) => {
168 let original = requirement
169 .expected_fence()
170 .ok_or(RestoreSafetyError::FenceMismatch)?;
171 if fence.binding.identity != original.identity {
172 return Err(RestoreSafetyError::FenceMismatch);
173 }
174 if fence.binding.membership_revision != original.membership_revision {
175 return Err(RestoreSafetyError::MembershipRevisionMismatch);
176 }
177 if fence.binding.external_obligations_revision != original.external_obligations_revision
178 {
179 return Err(RestoreSafetyError::ExternalObligationsRevisionMismatch);
180 }
181 if fence.state != ApplicationFenceState::Active {
182 return Err(RestoreSafetyError::FenceNotActive);
183 }
184 if before_start && fence.controlled_execution.is_none() {
185 return Err(RestoreSafetyError::ControlledExecutionRequired);
186 }
187 }
188 _ => return Err(RestoreSafetyError::SafetyLaneMismatch),
189 }
190 Ok(())
191}
192#[derive(Debug, Eq, Error, PartialEq)]
194pub enum RestoreSafetyError {
195 #[error("restore safety request mismatch")]
197 RequestMismatch,
198 #[error("restore safety observed {0} mismatch")]
200 ContextMismatch(&'static str),
201 #[error("restore safety inventory mismatch")]
203 InventoryMismatch,
204 #[error("restore safety selection mismatch")]
206 SelectionMismatch,
207 #[error("restore safety source plan mismatch")]
209 SourcePlanMismatch,
210 #[error("restore safety source artifacts mismatch")]
212 SourceArtifactsMismatch,
213 #[error("restore safety evidence lane mismatch")]
215 SafetyLaneMismatch,
216 #[error("restore safety external obligations unresolved")]
218 UnresolvedExternalObligations,
219 #[error("restore safety target is not stopped")]
221 TargetNotStopped,
222 #[error("restore safety restored-state acceptance required")]
224 RestoredAcceptanceRequired,
225 #[error("restore safety fence identity mismatch")]
227 FenceMismatch,
228 #[error("restore safety membership revision mismatch")]
230 MembershipRevisionMismatch,
231 #[error("restore safety external-obligations revision mismatch")]
233 ExternalObligationsRevisionMismatch,
234 #[error("restore safety fence not active")]
236 FenceNotActive,
237 #[error("restore safety controlled execution evidence required")]
239 ControlledExecutionRequired,
240 #[error("restore safety reports {reported} observations above ceiling {limit}")]
242 ObservationLimitExceeded {
243 limit: u32,
245 reported: u32,
247 },
248}
249
250#[cfg(test)]
251mod tests;