1use xmtp_proto::xmtp::mls::message_contents::MetadataPolicy as MetadataPolicyProto;
2use xmtp_proto::xmtp::mls::message_contents::metadata_policy::{
3 Kind as MetadataPolicyKind, MetadataBasePolicy,
4};
5
6use super::component_id::ComponentId;
7use super::component_registry::{ComponentOp, ComponentRegistry, ComponentRegistryError};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ActorAuthority {
18 pub is_admin: bool,
19 pub is_super_admin: bool,
20}
21
22#[derive(Debug, Clone, bon::Builder)]
54pub struct ComponentChange<'a> {
55 pub component_id: ComponentId,
56 pub op: ComponentOp,
57 pub actor: ActorAuthority,
58 pub old_value: Option<&'a [u8]>,
59 pub new_value: Option<&'a [u8]>,
60}
61
62#[derive(Debug, thiserror::Error)]
63pub enum ComponentPermissionError {
64 #[error("immutable component {0} does not allow {1}")]
65 ImmutableViolation(ComponentId, ComponentOp),
66 #[error("component {0} requires super admin")]
67 SuperAdminRequired(ComponentId),
68 #[error("no registry entry for component {0}")]
69 NoRegistryEntry(ComponentId),
70 #[error("missing permissions in metadata for component {0}")]
71 MissingPermissions(ComponentId),
72 #[error("missing policy field for component {0} op {1}")]
73 MissingPolicyField(ComponentId, ComponentOp),
74 #[error("invalid policy for component {0} op {1}")]
75 InvalidPolicy(ComponentId, ComponentOp),
76 #[error("permission denied for component {0} op {1}")]
77 PermissionDenied(ComponentId, ComponentOp),
78 #[error("registry error: {0}")]
79 RegistryError(#[from] ComponentRegistryError),
80}
81
82pub fn validate_component_write(
94 change: &ComponentChange<'_>,
95 registry: &ComponentRegistry,
96) -> Result<(), ComponentPermissionError> {
97 let component_id = change.component_id;
98 let op = change.op;
99
100 if component_id.is_immutable() && matches!(op, ComponentOp::Update | ComponentOp::Delete) {
102 return Err(ComponentPermissionError::ImmutableViolation(
103 component_id,
104 op,
105 ));
106 }
107
108 if component_id.is_hardcoded() {
113 return if change.actor.is_super_admin {
114 Ok(())
115 } else {
116 Err(ComponentPermissionError::SuperAdminRequired(component_id))
117 };
118 }
119
120 let meta = registry
122 .get(&component_id)?
123 .ok_or(ComponentPermissionError::NoRegistryEntry(component_id))?;
124
125 let permissions = meta
130 .permissions
131 .ok_or(ComponentPermissionError::MissingPermissions(component_id))?;
132
133 let policy_proto: MetadataPolicyProto = match op {
134 ComponentOp::Insert => permissions.insert_policy,
135 ComponentOp::Update => permissions.update_policy,
136 ComponentOp::Delete => permissions.delete_policy,
137 }
138 .ok_or(ComponentPermissionError::MissingPolicyField(
139 component_id,
140 op,
141 ))?;
142
143 match evaluate_policy_proto(&policy_proto, change) {
144 PolicyOutcome::Allow => Ok(()),
145 PolicyOutcome::Deny => Err(ComponentPermissionError::PermissionDenied(component_id, op)),
146 PolicyOutcome::Invalid => Err(ComponentPermissionError::InvalidPolicy(component_id, op)),
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152enum PolicyOutcome {
153 Allow,
155 Deny,
157 Invalid,
160}
161
162impl PolicyOutcome {
163 fn from_bool(allowed: bool) -> Self {
164 if allowed { Self::Allow } else { Self::Deny }
165 }
166}
167
168fn evaluate_policy_proto(
190 proto: &MetadataPolicyProto,
191 change: &ComponentChange<'_>,
192) -> PolicyOutcome {
193 match &proto.kind {
194 Some(MetadataPolicyKind::Base(base)) => evaluate_base_policy(*base, change.actor),
195 Some(MetadataPolicyKind::AndCondition(and)) => {
196 if and.policies.is_empty() {
197 return PolicyOutcome::Invalid;
198 }
199 for inner in &and.policies {
200 match evaluate_policy_proto(inner, change) {
201 PolicyOutcome::Allow => continue,
202 other => return other,
203 }
204 }
205 PolicyOutcome::Allow
206 }
207 Some(MetadataPolicyKind::AnyCondition(any)) => {
208 if any.policies.is_empty() {
209 return PolicyOutcome::Invalid;
210 }
211 for inner in &any.policies {
212 match evaluate_policy_proto(inner, change) {
213 PolicyOutcome::Allow => return PolicyOutcome::Allow,
214 PolicyOutcome::Deny => {}
215 PolicyOutcome::Invalid => return PolicyOutcome::Invalid,
216 }
217 }
218 PolicyOutcome::Deny
219 }
220 None => PolicyOutcome::Invalid,
221 }
222}
223
224fn evaluate_base_policy(base: i32, actor: ActorAuthority) -> PolicyOutcome {
225 let base = match MetadataBasePolicy::try_from(base) {
226 Ok(b) => b,
227 Err(_) => return PolicyOutcome::Invalid,
228 };
229 match base {
230 MetadataBasePolicy::Allow => PolicyOutcome::Allow,
231 MetadataBasePolicy::Deny => PolicyOutcome::Deny,
232 MetadataBasePolicy::AllowIfAdmin => {
233 PolicyOutcome::from_bool(actor.is_admin || actor.is_super_admin)
234 }
235 MetadataBasePolicy::AllowIfSuperAdmin => PolicyOutcome::from_bool(actor.is_super_admin),
236 MetadataBasePolicy::Unspecified => PolicyOutcome::Invalid,
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::app_data::component_permissions::component_permissions;
244 use crate::app_data::component_registry::new_component_metadata;
245 use xmtp_proto::xmtp::mls::message_contents::{
246 ComponentType, MetadataPolicy as MetadataPolicyProto,
247 metadata_policy::{Kind as MetadataPolicyKind, MetadataBasePolicy},
248 };
249
250 fn make_policy(base: MetadataBasePolicy) -> MetadataPolicyProto {
251 MetadataPolicyProto {
252 kind: Some(MetadataPolicyKind::Base(base as i32)),
253 }
254 }
255
256 fn allow() -> MetadataPolicyProto {
257 make_policy(MetadataBasePolicy::Allow)
258 }
259
260 fn deny() -> MetadataPolicyProto {
261 make_policy(MetadataBasePolicy::Deny)
262 }
263
264 fn admin_only() -> MetadataPolicyProto {
265 make_policy(MetadataBasePolicy::AllowIfAdmin)
266 }
267
268 fn super_admin_only() -> MetadataPolicyProto {
269 make_policy(MetadataBasePolicy::AllowIfSuperAdmin)
270 }
271
272 fn make_actor(is_admin: bool, is_super_admin: bool) -> ActorAuthority {
273 ActorAuthority {
274 is_admin,
275 is_super_admin,
276 }
277 }
278
279 fn change<'a>(id: ComponentId, op: ComponentOp, actor: ActorAuthority) -> ComponentChange<'a> {
284 ComponentChange::builder()
285 .component_id(id)
286 .op(op)
287 .actor(actor)
288 .build()
289 }
290
291 fn member() -> ActorAuthority {
292 make_actor(false, false)
293 }
294
295 fn admin() -> ActorAuthority {
296 make_actor(true, false)
297 }
298
299 fn super_admin() -> ActorAuthority {
300 make_actor(true, true)
301 }
302
303 fn setup_registry_with(
304 id: ComponentId,
305 insert: MetadataPolicyProto,
306 update: MetadataPolicyProto,
307 delete: MetadataPolicyProto,
308 ) -> ComponentRegistry {
309 let mut reg = ComponentRegistry::new();
310 reg.set(
311 id,
312 new_component_metadata(
313 component_permissions()
314 .insert(insert)
315 .update(update)
316 .delete(delete)
317 .call(),
318 ComponentType::Bytes,
319 ),
320 )
321 .unwrap();
322 reg
323 }
324
325 #[xmtp_common::test]
328 fn test_immutable_insert_allowed() {
329 let id = ComponentId::CONVERSATION_TYPE;
330 let reg = setup_registry_with(id, allow(), deny(), deny());
331 let actor = super_admin();
332 let result = validate_component_write(&change(id, ComponentOp::Insert, actor), ®);
333 assert!(result.is_ok());
334 }
335
336 #[xmtp_common::test]
337 fn test_immutable_update_rejected() {
338 let id = ComponentId::CONVERSATION_TYPE;
339 let reg = setup_registry_with(id, allow(), allow(), allow());
340 let actor = super_admin();
341 let result = validate_component_write(&change(id, ComponentOp::Update, actor), ®);
342 assert!(matches!(
343 result,
344 Err(ComponentPermissionError::ImmutableViolation(
345 _,
346 ComponentOp::Update
347 ))
348 ));
349 }
350
351 #[xmtp_common::test]
352 fn test_immutable_delete_rejected() {
353 let id = ComponentId::CONVERSATION_TYPE;
354 let reg = setup_registry_with(id, allow(), allow(), allow());
355 let actor = super_admin();
356 let result = validate_component_write(&change(id, ComponentOp::Delete, actor), ®);
357 assert!(matches!(
358 result,
359 Err(ComponentPermissionError::ImmutableViolation(
360 _,
361 ComponentOp::Delete
362 ))
363 ));
364 }
365
366 #[xmtp_common::test]
369 fn test_registry_super_admin_allowed() {
370 let reg = ComponentRegistry::new();
371 let actor = super_admin();
372 let result = validate_component_write(
373 &change(ComponentId::COMPONENT_REGISTRY, ComponentOp::Update, actor),
374 ®,
375 );
376 assert!(result.is_ok());
377 }
378
379 #[xmtp_common::test]
380 fn test_registry_admin_rejected() {
381 let reg = ComponentRegistry::new();
382 let actor = admin();
383 let result = validate_component_write(
384 &change(ComponentId::COMPONENT_REGISTRY, ComponentOp::Update, actor),
385 ®,
386 );
387 assert!(matches!(
388 result,
389 Err(ComponentPermissionError::SuperAdminRequired(_))
390 ));
391 }
392
393 #[xmtp_common::test]
394 fn test_registry_member_rejected() {
395 let reg = ComponentRegistry::new();
396 let actor = member();
397 let result = validate_component_write(
398 &change(ComponentId::COMPONENT_REGISTRY, ComponentOp::Update, actor),
399 ®,
400 );
401 assert!(matches!(
402 result,
403 Err(ComponentPermissionError::SuperAdminRequired(_))
404 ));
405 }
406
407 #[xmtp_common::test]
408 fn test_super_admin_list_super_admin_allowed() {
409 let reg = ComponentRegistry::new();
410 let actor = super_admin();
411 let result = validate_component_write(
412 &change(ComponentId::SUPER_ADMIN_LIST, ComponentOp::Insert, actor),
413 ®,
414 );
415 assert!(result.is_ok());
416 }
417
418 #[xmtp_common::test]
419 fn test_super_admin_list_admin_rejected() {
420 let reg = ComponentRegistry::new();
421 let actor = admin();
422 let result = validate_component_write(
423 &change(ComponentId::SUPER_ADMIN_LIST, ComponentOp::Insert, actor),
424 ®,
425 );
426 assert!(matches!(
427 result,
428 Err(ComponentPermissionError::SuperAdminRequired(_))
429 ));
430 }
431
432 #[xmtp_common::test]
433 fn test_admin_list_with_admin_policy_admin_allowed() {
434 let reg = setup_registry_with(
435 ComponentId::ADMIN_LIST,
436 admin_only(),
437 admin_only(),
438 admin_only(),
439 );
440 let actor = admin();
441 let result = validate_component_write(
442 &change(ComponentId::ADMIN_LIST, ComponentOp::Insert, actor),
443 ®,
444 );
445 assert!(result.is_ok());
446 }
447
448 #[xmtp_common::test]
449 fn test_admin_list_with_admin_policy_member_rejected() {
450 let reg = setup_registry_with(
451 ComponentId::ADMIN_LIST,
452 admin_only(),
453 admin_only(),
454 admin_only(),
455 );
456 let actor = member();
457 let result = validate_component_write(
458 &change(ComponentId::ADMIN_LIST, ComponentOp::Insert, actor),
459 ®,
460 );
461 assert!(matches!(
462 result,
463 Err(ComponentPermissionError::PermissionDenied(_, _))
464 ));
465 }
466
467 #[xmtp_common::test]
468 fn test_admin_list_with_super_admin_policy() {
469 let reg = setup_registry_with(
470 ComponentId::ADMIN_LIST,
471 super_admin_only(),
472 super_admin_only(),
473 super_admin_only(),
474 );
475 let admin_actor = admin();
476 let super_admin_actor = super_admin();
477 assert!(
479 validate_component_write(
480 &change(ComponentId::ADMIN_LIST, ComponentOp::Insert, admin_actor),
481 ®,
482 )
483 .is_err()
484 );
485 assert!(
487 validate_component_write(
488 &change(
489 ComponentId::ADMIN_LIST,
490 ComponentOp::Insert,
491 super_admin_actor,
492 ),
493 ®,
494 )
495 .is_ok()
496 );
497 }
498
499 #[xmtp_common::test]
502 fn test_deny_by_default_no_entry() {
503 let reg = ComponentRegistry::new();
504 let actor = super_admin();
505 let result = validate_component_write(
506 &change(ComponentId::GROUP_NAME, ComponentOp::Insert, actor),
507 ®,
508 );
509 assert!(matches!(
510 result,
511 Err(ComponentPermissionError::NoRegistryEntry(_))
512 ));
513 }
514
515 #[xmtp_common::test]
516 fn test_insert_allow_policy() {
517 let reg = setup_registry_with(ComponentId::GROUP_NAME, allow(), deny(), deny());
518 let actor = member();
519 let result = validate_component_write(
520 &change(ComponentId::GROUP_NAME, ComponentOp::Insert, actor),
521 ®,
522 );
523 assert!(result.is_ok());
524 }
525
526 #[xmtp_common::test]
527 fn test_update_admin_only_policy_admin_passes() {
528 let reg = setup_registry_with(ComponentId::GROUP_NAME, allow(), admin_only(), deny());
529 let actor = admin();
530 let result = validate_component_write(
531 &change(ComponentId::GROUP_NAME, ComponentOp::Update, actor),
532 ®,
533 );
534 assert!(result.is_ok());
535 }
536
537 #[xmtp_common::test]
538 fn test_update_admin_only_policy_member_fails() {
539 let reg = setup_registry_with(ComponentId::GROUP_NAME, allow(), admin_only(), deny());
540 let actor = member();
541 let result = validate_component_write(
542 &change(ComponentId::GROUP_NAME, ComponentOp::Update, actor),
543 ®,
544 );
545 assert!(matches!(
546 result,
547 Err(ComponentPermissionError::PermissionDenied(
548 _,
549 ComponentOp::Update
550 ))
551 ));
552 }
553
554 #[xmtp_common::test]
555 fn test_delete_deny_policy() {
556 let reg = setup_registry_with(ComponentId::GROUP_NAME, allow(), allow(), deny());
557 let actor = super_admin();
558 let result = validate_component_write(
559 &change(ComponentId::GROUP_NAME, ComponentOp::Delete, actor),
560 ®,
561 );
562 assert!(matches!(
563 result,
564 Err(ComponentPermissionError::PermissionDenied(
565 _,
566 ComponentOp::Delete
567 ))
568 ));
569 }
570
571 #[xmtp_common::test]
572 fn test_delete_super_admin_only_policy() {
573 let reg = setup_registry_with(
574 ComponentId::GROUP_NAME,
575 allow(),
576 allow(),
577 super_admin_only(),
578 );
579 let actor = super_admin();
580 let result = validate_component_write(
581 &change(ComponentId::GROUP_NAME, ComponentOp::Delete, actor),
582 ®,
583 );
584 assert!(result.is_ok());
585 }
586
587 #[xmtp_common::test]
588 fn test_different_insert_vs_update_permissions() {
589 let reg = setup_registry_with(
591 ComponentId::GROUP_MEMBERSHIP,
592 admin_only(),
593 allow(),
594 admin_only(),
595 );
596 let member_actor = member();
597 let admin_actor = admin();
598
599 assert!(
601 validate_component_write(
602 &change(
603 ComponentId::GROUP_MEMBERSHIP,
604 ComponentOp::Update,
605 member_actor,
606 ),
607 ®,
608 )
609 .is_ok()
610 );
611
612 assert!(
614 validate_component_write(
615 &change(
616 ComponentId::GROUP_MEMBERSHIP,
617 ComponentOp::Insert,
618 member_actor,
619 ),
620 ®,
621 )
622 .is_err()
623 );
624
625 assert!(
627 validate_component_write(
628 &change(
629 ComponentId::GROUP_MEMBERSHIP,
630 ComponentOp::Insert,
631 admin_actor,
632 ),
633 ®,
634 )
635 .is_ok()
636 );
637 }
638
639 #[xmtp_common::test]
640 fn test_app_range_component() {
641 let app_id = ComponentId::new(0xC100);
642 let reg = setup_registry_with(app_id, allow(), allow(), deny());
643 let actor = member();
644 let result = validate_component_write(&change(app_id, ComponentOp::Insert, actor), ®);
645 assert!(result.is_ok());
646 }
647}