Skip to main content

xmtp_mls_validation/
group_permissions.rs

1use openmls::{extensions::Extensions, group::GroupContext};
2use prost::Message;
3use std::collections::HashMap;
4use thiserror::Error;
5use xmtp_common::ErrorCode;
6use xmtp_proto::xmtp::mls::message_contents::{
7    GroupMutablePermissionsV1 as GroupMutablePermissionsProto,
8    MembershipPolicy as MembershipPolicyProto, MetadataPolicy as MetadataPolicyProto,
9    PermissionsUpdatePolicy as PermissionsPolicyProto, PolicySet as PolicySetProto,
10    membership_policy::{
11        AndCondition as AndConditionProto, AnyCondition as AnyConditionProto,
12        BasePolicy as BasePolicyProto, Kind as PolicyKindProto,
13    },
14    metadata_policy::{
15        AndCondition as MetadataAndConditionProto, AnyCondition as MetadataAnyConditionProto,
16        Kind as MetadataPolicyKindProto, MetadataBasePolicy as MetadataBasePolicyProto,
17    },
18    permissions_update_policy::{
19        AndCondition as PermissionsAndConditionProto, AnyCondition as PermissionsAnyConditionProto,
20        Kind as PermissionsPolicyKindProto, PermissionsBasePolicy as PermissionsBasePolicyProto,
21    },
22};
23
24use crate::commit::{CommitParticipant, Inbox, MembershipValidationInfo, MetadataFieldChange};
25use xmtp_mls_common::group_mutable_metadata::{GroupMutableMetadata, MetadataField};
26
27/// Errors that can occur when working with GroupMutablePermissions.
28#[derive(Debug, Error, ErrorCode)]
29pub enum GroupMutablePermissionsError {
30    /// Serialization error.
31    ///
32    /// Failed to encode permissions protobuf. Not retryable.
33    #[error("serialization: {0}")]
34    Serialization(#[from] prost::EncodeError),
35    /// Deserialization error.
36    ///
37    /// Failed to decode permissions protobuf. Not retryable.
38    #[error("deserialization: {0}")]
39    Deserialization(#[from] prost::DecodeError),
40    /// Policy error.
41    ///
42    /// Permission policy validation failed. Not retryable.
43    #[error("policy error {0}")]
44    Policy(#[from] PolicyError),
45    /// Invalid conversation type.
46    ///
47    /// Wrong conversation type for this operation. Not retryable.
48    #[error("invalid conversation type")]
49    InvalidConversationType,
50    /// Missing policies.
51    ///
52    /// Required permission policies not present. Not retryable.
53    #[error("missing policies")]
54    MissingPolicies,
55    /// Missing extension.
56    ///
57    /// Required MLS extension not found. Not retryable.
58    #[error("missing extension")]
59    MissingExtension,
60    /// Invalid permission policy option.
61    ///
62    /// Invalid permission policy configuration. Not retryable.
63    #[error("invalid permission policy option")]
64    InvalidPermissionPolicyOption,
65    /// Invalid policy state in the component registry.
66    ///
67    /// The permission view could not be read from group state. Not retryable.
68    #[error("invalid component-registry policy state: {0}")]
69    PolicyProjection(#[from] xmtp_mls_common::app_data::policy_set::PolicyProjectionError),
70}
71
72/// Represents the mutable permissions for a group.
73///
74/// This struct is stored as an MLS Unknown Group Context Extension.
75#[derive(Debug, Clone, PartialEq)]
76pub struct GroupMutablePermissions {
77    /// The set of policies that define the permissions for the group.
78    pub policies: PolicySet,
79}
80
81impl GroupMutablePermissions {
82    /// Creates a new GroupMutablePermissions instance.
83    pub fn new(policies: PolicySet) -> Self {
84        Self { policies }
85    }
86
87    /// Returns the preconfigured policy for the group permissions.
88    pub fn preconfigured_policy(
89        &self,
90    ) -> Result<PreconfiguredPolicies, GroupMutablePermissionsError> {
91        Ok(PreconfiguredPolicies::from_policy_set(&self.policies)?)
92    }
93
94    /// Creates a GroupMutablePermissions instance from a proto representation.
95    pub fn from_proto(
96        proto: GroupMutablePermissionsProto,
97    ) -> Result<Self, GroupMutablePermissionsError> {
98        if proto.policies.is_none() {
99            return Err(GroupMutablePermissionsError::MissingPolicies);
100        }
101        let policies = proto.policies.expect("checked for none");
102
103        Ok(Self::new(PolicySet::from_proto(policies)?))
104    }
105
106    /// Converts the GroupMutablePermissions to its proto representation.
107    pub fn to_proto(&self) -> Result<GroupMutablePermissionsProto, GroupMutablePermissionsError> {
108        Ok(GroupMutablePermissionsProto {
109            policies: Some(self.policies.to_proto()?),
110        })
111    }
112}
113
114/// Implements conversion from GroupMutablePermissions to `Vec<u8>`.
115impl TryFrom<GroupMutablePermissions> for Vec<u8> {
116    type Error = GroupMutablePermissionsError;
117
118    fn try_from(value: GroupMutablePermissions) -> Result<Self, Self::Error> {
119        let mut buf = Vec::new();
120        let proto_val = value.to_proto()?;
121        proto_val.encode(&mut buf)?;
122
123        Ok(buf)
124    }
125}
126
127/// Implements conversion from `&Vec<u8>` to [`GroupMutablePermissions`].
128impl TryFrom<&Vec<u8>> for GroupMutablePermissions {
129    type Error = GroupMutablePermissionsError;
130
131    fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
132        let proto_val = GroupMutablePermissionsProto::decode(value.as_slice())?;
133        Self::from_proto(proto_val)
134    }
135}
136
137/// Implements conversion from GroupMutablePermissionsProto to GroupMutablePermissions.
138impl TryFrom<GroupMutablePermissionsProto> for GroupMutablePermissions {
139    type Error = GroupMutablePermissionsError;
140
141    fn try_from(value: GroupMutablePermissionsProto) -> Result<Self, Self::Error> {
142        Self::from_proto(value)
143    }
144}
145
146/// A trait for policies that can update Metadata for the group.
147pub trait MetadataPolicy: std::fmt::Debug {
148    /// Evaluates the policy for a given actor and metadata change.
149    ///
150    /// Verify relevant metadata is actually changed before evaluating against the MetadataPolicy.
151    /// See evaluate_metadata_policy.
152    fn evaluate(&self, actor: &CommitParticipant, change: &MetadataFieldChange) -> bool;
153
154    /// Converts the policy to its proto representation.
155    fn to_proto(&self) -> Result<MetadataPolicyProto, PolicyError>;
156}
157
158/// Represents the base policies for metadata updates.
159#[derive(Clone, Copy, Debug, PartialEq)]
160pub enum MetadataBasePolicies {
161    Allow,
162    Deny,
163    AllowIfActorAdminOrSuperAdmin,
164    AllowIfActorSuperAdmin,
165}
166
167/// Implements the MetadataPolicy trait for MetadataBasePolicies.
168impl MetadataPolicy for &MetadataBasePolicies {
169    fn evaluate(&self, actor: &CommitParticipant, _change: &MetadataFieldChange) -> bool {
170        match self {
171            MetadataBasePolicies::Allow => true,
172            MetadataBasePolicies::Deny => false,
173            MetadataBasePolicies::AllowIfActorAdminOrSuperAdmin => {
174                actor.is_admin || actor.is_super_admin
175            }
176            MetadataBasePolicies::AllowIfActorSuperAdmin => actor.is_super_admin,
177        }
178    }
179
180    fn to_proto(&self) -> Result<MetadataPolicyProto, PolicyError> {
181        let inner = match self {
182            MetadataBasePolicies::Allow => MetadataBasePolicyProto::Allow as i32,
183            MetadataBasePolicies::Deny => MetadataBasePolicyProto::Deny as i32,
184            MetadataBasePolicies::AllowIfActorAdminOrSuperAdmin => {
185                MetadataBasePolicyProto::AllowIfAdmin as i32
186            }
187            MetadataBasePolicies::AllowIfActorSuperAdmin => {
188                MetadataBasePolicyProto::AllowIfSuperAdmin as i32
189            }
190        };
191
192        Ok(MetadataPolicyProto {
193            kind: Some(MetadataPolicyKindProto::Base(inner)),
194        })
195    }
196}
197
198/// Represents the different types of metadata policies.
199#[derive(Debug, Clone, PartialEq)]
200#[allow(dead_code)]
201pub enum MetadataPolicies {
202    Standard(MetadataBasePolicies),
203    AndCondition(MetadataAndCondition),
204    AnyCondition(MetadataAnyCondition),
205}
206
207impl MetadataPolicies {
208    /// Creates a default map of metadata policies.
209    pub fn default_map(policies: MetadataPolicies) -> HashMap<String, MetadataPolicies> {
210        let mut map: HashMap<String, MetadataPolicies> = HashMap::new();
211        for field in GroupMutableMetadata::supported_fields() {
212            match field {
213                MetadataField::MessageDisappearInNS => {
214                    map.insert(field.to_string(), MetadataPolicies::allow_if_actor_admin());
215                }
216                MetadataField::MessageDisappearFromNS => {
217                    map.insert(field.to_string(), MetadataPolicies::allow_if_actor_admin());
218                }
219                MetadataField::MinimumSupportedProtocolVersion => {
220                    map.insert(
221                        field.to_string(),
222                        MetadataPolicies::allow_if_actor_super_admin(),
223                    );
224                }
225                _ => {
226                    map.insert(field.to_string(), policies.clone());
227                }
228            }
229        }
230        map
231    }
232
233    // by default members of DM groups can update all metadata
234    pub fn dm_map() -> HashMap<String, MetadataPolicies> {
235        let mut map: HashMap<String, MetadataPolicies> = HashMap::new();
236        for field in GroupMutableMetadata::supported_fields() {
237            map.insert(field.to_string(), MetadataPolicies::allow());
238        }
239        map
240    }
241
242    /// Creates an "Allow" metadata policy.
243    pub fn allow() -> Self {
244        MetadataPolicies::Standard(MetadataBasePolicies::Allow)
245    }
246
247    /// Creates a "Deny" metadata policy.
248    pub fn deny() -> Self {
249        MetadataPolicies::Standard(MetadataBasePolicies::Deny)
250    }
251
252    /// Creates an "Allow if actor is admin" metadata policy.
253    pub fn allow_if_actor_admin() -> Self {
254        MetadataPolicies::Standard(MetadataBasePolicies::AllowIfActorAdminOrSuperAdmin)
255    }
256
257    /// Creates an "Allow if actor is super admin" metadata policy.
258    pub fn allow_if_actor_super_admin() -> Self {
259        MetadataPolicies::Standard(MetadataBasePolicies::AllowIfActorSuperAdmin)
260    }
261
262    /// Creates an "And" condition metadata policy.
263    pub fn and(policies: Vec<MetadataPolicies>) -> Self {
264        MetadataPolicies::AndCondition(MetadataAndCondition::new(policies))
265    }
266
267    /// Creates an "Any" condition metadata policy.
268    pub fn any(policies: Vec<MetadataPolicies>) -> Self {
269        MetadataPolicies::AnyCondition(MetadataAnyCondition::new(policies))
270    }
271}
272
273/// Implements conversion from MetadataPolicyProto to MetadataPolicies.
274impl TryFrom<MetadataPolicyProto> for MetadataPolicies {
275    type Error = PolicyError;
276
277    fn try_from(proto: MetadataPolicyProto) -> Result<Self, Self::Error> {
278        match proto.kind {
279            Some(MetadataPolicyKindProto::Base(inner)) => match inner {
280                1 => Ok(MetadataPolicies::allow()),
281                2 => Ok(MetadataPolicies::deny()),
282                3 => Ok(MetadataPolicies::allow_if_actor_admin()),
283                4 => Ok(MetadataPolicies::allow_if_actor_super_admin()),
284                _ => Err(PolicyError::InvalidMetadataPolicy),
285            },
286            Some(MetadataPolicyKindProto::AndCondition(inner)) => {
287                if inner.policies.is_empty() {
288                    return Err(PolicyError::InvalidMetadataPolicy);
289                }
290                let policies = inner
291                    .policies
292                    .into_iter()
293                    .map(|policy| policy.try_into())
294                    .collect::<Result<Vec<MetadataPolicies>, PolicyError>>()?;
295
296                Ok(MetadataPolicies::and(policies))
297            }
298            Some(MetadataPolicyKindProto::AnyCondition(inner)) => {
299                if inner.policies.is_empty() {
300                    return Err(PolicyError::InvalidMetadataPolicy);
301                }
302
303                let policies = inner
304                    .policies
305                    .into_iter()
306                    .map(|policy| policy.try_into())
307                    .collect::<Result<Vec<MetadataPolicies>, PolicyError>>()?;
308
309                Ok(MetadataPolicies::any(policies))
310            }
311            None => Err(PolicyError::InvalidMetadataPolicy),
312        }
313    }
314}
315
316/// Implements the MetadataPolicy trait for MetadataPolicies.
317impl MetadataPolicy for MetadataPolicies {
318    fn evaluate(&self, actor: &CommitParticipant, change: &MetadataFieldChange) -> bool {
319        match self {
320            MetadataPolicies::Standard(policy) => policy.evaluate(actor, change),
321            MetadataPolicies::AndCondition(policy) => policy.evaluate(actor, change),
322            MetadataPolicies::AnyCondition(policy) => policy.evaluate(actor, change),
323        }
324    }
325
326    fn to_proto(&self) -> Result<MetadataPolicyProto, PolicyError> {
327        Ok(match self {
328            MetadataPolicies::Standard(policy) => policy.to_proto()?,
329            MetadataPolicies::AndCondition(policy) => policy.to_proto()?,
330            MetadataPolicies::AnyCondition(policy) => policy.to_proto()?,
331        })
332    }
333}
334
335/// An AndCondition evaluates to true if all the policies it contains evaluate to true.
336#[derive(Clone, Debug, PartialEq)]
337pub struct MetadataAndCondition {
338    policies: Vec<MetadataPolicies>,
339}
340
341impl MetadataAndCondition {
342    pub(super) fn new(policies: Vec<MetadataPolicies>) -> Self {
343        Self { policies }
344    }
345}
346
347/// Implements the MetadataPolicy trait for MetadataAndCondition.
348impl MetadataPolicy for MetadataAndCondition {
349    fn evaluate(&self, actor: &CommitParticipant, change: &MetadataFieldChange) -> bool {
350        self.policies
351            .iter()
352            .all(|policy| policy.evaluate(actor, change))
353    }
354
355    fn to_proto(&self) -> Result<MetadataPolicyProto, PolicyError> {
356        Ok(MetadataPolicyProto {
357            kind: Some(MetadataPolicyKindProto::AndCondition(
358                MetadataAndConditionProto {
359                    policies: self
360                        .policies
361                        .iter()
362                        .map(|policy| policy.to_proto())
363                        .collect::<Result<Vec<MetadataPolicyProto>, PolicyError>>()?,
364                },
365            )),
366        })
367    }
368}
369
370/// An AnyCondition evaluates to true if any of the contained policies evaluate to true.
371#[derive(Clone, Debug, PartialEq)]
372pub struct MetadataAnyCondition {
373    policies: Vec<MetadataPolicies>,
374}
375
376#[allow(dead_code)]
377impl MetadataAnyCondition {
378    pub(super) fn new(policies: Vec<MetadataPolicies>) -> Self {
379        Self { policies }
380    }
381}
382
383/// Implements the MetadataPolicy trait for MetadataAnyCondition.
384impl MetadataPolicy for MetadataAnyCondition {
385    fn evaluate(&self, actor: &CommitParticipant, change: &MetadataFieldChange) -> bool {
386        self.policies
387            .iter()
388            .any(|policy| policy.evaluate(actor, change))
389    }
390
391    fn to_proto(&self) -> Result<MetadataPolicyProto, PolicyError> {
392        Ok(MetadataPolicyProto {
393            kind: Some(MetadataPolicyKindProto::AnyCondition(
394                MetadataAnyConditionProto {
395                    policies: self
396                        .policies
397                        .iter()
398                        .map(|policy| policy.to_proto())
399                        .collect::<Result<Vec<MetadataPolicyProto>, PolicyError>>()?,
400                },
401            )),
402        })
403    }
404}
405
406/// A trait for policies that can update Permissions for the group.
407pub trait PermissionsPolicy: std::fmt::Debug {
408    /// Evaluates the policy for a given actor.
409    fn evaluate(&self, actor: &CommitParticipant) -> bool;
410
411    /// Converts the policy to its proto representation.
412    fn to_proto(&self) -> Result<PermissionsPolicyProto, PolicyError>;
413}
414
415/// Represents the base policies for permissions updates.
416#[derive(Clone, Copy, Debug, PartialEq)]
417pub enum PermissionsBasePolicies {
418    Deny,
419    AllowIfActorAdminOrSuperAdmin,
420    AllowIfActorSuperAdmin,
421}
422
423/// Implements the PermissionsPolicy trait for PermissionsBasePolicies.
424impl PermissionsPolicy for &PermissionsBasePolicies {
425    fn evaluate(&self, actor: &CommitParticipant) -> bool {
426        match self {
427            PermissionsBasePolicies::Deny => false,
428            PermissionsBasePolicies::AllowIfActorAdminOrSuperAdmin => {
429                actor.is_admin || actor.is_super_admin
430            }
431            PermissionsBasePolicies::AllowIfActorSuperAdmin => actor.is_super_admin,
432        }
433    }
434
435    fn to_proto(&self) -> Result<PermissionsPolicyProto, PolicyError> {
436        let inner = match self {
437            PermissionsBasePolicies::Deny => PermissionsBasePolicyProto::Deny as i32,
438            PermissionsBasePolicies::AllowIfActorAdminOrSuperAdmin => {
439                PermissionsBasePolicyProto::AllowIfAdmin as i32
440            }
441            PermissionsBasePolicies::AllowIfActorSuperAdmin => {
442                PermissionsBasePolicyProto::AllowIfSuperAdmin as i32
443            }
444        };
445
446        Ok(PermissionsPolicyProto {
447            kind: Some(PermissionsPolicyKindProto::Base(inner)),
448        })
449    }
450}
451
452/// Represents the different types of permissions policies.
453#[derive(Debug, Clone, PartialEq)]
454#[allow(dead_code)]
455pub enum PermissionsPolicies {
456    Standard(PermissionsBasePolicies),
457    AndCondition(PermissionsAndCondition),
458    AnyCondition(PermissionsAnyCondition),
459}
460
461impl PermissionsPolicies {
462    /// Creates a "Deny" permissions policy.
463    pub fn deny() -> Self {
464        PermissionsPolicies::Standard(PermissionsBasePolicies::Deny)
465    }
466
467    /// Creates an "Allow if actor is admin" permissions policy.
468    pub fn allow_if_actor_admin() -> Self {
469        PermissionsPolicies::Standard(PermissionsBasePolicies::AllowIfActorAdminOrSuperAdmin)
470    }
471
472    /// Creates an "Allow if actor is super admin" permissions policy.
473    pub fn allow_if_actor_super_admin() -> Self {
474        PermissionsPolicies::Standard(PermissionsBasePolicies::AllowIfActorSuperAdmin)
475    }
476
477    /// Creates an "And" condition permissions policy.
478    pub fn and(policies: Vec<PermissionsPolicies>) -> Self {
479        PermissionsPolicies::AndCondition(PermissionsAndCondition::new(policies))
480    }
481
482    /// Creates an "Any" condition permissions policy.
483    pub fn any(policies: Vec<PermissionsPolicies>) -> Self {
484        PermissionsPolicies::AnyCondition(PermissionsAnyCondition::new(policies))
485    }
486}
487
488/// Implements conversion from PermissionsPolicyProto to PermissionsPolicies.
489impl TryFrom<PermissionsPolicyProto> for PermissionsPolicies {
490    type Error = PolicyError;
491
492    fn try_from(proto: PermissionsPolicyProto) -> Result<Self, Self::Error> {
493        match proto.kind {
494            Some(PermissionsPolicyKindProto::Base(inner)) => match inner {
495                1 => Ok(PermissionsPolicies::deny()),
496                2 => Ok(PermissionsPolicies::allow_if_actor_admin()),
497                3 => Ok(PermissionsPolicies::allow_if_actor_super_admin()),
498                _ => Err(PolicyError::InvalidPermissionsPolicy),
499            },
500            Some(PermissionsPolicyKindProto::AndCondition(inner)) => {
501                if inner.policies.is_empty() {
502                    return Err(PolicyError::InvalidPermissionsPolicy);
503                }
504                let policies = inner
505                    .policies
506                    .into_iter()
507                    .map(|policy| policy.try_into())
508                    .collect::<Result<Vec<PermissionsPolicies>, PolicyError>>()?;
509
510                Ok(PermissionsPolicies::and(policies))
511            }
512            Some(PermissionsPolicyKindProto::AnyCondition(inner)) => {
513                if inner.policies.is_empty() {
514                    return Err(PolicyError::InvalidPermissionsPolicy);
515                }
516
517                let policies = inner
518                    .policies
519                    .into_iter()
520                    .map(|policy| policy.try_into())
521                    .collect::<Result<Vec<PermissionsPolicies>, PolicyError>>()?;
522
523                Ok(PermissionsPolicies::any(policies))
524            }
525            None => Err(PolicyError::InvalidPermissionsPolicy),
526        }
527    }
528}
529
530/// Implements the PermissionsPolicy trait for PermissionsPolicies.
531impl PermissionsPolicy for PermissionsPolicies {
532    fn evaluate(&self, actor: &CommitParticipant) -> bool {
533        match self {
534            PermissionsPolicies::Standard(policy) => policy.evaluate(actor),
535            PermissionsPolicies::AndCondition(policy) => policy.evaluate(actor),
536            PermissionsPolicies::AnyCondition(policy) => policy.evaluate(actor),
537        }
538    }
539
540    fn to_proto(&self) -> Result<PermissionsPolicyProto, PolicyError> {
541        Ok(match self {
542            PermissionsPolicies::Standard(policy) => policy.to_proto()?,
543            PermissionsPolicies::AndCondition(policy) => policy.to_proto()?,
544            PermissionsPolicies::AnyCondition(policy) => policy.to_proto()?,
545        })
546    }
547}
548
549/// An AndCondition evaluates to true if all the policies it contains evaluate to true.
550#[derive(Clone, Debug, PartialEq)]
551pub struct PermissionsAndCondition {
552    policies: Vec<PermissionsPolicies>,
553}
554
555impl PermissionsAndCondition {
556    pub(super) fn new(policies: Vec<PermissionsPolicies>) -> Self {
557        Self { policies }
558    }
559}
560
561/// Implements the PermissionsPolicy trait for PermissionsAndCondition.
562impl PermissionsPolicy for PermissionsAndCondition {
563    fn evaluate(&self, actor: &CommitParticipant) -> bool {
564        self.policies.iter().all(|policy| policy.evaluate(actor))
565    }
566
567    fn to_proto(&self) -> Result<PermissionsPolicyProto, PolicyError> {
568        Ok(PermissionsPolicyProto {
569            kind: Some(PermissionsPolicyKindProto::AndCondition(
570                PermissionsAndConditionProto {
571                    policies: self
572                        .policies
573                        .iter()
574                        .map(|policy| policy.to_proto())
575                        .collect::<Result<Vec<PermissionsPolicyProto>, PolicyError>>()?,
576                },
577            )),
578        })
579    }
580}
581
582/// An AnyCondition evaluates to true if any of the contained policies evaluate to true.
583#[derive(Clone, Debug, PartialEq)]
584pub struct PermissionsAnyCondition {
585    policies: Vec<PermissionsPolicies>,
586}
587
588#[allow(dead_code)]
589impl PermissionsAnyCondition {
590    pub(super) fn new(policies: Vec<PermissionsPolicies>) -> Self {
591        Self { policies }
592    }
593}
594
595/// Implements the PermissionsPolicy trait for PermissionsAnyCondition.
596impl PermissionsPolicy for PermissionsAnyCondition {
597    fn evaluate(&self, actor: &CommitParticipant) -> bool {
598        self.policies.iter().any(|policy| policy.evaluate(actor))
599    }
600
601    fn to_proto(&self) -> Result<PermissionsPolicyProto, PolicyError> {
602        Ok(PermissionsPolicyProto {
603            kind: Some(PermissionsPolicyKindProto::AnyCondition(
604                PermissionsAnyConditionProto {
605                    policies: self
606                        .policies
607                        .iter()
608                        .map(|policy| policy.to_proto())
609                        .collect::<Result<Vec<PermissionsPolicyProto>, PolicyError>>()?,
610                },
611            )),
612        })
613    }
614}
615
616/// A trait for policies that can add/remove members and installations for the group.
617pub trait MembershipPolicy: std::fmt::Debug {
618    /// Evaluates the policy for a given actor and inbox change.
619    fn evaluate(&self, actor: &CommitParticipant, change: &Inbox) -> bool;
620
621    /// Converts the policy to its proto representation.
622    fn to_proto(&self) -> Result<MembershipPolicyProto, PolicyError>;
623}
624
625/// Errors that can occur when working with policies.
626#[derive(Debug, Error)]
627pub enum PolicyError {
628    #[error("serialization {0}")]
629    Serialization(#[from] prost::EncodeError),
630    #[error("deserialization {0}")]
631    Deserialization(#[from] prost::DecodeError),
632    #[error("Missing metadata policy field: {name}")]
633    MissingMetadataPolicyField { name: String },
634    #[error("invalid policy")]
635    InvalidPolicy,
636    #[error("unexpected preset policy")]
637    InvalidPresetPolicy,
638    #[error("invalid metadata policy")]
639    InvalidMetadataPolicy,
640    #[error("invalid membership policy")]
641    InvalidMembershipPolicy,
642    #[error("invalid permissions policy")]
643    InvalidPermissionsPolicy,
644    #[error("from proto add member invalid policy")]
645    FromProtoAddMemberInvalidPolicy,
646    #[error("from proto remove member invalid policy")]
647    FromProtoRemoveMemberInvalidPolicy,
648    #[error("from proto add admin invalid policy")]
649    FromProtoAddAdminInvalidPolicy,
650    #[error("from proto remove admin invalid policy")]
651    FromProtoRemoveAdminInvalidPolicy,
652    #[error("from proto update permissions invalid policy")]
653    FromProtoUpdatePermissionsInvalidPolicy,
654}
655
656/// Represents the base policies for membership updates.
657#[derive(Debug, Clone, Copy, PartialEq)]
658#[allow(dead_code)]
659#[repr(u8)]
660pub enum BasePolicies {
661    Allow,
662    Deny,
663    // Allow if the change only applies to subject installations with the same account address as the actor
664    AllowSameMember,
665    AllowIfAdminOrSuperAdmin,
666    AllowIfSuperAdmin,
667}
668
669/// Implements the MembershipPolicy trait for BasePolicies.
670impl MembershipPolicy for BasePolicies {
671    fn evaluate(&self, actor: &CommitParticipant, inbox: &Inbox) -> bool {
672        match self {
673            BasePolicies::Allow => true,
674            BasePolicies::Deny => false,
675            BasePolicies::AllowSameMember => inbox.inbox_id == actor.inbox_id,
676            BasePolicies::AllowIfAdminOrSuperAdmin => actor.is_admin || actor.is_super_admin,
677            BasePolicies::AllowIfSuperAdmin => actor.is_super_admin,
678        }
679    }
680
681    fn to_proto(&self) -> Result<MembershipPolicyProto, PolicyError> {
682        let inner = match self {
683            BasePolicies::Allow => BasePolicyProto::Allow as i32,
684            BasePolicies::Deny => BasePolicyProto::Deny as i32,
685            BasePolicies::AllowSameMember => return Err(PolicyError::InvalidPolicy), // AllowSameMember is not needed on any of the wire format protos
686            BasePolicies::AllowIfAdminOrSuperAdmin => {
687                BasePolicyProto::AllowIfAdminOrSuperAdmin as i32
688            }
689            BasePolicies::AllowIfSuperAdmin => BasePolicyProto::AllowIfSuperAdmin as i32,
690        };
691
692        Ok(MembershipPolicyProto {
693            kind: Some(PolicyKindProto::Base(inner)),
694        })
695    }
696}
697
698/// Represents the different types of membership policies.
699#[derive(Debug, Clone, PartialEq)]
700#[allow(dead_code)]
701pub enum MembershipPolicies {
702    Standard(BasePolicies),
703    AndCondition(AndCondition),
704    AnyCondition(AnyCondition),
705}
706
707impl MembershipPolicies {
708    /// Creates an "Allow" membership policy.
709    pub fn allow() -> Self {
710        MembershipPolicies::Standard(BasePolicies::Allow)
711    }
712
713    /// Creates a "Deny" membership policy.
714    pub fn deny() -> Self {
715        MembershipPolicies::Standard(BasePolicies::Deny)
716    }
717
718    /// Creates an "Allow if actor is admin" membership policy.
719    #[allow(dead_code)]
720    pub fn allow_if_actor_admin() -> Self {
721        MembershipPolicies::Standard(BasePolicies::AllowIfAdminOrSuperAdmin)
722    }
723
724    /// Creates an "Allow if actor is super admin" membership policy.
725    #[allow(dead_code)]
726    pub fn allow_if_actor_super_admin() -> Self {
727        MembershipPolicies::Standard(BasePolicies::AllowIfSuperAdmin)
728    }
729
730    /// Creates an "And" condition membership policy.
731    pub fn and(policies: Vec<MembershipPolicies>) -> Self {
732        MembershipPolicies::AndCondition(AndCondition::new(policies))
733    }
734
735    /// Creates an "Any" condition membership policy.
736    pub fn any(policies: Vec<MembershipPolicies>) -> Self {
737        MembershipPolicies::AnyCondition(AnyCondition::new(policies))
738    }
739}
740
741/// Implements conversion from MembershipPolicyProto to MembershipPolicies.
742impl TryFrom<MembershipPolicyProto> for MembershipPolicies {
743    type Error = PolicyError;
744
745    fn try_from(proto: MembershipPolicyProto) -> Result<Self, Self::Error> {
746        match proto.kind {
747            Some(PolicyKindProto::Base(inner)) => match inner {
748                1 => Ok(MembershipPolicies::allow()),
749                2 => Ok(MembershipPolicies::deny()),
750                3 => Ok(MembershipPolicies::allow_if_actor_admin()),
751                4 => Ok(MembershipPolicies::allow_if_actor_super_admin()),
752                _ => Err(PolicyError::InvalidMembershipPolicy),
753            },
754            Some(PolicyKindProto::AndCondition(inner)) => {
755                if inner.policies.is_empty() {
756                    return Err(PolicyError::InvalidMembershipPolicy);
757                }
758                let policies = inner
759                    .policies
760                    .into_iter()
761                    .map(|policy| policy.try_into())
762                    .collect::<Result<Vec<MembershipPolicies>, PolicyError>>()?;
763
764                Ok(MembershipPolicies::and(policies))
765            }
766            Some(PolicyKindProto::AnyCondition(inner)) => {
767                if inner.policies.is_empty() {
768                    return Err(PolicyError::InvalidMembershipPolicy);
769                }
770
771                let policies = inner
772                    .policies
773                    .into_iter()
774                    .map(|policy| policy.try_into())
775                    .collect::<Result<Vec<MembershipPolicies>, PolicyError>>()?;
776
777                Ok(MembershipPolicies::any(policies))
778            }
779            None => Err(PolicyError::InvalidMembershipPolicy),
780        }
781    }
782}
783
784/// Implements the MembershipPolicy trait for MembershipPolicies.
785impl MembershipPolicy for MembershipPolicies {
786    fn evaluate(&self, actor: &CommitParticipant, inbox: &Inbox) -> bool {
787        match self {
788            MembershipPolicies::Standard(policy) => policy.evaluate(actor, inbox),
789            MembershipPolicies::AndCondition(policy) => policy.evaluate(actor, inbox),
790            MembershipPolicies::AnyCondition(policy) => policy.evaluate(actor, inbox),
791        }
792    }
793
794    fn to_proto(&self) -> Result<MembershipPolicyProto, PolicyError> {
795        Ok(match self {
796            MembershipPolicies::Standard(policy) => policy.to_proto()?,
797            MembershipPolicies::AndCondition(policy) => policy.to_proto()?,
798            MembershipPolicies::AnyCondition(policy) => policy.to_proto()?,
799        })
800    }
801}
802
803/// An AndCondition evaluates to true if all the policies it contains evaluate to true.
804#[derive(Clone, Debug, PartialEq)]
805pub struct AndCondition {
806    policies: Vec<MembershipPolicies>,
807}
808
809impl AndCondition {
810    pub(super) fn new(policies: Vec<MembershipPolicies>) -> Self {
811        Self { policies }
812    }
813}
814
815/// Implements the MembershipPolicy trait for AndCondition.
816impl MembershipPolicy for AndCondition {
817    fn evaluate(&self, actor: &CommitParticipant, inbox: &Inbox) -> bool {
818        self.policies
819            .iter()
820            .all(|policy| policy.evaluate(actor, inbox))
821    }
822
823    fn to_proto(&self) -> Result<MembershipPolicyProto, PolicyError> {
824        Ok(MembershipPolicyProto {
825            kind: Some(PolicyKindProto::AndCondition(AndConditionProto {
826                policies: self
827                    .policies
828                    .iter()
829                    .map(|policy| policy.to_proto())
830                    .collect::<Result<Vec<MembershipPolicyProto>, PolicyError>>()?,
831            })),
832        })
833    }
834}
835
836/// An AnyCondition evaluates to true if any of the contained policies evaluate to true.
837#[derive(Clone, Debug, PartialEq)]
838pub struct AnyCondition {
839    policies: Vec<MembershipPolicies>,
840}
841
842#[allow(dead_code)]
843impl AnyCondition {
844    pub(super) fn new(policies: Vec<MembershipPolicies>) -> Self {
845        Self { policies }
846    }
847}
848
849/// Implements the MembershipPolicy trait for AnyCondition.
850impl MembershipPolicy for AnyCondition {
851    fn evaluate(&self, actor: &CommitParticipant, inbox: &Inbox) -> bool {
852        self.policies
853            .iter()
854            .any(|policy| policy.evaluate(actor, inbox))
855    }
856
857    fn to_proto(&self) -> Result<MembershipPolicyProto, PolicyError> {
858        Ok(MembershipPolicyProto {
859            kind: Some(PolicyKindProto::AnyCondition(AnyConditionProto {
860                policies: self
861                    .policies
862                    .iter()
863                    .map(|policy| policy.to_proto())
864                    .collect::<Result<Vec<MembershipPolicyProto>, PolicyError>>()?,
865            })),
866        })
867    }
868}
869
870/// Represents a set of policies for a group.
871#[derive(Debug, Clone, PartialEq)]
872#[allow(dead_code)]
873pub struct PolicySet {
874    /// The policy for adding members to the group.
875    pub add_member_policy: MembershipPolicies,
876    /// The policy for removing members from the group.
877    pub remove_member_policy: MembershipPolicies,
878    /// The policies for updating metadata fields.
879    pub update_metadata_policy: HashMap<String, MetadataPolicies>,
880    /// The policy for adding admins to the group.
881    pub add_admin_policy: PermissionsPolicies,
882    /// The policy for removing admins from the group.
883    pub remove_admin_policy: PermissionsPolicies,
884    /// The policy for updating permissions.
885    pub update_permissions_policy: PermissionsPolicies,
886}
887
888impl PolicySet {
889    /// Creates a new PolicySet instance.
890    pub fn new(
891        add_member_policy: MembershipPolicies,
892        remove_member_policy: MembershipPolicies,
893        update_metadata_policy: HashMap<String, MetadataPolicies>,
894        add_admin_policy: PermissionsPolicies,
895        remove_admin_policy: PermissionsPolicies,
896        update_permissions_policy: PermissionsPolicies,
897    ) -> Self {
898        Self {
899            add_member_policy,
900            remove_member_policy,
901            update_metadata_policy,
902            add_admin_policy,
903            remove_admin_policy,
904            update_permissions_policy,
905        }
906    }
907
908    pub fn new_dm() -> Self {
909        Self {
910            add_member_policy: MembershipPolicies::deny(),
911            remove_member_policy: MembershipPolicies::deny(),
912            update_metadata_policy: MetadataPolicies::dm_map(),
913            add_admin_policy: PermissionsPolicies::deny(),
914            remove_admin_policy: PermissionsPolicies::deny(),
915            update_permissions_policy: PermissionsPolicies::deny(),
916        }
917    }
918
919    /// Check membership policies against each proposer, or the committer
920    /// when no proposer is recorded. Component policies check all other writes.
921    pub fn evaluate_membership(&self, commit: &MembershipValidationInfo<'_>) -> bool {
922        // Verify add member policy was not violated
923        // For each added inbox, check the proposer's permissions (if known), otherwise use actor
924        let mut added_inboxes_valid = self.evaluate_policy_with_proposer(
925            commit.added_inboxes.iter(),
926            &self.add_member_policy,
927            commit.actor,
928        );
929
930        // We can always add DM member's inboxId to a DM
931        if let Some(dm_members) = &commit.dm_members
932            && commit.added_inboxes.len() == 1
933        {
934            let added_inbox_id = &commit.added_inboxes[0].inbox_id;
935            if (added_inbox_id == &dm_members.member_one_inbox_id
936                || added_inbox_id == &dm_members.member_two_inbox_id)
937                && added_inbox_id != &commit.actor.inbox_id
938            {
939                added_inboxes_valid = true;
940            }
941        }
942
943        // Verify remove member policy was not violated
944        // Super admin can not be removed from a group
945        // For each removed inbox, check the proposer's permissions (if known), otherwise use actor
946        let removed_inboxes_valid = self.evaluate_policy_with_proposer(
947            commit.removed_inboxes.iter(),
948            &self.remove_member_policy,
949            commit.actor,
950        ) && !commit
951            .removed_inboxes
952            .iter()
953            .any(|inbox| inbox.is_super_admin);
954
955        added_inboxes_valid && removed_inboxes_valid
956    }
957
958    /// Evaluates a policy for a given set of changes, using the proposer for each inbox when available.
959    /// This allows permissions to be checked against the proposer instead of the committer.
960    fn evaluate_policy_with_proposer<'a, I, P>(
961        &self,
962        mut changes: I,
963        policy: &P,
964        default_actor: &CommitParticipant,
965    ) -> bool
966    where
967        I: Iterator<Item = &'a Inbox>,
968        P: MembershipPolicy + std::fmt::Debug,
969    {
970        changes.all(|change| {
971            // Use the proposer if available, otherwise fall back to the default actor (committer)
972            let actor = change.proposer.as_ref().unwrap_or(default_actor);
973            let is_ok = policy.evaluate(actor, change);
974            if !is_ok {
975                tracing::info!(
976                    has_proposer = change.proposer.is_some(),
977                    actor_is_creator = actor.is_creator,
978                    actor_is_admin = actor.is_admin,
979                    actor_is_super_admin = actor.is_super_admin,
980                    subject_is_creator = change.is_creator,
981                    subject_is_admin = change.is_admin,
982                    subject_is_super_admin = change.is_super_admin,
983                    "Membership policy rejected change"
984                );
985            }
986            is_ok
987        })
988    }
989
990    /// Converts the PolicySet to its proto representation.
991    pub fn to_proto(&self) -> Result<PolicySetProto, PolicyError> {
992        let add_member_policy = Some(self.add_member_policy.to_proto()?);
993        let remove_member_policy = Some(self.remove_member_policy.to_proto()?);
994
995        let mut update_metadata_policy = HashMap::new();
996        for (key, policy) in &self.update_metadata_policy {
997            let policy_proto = policy.to_proto()?;
998            update_metadata_policy.insert(key.clone(), policy_proto);
999        }
1000        let add_admin_policy = Some(self.add_admin_policy.to_proto()?);
1001        let remove_admin_policy = Some(self.remove_admin_policy.to_proto()?);
1002        let update_permissions_policy = Some(self.update_permissions_policy.to_proto()?);
1003        Ok(PolicySetProto {
1004            add_member_policy,
1005            remove_member_policy,
1006            update_metadata_policy,
1007            add_admin_policy,
1008            remove_admin_policy,
1009            update_permissions_policy,
1010        })
1011    }
1012
1013    /// Creates a PolicySet from its proto representation.
1014    pub fn from_proto(proto: PolicySetProto) -> Result<Self, PolicyError> {
1015        let add_member_policy = MembershipPolicies::try_from(
1016            proto
1017                .add_member_policy
1018                .ok_or(PolicyError::FromProtoAddMemberInvalidPolicy)?,
1019        )?;
1020        let remove_member_policy = MembershipPolicies::try_from(
1021            proto
1022                .remove_member_policy
1023                .ok_or(PolicyError::FromProtoRemoveMemberInvalidPolicy)?,
1024        )?;
1025        let add_admin_policy = PermissionsPolicies::try_from(
1026            proto
1027                .add_admin_policy
1028                .ok_or(PolicyError::FromProtoAddAdminInvalidPolicy)?,
1029        )?;
1030        let remove_admin_policy = PermissionsPolicies::try_from(
1031            proto
1032                .remove_admin_policy
1033                .ok_or(PolicyError::FromProtoRemoveAdminInvalidPolicy)?,
1034        )?;
1035        let update_permissions_policy = PermissionsPolicies::try_from(
1036            proto
1037                .update_permissions_policy
1038                .ok_or(PolicyError::FromProtoUpdatePermissionsInvalidPolicy)?,
1039        )?;
1040
1041        let mut update_metadata_policy = HashMap::new();
1042        for (key, policy_proto) in proto.update_metadata_policy {
1043            let policy = MetadataPolicies::try_from(policy_proto)?;
1044            update_metadata_policy.insert(key, policy);
1045        }
1046        Ok(Self::new(
1047            add_member_policy,
1048            remove_member_policy,
1049            update_metadata_policy,
1050            add_admin_policy,
1051            remove_admin_policy,
1052            update_permissions_policy,
1053        ))
1054    }
1055
1056    /// Converts the PolicySet to a `Vec<u8>`.
1057    pub fn to_bytes(&self) -> Result<Vec<u8>, PolicyError> {
1058        let proto = self.to_proto()?;
1059        let mut buf = Vec::new();
1060        proto.encode(&mut buf)?;
1061        Ok(buf)
1062    }
1063
1064    /// Creates a PolicySet from a `Vec<u8>`.
1065    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PolicyError> {
1066        let proto = PolicySetProto::decode(bytes)?;
1067        Self::from_proto(proto)
1068    }
1069}
1070
1071/// Project action policies from the validated component registry.
1072pub fn policy_set_from_dictionary(
1073    extensions: &Extensions<GroupContext>,
1074) -> Result<GroupMutablePermissions, GroupMutablePermissionsError> {
1075    let proto = xmtp_mls_common::app_data::policy_set::policy_set_from_dictionary(extensions)
1076        .map_err(|error| {
1077            tracing::warn!(%error, "invalid registry action policy");
1078            GroupMutablePermissionsError::PolicyProjection(error)
1079        })?;
1080    Ok(GroupMutablePermissions::new(PolicySet::from_proto(proto)?))
1081}
1082
1083/// Checks if a PolicySet is equivalent to the "All Members" preconfigured policy.
1084///
1085/// Depending on if the client is on a newer or older version of libxmtp
1086/// since the group was created, the number of metadata policies might not match
1087/// the default All Members Policy Set. As long as all metadata policies are allow, we will
1088/// match against All Members Preconfigured Policy
1089pub fn is_policy_default(policy: &PolicySet) -> Result<bool, PolicyError> {
1090    let mut metadata_policies_equal = true;
1091    for field_name in policy.update_metadata_policy.keys() {
1092        let metadata_policy = policy.update_metadata_policy.get(field_name).ok_or(
1093            PolicyError::MissingMetadataPolicyField {
1094                name: field_name.to_string(),
1095            },
1096        )?;
1097        if field_name == MetadataField::MessageDisappearInNS.as_str()
1098            || field_name == MetadataField::MessageDisappearFromNS.as_str()
1099        {
1100            metadata_policies_equal = metadata_policies_equal
1101                && metadata_policy.eq(&MetadataPolicies::allow_if_actor_admin());
1102        } else if field_name == MetadataField::MinimumSupportedProtocolVersion.as_str() {
1103            metadata_policies_equal = metadata_policies_equal
1104                && metadata_policy.eq(&MetadataPolicies::allow_if_actor_super_admin());
1105        } else {
1106            metadata_policies_equal =
1107                metadata_policies_equal && metadata_policy.eq(&MetadataPolicies::allow());
1108        }
1109    }
1110    Ok(metadata_policies_equal
1111        && policy.add_member_policy == MembershipPolicies::allow()
1112        && policy.remove_member_policy == MembershipPolicies::allow_if_actor_admin()
1113        && policy.add_admin_policy == PermissionsPolicies::allow_if_actor_super_admin()
1114        && policy.remove_admin_policy == PermissionsPolicies::allow_if_actor_super_admin()
1115        && policy.update_permissions_policy == PermissionsPolicies::allow_if_actor_super_admin())
1116}
1117
1118/// Checks if a PolicySet is equivalent to the "Admin Only" preconfigured policy.
1119///
1120/// Depending on if the client is on a newer or older version of libxmtp
1121/// since the group was created, the number of metadata policies might not match
1122/// the default Admin Only Policy Set. As long as all metadata policies are admin only, we will
1123/// match against Admin Only Preconfigured Policy
1124pub fn is_policy_admin_only(policy: &PolicySet) -> Result<bool, PolicyError> {
1125    let mut metadata_policies_equal = true;
1126    for field_name in policy.update_metadata_policy.keys() {
1127        let metadata_policy = policy.update_metadata_policy.get(field_name).ok_or(
1128            PolicyError::MissingMetadataPolicyField {
1129                name: field_name.to_string(),
1130            },
1131        )?;
1132        if field_name == MetadataField::MinimumSupportedProtocolVersion.as_str() {
1133            metadata_policies_equal = metadata_policies_equal
1134                && metadata_policy.eq(&MetadataPolicies::allow_if_actor_super_admin());
1135        } else {
1136            metadata_policies_equal = metadata_policies_equal
1137                && metadata_policy.eq(&MetadataPolicies::allow_if_actor_admin());
1138        }
1139    }
1140    Ok(metadata_policies_equal
1141        && policy.add_member_policy == MembershipPolicies::allow_if_actor_admin()
1142        && policy.remove_member_policy == MembershipPolicies::allow_if_actor_admin()
1143        && policy.add_admin_policy == PermissionsPolicies::allow_if_actor_super_admin()
1144        && policy.remove_admin_policy == PermissionsPolicies::allow_if_actor_super_admin()
1145        && policy.update_permissions_policy == PermissionsPolicies::allow_if_actor_super_admin())
1146}
1147
1148/// Returns the "All Members" preconfigured policy.
1149///
1150/// A policy where any member can add or remove any other member
1151pub fn default_policy() -> PolicySet {
1152    let mut metadata_policies_map: HashMap<String, MetadataPolicies> = HashMap::new();
1153    for field in GroupMutableMetadata::supported_fields() {
1154        match field {
1155            MetadataField::MessageDisappearInNS => {
1156                metadata_policies_map
1157                    .insert(field.to_string(), MetadataPolicies::allow_if_actor_admin());
1158            }
1159            MetadataField::MessageDisappearFromNS => {
1160                metadata_policies_map
1161                    .insert(field.to_string(), MetadataPolicies::allow_if_actor_admin());
1162            }
1163            MetadataField::MinimumSupportedProtocolVersion => {
1164                metadata_policies_map.insert(
1165                    field.to_string(),
1166                    MetadataPolicies::allow_if_actor_super_admin(),
1167                );
1168            }
1169            _ => {
1170                metadata_policies_map.insert(field.to_string(), MetadataPolicies::allow());
1171            }
1172        }
1173    }
1174
1175    PolicySet::new(
1176        MembershipPolicies::allow(),
1177        MembershipPolicies::allow_if_actor_admin(),
1178        metadata_policies_map,
1179        PermissionsPolicies::allow_if_actor_super_admin(),
1180        PermissionsPolicies::allow_if_actor_super_admin(),
1181        PermissionsPolicies::allow_if_actor_super_admin(),
1182    )
1183}
1184
1185/// Returns the "Admin Only" preconfigured policy.
1186///
1187/// A policy where only the admins can add or remove members
1188pub fn policy_admin_only() -> PolicySet {
1189    let mut metadata_policies_map: HashMap<String, MetadataPolicies> = HashMap::new();
1190    for field in GroupMutableMetadata::supported_fields() {
1191        match field {
1192            MetadataField::MessageDisappearInNS => {
1193                metadata_policies_map
1194                    .insert(field.to_string(), MetadataPolicies::allow_if_actor_admin());
1195            }
1196            MetadataField::MessageDisappearFromNS => {
1197                metadata_policies_map
1198                    .insert(field.to_string(), MetadataPolicies::allow_if_actor_admin());
1199            }
1200            MetadataField::MinimumSupportedProtocolVersion => {
1201                metadata_policies_map.insert(
1202                    field.to_string(),
1203                    MetadataPolicies::allow_if_actor_super_admin(),
1204                );
1205            }
1206            _ => {
1207                metadata_policies_map
1208                    .insert(field.to_string(), MetadataPolicies::allow_if_actor_admin());
1209            }
1210        }
1211    }
1212
1213    PolicySet::new(
1214        MembershipPolicies::allow_if_actor_admin(),
1215        MembershipPolicies::allow_if_actor_admin(),
1216        metadata_policies_map,
1217        PermissionsPolicies::allow_if_actor_super_admin(),
1218        PermissionsPolicies::allow_if_actor_super_admin(),
1219        PermissionsPolicies::allow_if_actor_super_admin(),
1220    )
1221}
1222
1223/// Implements the Default trait for PolicySet.
1224impl Default for PolicySet {
1225    fn default() -> Self {
1226        PreconfiguredPolicies::default().to_policy_set()
1227    }
1228}
1229
1230/// Represents preconfigured policies for a group.
1231#[derive(Debug, Clone, PartialEq, Default)]
1232pub enum PreconfiguredPolicies {
1233    /// The "All Members" preconfigured policy.
1234    #[default]
1235    Default,
1236    /// The "Admin Only" preconfigured policy.
1237    AdminsOnly,
1238}
1239
1240impl PreconfiguredPolicies {
1241    /// Converts the PreconfiguredPolicies to a PolicySet.
1242    pub fn to_policy_set(&self) -> PolicySet {
1243        match self {
1244            PreconfiguredPolicies::Default => default_policy(),
1245            PreconfiguredPolicies::AdminsOnly => policy_admin_only(),
1246        }
1247    }
1248
1249    /// Creates a PreconfiguredPolicies from a PolicySet.
1250    pub fn from_policy_set(policy_set: &PolicySet) -> Result<Self, PolicyError> {
1251        if is_policy_default(policy_set)? {
1252            Ok(PreconfiguredPolicies::Default)
1253        } else if is_policy_admin_only(policy_set)? {
1254            Ok(PreconfiguredPolicies::AdminsOnly)
1255        } else {
1256            Err(PolicyError::InvalidPresetPolicy)
1257        }
1258    }
1259}
1260
1261/// Implements the Display trait for PreconfiguredPolicies.
1262impl std::fmt::Display for PreconfiguredPolicies {
1263    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1264        write!(f, "{:?}", self)
1265    }
1266}
1267
1268#[cfg(test)]
1269pub mod tests {
1270
1271    use openmls::extensions::{AppDataDictionary, AppDataDictionaryExtension, Extension};
1272    use xmtp_common::{rand_string, rand_vec};
1273    use xmtp_mls_common::{
1274        app_data::{component_id::ComponentId, creation::synthesize_registry_from_policy_set},
1275        group_metadata::DmMembers,
1276    };
1277
1278    use super::*;
1279
1280    fn dictionary_extensions(policy_set: &PolicySet) -> Extensions<GroupContext> {
1281        let proto = policy_set.to_proto().unwrap();
1282        let registry = synthesize_registry_from_policy_set(&proto).unwrap();
1283        let mut dictionary = AppDataDictionary::new();
1284        dictionary.insert(
1285            ComponentId::COMPONENT_REGISTRY.as_u16(),
1286            registry.to_bytes().unwrap(),
1287        );
1288        Extensions::from_vec(vec![Extension::AppDataDictionary(
1289            AppDataDictionaryExtension::new(dictionary),
1290        )])
1291        .unwrap()
1292    }
1293
1294    fn build_change(inbox_id: Option<String>, is_admin: bool, is_super_admin: bool) -> Inbox {
1295        Inbox {
1296            inbox_id: inbox_id.unwrap_or(rand_string::<24>()),
1297            is_creator: is_super_admin,
1298            is_super_admin,
1299            is_admin,
1300            proposer: None,
1301        }
1302    }
1303
1304    /// Test helper function for building a CommitParticipant.
1305    fn build_actor(
1306        inbox_id: Option<String>,
1307        installation_id: Option<Vec<u8>>,
1308        is_admin: bool,
1309        is_super_admin: bool,
1310    ) -> CommitParticipant {
1311        CommitParticipant {
1312            inbox_id: inbox_id.unwrap_or(rand_string::<24>()),
1313            installation_id: installation_id.unwrap_or_else(rand_vec::<24>),
1314            is_creator: is_super_admin,
1315            is_admin,
1316            is_super_admin,
1317        }
1318    }
1319
1320    enum MemberType {
1321        SameAsActor,
1322        DmTarget,
1323        Random,
1324    }
1325
1326    struct TestMembershipChanges {
1327        actor: CommitParticipant,
1328        added_inboxes: Vec<Inbox>,
1329        removed_inboxes: Vec<Inbox>,
1330        dm_members: Option<DmMembers<String>>,
1331    }
1332
1333    impl TestMembershipChanges {
1334        fn validation_info(&self) -> MembershipValidationInfo<'_> {
1335            MembershipValidationInfo {
1336                actor: &self.actor,
1337                added_inboxes: &self.added_inboxes,
1338                removed_inboxes: &self.removed_inboxes,
1339                dm_members: self.dm_members.as_ref(),
1340            }
1341        }
1342    }
1343
1344    /// Build membership changes for policy tests.
1345    fn build_membership_changes(
1346        // Add a member with the same account address as the actor if true, random account address if false
1347        member_added: Option<MemberType>,
1348        member_removed: Option<MemberType>,
1349        actor_is_admin: bool,
1350        actor_is_super_admin: bool,
1351        dm_target_inbox_id: Option<String>,
1352    ) -> TestMembershipChanges {
1353        let actor = build_actor(None, None, actor_is_admin, actor_is_super_admin);
1354        let dm_target_inbox_id_clone = dm_target_inbox_id.clone();
1355        let build_membership_change = |member_type: MemberType| match member_type {
1356            MemberType::SameAsActor => vec![build_change(
1357                Some(actor.inbox_id.clone()),
1358                actor_is_admin,
1359                actor_is_super_admin,
1360            )],
1361            MemberType::DmTarget => {
1362                vec![build_change(dm_target_inbox_id_clone.clone(), false, false)]
1363            }
1364            MemberType::Random => vec![build_change(None, false, false)],
1365        };
1366
1367        let dm_members = if let Some(dm_target_inbox_id) = dm_target_inbox_id {
1368            Some(DmMembers {
1369                member_one_inbox_id: actor.inbox_id.clone(),
1370                member_two_inbox_id: dm_target_inbox_id,
1371            })
1372        } else {
1373            None
1374        };
1375
1376        TestMembershipChanges {
1377            actor: actor.clone(),
1378            added_inboxes: member_added
1379                .map(build_membership_change)
1380                .unwrap_or_default(),
1381            removed_inboxes: member_removed
1382                .map(build_membership_change)
1383                .unwrap_or_default(),
1384            dm_members,
1385        }
1386    }
1387
1388    /// Tests that a commit by a non admin/super admin can add and remove members
1389    /// with allow policies.
1390    #[xmtp_common::test]
1391    fn test_allow_all() {
1392        let permissions = PolicySet::new(
1393            MembershipPolicies::allow(),
1394            MembershipPolicies::allow(),
1395            MetadataPolicies::default_map(MetadataPolicies::allow()),
1396            PermissionsPolicies::allow_if_actor_super_admin(),
1397            PermissionsPolicies::allow_if_actor_super_admin(),
1398            PermissionsPolicies::allow_if_actor_super_admin(),
1399        );
1400
1401        let commit = build_membership_changes(
1402            Some(MemberType::SameAsActor),
1403            Some(MemberType::SameAsActor),
1404            false,
1405            false,
1406            None,
1407        );
1408        assert!(permissions.evaluate_membership(&commit.validation_info()));
1409    }
1410
1411    /// Tests that a commit by a non admin/super admin is denied for add and remove member policies.
1412    #[xmtp_common::test]
1413    fn test_deny() {
1414        let permissions = PolicySet::new(
1415            MembershipPolicies::deny(),
1416            MembershipPolicies::deny(),
1417            MetadataPolicies::default_map(MetadataPolicies::deny()),
1418            PermissionsPolicies::allow_if_actor_super_admin(),
1419            PermissionsPolicies::allow_if_actor_super_admin(),
1420            PermissionsPolicies::allow_if_actor_super_admin(),
1421        );
1422
1423        let member_added_commit =
1424            build_membership_changes(Some(MemberType::Random), None, false, false, None);
1425        assert!(!permissions.evaluate_membership(&member_added_commit.validation_info()));
1426
1427        let member_removed_commit =
1428            build_membership_changes(None, Some(MemberType::Random), false, false, None);
1429        assert!(!permissions.evaluate_membership(&member_removed_commit.validation_info()));
1430    }
1431
1432    /// Tests that a group creator can perform super admin actions.
1433    #[xmtp_common::test]
1434    fn test_actor_is_creator() {
1435        let permissions = PolicySet::new(
1436            MembershipPolicies::allow_if_actor_super_admin(),
1437            MembershipPolicies::allow_if_actor_super_admin(),
1438            MetadataPolicies::default_map(MetadataPolicies::deny()),
1439            PermissionsPolicies::allow_if_actor_super_admin(),
1440            PermissionsPolicies::allow_if_actor_super_admin(),
1441            PermissionsPolicies::allow_if_actor_super_admin(),
1442        );
1443
1444        // Can not remove the creator if they are the only super admin
1445        let commit_with_creator = build_membership_changes(
1446            Some(MemberType::SameAsActor),
1447            Some(MemberType::SameAsActor),
1448            false,
1449            true,
1450            None,
1451        );
1452        assert!(!permissions.evaluate_membership(&commit_with_creator.validation_info()));
1453
1454        let commit_with_creator = build_membership_changes(
1455            Some(MemberType::SameAsActor),
1456            Some(MemberType::Random),
1457            false,
1458            true,
1459            None,
1460        );
1461        assert!(permissions.evaluate_membership(&commit_with_creator.validation_info()));
1462
1463        let commit_without_creator = build_membership_changes(
1464            Some(MemberType::SameAsActor),
1465            Some(MemberType::SameAsActor),
1466            false,
1467            false,
1468            None,
1469        );
1470        assert!(!permissions.evaluate_membership(&commit_without_creator.validation_info()));
1471    }
1472
1473    /// Tests that and conditions are enforced as expected.
1474    #[xmtp_common::test]
1475    fn test_and_condition() {
1476        let permissions = PolicySet::new(
1477            MembershipPolicies::and(vec![
1478                MembershipPolicies::Standard(BasePolicies::Deny),
1479                MembershipPolicies::Standard(BasePolicies::Allow),
1480            ]),
1481            MembershipPolicies::allow(),
1482            MetadataPolicies::default_map(MetadataPolicies::deny()),
1483            PermissionsPolicies::allow_if_actor_super_admin(),
1484            PermissionsPolicies::allow_if_actor_super_admin(),
1485            PermissionsPolicies::allow_if_actor_super_admin(),
1486        );
1487
1488        let member_added_commit =
1489            build_membership_changes(Some(MemberType::SameAsActor), None, false, false, None);
1490        assert!(!permissions.evaluate_membership(&member_added_commit.validation_info()));
1491    }
1492
1493    /// Tests that any conditions are enforced as expected.
1494    #[xmtp_common::test]
1495    fn test_any_condition() {
1496        let permissions = PolicySet::new(
1497            MembershipPolicies::any(vec![
1498                MembershipPolicies::deny(),
1499                MembershipPolicies::allow(),
1500            ]),
1501            MembershipPolicies::allow(),
1502            MetadataPolicies::default_map(MetadataPolicies::deny()),
1503            PermissionsPolicies::allow_if_actor_super_admin(),
1504            PermissionsPolicies::allow_if_actor_super_admin(),
1505            PermissionsPolicies::allow_if_actor_super_admin(),
1506        );
1507
1508        let member_added_commit =
1509            build_membership_changes(Some(MemberType::SameAsActor), None, false, false, None);
1510        assert!(permissions.evaluate_membership(&member_added_commit.validation_info()));
1511    }
1512
1513    /// Tests that the PolicySet can be serialized and deserialized.
1514    #[xmtp_common::test]
1515    fn test_serialize() {
1516        let permissions = PolicySet::new(
1517            MembershipPolicies::any(vec![
1518                MembershipPolicies::allow(),
1519                MembershipPolicies::deny(),
1520            ]),
1521            MembershipPolicies::and(vec![
1522                MembershipPolicies::allow_if_actor_super_admin(),
1523                MembershipPolicies::deny(),
1524            ]),
1525            MetadataPolicies::default_map(MetadataPolicies::deny()),
1526            PermissionsPolicies::allow_if_actor_super_admin(),
1527            PermissionsPolicies::allow_if_actor_super_admin(),
1528            PermissionsPolicies::allow_if_actor_super_admin(),
1529        );
1530
1531        let proto = permissions.to_proto().unwrap();
1532        assert!(proto.add_member_policy.is_some());
1533        assert!(proto.remove_member_policy.is_some());
1534
1535        let as_bytes = permissions.to_bytes().expect("serialization failed");
1536        let restored = PolicySet::from_bytes(as_bytes.as_slice()).expect("proto conversion failed");
1537        // All fields implement PartialEq so this should test equality all the way down
1538        assert!(permissions.eq(&restored))
1539    }
1540
1541    /// Tests that the preconfigured policy functions work as expected
1542    #[xmtp_common::test]
1543    fn test_preconfigured_policy() {
1544        let group_permissions = GroupMutablePermissions::new(default_policy());
1545
1546        assert_eq!(
1547            group_permissions.preconfigured_policy().unwrap(),
1548            PreconfiguredPolicies::Default
1549        );
1550
1551        let group_group_permissions_creator_admin =
1552            GroupMutablePermissions::new(policy_admin_only());
1553
1554        assert_eq!(
1555            group_group_permissions_creator_admin
1556                .preconfigured_policy()
1557                .unwrap(),
1558            PreconfiguredPolicies::AdminsOnly
1559        );
1560    }
1561
1562    #[xmtp_common::test(unwrap_try = true)]
1563    fn dictionary_round_trip_preserves_presets_and_dm_permissions() {
1564        for policy_set in [
1565            PreconfiguredPolicies::Default.to_policy_set(),
1566            PreconfiguredPolicies::AdminsOnly.to_policy_set(),
1567        ] {
1568            let reconstructed = policy_set_from_dictionary(&dictionary_extensions(&policy_set))?;
1569            assert_eq!(reconstructed.policies, policy_set);
1570        }
1571
1572        // A legacy DM stores Deny, but the dictionary reader reports the
1573        // enforced super-admin-only policy. DMs have empty admin lists, so
1574        // no actor can satisfy that reported policy.
1575        let legacy_dm = PolicySet::new_dm();
1576        assert_eq!(
1577            legacy_dm.update_permissions_policy,
1578            PermissionsPolicies::deny()
1579        );
1580        let mut expected_dm = legacy_dm.clone();
1581        expected_dm.update_permissions_policy = PermissionsPolicies::allow_if_actor_super_admin();
1582        let reconstructed = policy_set_from_dictionary(&dictionary_extensions(&legacy_dm))?;
1583        assert_eq!(reconstructed.policies, expected_dm);
1584
1585        for preset in [
1586            PreconfiguredPolicies::Default,
1587            PreconfiguredPolicies::AdminsOnly,
1588        ] {
1589            let reconstructed =
1590                policy_set_from_dictionary(&dictionary_extensions(&preset.to_policy_set()))?;
1591            assert_eq!(
1592                PreconfiguredPolicies::from_policy_set(&reconstructed.policies)?,
1593                preset
1594            );
1595        }
1596    }
1597
1598    #[xmtp_common::test(unwrap_try = true)]
1599    fn dictionary_reader_denies_only_malformed_metadata_field() {
1600        let mut expected = PreconfiguredPolicies::Default.to_policy_set();
1601        let proto = expected.to_proto()?;
1602        let mut registry = synthesize_registry_from_policy_set(&proto)?;
1603        let malformed = MetadataPolicyProto {
1604            kind: Some(MetadataPolicyKindProto::AndCondition(
1605                MetadataAndConditionProto { policies: vec![] },
1606            )),
1607        };
1608        assert!(matches!(
1609            MetadataPolicies::try_from(malformed.clone()),
1610            Err(PolicyError::InvalidMetadataPolicy)
1611        ));
1612        let mut group_name = registry.get(&ComponentId::GROUP_NAME)?.unwrap();
1613        group_name.permissions.as_mut()?.update_policy = Some(malformed);
1614        registry.set(ComponentId::GROUP_NAME, group_name)?;
1615
1616        let mut dictionary = AppDataDictionary::new();
1617        dictionary.insert(
1618            ComponentId::COMPONENT_REGISTRY.as_u16(),
1619            registry.to_bytes()?,
1620        );
1621        let extensions = Extensions::from_vec(vec![Extension::AppDataDictionary(
1622            AppDataDictionaryExtension::new(dictionary),
1623        )])?;
1624
1625        expected.update_metadata_policy.insert(
1626            MetadataField::GroupName.to_string(),
1627            MetadataPolicies::deny(),
1628        );
1629        assert_eq!(policy_set_from_dictionary(&extensions)?.policies, expected);
1630    }
1631
1632    /// Tests that the preconfigured policy functions work as expected with new metadata fields.
1633    #[xmtp_common::test]
1634    fn test_preconfigured_policy_equality_new_metadata() {
1635        let mut metadata_policies_map = MetadataPolicies::default_map(MetadataPolicies::allow());
1636        metadata_policies_map.insert("new_metadata_field".to_string(), MetadataPolicies::allow());
1637        let policy_set_new_metadata_permission = PolicySet {
1638            add_member_policy: MembershipPolicies::allow(),
1639            remove_member_policy: MembershipPolicies::allow_if_actor_admin(),
1640            update_metadata_policy: metadata_policies_map,
1641            add_admin_policy: PermissionsPolicies::allow_if_actor_super_admin(),
1642            remove_admin_policy: PermissionsPolicies::allow_if_actor_super_admin(),
1643            update_permissions_policy: PermissionsPolicies::allow_if_actor_super_admin(),
1644        };
1645
1646        assert!(is_policy_default(&policy_set_new_metadata_permission).unwrap());
1647
1648        let mut metadata_policies_map =
1649            MetadataPolicies::default_map(MetadataPolicies::allow_if_actor_admin());
1650        metadata_policies_map.insert(
1651            "new_metadata_field_2".to_string(),
1652            MetadataPolicies::allow_if_actor_admin(),
1653        );
1654        let policy_set_new_metadata_permission = PolicySet {
1655            add_member_policy: MembershipPolicies::allow_if_actor_admin(),
1656            remove_member_policy: MembershipPolicies::allow_if_actor_admin(),
1657            update_metadata_policy: metadata_policies_map,
1658            add_admin_policy: PermissionsPolicies::allow_if_actor_super_admin(),
1659            remove_admin_policy: PermissionsPolicies::allow_if_actor_super_admin(),
1660            update_permissions_policy: PermissionsPolicies::allow_if_actor_super_admin(),
1661        };
1662
1663        assert!(is_policy_admin_only(&policy_set_new_metadata_permission).unwrap());
1664    }
1665
1666    #[xmtp_common::test]
1667    fn test_dm_group_permissions() {
1668        // Simulate a group with DM Permissions
1669        let permissions = PolicySet::new_dm();
1670
1671        // String below represents the inbox id of the DM target
1672        const TARGET_INBOX_ID: &str = "example_target_dm_id";
1673
1674        // DM group can not add a random inbox
1675        let commit = build_membership_changes(
1676            Some(MemberType::Random),
1677            None,
1678            false,
1679            false,
1680            Some(TARGET_INBOX_ID.to_string()),
1681        );
1682        assert!(!permissions.evaluate_membership(&commit.validation_info()));
1683
1684        // DM group can not add themselves
1685        let commit = build_membership_changes(
1686            Some(MemberType::SameAsActor),
1687            None,
1688            false,
1689            false,
1690            Some(TARGET_INBOX_ID.to_string()),
1691        );
1692        assert!(!permissions.evaluate_membership(&commit.validation_info()));
1693
1694        // DM group can add the target inbox
1695        let commit = build_membership_changes(
1696            Some(MemberType::DmTarget),
1697            None,
1698            false,
1699            false,
1700            Some(TARGET_INBOX_ID.to_string()),
1701        );
1702        assert!(permissions.evaluate_membership(&commit.validation_info()));
1703
1704        // DM group can not remove
1705        let commit = build_membership_changes(
1706            None,
1707            Some(MemberType::Random),
1708            false,
1709            false,
1710            Some(TARGET_INBOX_ID.to_string()),
1711        );
1712        assert!(!permissions.evaluate_membership(&commit.validation_info()));
1713        let commit = build_membership_changes(
1714            None,
1715            Some(MemberType::DmTarget),
1716            false,
1717            false,
1718            Some(TARGET_INBOX_ID.to_string()),
1719        );
1720        assert!(!permissions.evaluate_membership(&commit.validation_info()));
1721        let commit = build_membership_changes(
1722            None,
1723            Some(MemberType::SameAsActor),
1724            false,
1725            false,
1726            Some(TARGET_INBOX_ID.to_string()),
1727        );
1728        assert!(!permissions.evaluate_membership(&commit.validation_info()));
1729    }
1730}