tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

write_gate.rs (9773B)


      1 use crate::{
      2     GroupEventClass, GroupLimitsConfig,
      3     classification::classify_group_event,
      4     errors::{GroupError, GroupErrorKind},
      5     event_view::GroupEventView,
      6     kinds::{KIND_GROUP_DELETE_EVENT, KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER},
      7     tags::ensure_group_tag_limit,
      8 };
      9 use std::collections::BTreeSet;
     10 use tangle_protocol::PublicKeyHex;
     11 
     12 #[derive(Debug, Clone, PartialEq, Eq, Default)]
     13 pub struct GroupAuthContext {
     14     authenticated_pubkeys: BTreeSet<PublicKeyHex>,
     15 }
     16 
     17 impl GroupAuthContext {
     18     pub fn unauthenticated() -> Self {
     19         Self::default()
     20     }
     21 
     22     pub fn new(pubkeys: impl IntoIterator<Item = PublicKeyHex>) -> Self {
     23         Self {
     24             authenticated_pubkeys: pubkeys.into_iter().collect(),
     25         }
     26     }
     27 
     28     pub fn contains(&self, pubkey: &PublicKeyHex) -> bool {
     29         self.authenticated_pubkeys.contains(pubkey)
     30     }
     31 
     32     pub fn authenticated_pubkeys(&self) -> &BTreeSet<PublicKeyHex> {
     33         &self.authenticated_pubkeys
     34     }
     35 }
     36 
     37 pub fn validate_client_group_event_structure(
     38     event: &(impl GroupEventView + ?Sized),
     39     limits: GroupLimitsConfig,
     40 ) -> Result<GroupEventClass, GroupError> {
     41     ensure_group_tag_limit(event, limits)?;
     42     let class = classify_group_event(event, limits)?;
     43     match &class {
     44         GroupEventClass::RelayGeneratedSnapshot { .. } => Err(GroupError::blocked(
     45             GroupErrorKind::DirectRelayGeneratedSubmission,
     46             "relay-generated group state events cannot be submitted by clients",
     47         )),
     48         GroupEventClass::Moderation { kind, .. } => {
     49             validate_moderation_targets(event, kind.as_u32())?;
     50             Ok(class)
     51         }
     52         GroupEventClass::Normal { .. } | GroupEventClass::NonGroup => Ok(class),
     53     }
     54 }
     55 
     56 pub fn require_group_auth_as_author(
     57     event: &(impl GroupEventView + ?Sized),
     58     class: &GroupEventClass,
     59     auth: &GroupAuthContext,
     60 ) -> Result<(), GroupError> {
     61     if matches!(class, GroupEventClass::NonGroup) {
     62         return Ok(());
     63     }
     64     if auth.contains(&event.pubkey()?) {
     65         return Ok(());
     66     }
     67     Err(GroupError::auth_required(
     68         "group event author must authenticate with AUTH",
     69     ))
     70 }
     71 
     72 fn validate_moderation_targets(
     73     event: &(impl GroupEventView + ?Sized),
     74     kind: u32,
     75 ) -> Result<(), GroupError> {
     76     match kind {
     77         KIND_GROUP_PUT_USER | KIND_GROUP_REMOVE_USER => require_valid_p_tag(event),
     78         KIND_GROUP_DELETE_EVENT => require_indexed_tag_value(event, "e").map(|_| ()),
     79         _ => Ok(()),
     80     }
     81 }
     82 
     83 fn require_valid_p_tag(event: &(impl GroupEventView + ?Sized)) -> Result<(), GroupError> {
     84     let value = require_indexed_tag_value(event, "p")?;
     85     PublicKeyHex::new(&value).map_err(|reason| {
     86         GroupError::invalid(
     87             GroupErrorKind::MalformedTargetTag,
     88             format!("malformed p target tag: {reason}"),
     89         )
     90     })?;
     91     Ok(())
     92 }
     93 
     94 fn require_indexed_tag_value(
     95     event: &(impl GroupEventView + ?Sized),
     96     name: &str,
     97 ) -> Result<String, GroupError> {
     98     let mut found = None;
     99     event.visit_tags(|tag| {
    100         if tag.first_value().is_none_or(|tag_name| tag_name != name) {
    101             return Ok(());
    102         }
    103         let Some((_, value)) = tag.indexed_pair() else {
    104             return Err(GroupError::invalid(
    105                 GroupErrorKind::MalformedTargetTag,
    106                 format!("malformed {name} target tag"),
    107             ));
    108         };
    109         found = Some(value.to_owned());
    110         Ok(())
    111     })?;
    112     found.ok_or_else(|| {
    113         GroupError::invalid(
    114             GroupErrorKind::MissingTargetTag,
    115             format!("missing {name} target tag"),
    116         )
    117     })
    118 }
    119 
    120 #[cfg(test)]
    121 mod tests {
    122     use super::{
    123         GroupAuthContext, require_group_auth_as_author, validate_client_group_event_structure,
    124     };
    125     use crate::{
    126         GroupErrorKind, GroupEventClass, GroupLimitsConfig, KIND_GROUP_DELETE_EVENT,
    127         KIND_GROUP_JOIN_REQUEST, KIND_GROUP_PUT_USER, NIP29_RELAY_GENERATED_KIND_VALUES,
    128     };
    129     use pocket_types::Event as PocketEvent;
    130     use tangle_protocol::{
    131         Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent,
    132         event_to_value,
    133     };
    134 
    135     #[test]
    136     fn client_submitted_relay_generated_events_are_rejected() {
    137         for kind in NIP29_RELAY_GENERATED_KIND_VALUES {
    138             let event = event(kind, vec![Tag::from_parts("d", &["Farm"]).expect("d")]);
    139             let error = validate_client_group_event_structure(&event, GroupLimitsConfig::default())
    140                 .expect_err("relay generated");
    141 
    142             assert_eq!(error.kind(), GroupErrorKind::DirectRelayGeneratedSubmission);
    143             assert_eq!(
    144                 error.prefixed_message(),
    145                 "blocked: relay-generated group state events cannot be submitted by clients"
    146             );
    147 
    148             let mut buffer = vec![0; 4096];
    149             let error = validate_client_group_event_structure(
    150                 pocket_event(&event, &mut buffer),
    151                 GroupLimitsConfig::default(),
    152             )
    153             .expect_err("pocket relay generated");
    154             assert_eq!(error.kind(), GroupErrorKind::DirectRelayGeneratedSubmission);
    155         }
    156     }
    157 
    158     #[test]
    159     fn validates_moderation_target_tags() {
    160         assert_eq!(
    161             validate_client_group_event_structure(
    162                 &event(
    163                     KIND_GROUP_PUT_USER,
    164                     vec![Tag::from_parts("h", &["Farm"]).expect("h")]
    165                 ),
    166                 GroupLimitsConfig::default()
    167             )
    168             .expect_err("missing p")
    169             .kind(),
    170             GroupErrorKind::MissingTargetTag
    171         );
    172         assert_eq!(
    173             validate_client_group_event_structure(
    174                 &event(
    175                     KIND_GROUP_PUT_USER,
    176                     vec![
    177                         Tag::from_parts("h", &["Farm"]).expect("h"),
    178                         Tag::from_parts("p", &["bad"]).expect("p")
    179                     ]
    180                 ),
    181                 GroupLimitsConfig::default()
    182             )
    183             .expect_err("bad p")
    184             .kind(),
    185             GroupErrorKind::MalformedTargetTag
    186         );
    187         assert_eq!(
    188             validate_client_group_event_structure(
    189                 &event(
    190                     KIND_GROUP_DELETE_EVENT,
    191                     vec![Tag::from_parts("h", &["Farm"]).expect("h")]
    192                 ),
    193                 GroupLimitsConfig::default()
    194             )
    195             .expect_err("missing e")
    196             .kind(),
    197             GroupErrorKind::MissingTargetTag
    198         );
    199     }
    200 
    201     #[test]
    202     fn validates_non_group_and_normal_group_structure() {
    203         assert_eq!(
    204             validate_client_group_event_structure(
    205                 &event(1, Vec::new()),
    206                 GroupLimitsConfig::default()
    207             )
    208             .expect("non-group"),
    209             GroupEventClass::NonGroup
    210         );
    211         assert!(matches!(
    212             validate_client_group_event_structure(
    213                 &event(1, vec![Tag::from_parts("h", &["Farm"]).expect("h")]),
    214                 GroupLimitsConfig::default()
    215             )
    216             .expect("normal"),
    217             GroupEventClass::Normal { group_id } if group_id.as_str() == "Farm"
    218         ));
    219         assert!(matches!(
    220             validate_client_group_event_structure(
    221                 &event(
    222                     KIND_GROUP_JOIN_REQUEST,
    223                     vec![Tag::from_parts("h", &["Farm"]).expect("h")]
    224                 ),
    225                 GroupLimitsConfig::default()
    226             )
    227             .expect("join"),
    228             GroupEventClass::Normal { group_id } if group_id.as_str() == "Farm"
    229         ));
    230     }
    231 
    232     #[test]
    233     fn group_write_auth_requires_event_author() {
    234         let group_event = event(1, vec![Tag::from_parts("h", &["Farm"]).expect("h")]);
    235         let class =
    236             validate_client_group_event_structure(&group_event, GroupLimitsConfig::default())
    237                 .expect("class");
    238 
    239         assert_eq!(
    240             require_group_auth_as_author(
    241                 &group_event,
    242                 &class,
    243                 &GroupAuthContext::unauthenticated()
    244             )
    245             .expect_err("auth")
    246             .kind(),
    247             GroupErrorKind::AuthenticationRequired
    248         );
    249         assert!(
    250             require_group_auth_as_author(
    251                 &group_event,
    252                 &class,
    253                 &GroupAuthContext::new([PublicKeyHex::new(&"1".repeat(64)).expect("pubkey")])
    254             )
    255             .is_ok()
    256         );
    257         assert_eq!(
    258             require_group_auth_as_author(
    259                 &group_event,
    260                 &class,
    261                 &GroupAuthContext::new([PublicKeyHex::new(&"3".repeat(64)).expect("pubkey")])
    262             )
    263             .expect_err("wrong author")
    264             .kind(),
    265             GroupErrorKind::AuthenticationRequired
    266         );
    267         assert!(
    268             require_group_auth_as_author(
    269                 &event(1, Vec::new()),
    270                 &GroupEventClass::NonGroup,
    271                 &GroupAuthContext::unauthenticated()
    272             )
    273             .is_ok()
    274         );
    275     }
    276 
    277     fn event(kind: u32, tags: Vec<Tag>) -> Event {
    278         Event::new(
    279             EventId::new(&"0".repeat(64)).expect("id"),
    280             UnsignedEvent::new(
    281                 PublicKeyHex::new(&"1".repeat(64)).expect("pubkey"),
    282                 UnixTimestamp::new(1),
    283                 Kind::new(kind.into()).expect("kind"),
    284                 tags,
    285                 "",
    286             ),
    287             SignatureHex::new(&"2".repeat(128)).expect("sig"),
    288         )
    289     }
    290 
    291     fn pocket_event<'a>(event: &Event, buffer: &'a mut [u8]) -> &'a PocketEvent {
    292         let raw = event_to_value(event).to_string();
    293         let (_, pocket) = PocketEvent::from_json(raw.as_bytes(), buffer).expect("pocket");
    294         pocket
    295     }
    296 }