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