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 if let Some(field) = super::observed_context::first_mismatch(binding, observation.context()) {
77 return Err(SnapshotReadError::ContextMismatch(field));
78 }
79 if observation.target() != binding.target() {
80 return Err(SnapshotReadError::TargetMismatch);
81 }
82 if observation.remote_observations() > request.max_remote_observations() {
83 return Err(SnapshotReadError::ObservationLimitExceeded {
84 limit: request.max_remote_observations(),
85 reported: observation.remote_observations(),
86 });
87 }
88 let controller = observation
89 .controllers()
90 .is_some_and(|set| set.contains_caller(binding));
91 let path = if controller {
92 SnapshotReadPath::Controller
93 } else {
94 match observation.visibility() {
95 SnapshotVisibility::Public => SnapshotReadPath::Public,
96 SnapshotVisibility::AllowedViewers(viewers) if viewers.contains_caller(binding) => {
97 SnapshotReadPath::AllowedViewer
98 }
99 SnapshotVisibility::Controllers | SnapshotVisibility::AllowedViewers(_) => {
100 return Err(if observation.controllers().is_none() {
101 SnapshotReadError::ControllersUnobserved
102 } else {
103 SnapshotReadError::CallerCannotRead
104 });
105 }
106 }
107 };
108 Ok(SnapshotReadView {
109 request: digest,
110 observation,
111 path,
112 })
113}
114#[derive(Debug, Eq, Error, PartialEq)]
116pub enum SnapshotReadError {
117 #[error("snapshot read request mismatch")]
119 RequestMismatch,
120 #[error("snapshot read observed {0} mismatch")]
122 ContextMismatch(&'static str),
123 #[error("snapshot read observed target mismatch")]
125 TargetMismatch,
126 #[error("snapshot read requires unobserved controller evidence")]
128 ControllersUnobserved,
129 #[error("selected caller cannot read observed snapshots")]
131 CallerCannotRead,
132 #[error("snapshot read reports {reported} observations above ceiling {limit}")]
134 ObservationLimitExceeded {
135 limit: u32,
137 reported: u32,
139 },
140}
141
142#[cfg(test)]
143mod tests;