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