lib

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

profile.rs (9094B)


      1 //! Validated product relay profiles and directional access policy.
      2 
      3 use crate::{Error, RelayUrl, RelayUrlPolicy};
      4 use std::collections::BTreeSet;
      5 
      6 /// Directional access authorized for one configured relay.
      7 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
      8 #[non_exhaustive]
      9 pub enum RelayAccess {
     10     /// Queries and subscriptions are allowed; publication is never attempted.
     11     ReadOnly,
     12     /// Queries, subscriptions, and publication are allowed.
     13     ReadWrite,
     14 }
     15 
     16 impl RelayAccess {
     17     /// Returns whether the profile authorizes event reads.
     18     #[must_use]
     19     pub const fn can_read(self) -> bool {
     20         true
     21     }
     22 
     23     /// Returns whether the profile authorizes event publication.
     24     #[must_use]
     25     pub const fn can_write(self) -> bool {
     26         matches!(self, Self::ReadWrite)
     27     }
     28 }
     29 
     30 /// Host environment whose network trust rules produced a relay profile.
     31 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     32 #[non_exhaustive]
     33 pub enum RelayProfileKind {
     34     /// Public-Internet profile.
     35     Public,
     36     /// Development-only profile restricted to exact loopback destinations.
     37     Simulator,
     38     /// Physical-device profile using public TLS or literal private-network endpoints.
     39     Device,
     40 }
     41 
     42 /// One canonical relay endpoint with explicit network and access policy.
     43 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     44 pub struct RelayEndpoint {
     45     url: RelayUrl,
     46     policy: RelayUrlPolicy,
     47     access: RelayAccess,
     48 }
     49 
     50 impl RelayEndpoint {
     51     /// Parses one endpoint with explicit destination and access policy.
     52     pub fn new(
     53         value: impl AsRef<str>,
     54         policy: RelayUrlPolicy,
     55         access: RelayAccess,
     56     ) -> Result<Self, Error> {
     57         Ok(Self {
     58             url: RelayUrl::parse(value, policy)?,
     59             policy,
     60             access,
     61         })
     62     }
     63 
     64     /// Returns the canonical relay URL.
     65     #[must_use]
     66     pub const fn url(&self) -> &RelayUrl {
     67         &self.url
     68     }
     69 
     70     /// Returns the destination policy applied before and after DNS resolution.
     71     #[must_use]
     72     pub const fn policy(&self) -> RelayUrlPolicy {
     73         self.policy
     74     }
     75 
     76     /// Returns the read/write authority declared by the profile.
     77     #[must_use]
     78     pub const fn access(&self) -> RelayAccess {
     79         self.access
     80     }
     81 }
     82 
     83 /// Complete validated relay selection for one host environment.
     84 #[derive(Clone, Debug, Eq, PartialEq)]
     85 pub struct RelayProfile {
     86     kind: RelayProfileKind,
     87     endpoints: Vec<RelayEndpoint>,
     88 }
     89 
     90 impl RelayProfile {
     91     /// Builds an explicit profile with directional access per endpoint.
     92     ///
     93     /// This constructor does not inject a bundled endpoint. Callers provide the
     94     /// complete validated set they intend to use.
     95     pub fn explicit<I>(kind: RelayProfileKind, endpoints: I) -> Result<Self, Error>
     96     where
     97         I: IntoIterator<Item = RelayEndpoint>,
     98     {
     99         let endpoints = endpoints
    100             .into_iter()
    101             .take(crate::client::MAX_RELAYS + 1)
    102             .collect::<Vec<_>>();
    103         Self::validated(kind, endpoints)
    104     }
    105 
    106     fn validated(kind: RelayProfileKind, endpoints: Vec<RelayEndpoint>) -> Result<Self, Error> {
    107         if endpoints.is_empty() {
    108             return Err(Error::EmptyRelaySet);
    109         }
    110         if endpoints.len() > crate::client::MAX_RELAYS {
    111             return Err(Error::TooManyRelays {
    112                 max: crate::client::MAX_RELAYS,
    113                 actual: endpoints.len(),
    114             });
    115         }
    116         let mut seen = BTreeSet::new();
    117         for endpoint in &endpoints {
    118             let policy_allowed = match kind {
    119                 RelayProfileKind::Public => endpoint.policy == RelayUrlPolicy::Public,
    120                 RelayProfileKind::Simulator => endpoint.policy == RelayUrlPolicy::Local,
    121                 RelayProfileKind::Device => matches!(
    122                     endpoint.policy,
    123                     RelayUrlPolicy::Public | RelayUrlPolicy::PrivateNetwork
    124                 ),
    125             };
    126             if !policy_allowed {
    127                 return Err(Error::RelayProfilePolicyMismatch);
    128             }
    129             if !seen.insert(endpoint.url.clone()) {
    130                 return Err(Error::DuplicateRelayUrl);
    131             }
    132         }
    133         Ok(Self { kind, endpoints })
    134     }
    135 
    136     /// Returns the selected host-environment profile.
    137     #[must_use]
    138     pub const fn kind(&self) -> RelayProfileKind {
    139         self.kind
    140     }
    141 
    142     /// Returns endpoints in deterministic profile order.
    143     #[must_use]
    144     pub fn endpoints(&self) -> &[RelayEndpoint] {
    145         self.endpoints.as_slice()
    146     }
    147 }
    148 
    149 #[cfg(test)]
    150 pub(crate) fn test_profile<I, S>(
    151     kind: RelayProfileKind,
    152     policy: RelayUrlPolicy,
    153     values: I,
    154 ) -> Result<RelayProfile, Error>
    155 where
    156     I: IntoIterator<Item = S>,
    157     S: AsRef<str>,
    158 {
    159     let endpoints = values
    160         .into_iter()
    161         .map(|value| RelayEndpoint::new(value, policy, RelayAccess::ReadWrite))
    162         .collect::<Result<Vec<_>, _>>()?;
    163     RelayProfile::explicit(kind, endpoints)
    164 }
    165 
    166 #[cfg(test)]
    167 mod tests {
    168     use super::*;
    169 
    170     #[test]
    171     fn explicit_profile_requires_every_public_destination_and_authority() {
    172         let profile = RelayProfile::explicit(
    173             RelayProfileKind::Public,
    174             [RelayEndpoint::new(
    175                 "wss://write.example",
    176                 RelayUrlPolicy::Public,
    177                 RelayAccess::ReadWrite,
    178             )
    179             .expect("endpoint")],
    180         )
    181         .expect("public profile");
    182         assert_eq!(profile.kind(), RelayProfileKind::Public);
    183         assert_eq!(profile.endpoints().len(), 1);
    184         assert_eq!(profile.endpoints()[0].url().as_str(), "wss://write.example");
    185         assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadWrite);
    186     }
    187 
    188     #[test]
    189     fn explicit_profile_preserves_directional_access_without_injecting_endpoints() {
    190         let profile = RelayProfile::explicit(
    191             RelayProfileKind::Public,
    192             [
    193                 RelayEndpoint::new(
    194                     "wss://read.example",
    195                     RelayUrlPolicy::Public,
    196                     RelayAccess::ReadOnly,
    197                 )
    198                 .expect("read endpoint"),
    199                 RelayEndpoint::new(
    200                     "wss://write.example",
    201                     RelayUrlPolicy::Public,
    202                     RelayAccess::ReadWrite,
    203                 )
    204                 .expect("write endpoint"),
    205             ],
    206         )
    207         .expect("explicit profile");
    208 
    209         assert_eq!(profile.endpoints().len(), 2);
    210         assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadOnly);
    211         assert_eq!(profile.endpoints()[1].access(), RelayAccess::ReadWrite);
    212         assert!(!profile.endpoints()[0].access().can_write());
    213         assert!(profile.endpoints()[1].access().can_write());
    214     }
    215 
    216     #[test]
    217     fn profile_kind_rejects_mismatched_explicit_destination_policy() {
    218         let public = RelayEndpoint::new(
    219             "wss://relay.example",
    220             RelayUrlPolicy::Public,
    221             RelayAccess::ReadOnly,
    222         )
    223         .expect("public endpoint");
    224         let local = RelayEndpoint::new(
    225             "ws://127.0.0.1:7447",
    226             RelayUrlPolicy::Local,
    227             RelayAccess::ReadWrite,
    228         )
    229         .expect("local endpoint");
    230         let private = RelayEndpoint::new(
    231             "wss://10.0.0.5:7447",
    232             RelayUrlPolicy::PrivateNetwork,
    233             RelayAccess::ReadWrite,
    234         )
    235         .expect("private endpoint");
    236 
    237         assert!(RelayProfile::explicit(RelayProfileKind::Public, [public.clone()]).is_ok());
    238         assert!(RelayProfile::explicit(RelayProfileKind::Simulator, [local.clone()]).is_ok());
    239         assert!(RelayProfile::explicit(RelayProfileKind::Device, [public]).is_ok());
    240         assert!(RelayProfile::explicit(RelayProfileKind::Device, [private]).is_ok());
    241         assert_eq!(
    242             RelayProfile::explicit(RelayProfileKind::Public, [local]).unwrap_err(),
    243             Error::RelayProfilePolicyMismatch
    244         );
    245     }
    246 
    247     #[test]
    248     fn empty_and_duplicate_profiles_remain_rejected() {
    249         assert!(
    250             RelayProfile::explicit(RelayProfileKind::Public, Vec::<RelayEndpoint>::new()).is_err()
    251         );
    252         let endpoint = RelayEndpoint::new(
    253             "wss://relay.example",
    254             RelayUrlPolicy::Public,
    255             RelayAccess::ReadOnly,
    256         )
    257         .expect("endpoint");
    258         assert!(
    259             RelayProfile::explicit(RelayProfileKind::Public, [endpoint.clone(), endpoint]).is_err()
    260         );
    261     }
    262 
    263     #[test]
    264     fn excessive_and_infinite_endpoint_iterators_terminate_at_the_public_bound() {
    265         let endpoint = RelayEndpoint::new(
    266             "wss://relay.example",
    267             RelayUrlPolicy::Public,
    268             RelayAccess::ReadOnly,
    269         )
    270         .expect("endpoint");
    271         assert_eq!(
    272             RelayProfile::explicit(RelayProfileKind::Public, std::iter::repeat(endpoint),)
    273                 .unwrap_err(),
    274             Error::TooManyRelays {
    275                 max: crate::client::MAX_RELAYS,
    276                 actual: crate::client::MAX_RELAYS + 1,
    277             }
    278         );
    279     }
    280 }