1use 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
43pub 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 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
87pub 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
122pub 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(®istry_policy(
130 registry,
131 ComponentId::GROUP_MEMBERSHIP,
132 op,
133 )?)?;
134 metadata_policy_to_admin_policy(®istry_policy(registry, ComponentId::ADMIN_LIST, op)?)?;
135 }
136 Ok(())
137}
138
139fn 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
172pub 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(®istry_policy(
190 ®istry,
191 ComponentId::GROUP_MEMBERSHIP,
192 op,
193 )?)
194 };
195 let admin = |op| {
196 metadata_policy_to_admin_policy(®istry_policy(®istry, 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(®istry, *component_id, ComponentOp::Update),
202 "registry metadata policy is missing or malformed"
203 )
204 .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 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 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(®istry))?.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(®istry))?,
344 expected
345 );
346 }
347 }
348
349 #[xmtp_common::test(unwrap_try = true)]
350 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(®istry).is_err());
371 assert!(policy_set_from_dictionary(&extensions(®istry)).is_err());
372 }
373 }
374
375 #[xmtp_common::test(unwrap_try = true)]
376 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 #[xmtp_common::test(unwrap_try = true)]
408 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(®istry).is_err());
426 }
427
428 #[xmtp_common::test(unwrap_try = true)]
432 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(®istry))?;
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 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(®istry).is_err());
469 assert!(policy_set_from_dictionary(&extensions(®istry)).is_err());
470 }
471 assert!(policy_set_from_dictionary(&Extensions::default()).is_err());
472 }
473}