tangle


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

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 }