policy.rs (28948B)
1 use std::collections::BTreeSet; 2 3 use crate::{ 4 Capability, CapabilitySet, GroupError, GroupErrorKind, GroupEventClass, GroupId, 5 GroupLifecycleState, GroupPolicyConfig, GroupProjection, KIND_GROUP_CREATE_GROUP, 6 KIND_GROUP_CREATE_INVITE, KIND_GROUP_DELETE_EVENT, KIND_GROUP_DELETE_GROUP, 7 KIND_GROUP_EDIT_METADATA, KIND_GROUP_JOIN_REQUEST, KIND_GROUP_LEAVE_REQUEST, 8 KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER, MemberStatus, RoleDefinition, RoleName, 9 SupportedKinds, event_view::GroupEventView, require_group_auth_as_author, resolve_capabilities, 10 }; 11 use tangle_protocol::PublicKeyHex; 12 13 #[derive(Debug, Clone, PartialEq, Eq, Default)] 14 pub struct GroupAuthority { 15 owner_pubkeys: BTreeSet<PublicKeyHex>, 16 admin_pubkeys: BTreeSet<PublicKeyHex>, 17 } 18 19 impl GroupAuthority { 20 pub fn new( 21 owner_pubkeys: impl IntoIterator<Item = PublicKeyHex>, 22 admin_pubkeys: impl IntoIterator<Item = PublicKeyHex>, 23 ) -> Self { 24 Self { 25 owner_pubkeys: owner_pubkeys.into_iter().collect(), 26 admin_pubkeys: admin_pubkeys.into_iter().collect(), 27 } 28 } 29 30 pub fn empty() -> Self { 31 Self::default() 32 } 33 34 pub fn is_owner(&self, pubkey: &PublicKeyHex) -> bool { 35 self.owner_pubkeys.contains(pubkey) 36 } 37 38 pub fn owner_pubkeys(&self) -> &BTreeSet<PublicKeyHex> { 39 &self.owner_pubkeys 40 } 41 42 pub fn admin_pubkeys(&self) -> &BTreeSet<PublicKeyHex> { 43 &self.admin_pubkeys 44 } 45 46 pub fn is_admin(&self, pubkey: &PublicKeyHex) -> bool { 47 self.admin_pubkeys.contains(pubkey) || self.is_owner(pubkey) 48 } 49 50 pub fn is_permanent_admin(&self, pubkey: &PublicKeyHex) -> bool { 51 self.is_admin(pubkey) 52 } 53 } 54 55 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 56 pub enum GroupWriteDecision { 57 Accept, 58 IgnoreNonGroup, 59 } 60 61 #[derive(Debug, Clone, Copy)] 62 pub struct GroupWritePolicy<'a> { 63 projection: &'a GroupProjection, 64 authority: &'a GroupAuthority, 65 policy: GroupPolicyConfig, 66 } 67 68 impl<'a> GroupWritePolicy<'a> { 69 pub fn new( 70 projection: &'a GroupProjection, 71 authority: &'a GroupAuthority, 72 policy: GroupPolicyConfig, 73 ) -> Self { 74 Self { 75 projection, 76 authority, 77 policy, 78 } 79 } 80 81 pub fn check_event( 82 &self, 83 event: &(impl GroupEventView + ?Sized), 84 class: &GroupEventClass, 85 auth: &crate::GroupAuthContext, 86 ) -> Result<GroupWriteDecision, GroupError> { 87 require_group_auth_as_author(event, class, auth)?; 88 match class { 89 GroupEventClass::NonGroup => Ok(GroupWriteDecision::IgnoreNonGroup), 90 GroupEventClass::RelayGeneratedSnapshot { .. } => Err(GroupError::blocked( 91 GroupErrorKind::DirectRelayGeneratedSubmission, 92 "relay-generated group state events cannot be submitted by clients", 93 )), 94 GroupEventClass::Moderation { kind, group_id } => { 95 self.check_moderation_event(event, kind.as_u32(), group_id) 96 } 97 GroupEventClass::Normal { group_id } => self.check_normal_event(event, group_id), 98 } 99 } 100 101 pub fn can_read_group(&self, group_id: &GroupId, reader: Option<&PublicKeyHex>) -> bool { 102 let Some(reader) = reader else { 103 return false; 104 }; 105 self.authority.is_admin(reader) || self.is_current_member(group_id, reader) 106 } 107 108 pub fn has_relay_override(&self, group_id: &GroupId, pubkey: &PublicKeyHex) -> bool { 109 if self.authority.is_admin(pubkey) { 110 return true; 111 } 112 self.projection 113 .member(group_id, pubkey) 114 .filter(|member| member.status() == MemberStatus::Member) 115 .is_some_and(|member| { 116 member 117 .roles() 118 .contains(&RoleName::permanent_relay_override()) 119 }) 120 } 121 122 fn check_moderation_event( 123 &self, 124 event: &(impl GroupEventView + ?Sized), 125 kind: u32, 126 group_id: &GroupId, 127 ) -> Result<GroupWriteDecision, GroupError> { 128 if kind == KIND_GROUP_CREATE_GROUP { 129 return self.check_create_group(event, group_id); 130 } 131 let group = self.require_active_group(group_id)?; 132 if kind == KIND_GROUP_CREATE_INVITE && !self.policy.invites_enabled() { 133 return Err(GroupError::restricted( 134 GroupErrorKind::MissingCapability, 135 "invites not enabled", 136 )); 137 } 138 let actor = event.pubkey()?; 139 let required = required_capability(kind, event)?; 140 if let Some(required) = required { 141 self.require_capability(group_id, &actor, required)?; 142 } 143 if kind == KIND_GROUP_REMOVE_USER { 144 let target = target_pubkey(event, "p")?; 145 if self.is_protected_admin(group_id, &target) { 146 return Err(GroupError::restricted( 147 GroupErrorKind::MissingCapability, 148 "permanent group admins cannot be removed", 149 )); 150 } 151 } 152 if kind == KIND_GROUP_EDIT_METADATA 153 && group.metadata().hidden() 154 && !self.can_read_group(group_id, Some(&actor)) 155 { 156 return Err(non_enumerating_group_error()); 157 } 158 Ok(GroupWriteDecision::Accept) 159 } 160 161 fn check_create_group( 162 &self, 163 event: &(impl GroupEventView + ?Sized), 164 group_id: &GroupId, 165 ) -> Result<GroupWriteDecision, GroupError> { 166 if !self.authority.is_owner(&event.pubkey()?) { 167 return Err(GroupError::restricted( 168 GroupErrorKind::MissingCapability, 169 "group creation is restricted to relay owners", 170 )); 171 } 172 if self.projection.tombstone(group_id).is_some() { 173 return Err(GroupError::blocked( 174 GroupErrorKind::GroupDeleted, 175 "group is deleted", 176 )); 177 } 178 if self.projection.group(group_id).is_some() { 179 return Err(GroupError::invalid( 180 GroupErrorKind::GroupAlreadyExists, 181 "group already exists", 182 )); 183 } 184 Ok(GroupWriteDecision::Accept) 185 } 186 187 fn check_normal_event( 188 &self, 189 event: &(impl GroupEventView + ?Sized), 190 group_id: &GroupId, 191 ) -> Result<GroupWriteDecision, GroupError> { 192 let group = self.require_active_group(group_id)?; 193 match event.kind_u32() { 194 KIND_GROUP_JOIN_REQUEST => self.check_join(event, group_id), 195 KIND_GROUP_LEAVE_REQUEST => self.check_leave(event, group_id), 196 _ => { 197 let actor = event.pubkey()?; 198 if group.metadata().restricted() && !self.can_read_group(group_id, Some(&actor)) { 199 return Err(non_enumerating_group_error()); 200 } 201 let kind = event.kind()?; 202 match group.metadata().supported_kinds() { 203 SupportedKinds::UnspecifiedAll => {} 204 SupportedKinds::None => { 205 return Err(GroupError::restricted( 206 GroupErrorKind::UnsupportedGroupKind, 207 "group does not accept normal event kinds", 208 )); 209 } 210 SupportedKinds::Only(kinds) => { 211 if !kinds.contains(&kind) { 212 return Err(GroupError::restricted( 213 GroupErrorKind::UnsupportedGroupKind, 214 "event kind is not supported by this group", 215 )); 216 } 217 } 218 } 219 Ok(GroupWriteDecision::Accept) 220 } 221 } 222 } 223 224 fn check_join( 225 &self, 226 event: &(impl GroupEventView + ?Sized), 227 group_id: &GroupId, 228 ) -> Result<GroupWriteDecision, GroupError> { 229 let group = self.require_active_group(group_id)?; 230 if self.is_current_member(group_id, &event.pubkey()?) { 231 return Err(GroupError::duplicate( 232 GroupErrorKind::DuplicateMember, 233 "group member already exists", 234 )); 235 } 236 if group.metadata().closed() || !self.policy.public_join() { 237 return Err(non_enumerating_group_error()); 238 } 239 Ok(GroupWriteDecision::Accept) 240 } 241 242 fn check_leave( 243 &self, 244 event: &(impl GroupEventView + ?Sized), 245 group_id: &GroupId, 246 ) -> Result<GroupWriteDecision, GroupError> { 247 self.require_active_group(group_id)?; 248 if !self.is_current_member(group_id, &event.pubkey()?) { 249 return Err(GroupError::duplicate( 250 GroupErrorKind::DuplicateMember, 251 "group member does not exist", 252 )); 253 } 254 Ok(GroupWriteDecision::Accept) 255 } 256 257 fn require_active_group(&self, group_id: &GroupId) -> Result<&crate::GroupState, GroupError> { 258 let Some(group) = self.projection.group(group_id) else { 259 return Err(non_enumerating_group_error()); 260 }; 261 if group.lifecycle() == GroupLifecycleState::Deleted 262 || self.projection.tombstone(group_id).is_some() 263 { 264 return Err(GroupError::blocked( 265 GroupErrorKind::GroupDeleted, 266 "group is deleted", 267 )); 268 } 269 Ok(group) 270 } 271 272 fn require_capability( 273 &self, 274 group_id: &GroupId, 275 actor: &PublicKeyHex, 276 required: Capability, 277 ) -> Result<(), GroupError> { 278 if self.authority.is_admin(actor) { 279 return Ok(()); 280 } 281 let capabilities = self.actor_capabilities(group_id, actor)?; 282 if capabilities.contains(required) { 283 return Ok(()); 284 } 285 Err(GroupError::restricted( 286 GroupErrorKind::MissingCapability, 287 format!("missing group capability {}", required.as_str()), 288 )) 289 } 290 291 fn actor_capabilities( 292 &self, 293 group_id: &GroupId, 294 actor: &PublicKeyHex, 295 ) -> Result<CapabilitySet, GroupError> { 296 let Some(member) = self.projection.member(group_id, actor) else { 297 return Ok(CapabilitySet::empty()); 298 }; 299 if member.status() != MemberStatus::Member { 300 return Ok(CapabilitySet::empty()); 301 } 302 let definitions = self 303 .projection 304 .roles() 305 .iter() 306 .filter(|((candidate_group, _), _)| candidate_group == group_id) 307 .map(|(_, role)| role.definition()) 308 .collect::<Vec<&RoleDefinition>>(); 309 resolve_capabilities(definitions, member.roles().iter()) 310 } 311 312 fn is_current_member(&self, group_id: &GroupId, pubkey: &PublicKeyHex) -> bool { 313 self.projection 314 .member(group_id, pubkey) 315 .is_some_and(|member| member.status() == MemberStatus::Member) 316 } 317 318 fn is_protected_admin(&self, group_id: &GroupId, pubkey: &PublicKeyHex) -> bool { 319 self.authority.is_permanent_admin(pubkey) || self.has_relay_override(group_id, pubkey) 320 } 321 } 322 323 pub fn non_enumerating_group_error() -> GroupError { 324 GroupError::restricted(GroupErrorKind::GroupUnavailable, "group is unavailable") 325 } 326 327 fn required_capability( 328 kind: u32, 329 event: &(impl GroupEventView + ?Sized), 330 ) -> Result<Option<Capability>, GroupError> { 331 match kind { 332 KIND_GROUP_PUT_USER => { 333 if has_role_tag(event)? { 334 Ok(Some(Capability::ManageRoles)) 335 } else { 336 Ok(Some(Capability::ManageMembers)) 337 } 338 } 339 KIND_GROUP_REMOVE_USER => Ok(Some(Capability::ManageMembers)), 340 KIND_GROUP_EDIT_METADATA => Ok(Some(Capability::ManageMetadata)), 341 KIND_GROUP_DELETE_EVENT => Ok(Some(Capability::DeleteEvents)), 342 KIND_GROUP_DELETE_GROUP => Ok(Some(Capability::DeleteGroup)), 343 KIND_GROUP_CREATE_INVITE => Ok(Some(Capability::CreateInvites)), 344 _ => Ok(None), 345 } 346 } 347 348 fn has_role_tag(event: &(impl GroupEventView + ?Sized)) -> Result<bool, GroupError> { 349 let mut found = false; 350 event.visit_tags(|tag| { 351 if tag.first_value().is_some_and(|name| name == "role") { 352 found = true; 353 } 354 Ok(()) 355 })?; 356 Ok(found) 357 } 358 359 fn target_pubkey( 360 event: &(impl GroupEventView + ?Sized), 361 tag_name: &str, 362 ) -> Result<PublicKeyHex, GroupError> { 363 let mut found = None; 364 event.visit_tags(|tag| { 365 if tag 366 .first_value() 367 .is_none_or(|candidate| candidate != tag_name) 368 { 369 return Ok(()); 370 } 371 let Some((_, value)) = tag.indexed_pair() else { 372 return Err(GroupError::invalid( 373 GroupErrorKind::MalformedTargetTag, 374 format!("malformed {tag_name} target tag"), 375 )); 376 }; 377 found = Some(PublicKeyHex::new(value).map_err(|reason| { 378 GroupError::invalid( 379 GroupErrorKind::MalformedTargetTag, 380 format!("malformed {tag_name} target tag: {reason}"), 381 ) 382 })?); 383 Ok(()) 384 })?; 385 found.ok_or_else(|| { 386 GroupError::invalid( 387 GroupErrorKind::MissingTargetTag, 388 format!("missing {tag_name} target tag"), 389 ) 390 }) 391 } 392 393 #[cfg(test)] 394 mod tests { 395 use super::{GroupAuthority, GroupWriteDecision, GroupWritePolicy}; 396 use crate::{ 397 Capability, CapabilitySet, GroupAuthContext, GroupErrorKind, GroupEventClass, GroupId, 398 GroupMetadata, GroupMetadataFlags, GroupMetadataText, GroupPolicyConfig, GroupProjection, 399 GroupState, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE, KIND_GROUP_DELETE_GROUP, 400 KIND_GROUP_JOIN_REQUEST, KIND_GROUP_LEAVE_REQUEST, KIND_GROUP_REMOVE_USER, MemberState, 401 MemberStatus, ProjectedRoleDefinition, ProjectionOrderTuple, RoleDefinition, RoleName, 402 StoreOffset, SupportedKinds, 403 }; 404 use tangle_protocol::{ 405 Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, 406 }; 407 408 #[test] 409 fn group_create_requires_relay_owner_and_unused_group_id() { 410 let projection = GroupProjection::new(); 411 let owner = pubkey("1"); 412 let author = pubkey("2"); 413 let authority = GroupAuthority::new([owner.clone()], Vec::<PublicKeyHex>::new()); 414 let policy = GroupWritePolicy::new(&projection, &authority, GroupPolicyConfig::strict()); 415 let create_by_non_owner = event(KIND_GROUP_CREATE_GROUP, author.clone(), vec![h("Farm")]); 416 let class = GroupEventClass::Moderation { 417 kind: create_by_non_owner.unsigned().kind(), 418 group_id: group("Farm"), 419 }; 420 421 assert_eq!( 422 policy 423 .check_event( 424 &create_by_non_owner, 425 &class, 426 &GroupAuthContext::new([author.clone()]) 427 ) 428 .expect_err("owner") 429 .kind(), 430 GroupErrorKind::MissingCapability 431 ); 432 433 let owner_event = event(KIND_GROUP_CREATE_GROUP, owner.clone(), vec![h("Farm")]); 434 assert_eq!( 435 policy 436 .check_event( 437 &owner_event, 438 &class, 439 &GroupAuthContext::new([owner.clone()]) 440 ) 441 .expect("accept"), 442 GroupWriteDecision::Accept 443 ); 444 } 445 446 #[test] 447 fn lifecycle_policy_rejects_nonexistent_deleted_and_duplicate_groups() { 448 let owner = pubkey("1"); 449 let mut projection = projection_with_group( 450 "Farm", 451 metadata(false, false, false, false, SupportedKinds::UnspecifiedAll), 452 owner.clone(), 453 ); 454 let group_id = group("Farm"); 455 let class = GroupEventClass::Moderation { 456 kind: kind(KIND_GROUP_CREATE_GROUP), 457 group_id: group_id.clone(), 458 }; 459 let authority = GroupAuthority::new([owner.clone()], Vec::<PublicKeyHex>::new()); 460 let policy = GroupWritePolicy::new(&projection, &authority, GroupPolicyConfig::strict()); 461 let create = event(KIND_GROUP_CREATE_GROUP, owner.clone(), vec![h("Farm")]); 462 463 assert_eq!( 464 policy 465 .check_event(&create, &class, &GroupAuthContext::new([owner.clone()])) 466 .expect_err("duplicate") 467 .kind(), 468 GroupErrorKind::GroupAlreadyExists 469 ); 470 471 let delete = event(KIND_GROUP_DELETE_GROUP, owner.clone(), vec![h("Farm")]); 472 projection 473 .apply_canonical_event(&delete, StoreOffset::new(2), Default::default()) 474 .expect("delete"); 475 let authority = GroupAuthority::new([owner.clone()], Vec::<PublicKeyHex>::new()); 476 let policy = GroupWritePolicy::new(&projection, &authority, GroupPolicyConfig::strict()); 477 let normal = event(1, owner.clone(), vec![h("Farm")]); 478 479 assert_eq!( 480 policy 481 .check_event( 482 &normal, 483 &GroupEventClass::Normal { 484 group_id: group_id.clone() 485 }, 486 &GroupAuthContext::new([owner]) 487 ) 488 .expect_err("deleted") 489 .kind(), 490 GroupErrorKind::GroupDeleted 491 ); 492 } 493 494 #[test] 495 fn restricted_and_supported_kind_rules_gate_normal_writes() { 496 let owner = pubkey("1"); 497 let member = pubkey("2"); 498 let outsider = pubkey("3"); 499 let mut projection = projection_with_group( 500 "Farm", 501 metadata( 502 true, 503 false, 504 false, 505 false, 506 SupportedKinds::Only([kind(1)].into_iter().collect()), 507 ), 508 owner.clone(), 509 ); 510 put_member(&mut projection, "Farm", member.clone(), []); 511 let authority = GroupAuthority::new([owner], Vec::<PublicKeyHex>::new()); 512 let policy = GroupWritePolicy::new(&projection, &authority, GroupPolicyConfig::strict()); 513 514 assert_eq!( 515 policy 516 .check_event( 517 &event(1, outsider.clone(), vec![h("Farm")]), 518 &GroupEventClass::Normal { 519 group_id: group("Farm") 520 }, 521 &GroupAuthContext::new([outsider.clone()]) 522 ) 523 .expect_err("restricted") 524 .kind(), 525 GroupErrorKind::GroupUnavailable 526 ); 527 assert_eq!( 528 policy 529 .check_event( 530 &event(7, member.clone(), vec![h("Farm")]), 531 &GroupEventClass::Normal { 532 group_id: group("Farm") 533 }, 534 &GroupAuthContext::new([member]) 535 ) 536 .expect_err("kind") 537 .kind(), 538 GroupErrorKind::UnsupportedGroupKind 539 ); 540 } 541 542 #[test] 543 fn moderation_policy_uses_roles_and_protects_permanent_admins() { 544 let owner = pubkey("1"); 545 let moderator = pubkey("2"); 546 let protected = pubkey("3"); 547 let target = pubkey("4"); 548 let mut projection = projection_with_group( 549 "Farm", 550 metadata(false, false, false, false, SupportedKinds::UnspecifiedAll), 551 owner.clone(), 552 ); 553 let moderator_role = RoleName::new("moderator").expect("role"); 554 projection.put_role( 555 group("Farm"), 556 ProjectedRoleDefinition::new( 557 RoleDefinition::new( 558 moderator_role.clone(), 559 CapabilitySet::new([Capability::ManageMembers]), 560 None, 561 ), 562 event_id("30"), 563 tuple(30, "30", 3), 564 ), 565 ); 566 put_member(&mut projection, "Farm", moderator.clone(), [moderator_role]); 567 let authority = GroupAuthority::new([owner], [protected.clone()]); 568 let policy = GroupWritePolicy::new(&projection, &authority, GroupPolicyConfig::strict()); 569 570 assert_eq!( 571 policy 572 .check_event( 573 &event( 574 KIND_GROUP_REMOVE_USER, 575 moderator.clone(), 576 vec![h("Farm"), p(&target)] 577 ), 578 &GroupEventClass::Moderation { 579 kind: kind(KIND_GROUP_REMOVE_USER), 580 group_id: group("Farm") 581 }, 582 &GroupAuthContext::new([moderator.clone()]) 583 ) 584 .expect("moderator"), 585 GroupWriteDecision::Accept 586 ); 587 assert_eq!( 588 policy 589 .check_event( 590 &event( 591 KIND_GROUP_REMOVE_USER, 592 moderator.clone(), 593 vec![h("Farm"), p(&protected)] 594 ), 595 &GroupEventClass::Moderation { 596 kind: kind(KIND_GROUP_REMOVE_USER), 597 group_id: group("Farm") 598 }, 599 &GroupAuthContext::new([moderator]) 600 ) 601 .expect_err("protected") 602 .kind(), 603 GroupErrorKind::MissingCapability 604 ); 605 } 606 607 #[test] 608 fn join_and_leave_policy_is_immediate_and_membership_based() { 609 let owner = pubkey("1"); 610 let joiner = pubkey("2"); 611 let member = pubkey("3"); 612 let mut projection = projection_with_group( 613 "Farm", 614 metadata(false, false, false, false, SupportedKinds::UnspecifiedAll), 615 owner.clone(), 616 ); 617 put_member(&mut projection, "Farm", member.clone(), []); 618 let authority = GroupAuthority::new([owner], Vec::<PublicKeyHex>::new()); 619 let strict_policy = 620 GroupWritePolicy::new(&projection, &authority, GroupPolicyConfig::strict()); 621 622 let public_join_error = strict_policy 623 .check_event( 624 &event(KIND_GROUP_JOIN_REQUEST, joiner.clone(), vec![h("Farm")]), 625 &GroupEventClass::Normal { 626 group_id: group("Farm"), 627 }, 628 &GroupAuthContext::new([joiner.clone()]), 629 ) 630 .expect_err("public join"); 631 assert_eq!(public_join_error.kind(), GroupErrorKind::GroupUnavailable); 632 assert_eq!( 633 public_join_error.prefixed_message(), 634 "restricted: group is unavailable" 635 ); 636 let public_policy = GroupWritePolicy::new( 637 &projection, 638 &authority, 639 GroupPolicyConfig::new(true, false).expect("policy"), 640 ); 641 assert_eq!( 642 public_policy 643 .check_event( 644 &event(KIND_GROUP_JOIN_REQUEST, joiner.clone(), vec![h("Farm")]), 645 &GroupEventClass::Normal { 646 group_id: group("Farm") 647 }, 648 &GroupAuthContext::new([joiner]) 649 ) 650 .expect("public join"), 651 GroupWriteDecision::Accept 652 ); 653 let duplicate_join = strict_policy 654 .check_event( 655 &event(KIND_GROUP_JOIN_REQUEST, member.clone(), vec![h("Farm")]), 656 &GroupEventClass::Normal { 657 group_id: group("Farm"), 658 }, 659 &GroupAuthContext::new([member.clone()]), 660 ) 661 .expect_err("duplicate join"); 662 assert_eq!(duplicate_join.kind(), GroupErrorKind::DuplicateMember); 663 assert_eq!( 664 duplicate_join.prefixed_message(), 665 "duplicate: group member already exists" 666 ); 667 assert_eq!( 668 strict_policy 669 .check_event( 670 &event(KIND_GROUP_LEAVE_REQUEST, member.clone(), vec![h("Farm")]), 671 &GroupEventClass::Normal { 672 group_id: group("Farm") 673 }, 674 &GroupAuthContext::new([member]) 675 ) 676 .expect("leave"), 677 GroupWriteDecision::Accept 678 ); 679 } 680 681 #[test] 682 fn closed_group_denies_public_join_strictly() { 683 let owner = pubkey("1"); 684 let joiner = pubkey("2"); 685 let projection = projection_with_group( 686 "Farm", 687 metadata(false, false, false, true, SupportedKinds::UnspecifiedAll), 688 owner.clone(), 689 ); 690 let authority = GroupAuthority::new([owner], Vec::<PublicKeyHex>::new()); 691 let policy = GroupWritePolicy::new( 692 &projection, 693 &authority, 694 GroupPolicyConfig::new(true, false).expect("policy"), 695 ); 696 697 assert_eq!( 698 policy 699 .check_event( 700 &event(KIND_GROUP_JOIN_REQUEST, joiner.clone(), vec![h("Farm")]), 701 &GroupEventClass::Normal { 702 group_id: group("Farm") 703 }, 704 &GroupAuthContext::new([joiner]) 705 ) 706 .expect_err("closed") 707 .kind(), 708 GroupErrorKind::GroupUnavailable 709 ); 710 } 711 712 #[test] 713 fn invite_creation_is_rejected_while_invites_are_disabled() { 714 let owner = pubkey("1"); 715 let projection = projection_with_group( 716 "Farm", 717 metadata(false, false, false, false, SupportedKinds::UnspecifiedAll), 718 owner.clone(), 719 ); 720 let authority = GroupAuthority::new([owner.clone()], Vec::<PublicKeyHex>::new()); 721 let policy = GroupWritePolicy::new(&projection, &authority, GroupPolicyConfig::strict()); 722 let invite = event(KIND_GROUP_CREATE_INVITE, owner.clone(), vec![h("Farm")]); 723 724 let error = policy 725 .check_event( 726 &invite, 727 &GroupEventClass::Moderation { 728 kind: kind(KIND_GROUP_CREATE_INVITE), 729 group_id: group("Farm"), 730 }, 731 &GroupAuthContext::new([owner]), 732 ) 733 .expect_err("invite"); 734 735 assert_eq!(error.kind(), GroupErrorKind::MissingCapability); 736 assert_eq!(error.prefixed_message(), "restricted: invites not enabled"); 737 } 738 739 fn projection_with_group( 740 group_id: &str, 741 metadata: GroupMetadata, 742 author: PublicKeyHex, 743 ) -> GroupProjection { 744 let mut projection = GroupProjection::new(); 745 projection.put_group(GroupState::new( 746 group(group_id), 747 metadata, 748 author, 749 event_id("10"), 750 tuple(10, "10", 1), 751 )); 752 projection 753 } 754 755 fn put_member( 756 projection: &mut GroupProjection, 757 group_id: &str, 758 pubkey: PublicKeyHex, 759 roles: impl IntoIterator<Item = RoleName>, 760 ) { 761 projection.put_member( 762 group(group_id), 763 MemberState::new( 764 pubkey, 765 MemberStatus::Member, 766 roles.into_iter().collect(), 767 event_id("20"), 768 tuple(20, "20", 2), 769 ), 770 ); 771 } 772 773 fn metadata( 774 restricted: bool, 775 private: bool, 776 hidden: bool, 777 closed: bool, 778 supported_kinds: SupportedKinds, 779 ) -> GroupMetadata { 780 GroupMetadata::from_parts( 781 GroupMetadataText::empty(), 782 GroupMetadataFlags::new(private, restricted, hidden, closed), 783 supported_kinds, 784 ) 785 } 786 787 fn event(kind_value: u32, pubkey: PublicKeyHex, tags: Vec<Tag>) -> Event { 788 Event::new( 789 event_id("01"), 790 UnsignedEvent::new(pubkey, UnixTimestamp::new(1), kind(kind_value), tags, ""), 791 SignatureHex::new(&"2".repeat(128)).expect("sig"), 792 ) 793 } 794 795 fn h(group_id: &str) -> Tag { 796 Tag::from_parts("h", &[group_id]).expect("h") 797 } 798 799 fn p(pubkey: &PublicKeyHex) -> Tag { 800 Tag::from_parts("p", &[pubkey.as_str()]).expect("p") 801 } 802 803 fn group(value: &str) -> GroupId { 804 GroupId::new(value).expect("group") 805 } 806 807 fn pubkey(suffix: &str) -> PublicKeyHex { 808 PublicKeyHex::new(&suffix.repeat(64)).expect("pubkey") 809 } 810 811 fn kind(value: u32) -> Kind { 812 Kind::new(value.into()).expect("kind") 813 } 814 815 fn tuple(created_at: u64, suffix: &str, offset: u64) -> ProjectionOrderTuple { 816 ProjectionOrderTuple::new( 817 UnixTimestamp::new(created_at), 818 event_id(suffix), 819 StoreOffset::new(offset), 820 ) 821 } 822 823 fn event_id(suffix: &str) -> EventId { 824 let mut value = "0".repeat(64 - suffix.len()); 825 value.push_str(suffix); 826 EventId::new(&value).expect("id") 827 } 828 }