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 if let Some(field) = super::observed_context::first_mismatch(binding, observation.context()) {
85 return Err(RestoreSafetyError::ContextMismatch(field));
86 }
87 if observation.inventory() != request.inventory() {
88 return Err(RestoreSafetyError::InventoryMismatch);
89 }
90 if !observation
91 .targets()
92 .iter()
93 .map(|target| &target.target)
94 .eq(request.selected_targets())
95 {
96 return Err(RestoreSafetyError::SelectionMismatch);
97 }
98 let requirement = request.requirement();
99 if observation.source_plan_intent().hash() != requirement.source_plan_intent() {
100 return Err(RestoreSafetyError::SourcePlanMismatch);
101 }
102 if observation.source_artifacts() != requirement.source_artifacts() {
103 return Err(RestoreSafetyError::SourceArtifactsMismatch);
104 }
105 if observation.remote_observations() > request.max_remote_observations() {
106 return Err(RestoreSafetyError::ObservationLimitExceeded {
107 limit: request.max_remote_observations(),
108 reported: observation.remote_observations(),
109 });
110 }
111 let before_start = request.wire().method() == IcManagementMethodRecord::StartCanister;
112 let lifecycle_matches = observation.targets().iter().all(|target| {
113 if before_start && target.target != binding.target() {
114 target.state != CanisterStatusType::Stopping
115 } else {
116 target.state == CanisterStatusType::Stopped
117 }
118 });
119 if !lifecycle_matches {
120 return Err(RestoreSafetyError::TargetNotStopped);
121 }
122 if before_start
123 && observation
124 .targets()
125 .iter()
126 .any(|target| target.restored_acceptance.is_none())
127 {
128 return Err(RestoreSafetyError::RestoredAcceptanceRequired);
129 }
130 validate_safety(requirement, observation.safety(), before_start)?;
131 Ok(RestoreSafetyView {
132 request: digest,
133 requirement: requirement.digest(),
134 observation,
135 })
136}
137fn validate_safety(
138 requirement: &RestoreSafetyRequirementRecord,
139 evidence: &RestoreSafetyEvidence,
140 before_start: bool,
141) -> Result<(), RestoreSafetyError> {
142 match (requirement.safety(), evidence) {
143 (_, RestoreSafetyEvidence::Unresolved(_)) => {
144 return Err(RestoreSafetyError::UnresolvedExternalObligations);
145 }
146 (
147 RestoreSafetyLaneRecord::NoIrreversibleEffects,
148 RestoreSafetyEvidence::NoIrreversibleEffects(_),
149 ) => {}
150 (
151 RestoreSafetyLaneRecord::ApplicationFenced,
152 RestoreSafetyEvidence::ApplicationFenced(fence),
153 ) => {
154 let original = requirement
155 .expected_fence()
156 .ok_or(RestoreSafetyError::FenceMismatch)?;
157 if fence.binding.identity != original.identity {
158 return Err(RestoreSafetyError::FenceMismatch);
159 }
160 if fence.binding.membership_revision != original.membership_revision {
161 return Err(RestoreSafetyError::MembershipRevisionMismatch);
162 }
163 if fence.binding.external_obligations_revision != original.external_obligations_revision
164 {
165 return Err(RestoreSafetyError::ExternalObligationsRevisionMismatch);
166 }
167 if fence.state != ApplicationFenceState::Active {
168 return Err(RestoreSafetyError::FenceNotActive);
169 }
170 if before_start && fence.controlled_execution.is_none() {
171 return Err(RestoreSafetyError::ControlledExecutionRequired);
172 }
173 }
174 _ => return Err(RestoreSafetyError::SafetyLaneMismatch),
175 }
176 Ok(())
177}
178#[derive(Debug, Eq, Error, PartialEq)]
180pub enum RestoreSafetyError {
181 #[error("restore safety request mismatch")]
183 RequestMismatch,
184 #[error("restore safety observed {0} mismatch")]
186 ContextMismatch(&'static str),
187 #[error("restore safety inventory mismatch")]
189 InventoryMismatch,
190 #[error("restore safety selection mismatch")]
192 SelectionMismatch,
193 #[error("restore safety source plan mismatch")]
195 SourcePlanMismatch,
196 #[error("restore safety source artifacts mismatch")]
198 SourceArtifactsMismatch,
199 #[error("restore safety evidence lane mismatch")]
201 SafetyLaneMismatch,
202 #[error("restore safety external obligations unresolved")]
204 UnresolvedExternalObligations,
205 #[error("restore safety target is not stopped")]
207 TargetNotStopped,
208 #[error("restore safety restored-state acceptance required")]
210 RestoredAcceptanceRequired,
211 #[error("restore safety fence identity mismatch")]
213 FenceMismatch,
214 #[error("restore safety membership revision mismatch")]
216 MembershipRevisionMismatch,
217 #[error("restore safety external-obligations revision mismatch")]
219 ExternalObligationsRevisionMismatch,
220 #[error("restore safety fence not active")]
222 FenceNotActive,
223 #[error("restore safety controlled execution evidence required")]
225 ControlledExecutionRequired,
226 #[error("restore safety reports {reported} observations above ceiling {limit}")]
228 ObservationLimitExceeded {
229 limit: u32,
231 reported: u32,
233 },
234}
235
236#[cfg(test)]
237mod tests;