lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

admission.rs (7097B)


      1 //! Policy-authorized event admission and visibility typestates.
      2 
      3 pub use crate::verification::{
      4     ContractValidatedEvent, Error, IdVerifiedEvent, RawEvent, SignatureVerifiedEvent,
      5     SignatureVerifier,
      6 };
      7 
      8 use crate::envelope::EventEnvelope;
      9 
     10 /// **Host SPI:** authorizes a contract-validated event for admission.
     11 ///
     12 /// Downstream implementations are supported. The trait is dyn-compatible when
     13 /// its `Error` associated type is specified, and native implementations must be
     14 /// `Send + Sync`. Policy evaluation is synchronous, has no cancellation or
     15 /// deadline boundary, returns the implementation's error without translation,
     16 /// and must not durably commit state.
     17 pub trait AdmissionPolicy: Send + Sync {
     18     /// Host-owned rejection type returned unchanged by [`ContractValidatedEvent::admit_with`].
     19     type Error;
     20 
     21     /// Stable identifier for the policy whose decision produced the state.
     22     fn policy_id(&self) -> &'static str;
     23 
     24     /// Returns success only when this policy admits the supplied event.
     25     fn admit(&self, event: &ContractValidatedEvent) -> Result<(), Self::Error>;
     26 }
     27 
     28 /// **Host SPI:** authorizes an admitted event to become visible.
     29 ///
     30 /// Downstream implementations are supported. The trait is dyn-compatible when
     31 /// its `Error` associated type is specified, and native implementations must be
     32 /// `Send + Sync`. Policy evaluation is synchronous, has no cancellation or
     33 /// deadline boundary, returns the implementation's error without translation,
     34 /// and must not durably commit state.
     35 pub trait VisibilityPolicy: Send + Sync {
     36     /// Host-owned rejection type returned unchanged by [`AdmittedEvent::make_visible_with`].
     37     type Error;
     38 
     39     /// Stable identifier for the policy whose decision produced the state.
     40     fn policy_id(&self) -> &'static str;
     41 
     42     /// Returns success only when this policy permits the event to be visible.
     43     fn make_visible(&self, event: &AdmittedEvent) -> Result<(), Self::Error>;
     44 }
     45 
     46 /// A contract-validated event accepted by an explicit admission policy.
     47 ///
     48 /// ```compile_fail
     49 /// use radroots_event::admission::{AdmittedEvent, ContractValidatedEvent};
     50 ///
     51 /// fn bypass_policy(event: ContractValidatedEvent) -> AdmittedEvent {
     52 ///     AdmittedEvent::new(event, "allow-all")
     53 /// }
     54 /// ```
     55 #[derive(Clone, Debug, PartialEq, Eq)]
     56 pub struct AdmittedEvent {
     57     event: ContractValidatedEvent,
     58     policy_id: &'static str,
     59 }
     60 
     61 impl ContractValidatedEvent {
     62     /// Runs an admission policy and advances only when it succeeds.
     63     pub fn admit_with<P>(self, policy: &P) -> Result<AdmittedEvent, P::Error>
     64     where
     65         P: AdmissionPolicy + ?Sized,
     66     {
     67         policy.admit(&self)?;
     68         Ok(AdmittedEvent {
     69             event: self,
     70             policy_id: policy.policy_id(),
     71         })
     72     }
     73 }
     74 
     75 impl AdmittedEvent {
     76     #[must_use]
     77     pub const fn validated_event(&self) -> &ContractValidatedEvent {
     78         &self.event
     79     }
     80 
     81     #[must_use]
     82     pub const fn event(&self) -> &EventEnvelope {
     83         self.event.event()
     84     }
     85 
     86     #[must_use]
     87     pub const fn policy_id(&self) -> &'static str {
     88         self.policy_id
     89     }
     90 
     91     #[must_use]
     92     pub fn into_validated_event(self) -> ContractValidatedEvent {
     93         self.event
     94     }
     95 
     96     /// Runs a visibility policy and advances only when it succeeds.
     97     pub fn make_visible_with<P>(self, policy: &P) -> Result<VisibleEvent, P::Error>
     98     where
     99         P: VisibilityPolicy + ?Sized,
    100     {
    101         policy.make_visible(&self)?;
    102         Ok(VisibleEvent {
    103             event: self,
    104             policy_id: policy.policy_id(),
    105         })
    106     }
    107 }
    108 
    109 /// An admitted event accepted by an explicit visibility policy.
    110 ///
    111 /// ```compile_fail
    112 /// use radroots_event::admission::{AdmittedEvent, VisibleEvent};
    113 ///
    114 /// fn bypass_visibility(event: AdmittedEvent) -> VisibleEvent {
    115 ///     VisibleEvent::from(event)
    116 /// }
    117 /// ```
    118 #[derive(Clone, Debug, PartialEq, Eq)]
    119 pub struct VisibleEvent {
    120     event: AdmittedEvent,
    121     policy_id: &'static str,
    122 }
    123 
    124 impl VisibleEvent {
    125     #[must_use]
    126     pub const fn admitted_event(&self) -> &AdmittedEvent {
    127         &self.event
    128     }
    129 
    130     #[must_use]
    131     pub const fn event(&self) -> &EventEnvelope {
    132         self.event.event()
    133     }
    134 
    135     #[must_use]
    136     pub const fn policy_id(&self) -> &'static str {
    137         self.policy_id
    138     }
    139 
    140     #[must_use]
    141     pub fn into_admitted_event(self) -> AdmittedEvent {
    142         self.event
    143     }
    144 }
    145 
    146 #[cfg(test)]
    147 #[cfg_attr(coverage_nightly, coverage(off))]
    148 mod tests {
    149     use super::*;
    150     use crate::envelope::{EventEnvelope, EventEnvelopeParts};
    151 
    152     struct Allow;
    153 
    154     impl SignatureVerifier for Allow {
    155         fn verify_signature(&self, _event: &EventEnvelope) -> Result<(), Error> {
    156             Ok(())
    157         }
    158     }
    159 
    160     impl AdmissionPolicy for Allow {
    161         type Error = core::convert::Infallible;
    162 
    163         fn policy_id(&self) -> &'static str {
    164             "test.admission.allow.v1"
    165         }
    166 
    167         fn admit(&self, _event: &ContractValidatedEvent) -> Result<(), Self::Error> {
    168             Ok(())
    169         }
    170     }
    171 
    172     impl VisibilityPolicy for Allow {
    173         type Error = core::convert::Infallible;
    174 
    175         fn policy_id(&self) -> &'static str {
    176             "test.visibility.allow.v1"
    177         }
    178 
    179         fn make_visible(&self, _event: &AdmittedEvent) -> Result<(), Self::Error> {
    180             Ok(())
    181         }
    182     }
    183 
    184     #[test]
    185     fn positive_vector_traverses_the_complete_transition_graph() {
    186         let admission_policy: &dyn AdmissionPolicy<Error = core::convert::Infallible> = &Allow;
    187         let visibility_policy: &dyn VisibilityPolicy<Error = core::convert::Infallible> = &Allow;
    188         let envelope = EventEnvelope::new(EventEnvelopeParts {
    189             id: "762bee187e9e645b81ec26ade05a69b5e8398caf527be8de0d9a45311ed0c7a0"
    190                 .to_owned(),
    191             author: "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"
    192                 .to_owned(),
    193             created_at: 1_800_000_100,
    194             kind: 0,
    195             tags: vec![],
    196             content: "{\"display_name\":\"Moss Street Farm\",\"bot\":false,\"website\":\"https://mossstreet.example\",\"picture\":42}".to_owned(),
    197             sig: "4290da0bb6422986647bc8cd5f63bd52d49f41e7b665d3b47105b8109183e8d596f322c531d4061df53e1d2b70fda12d5d1c14f3720d7a56d9d0a03746af5109".to_owned(),
    198         })
    199         .expect("valid profile event");
    200 
    201         let visible = RawEvent::new(envelope)
    202             .verify_id()
    203             .expect("verified id")
    204             .verify_signature(&Allow)
    205             .expect("verified signature")
    206             .validate_contract()
    207             .expect("validated contract")
    208             .admit_with(admission_policy)
    209             .expect("admitted")
    210             .make_visible_with(visibility_policy)
    211             .expect("visible");
    212 
    213         assert_eq!(
    214             visible.admitted_event().policy_id(),
    215             "test.admission.allow.v1"
    216         );
    217         assert_eq!(visible.policy_id(), "test.visibility.allow.v1");
    218         assert_eq!(
    219             visible.event().id().to_hex(),
    220             "762bee187e9e645b81ec26ade05a69b5e8398caf527be8de0d9a45311ed0c7a0"
    221         );
    222     }
    223 }