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