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    let binding = request.binding();
73    super::observed_context::admit_request_context(
74        &digest,
75        observation.request(),
76        binding,
77        observation.context(),
78        SnapshotReadError::RequestMismatch,
79        SnapshotReadError::ContextMismatch,
80    )?;
81    super::observed_context::admit_target_limit(
82        binding,
83        observation.target(),
84        request.max_remote_observations(),
85        observation.remote_observations(),
86        SnapshotReadError::TargetMismatch,
87        |limit, reported| SnapshotReadError::ObservationLimitExceeded { limit, reported },
88    )?;
89    let controller = observation
90        .controllers()
91        .is_some_and(|set| set.contains_caller(binding));
92    let path = if controller {
93        SnapshotReadPath::Controller
94    } else {
95        match observation.visibility() {
96            SnapshotVisibility::Public => SnapshotReadPath::Public,
97            SnapshotVisibility::AllowedViewers(viewers) if viewers.contains_caller(binding) => {
98                SnapshotReadPath::AllowedViewer
99            }
100            SnapshotVisibility::Controllers | SnapshotVisibility::AllowedViewers(_) => {
101                return Err(if observation.controllers().is_none() {
102                    SnapshotReadError::ControllersUnobserved
103                } else {
104                    SnapshotReadError::CallerCannotRead
105                });
106            }
107        }
108    };
109    Ok(SnapshotReadView {
110        request: digest,
111        observation,
112        path,
113    })
114}
115/// Typed denial; no error alters consumption or admits any paid effect.
116#[derive(Debug, Eq, Error, PartialEq)]
117pub enum SnapshotReadError {
118    /// Current original intent/operation/read-payload/challenge/ceiling differs.
119    #[error("snapshot read request mismatch")]
120    RequestMismatch,
121    /// Actually observed context differs.
122    #[error("snapshot read observed {0} mismatch")]
123    ContextMismatch(&'static str),
124    /// Actually observed physical target differs.
125    #[error("snapshot read observed target mismatch")]
126    TargetMismatch,
127    /// No public/viewer path applies and controller evidence is unobserved.
128    #[error("snapshot read requires unobserved controller evidence")]
129    ControllersUnobserved,
130    /// Exact original caller is neither a known controller nor an admitted viewer.
131    #[error("selected caller cannot read observed snapshots")]
132    CallerCannotRead,
133    /// Actual call reporting exceeds the descriptive invocation ceiling.
134    #[error("snapshot read reports {reported} observations above ceiling {limit}")]
135    ObservationLimitExceeded {
136        /// Original descriptive ceiling, not spending authority.
137        limit: u32,
138        /// Reported actual calls.
139        reported: u32,
140    },
141}
142
143#[cfg(test)]
144mod tests;