Skip to main content

ic_backup/model/snapshot_read/
mod.rs

1//! Ephemeral snapshot-list permission evidence; no control or paid-call authority.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    attempt_journal::OperationBindingRecord,
6    control_authority::ControllerSet,
7    ic_request::{IcManagementMethodRecord, IcManagementRequestRecord, IcRequestError},
8    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
9};
10use thiserror::Error;
11
12/// Maximum principals in a maintained IC snapshot allowed-viewer list.
13pub const MAX_SNAPSHOT_VIEWERS: usize = 10;
14/// Maximum descriptive remote observations per invocation; never spending authority.
15pub const MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS: u32 = 1024;
16
17/// Known canonical snapshot viewers; equivalent duplicates reject.
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct SnapshotViewerSet {
20    principals: Vec<String>,
21}
22impl SnapshotViewerSet {
23    /// Normalize and sort a bounded exact principal set, including known empty.
24    /// # Errors
25    /// Rejects excessive counts, invalid principals and canonical duplicates.
26    pub fn new(mut principals: Vec<String>) -> Result<Self, SnapshotReadObservationError> {
27        if principals.len() > MAX_SNAPSHOT_VIEWERS {
28            return Err(SnapshotReadObservationError::TooManyViewers);
29        }
30        for principal in &mut principals {
31            *principal = super::principal::canonical_text(principal)
32                .ok_or(SnapshotReadObservationError::InvalidPrincipal)?;
33        }
34        principals.sort();
35        if principals.windows(2).any(|pair| pair[0] == pair[1]) {
36            return Err(SnapshotReadObservationError::DuplicateViewer);
37        }
38        Ok(Self { principals })
39    }
40    /// Read canonical viewers; ordering confers no control routing.
41    #[must_use]
42    pub fn principals(&self) -> &[String] {
43        &self.principals
44    }
45    /// Test the exact original canonical caller.
46    #[must_use]
47    pub fn contains_caller(&self, binding: &OperationBindingRecord) -> bool {
48        self.principals
49            .binary_search_by(|principal| principal.as_str().cmp(binding.caller()))
50            .is_ok()
51    }
52}
53
54/// Actually observed snapshot visibility, distinct from status/log visibility.
55///
56/// No default, unknown variant or Serde admission exists. An integration unable
57/// to qualify this exact setting must fail at its provider boundary.
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub enum SnapshotVisibility {
60    /// Only current controllers may read snapshots.
61    Controllers,
62    /// Anyone may read snapshots; this grants no mutation control.
63    Public,
64    /// These exact viewers and current controllers may read snapshots.
65    AllowedViewers(SnapshotViewerSet),
66}
67
68/// Original mutation identity and independently declared exact snapshot-list payload.
69///
70/// This request does not inspect journals or reserve calls. The caller supplies
71/// the observation digest from its own exact retained intent/reservation owner;
72/// digest equality alone proves neither retention nor spending admission.
73/// Challenge freshness, authenticated context and prior per-call accounting stay
74/// integration-owned. Only list is implemented; metadata/data codecs remain pending.
75#[derive(Clone, Debug)]
76pub struct SnapshotReadRequest<'a> {
77    binding: OperationBindingRecord,
78    wire: &'a IcManagementRequestRecord,
79    challenge: ArtifactChecksumRecord,
80    max_remote_observations: u32,
81}
82impl<'a> SnapshotReadRequest<'a> {
83    /// Bind exact list bytes to the original operation target and separate observation digest.
84    /// # Errors
85    /// Rejects unsupported methods, unknown operations, changed target/payload and excess ceiling.
86    pub fn new(
87        plan: &OperationPlanRecord,
88        sequence: u64,
89        wire: &'a IcManagementRequestRecord,
90        observation: &ArtifactChecksumRecord,
91        challenge: ArtifactChecksumRecord,
92        max_remote_observations: u32,
93    ) -> Result<Self, SnapshotReadRequestError> {
94        if max_remote_observations > MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS {
95            return Err(SnapshotReadRequestError::ObservationLimitTooLarge);
96        }
97        if wire.method() != IcManagementMethodRecord::ListCanisterSnapshots {
98            return Err(SnapshotReadRequestError::UnsupportedMethod);
99        }
100        let binding = plan.attempt_authority(sequence)?.binding().clone();
101        wire.validate_observation_binding(&binding, observation)?;
102        Ok(Self {
103            binding,
104            wire,
105            challenge,
106            max_remote_observations,
107        })
108    }
109    /// Read exact original mutation intent/context/target/request identity.
110    #[must_use]
111    pub const fn binding(&self) -> &OperationBindingRecord {
112        &self.binding
113    }
114    /// Read validated exact snapshot-list routing/method/Candid bytes.
115    #[must_use]
116    pub const fn wire(&self) -> &IcManagementRequestRecord {
117        self.wire
118    }
119    /// Read caller-owned challenge; its value does not establish freshness.
120    #[must_use]
121    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
122        &self.challenge
123    }
124    /// Read descriptive invocation ceiling, distinct from spending allowances.
125    #[must_use]
126    pub const fn max_remote_observations(&self) -> u32 {
127        self.max_remote_observations
128    }
129    /// Hash full original intent/sequence, separate exact read payload, challenge and ceiling.
130    ///
131    /// Encoding: NUL-terminated ASCII v1 domain, 64 ASCII intent bytes, u64
132    /// big-endian sequence, 64 ASCII list wire-digest bytes, 64 ASCII challenge
133    /// bytes and u32 big-endian ceiling. Observed visibility/evidence are excluded.
134    #[must_use]
135    pub fn digest(&self) -> ArtifactChecksumRecord {
136        let mut bytes = b"ic-backup/snapshot-read/v1\0".to_vec();
137        bytes.extend_from_slice(self.binding.intent().as_bytes());
138        bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
139        bytes.extend_from_slice(self.wire.digest().hash().as_bytes());
140        bytes.extend_from_slice(self.challenge.hash().as_bytes());
141        bytes.extend_from_slice(&self.max_remote_observations.to_be_bytes());
142        ArtifactChecksumRecord::from_bytes(&bytes)
143    }
144}
145
146/// Passive current provider data; no serialized authority or permissive defaults.
147#[derive(Clone, Debug)]
148pub struct SnapshotReadObservationInput {
149    /// Exact current request digest.
150    pub request: ArtifactChecksumRecord,
151    /// Actually observed canonical network/caller/release, not echoed labels.
152    pub context: PlanContextRecord,
153    /// Actually observed physical target; normalized at model admission.
154    pub target: String,
155    /// Qualified current snapshot visibility; status visibility never substitutes.
156    pub visibility: SnapshotVisibility,
157    /// Complete known controllers, or None when not observed.
158    ///
159    /// None cannot establish controller access. Independently known public access
160    /// or exact viewer membership needs no controller projection. Some(empty) is
161    /// known empty, distinct from unknown. No fallback guesses a controller set.
162    pub controllers: Option<ControllerSet>,
163    /// Opaque qualified evidence identifier, not a signature or dispatch permit.
164    pub evidence: ArtifactChecksumRecord,
165    /// Actual remote calls, separately accounted before each call by the integration.
166    pub remote_observations: u32,
167}
168/// Immutable model-admitted current evidence, without persisted authority admission.
169#[derive(Clone, Debug)]
170pub struct SnapshotReadObservation {
171    input: SnapshotReadObservationInput,
172}
173impl SnapshotReadObservation {
174    /// Normalize the actually observed physical target.
175    /// # Errors
176    /// Rejects malformed or oversized target principals.
177    pub fn new(
178        mut input: SnapshotReadObservationInput,
179    ) -> Result<Self, SnapshotReadObservationError> {
180        input.target = super::principal::canonical_text(&input.target)
181            .ok_or(SnapshotReadObservationError::InvalidPrincipal)?;
182        Ok(Self { input })
183    }
184    /// Read exact current request identity.
185    #[must_use]
186    pub const fn request(&self) -> &ArtifactChecksumRecord {
187        &self.input.request
188    }
189    /// Read actually observed canonical context.
190    #[must_use]
191    pub const fn context(&self) -> &PlanContextRecord {
192        &self.input.context
193    }
194    /// Read actually observed canonical target.
195    #[must_use]
196    pub fn target(&self) -> &str {
197        &self.input.target
198    }
199    /// Read known current snapshot visibility.
200    #[must_use]
201    pub const fn visibility(&self) -> &SnapshotVisibility {
202        &self.input.visibility
203    }
204    /// Read complete known controllers or explicitly unobserved evidence.
205    #[must_use]
206    pub const fn controllers(&self) -> Option<&ControllerSet> {
207        self.input.controllers.as_ref()
208    }
209    /// Read opaque integration-qualified evidence identifier.
210    #[must_use]
211    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
212        &self.input.evidence
213    }
214    /// Read reported calls, without consuming or replenishing any allowance.
215    #[must_use]
216    pub const fn remote_observations(&self) -> u32 {
217        self.input.remote_observations
218    }
219}
220
221/// Typed model observation admission failure; diagnostics retain no raw identities.
222#[derive(Debug, Eq, Error, PartialEq)]
223pub enum SnapshotReadObservationError {
224    /// Principal text is malformed or exceeds its owning boundary.
225    #[error("invalid snapshot read principal")]
226    InvalidPrincipal,
227    /// Viewer list exceeds the maintained IC limit.
228    #[error("snapshot viewer set exceeds {MAX_SNAPSHOT_VIEWERS}")]
229    TooManyViewers,
230    /// Equivalent principals cannot create duplicate viewer entries.
231    #[error("duplicate snapshot viewer")]
232    DuplicateViewer,
233}
234/// Typed exact original-plan/read-payload admission failure.
235#[derive(Debug, Error)]
236pub enum SnapshotReadRequestError {
237    /// The descriptive ceiling exceeds its maintained bound.
238    #[error("snapshot read observation ceiling exceeds {MAX_SNAPSHOT_READ_REMOTE_OBSERVATIONS}")]
239    ObservationLimitTooLarge,
240    /// Only the implemented snapshot-list codec is admitted.
241    #[error("snapshot read contract requires list_canister_snapshots")]
242    UnsupportedMethod,
243    /// Original operation plan rejects binding derivation.
244    #[error(transparent)]
245    Plan(#[from] OperationPlanError),
246    /// Exact read target or separate observation digest differs.
247    #[error(transparent)]
248    Payload(#[from] IcRequestError),
249}
250
251#[cfg(test)]
252mod tests;