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 }