Skip to main content

ic_backup/policy/snapshot_read/
mod.rs

1//! Pure exact-result and caller snapshot-read checks; no IO, control or dispatch.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    snapshot_read::{SnapshotReadObservation, SnapshotReadRequest, SnapshotVisibility},
6};
7use thiserror::Error;
8
9/// Read permission established by qualified current input; never mutation control.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum SnapshotReadPath {
12    /// Exact original caller is in known current controllers.
13    Controller,
14    /// Actually observed snapshot visibility is public.
15    Public,
16    /// Exact original caller is in the known snapshot viewer list.
17    AllowedViewer,
18}
19/// Read-only matching evidence; not a dispatch, spending or snapshot-completion permit.
20#[derive(Clone, Debug)]
21pub struct SnapshotReadView<'a> {
22    request: ArtifactChecksumRecord,
23    observation: &'a SnapshotReadObservation,
24    path: SnapshotReadPath,
25}
26impl SnapshotReadView<'_> {
27    /// Read current full-intent/operation/read-payload/challenge-bound request digest.
28    #[must_use]
29    pub const fn request(&self) -> &ArtifactChecksumRecord {
30        &self.request
31    }
32    /// Read actually observed matching canonical target.
33    #[must_use]
34    pub fn target(&self) -> &str {
35        self.observation.target()
36    }
37    /// Read known current snapshot visibility.
38    #[must_use]
39    pub const fn visibility(&self) -> &SnapshotVisibility {
40        self.observation.visibility()
41    }
42    /// Read the qualified caller read path; controller evidence takes precedence when known.
43    #[must_use]
44    pub const fn path(&self) -> SnapshotReadPath {
45        self.path
46    }
47    /// Read opaque qualified evidence identifier.
48    #[must_use]
49    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
50        self.observation.evidence()
51    }
52    /// Read reported actual calls, without spending or replenishing allowance.
53    #[must_use]
54    pub const fn remote_observations(&self) -> u32 {
55        self.observation.remote_observations()
56    }
57}
58/// Match exact current request/context/target/call bound and snapshot visibility permission.
59///
60/// The caller qualifies input authenticity, actual fresh observations and custody.
61/// Public or explicit viewer membership can admit read-only evidence without a
62/// controller projection; unknown controllers cannot satisfy the controller path.
63/// No status visibility, Root proxy or serialized Proven flag is considered. Policy
64/// performs no IO, provider invocation, serialization, journal transitions or scheduling.
65/// # Errors
66/// Rejects identity/call mismatches, unobserved necessary controllers and absent caller access.
67pub 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/// Typed denial; no error alters consumption or admits any paid effect.
115#[derive(Debug, Eq, Error, PartialEq)]
116pub enum SnapshotReadError {
117    /// Current original intent/operation/read-payload/challenge/ceiling differs.
118    #[error("snapshot read request mismatch")]
119    RequestMismatch,
120    /// Actually observed context differs.
121    #[error("snapshot read observed {0} mismatch")]
122    ContextMismatch(&'static str),
123    /// Actually observed physical target differs.
124    #[error("snapshot read observed target mismatch")]
125    TargetMismatch,
126    /// No public/viewer path applies and controller evidence is unobserved.
127    #[error("snapshot read requires unobserved controller evidence")]
128    ControllersUnobserved,
129    /// Exact original caller is neither a known controller nor an admitted viewer.
130    #[error("selected caller cannot read observed snapshots")]
131    CallerCannotRead,
132    /// Actual call reporting exceeds the descriptive invocation ceiling.
133    #[error("snapshot read reports {reported} observations above ceiling {limit}")]
134    ObservationLimitExceeded {
135        /// Original descriptive ceiling, not spending authority.
136        limit: u32,
137        /// Reported actual calls.
138        reported: u32,
139    },
140}
141
142#[cfg(test)]
143mod tests;