ic_backup/policy/snapshot_read/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 snapshot_read::{SnapshotReadObservation, SnapshotReadRequest, SnapshotVisibility},
6};
7use thiserror::Error;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum SnapshotReadPath {
12 Controller,
14 Public,
16 AllowedViewer,
18}
19#[derive(Clone, Debug)]
21pub struct SnapshotReadView<'a> {
22 request: ArtifactChecksumRecord,
23 observation: &'a SnapshotReadObservation,
24 path: SnapshotReadPath,
25}
26impl SnapshotReadView<'_> {
27 #[must_use]
29 pub const fn request(&self) -> &ArtifactChecksumRecord {
30 &self.request
31 }
32 #[must_use]
34 pub fn target(&self) -> &str {
35 self.observation.target()
36 }
37 #[must_use]
39 pub const fn visibility(&self) -> &SnapshotVisibility {
40 self.observation.visibility()
41 }
42 #[must_use]
44 pub const fn path(&self) -> SnapshotReadPath {
45 self.path
46 }
47 #[must_use]
49 pub const fn evidence(&self) -> &ArtifactChecksumRecord {
50 self.observation.evidence()
51 }
52 #[must_use]
54 pub const fn remote_observations(&self) -> u32 {
55 self.observation.remote_observations()
56 }
57}
58pub fn validate<'a>(
68 request: &SnapshotReadRequest<'_>,
69 observation: &'a SnapshotReadObservation,
70) -> Result<SnapshotReadView<'a>, SnapshotReadError> {
71 let digest = request.digest();
72 if *observation.request() != digest {
73 return Err(SnapshotReadError::RequestMismatch);
74 }
75 let binding = request.binding();
76 for (field, expected, actual) in [
77 (
78 "network",
79 binding.network(),
80 observation.context().network(),
81 ),
82 ("caller", binding.caller(), observation.context().caller()),
83 (
84 "release",
85 binding.release(),
86 observation.context().release(),
87 ),
88 ] {
89 if actual != expected {
90 return Err(SnapshotReadError::ContextMismatch(field));
91 }
92 }
93 if observation.target() != binding.target() {
94 return Err(SnapshotReadError::TargetMismatch);
95 }
96 if observation.remote_observations() > request.max_remote_observations() {
97 return Err(SnapshotReadError::ObservationLimitExceeded {
98 limit: request.max_remote_observations(),
99 reported: observation.remote_observations(),
100 });
101 }
102 let controller = observation
103 .controllers()
104 .is_some_and(|set| set.contains_caller(binding));
105 let path = if controller {
106 SnapshotReadPath::Controller
107 } else {
108 match observation.visibility() {
109 SnapshotVisibility::Public => SnapshotReadPath::Public,
110 SnapshotVisibility::AllowedViewers(viewers) if viewers.contains_caller(binding) => {
111 SnapshotReadPath::AllowedViewer
112 }
113 SnapshotVisibility::Controllers | SnapshotVisibility::AllowedViewers(_) => {
114 return Err(if observation.controllers().is_none() {
115 SnapshotReadError::ControllersUnobserved
116 } else {
117 SnapshotReadError::CallerCannotRead
118 });
119 }
120 }
121 };
122 Ok(SnapshotReadView {
123 request: digest,
124 observation,
125 path,
126 })
127}
128#[derive(Debug, Eq, Error, PartialEq)]
130pub enum SnapshotReadError {
131 #[error("snapshot read request mismatch")]
133 RequestMismatch,
134 #[error("snapshot read observed {0} mismatch")]
136 ContextMismatch(&'static str),
137 #[error("snapshot read observed target mismatch")]
139 TargetMismatch,
140 #[error("snapshot read requires unobserved controller evidence")]
142 ControllersUnobserved,
143 #[error("selected caller cannot read observed snapshots")]
145 CallerCannotRead,
146 #[error("snapshot read reports {reported} observations above ceiling {limit}")]
148 ObservationLimitExceeded {
149 limit: u32,
151 reported: u32,
153 },
154}
155
156#[cfg(test)]
157mod tests;