Skip to main content

ic_backup/model/membership/
mod.rs

1//! Ephemeral membership checks bound to original intent; no persisted fresh-authority flags.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    attempt_journal::OperationBindingRecord,
6    inventory::InventoryRecord,
7    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
8};
9use thiserror::Error;
10
11/// Maximum remote observations described by one membership request, without spending authority.
12pub const MAX_MEMBERSHIP_REMOTE_OBSERVATIONS: u32 = 1024;
13
14/// Explicit effect boundary being checked; neither variant admits an effect.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum MembershipBoundary {
17    /// Current membership before one reviewed effect.
18    BeforeEffect,
19    /// Current membership after that effect; not proof of uninterrupted membership.
20    AfterEffect,
21}
22
23/// Immutable ephemeral request derived from the original plan and exact operation.
24///
25/// The integration supplies a fresh unpredictable challenge and owns its uniqueness,
26/// current observation timing, coherent custody and separately approved observation
27/// spending. The call ceiling is descriptive, not an allowance or journal reservation.
28/// This type has no serialization, persistence or default-provider admission.
29#[derive(Clone, Debug)]
30pub struct MembershipObservationRequest<'a> {
31    plan: &'a OperationPlanRecord,
32    binding: OperationBindingRecord,
33    challenge: ArtifactChecksumRecord,
34    boundary: MembershipBoundary,
35    max_remote_observations: u32,
36}
37
38impl<'a> MembershipObservationRequest<'a> {
39    /// Bind a challenge, explicit boundary and bounded call ceiling to original intent.
40    ///
41    /// Zero remote calls permits a qualified local integration; it cannot authorize
42    /// a paid probe. Original mutation/reconciliation allowances are never changed.
43    /// # Errors
44    /// Rejects unknown operations or an excessive descriptive call ceiling.
45    pub fn new(
46        plan: &'a OperationPlanRecord,
47        operation_sequence: u64,
48        challenge: ArtifactChecksumRecord,
49        boundary: MembershipBoundary,
50        max_remote_observations: u32,
51    ) -> Result<Self, MembershipRequestError> {
52        if max_remote_observations > MAX_MEMBERSHIP_REMOTE_OBSERVATIONS {
53            return Err(MembershipRequestError::ObservationLimitTooLarge);
54        }
55        let binding = plan
56            .attempt_authority(operation_sequence)?
57            .binding()
58            .clone();
59        Ok(Self {
60            plan,
61            binding,
62            challenge,
63            boundary,
64            max_remote_observations,
65        })
66    }
67    /// Read original intent, operation, context, target and mutation-payload identity.
68    #[must_use]
69    pub const fn binding(&self) -> &OperationBindingRecord {
70        &self.binding
71    }
72    /// Read the full expected declared inventory, including unselected parents.
73    #[must_use]
74    pub const fn inventory(&self) -> &InventoryRecord {
75        self.plan.inventory()
76    }
77    /// Read exact canonical selected principals; this is not lifecycle order.
78    #[must_use]
79    pub fn selected_targets(&self) -> &[String] {
80        self.plan.selected_targets()
81    }
82    /// Read the integration-owned challenge; its value alone establishes no freshness.
83    #[must_use]
84    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
85        &self.challenge
86    }
87    /// Read the explicit effect boundary.
88    #[must_use]
89    pub const fn boundary(&self) -> MembershipBoundary {
90        self.boundary
91    }
92    /// Read the descriptive remote-call ceiling, not a dispatch or spending permit.
93    #[must_use]
94    pub const fn max_remote_observations(&self) -> u32 {
95        self.max_remote_observations
96    }
97    /// Hash original full plan intent, operation, challenge, boundary and call ceiling.
98    ///
99    /// Encoding uses the NUL-terminated ASCII v1 domain, canonical 64-byte ASCII
100    /// intent, big-endian u64 sequence, canonical 64-byte ASCII challenge, boundary
101    /// byte (before=0, after=1), then big-endian u32 descriptive call ceiling.
102    /// Context/inventory/selection/request/original allowances bind through full
103    /// original intent. Observation results, revision and evidence are excluded.
104    #[must_use]
105    pub fn digest(&self) -> ArtifactChecksumRecord {
106        let mut bytes = b"ic-backup/membership-request/v1\0".to_vec();
107        bytes.extend_from_slice(self.binding.intent().as_bytes());
108        bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
109        bytes.extend_from_slice(self.challenge.hash().as_bytes());
110        bytes.push(match self.boundary {
111            MembershipBoundary::BeforeEffect => 0,
112            MembershipBoundary::AfterEffect => 1,
113        });
114        bytes.extend_from_slice(&self.max_remote_observations.to_be_bytes());
115        ArtifactChecksumRecord::from_bytes(&bytes)
116    }
117}
118
119/// Passive current-observation result supplied by a trusted membership integration.
120///
121/// No Serialize/Deserialize implementation exists: retained JSON is not a current
122/// observation. The caller must qualify the provider and its actual observations;
123/// these fields and opaque evidence digests are not self-authenticating.
124#[derive(Clone, Debug)]
125pub struct MembershipObservation {
126    /// Exact digest of the current challenge/boundary-bound request.
127    pub request: ArtifactChecksumRecord,
128    /// Actually observed network/caller/release, not copied expected labels.
129    pub context: PlanContextRecord,
130    /// Complete current inventory under the existing 1,024-target forest bound.
131    pub inventory: InventoryRecord,
132    /// Optional opaque current authority revision; equality proves no continuity.
133    pub revision: Option<ArtifactChecksumRecord>,
134    /// Exact opaque evidence identifier qualified by the integration owner.
135    pub evidence: ArtifactChecksumRecord,
136    /// Actual remote observations; each needs separate prior approved accounting.
137    pub remote_observations: u32,
138}
139
140/// Typed rejection before requesting any provider observation.
141#[derive(Debug, Error)]
142pub enum MembershipRequestError {
143    /// Descriptive remote-call ceiling exceeds the maintained bound.
144    #[error("membership call ceiling exceeds {MAX_MEMBERSHIP_REMOTE_OBSERVATIONS}")]
145    ObservationLimitTooLarge,
146    /// Original plan cannot derive the named operation binding.
147    #[error(transparent)]
148    Plan(#[from] OperationPlanError),
149}
150
151#[cfg(test)]
152mod tests;