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    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/// Typed denial; no error alters consumption or admits any paid effect.
129#[derive(Debug, Eq, Error, PartialEq)]
130pub enum SnapshotReadError {
131    /// Current original intent/operation/read-payload/challenge/ceiling differs.
132    #[error("snapshot read request mismatch")]
133    RequestMismatch,
134    /// Actually observed context differs.
135    #[error("snapshot read observed {0} mismatch")]
136    ContextMismatch(&'static str),
137    /// Actually observed physical target differs.
138    #[error("snapshot read observed target mismatch")]
139    TargetMismatch,
140    /// No public/viewer path applies and controller evidence is unobserved.
141    #[error("snapshot read requires unobserved controller evidence")]
142    ControllersUnobserved,
143    /// Exact original caller is neither a known controller nor an admitted viewer.
144    #[error("selected caller cannot read observed snapshots")]
145    CallerCannotRead,
146    /// Actual call reporting exceeds the descriptive invocation ceiling.
147    #[error("snapshot read reports {reported} observations above ceiling {limit}")]
148    ObservationLimitExceeded {
149        /// Original descriptive ceiling, not spending authority.
150        limit: u32,
151        /// Reported actual calls.
152        reported: u32,
153    },
154}
155
156#[cfg(test)]
157mod tests;