Skip to main content

xmtp_mls_common/app_data/
policy_set.rs

1//! Project the public policy view from the component registry.
2//!
3//! Exact round trips cover nonempty membership And/Any trees over the four
4//! defined base policies, base admin policies (Deny, AllowIfAdmin,
5//! AllowIfSuperAdmin), super-admin permission updates, and complete maps of
6//! valid supported metadata policies. Child order, duplicate
7//! children, and all condition wrappers are preserved.
8//!
9//! A metadata field's `ALLOW_IF_SELF_OR_NON_MEMBER` projects as `Deny`, its
10//! result on every scalar; an action entry that uses it is rejected.
11//!
12//! This is not an identity for arbitrary PolicySet values. Admin combinators
13//! have no forward mapping. Permission updates always require a super admin
14//! (including DMs, whose legacy policy is Deny). Creation fills sparse metadata
15//! maps with defaults. These are existing limits, not additional stored state.
16
17use openmls::{extensions::Extensions, group::GroupContext};
18use xmtp_proto::xmtp::mls::message_contents::{
19    MembershipPolicy, MetadataPolicy, PermissionsUpdatePolicy, PolicySet,
20    membership_policy::{self, BasePolicy as MembershipBasePolicy, Kind as MembershipPolicyKind},
21    metadata_policy::{self, Kind as MetadataPolicyKind, MetadataBasePolicy},
22    permissions_update_policy::{Kind as PermissionsPolicyKind, PermissionsBasePolicy},
23};
24
25use crate::app_data::{
26    component_id::ComponentId,
27    component_registry::{ComponentOp, ComponentRegistry, ComponentRegistryError},
28    creation::metadata_field_registry_mapping,
29};
30
31#[derive(Debug, thiserror::Error)]
32pub enum PolicyProjectionError {
33    #[error("component registry is missing")]
34    MissingRegistry,
35    #[error(transparent)]
36    Registry(#[from] ComponentRegistryError),
37    #[error("invalid membership policy: {0:?}")]
38    InvalidMembershipPolicy(Option<i32>),
39    #[error("admin policy must be Deny, AllowIfAdmin, or AllowIfSuperAdmin: {0:?}")]
40    InvalidAdminPolicy(Option<i32>),
41}
42
43/// Invert the membership mapping without changing the policy tree.
44/// Visit all children. An earlier Allow must not hide an invalid OR child.
45pub fn metadata_policy_to_membership_policy(
46    policy: &MetadataPolicy,
47) -> Result<MembershipPolicy, PolicyProjectionError> {
48    let kind = match &policy.kind {
49        Some(MetadataPolicyKind::Base(base)) => {
50            let base = match MetadataBasePolicy::try_from(*base) {
51                Ok(MetadataBasePolicy::Allow) => MembershipBasePolicy::Allow,
52                Ok(MetadataBasePolicy::Deny) => MembershipBasePolicy::Deny,
53                // Both evaluators use is_admin || is_super_admin.
54                Ok(MetadataBasePolicy::AllowIfAdmin) => {
55                    MembershipBasePolicy::AllowIfAdminOrSuperAdmin
56                }
57                Ok(MetadataBasePolicy::AllowIfSuperAdmin) => {
58                    MembershipBasePolicy::AllowIfSuperAdmin
59                }
60                _ => return Err(PolicyProjectionError::InvalidMembershipPolicy(Some(*base))),
61            };
62            MembershipPolicyKind::Base(base as i32)
63        }
64        Some(MetadataPolicyKind::AndCondition(condition)) if !condition.policies.is_empty() => {
65            MembershipPolicyKind::AndCondition(membership_policy::AndCondition {
66                policies: condition
67                    .policies
68                    .iter()
69                    .map(metadata_policy_to_membership_policy)
70                    .collect::<Result<_, _>>()?,
71            })
72        }
73        Some(MetadataPolicyKind::AnyCondition(condition)) if !condition.policies.is_empty() => {
74            MembershipPolicyKind::AnyCondition(membership_policy::AnyCondition {
75                policies: condition
76                    .policies
77                    .iter()
78                    .map(metadata_policy_to_membership_policy)
79                    .collect::<Result<_, _>>()?,
80            })
81        }
82        _ => return Err(PolicyProjectionError::InvalidMembershipPolicy(None)),
83    };
84    Ok(MembershipPolicy { kind: Some(kind) })
85}
86
87/// Invert the constrained admin mapping. Combinators are not supported.
88pub fn metadata_policy_to_admin_policy(
89    policy: &MetadataPolicy,
90) -> Result<PermissionsUpdatePolicy, PolicyProjectionError> {
91    let Some(MetadataPolicyKind::Base(base)) = policy.kind else {
92        return Err(PolicyProjectionError::InvalidAdminPolicy(None));
93    };
94    let mapped = match MetadataBasePolicy::try_from(base) {
95        Ok(MetadataBasePolicy::Deny) => PermissionsBasePolicy::Deny,
96        Ok(MetadataBasePolicy::AllowIfAdmin) => PermissionsBasePolicy::AllowIfAdmin,
97        Ok(MetadataBasePolicy::AllowIfSuperAdmin) => PermissionsBasePolicy::AllowIfSuperAdmin,
98        _ => return Err(PolicyProjectionError::InvalidAdminPolicy(Some(base))),
99    };
100    Ok(PermissionsUpdatePolicy {
101        kind: Some(PermissionsPolicyKind::Base(mapped as i32)),
102    })
103}
104
105fn registry_policy(
106    registry: &ComponentRegistry,
107    id: ComponentId,
108    op: ComponentOp,
109) -> Result<MetadataPolicy, PolicyProjectionError> {
110    let permissions = registry
111        .get(&id)?
112        .and_then(|metadata| metadata.permissions)
113        .ok_or(ComponentRegistryError::MissingPermissions(id))?;
114    let policy = match op {
115        ComponentOp::Insert => permissions.insert_policy,
116        ComponentOp::Update => permissions.update_policy,
117        ComponentOp::Delete => permissions.delete_policy,
118    };
119    Ok(policy.ok_or(ComponentRegistryError::MissingPolicyField(id, op))?)
120}
121
122/// Require the action entries and validate each complete action policy tree.
123/// Use this on Welcome admission and on the final registry of a commit.
124// implements: PERM-017
125pub fn validate_registry_action_policies(
126    registry: &ComponentRegistry,
127) -> Result<(), PolicyProjectionError> {
128    for op in [ComponentOp::Insert, ComponentOp::Delete] {
129        metadata_policy_to_membership_policy(&registry_policy(
130            registry,
131            ComponentId::GROUP_MEMBERSHIP,
132            op,
133        )?)?;
134        metadata_policy_to_admin_policy(&registry_policy(registry, ComponentId::ADMIN_LIST, op)?)?;
135    }
136    Ok(())
137}
138
139/// Project a metadata field's registry policy into the legacy vocabulary,
140/// or `None` for a malformed tree. Every projected field is a scalar, on
141/// which `ALLOW_IF_SELF_OR_NON_MEMBER` always denies, so it becomes `Deny`:
142/// the legacy `PolicySet` cannot carry it, and dropping the whole tree
143/// would misreport a sibling that allows.
144fn legacy_metadata_policy(policy: &MetadataPolicy) -> Option<MetadataPolicy> {
145    let children = |policies: &[MetadataPolicy]| {
146        (!policies.is_empty())
147            .then(|| policies.iter().map(legacy_metadata_policy).collect())
148            .flatten()
149    };
150    let kind = match policy.kind.as_ref()? {
151        MetadataPolicyKind::Base(base) => {
152            MetadataPolicyKind::Base(match MetadataBasePolicy::try_from(*base).ok()? {
153                MetadataBasePolicy::Unspecified => return None,
154                MetadataBasePolicy::AllowIfSelfOrNonMember => MetadataBasePolicy::Deny as i32,
155                _ => *base,
156            })
157        }
158        MetadataPolicyKind::AndCondition(condition) => {
159            MetadataPolicyKind::AndCondition(metadata_policy::AndCondition {
160                policies: children(&condition.policies)?,
161            })
162        }
163        MetadataPolicyKind::AnyCondition(condition) => {
164            MetadataPolicyKind::AnyCondition(metadata_policy::AnyCondition {
165                policies: children(&condition.policies)?,
166            })
167        }
168    };
169    Some(MetadataPolicy { kind: Some(kind) })
170}
171
172/// Read the four action slots from COMPONENT_REGISTRY alone.
173/// Invalid action trees return an error; they never become a different policy.
174/// Metadata fields retain their existing per-field deny fallback.
175// implements: PERM-024
176pub fn policy_set_from_dictionary(
177    extensions: &Extensions<GroupContext>,
178) -> Result<PolicySet, PolicyProjectionError> {
179    let bytes = extensions
180        .app_data_dictionary()
181        .and_then(|dictionary| {
182            dictionary
183                .dictionary()
184                .get(&ComponentId::COMPONENT_REGISTRY.as_u16())
185        })
186        .ok_or(PolicyProjectionError::MissingRegistry)?;
187    let registry = ComponentRegistry::from_bytes(bytes)?;
188    let membership = |op| {
189        metadata_policy_to_membership_policy(&registry_policy(
190            &registry,
191            ComponentId::GROUP_MEMBERSHIP,
192            op,
193        )?)
194    };
195    let admin = |op| {
196        metadata_policy_to_admin_policy(&registry_policy(&registry, ComponentId::ADMIN_LIST, op)?)
197    };
198    let mut update_metadata_policy = std::collections::HashMap::new();
199    for (field, component_id, _) in metadata_field_registry_mapping() {
200        let policy = xmtp_common::optify!(
201            registry_policy(&registry, *component_id, ComponentOp::Update),
202            "registry metadata policy is missing or malformed"
203        )
204        // This view validates the whole metadata tree. Enforcement can
205        // short-circuit OR, so malformed trailing children can give a
206        // different result. This pre-existing difference is retained.
207        .and_then(|policy| legacy_metadata_policy(&policy))
208        .unwrap_or(MetadataPolicy {
209            kind: Some(MetadataPolicyKind::Base(MetadataBasePolicy::Deny as i32)),
210        });
211        update_metadata_policy.insert(field.as_str().to_string(), policy);
212    }
213    Ok(PolicySet {
214        add_member_policy: Some(membership(ComponentOp::Insert)?),
215        remove_member_policy: Some(membership(ComponentOp::Delete)?),
216        add_admin_policy: Some(admin(ComponentOp::Insert)?),
217        remove_admin_policy: Some(admin(ComponentOp::Delete)?),
218        update_metadata_policy,
219        // No stored value controls permission-update authority.
220        update_permissions_policy: Some(PermissionsUpdatePolicy {
221            kind: Some(PermissionsPolicyKind::Base(
222                PermissionsBasePolicy::AllowIfSuperAdmin as i32,
223            )),
224        }),
225    })
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::app_data::creation::{
232        admin_list_policy_to_metadata_policy, membership_policy_to_metadata_policy,
233        synthesize_registry_from_policy_set,
234    };
235    use openmls::extensions::{AppDataDictionary, AppDataDictionaryExtension, Extension};
236    use prost::Message;
237    use xmtp_proto::xmtp::mls::message_contents::metadata_policy::{AndCondition, AnyCondition};
238
239    fn base(base: MetadataBasePolicy) -> MetadataPolicy {
240        MetadataPolicy {
241            kind: Some(MetadataPolicyKind::Base(base as i32)),
242        }
243    }
244
245    fn policy_set() -> PolicySet {
246        PolicySet {
247            add_member_policy: Some(
248                metadata_policy_to_membership_policy(&base(MetadataBasePolicy::Allow)).unwrap(),
249            ),
250            remove_member_policy: Some(
251                metadata_policy_to_membership_policy(&base(MetadataBasePolicy::AllowIfAdmin))
252                    .unwrap(),
253            ),
254            add_admin_policy: Some(
255                metadata_policy_to_admin_policy(&base(MetadataBasePolicy::AllowIfSuperAdmin))
256                    .unwrap(),
257            ),
258            remove_admin_policy: Some(
259                metadata_policy_to_admin_policy(&base(MetadataBasePolicy::AllowIfSuperAdmin))
260                    .unwrap(),
261            ),
262            update_permissions_policy: Some(
263                metadata_policy_to_admin_policy(&base(MetadataBasePolicy::AllowIfSuperAdmin))
264                    .unwrap(),
265            ),
266            update_metadata_policy: metadata_field_registry_mapping()
267                .iter()
268                .map(|(field, _, _)| {
269                    (
270                        field.as_str().to_string(),
271                        base(MetadataBasePolicy::AllowIfAdmin),
272                    )
273                })
274                .collect(),
275        }
276    }
277
278    fn extensions(registry: &ComponentRegistry) -> Extensions<GroupContext> {
279        let mut dictionary = AppDataDictionary::new();
280        dictionary.insert(
281            ComponentId::COMPONENT_REGISTRY.as_u16(),
282            registry.to_bytes().unwrap(),
283        );
284        Extensions::from_vec(vec![Extension::AppDataDictionary(
285            AppDataDictionaryExtension::new(dictionary),
286        )])
287        .unwrap()
288    }
289
290    #[xmtp_common::test(unwrap_try = true)]
291    // verifies: PERM-024
292    fn update_permissions_reports_enforced_super_admin_policy() {
293        for stored in [
294            MetadataBasePolicy::Deny,
295            MetadataBasePolicy::AllowIfAdmin,
296            MetadataBasePolicy::AllowIfSuperAdmin,
297        ] {
298            let mut expected = policy_set();
299            expected.update_permissions_policy =
300                Some(metadata_policy_to_admin_policy(&base(stored))?);
301            let registry = synthesize_registry_from_policy_set(&expected)?;
302            assert_eq!(
303                policy_set_from_dictionary(&extensions(&registry))?.update_permissions_policy,
304                policy_set().update_permissions_policy
305            );
306        }
307    }
308
309    #[xmtp_common::test(unwrap_try = true)]
310    fn inverse_preserves_nested_membership_trees_and_all_base_variants() {
311        let leaves = [
312            MetadataBasePolicy::Allow,
313            MetadataBasePolicy::Deny,
314            MetadataBasePolicy::AllowIfAdmin,
315            MetadataBasePolicy::AllowIfSuperAdmin,
316        ];
317        for leaf in leaves {
318            let tree = MetadataPolicy {
319                kind: Some(MetadataPolicyKind::AndCondition(AndCondition {
320                    policies: vec![
321                        base(leaf),
322                        MetadataPolicy {
323                            kind: Some(MetadataPolicyKind::AnyCondition(AnyCondition {
324                                policies: vec![
325                                    base(MetadataBasePolicy::Deny),
326                                    base(leaf),
327                                    base(leaf),
328                                ],
329                            })),
330                        },
331                    ],
332                })),
333            };
334            let membership = metadata_policy_to_membership_policy(&tree)?;
335            assert_eq!(
336                membership_policy_to_metadata_policy(&membership)?.encode_to_vec(),
337                tree.encode_to_vec()
338            );
339            let mut expected = policy_set();
340            expected.add_member_policy = Some(membership);
341            let registry = synthesize_registry_from_policy_set(&expected)?;
342            assert_eq!(
343                policy_set_from_dictionary(&extensions(&registry))?,
344                expected
345            );
346        }
347    }
348
349    #[xmtp_common::test(unwrap_try = true)]
350    // verifies: PERM-017
351    fn inverse_rejects_malformed_trailing_or_child() {
352        for bad in [
353            None,
354            Some(MetadataPolicyKind::Base(0)),
355            Some(MetadataPolicyKind::Base(i32::MAX)),
356        ] {
357            let tree = MetadataPolicy {
358                kind: Some(MetadataPolicyKind::AnyCondition(AnyCondition {
359                    policies: vec![
360                        base(MetadataBasePolicy::Allow),
361                        MetadataPolicy { kind: bad },
362                    ],
363                })),
364            };
365            assert!(metadata_policy_to_membership_policy(&tree).is_err());
366            let mut registry = synthesize_registry_from_policy_set(&policy_set())?;
367            let mut metadata = registry.get(&ComponentId::GROUP_MEMBERSHIP)??;
368            metadata.permissions.as_mut()?.insert_policy = Some(tree);
369            registry.set(ComponentId::GROUP_MEMBERSHIP, metadata)?;
370            assert!(validate_registry_action_policies(&registry).is_err());
371            assert!(policy_set_from_dictionary(&extensions(&registry)).is_err());
372        }
373    }
374
375    #[xmtp_common::test(unwrap_try = true)]
376    // verifies: PERM-017
377    fn inverse_rejects_admin_combinators_and_preserves_supported_bases() {
378        for leaf in [
379            MetadataBasePolicy::Deny,
380            MetadataBasePolicy::AllowIfAdmin,
381            MetadataBasePolicy::AllowIfSuperAdmin,
382        ] {
383            let policy = base(leaf);
384            assert_eq!(
385                admin_list_policy_to_metadata_policy(&metadata_policy_to_admin_policy(&policy)?)?,
386                policy
387            );
388            for kind in [
389                MetadataPolicyKind::AndCondition(AndCondition {
390                    policies: vec![policy.clone()],
391                }),
392                MetadataPolicyKind::AnyCondition(AnyCondition {
393                    policies: vec![policy],
394                }),
395            ] {
396                assert!(
397                    metadata_policy_to_admin_policy(&MetadataPolicy { kind: Some(kind) }).is_err()
398                );
399            }
400        }
401        assert!(metadata_policy_to_admin_policy(&base(MetadataBasePolicy::Allow)).is_err());
402    }
403
404    /// The legacy membership and admin policies have no self-owned base, so
405    /// an action entry that uses one anywhere in its tree must be refused
406    /// rather than projected into a policy with another meaning.
407    #[xmtp_common::test(unwrap_try = true)]
408    // verifies: PERM-017
409    fn action_projection_rejects_self_or_non_member() {
410        let own = base(MetadataBasePolicy::AllowIfSelfOrNonMember);
411        let nested = MetadataPolicy {
412            kind: Some(MetadataPolicyKind::AnyCondition(AnyCondition {
413                policies: vec![base(MetadataBasePolicy::AllowIfAdmin), own.clone()],
414            })),
415        };
416        for policy in [&own, &nested] {
417            assert!(metadata_policy_to_membership_policy(policy).is_err());
418        }
419        assert!(metadata_policy_to_admin_policy(&own).is_err());
420
421        let mut registry = synthesize_registry_from_policy_set(&policy_set())?;
422        let mut metadata = registry.get(&ComponentId::GROUP_MEMBERSHIP)??;
423        metadata.permissions.as_mut()?.delete_policy = Some(nested);
424        registry.set(ComponentId::GROUP_MEMBERSHIP, metadata)?;
425        assert!(validate_registry_action_policies(&registry).is_err());
426    }
427
428    /// A self-owned base on a scalar metadata field always denies, so the
429    /// legacy view reports it as `Deny` in place and keeps an allowing
430    /// sibling, instead of failing the whole `PolicySet` or hiding the tree.
431    #[xmtp_common::test(unwrap_try = true)]
432    // verifies: PERM-024
433    fn metadata_projection_reports_self_or_non_member_as_deny() {
434        let (field, id, _) = &metadata_field_registry_mapping()[0];
435        let stored = MetadataPolicy {
436            kind: Some(MetadataPolicyKind::AnyCondition(AnyCondition {
437                policies: vec![
438                    base(MetadataBasePolicy::AllowIfSelfOrNonMember),
439                    base(MetadataBasePolicy::AllowIfAdmin),
440                ],
441            })),
442        };
443        let mut registry = synthesize_registry_from_policy_set(&policy_set())?;
444        let mut metadata = registry.get(id)??;
445        metadata.permissions.as_mut()?.update_policy = Some(stored);
446        registry.set(*id, metadata)?;
447
448        let projected = policy_set_from_dictionary(&extensions(&registry))?;
449        assert_eq!(
450            projected.update_metadata_policy[field.as_str()],
451            MetadataPolicy {
452                kind: Some(MetadataPolicyKind::AnyCondition(AnyCondition {
453                    policies: vec![
454                        base(MetadataBasePolicy::Deny),
455                        base(MetadataBasePolicy::AllowIfAdmin),
456                    ],
457                })),
458            }
459        );
460    }
461
462    #[xmtp_common::test(unwrap_try = true)]
463    // verifies: PERM-017
464    fn action_entries_cannot_be_absent() {
465        for id in [ComponentId::GROUP_MEMBERSHIP, ComponentId::ADMIN_LIST] {
466            let mut registry = synthesize_registry_from_policy_set(&policy_set())?;
467            registry.remove(&id)?;
468            assert!(validate_registry_action_policies(&registry).is_err());
469            assert!(policy_set_from_dictionary(&extensions(&registry)).is_err());
470        }
471        assert!(policy_set_from_dictionary(&Extensions::default()).is_err());
472    }
473}