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