canic_core/domain/policy/topology/
mod.rs1pub mod registry;
2
3pub use crate::view::topology::{RegistryPolicyInput, TopologyPolicyInput};
4
5use crate::{InternalError, cdk::types::Principal, domain::policy::PolicyError, ids::CanisterRole};
6use std::collections::BTreeSet;
7use thiserror::Error as ThisError;
8
9#[derive(Debug, ThisError)]
14pub enum TopologyPolicyError {
15 #[error("directory entry role mismatch for pid {pid}: expected {expected}, got {found}")]
16 DirectoryRoleMismatch {
17 pid: Principal,
18 expected: CanisterRole,
19 found: CanisterRole,
20 },
21
22 #[error("directory role {0} appears more than once")]
23 DuplicateDirectoryRole(CanisterRole),
24
25 #[error("immediate-parent mismatch: canister {pid} expects parent {expected}, got {found:?}")]
26 ImmediateParentMismatch {
27 pid: Principal,
28 expected: Principal,
29 found: Option<Principal>,
30 },
31
32 #[error("module hash mismatch for {0}")]
33 ModuleHashMismatch(Principal),
34
35 #[error("parent {0} not found in registry")]
36 ParentNotFound(Principal),
37
38 #[error("registry entry missing for {0}")]
39 RegistryEntryMissing(Principal),
40
41 #[error(transparent)]
42 RegistryPolicy(#[from] registry::RegistryPolicyError),
43}
44
45impl From<TopologyPolicyError> for InternalError {
46 fn from(err: TopologyPolicyError) -> Self {
47 PolicyError::from(err).into()
48 }
49}
50
51pub struct TopologyPolicy;
56
57impl TopologyPolicy {
58 fn registry_record(
63 registry: &'_ RegistryPolicyInput,
64 pid: Principal,
65 ) -> Result<&'_ TopologyPolicyInput, TopologyPolicyError> {
66 registry
67 .entries
68 .iter()
69 .find(|entry| entry.pid == pid)
70 .ok_or(TopologyPolicyError::RegistryEntryMissing(pid))
71 }
72
73 pub(crate) fn assert_parent_exists(
78 registry: &RegistryPolicyInput,
79 parent_pid: Principal,
80 ) -> Result<(), InternalError> {
81 if registry.entries.iter().any(|entry| entry.pid == parent_pid) {
82 Ok(())
83 } else {
84 Err(TopologyPolicyError::ParentNotFound(parent_pid).into())
85 }
86 }
87
88 pub(crate) fn assert_module_hash(
89 registry: &RegistryPolicyInput,
90 pid: Principal,
91 expected_hash: &[u8],
92 ) -> Result<(), InternalError> {
93 let record = Self::registry_record(registry, pid)?;
94
95 if record.module_hash.as_deref() == Some(expected_hash) {
96 Ok(())
97 } else {
98 Err(TopologyPolicyError::ModuleHashMismatch(pid).into())
99 }
100 }
101
102 pub(crate) fn assert_immediate_parent(
103 registry: &RegistryPolicyInput,
104 pid: Principal,
105 expected_parent: Principal,
106 ) -> Result<(), InternalError> {
107 let record = Self::registry_record(registry, pid)?;
108
109 match record.parent_pid {
110 Some(pp) if pp == expected_parent => Ok(()),
111 other => Err(TopologyPolicyError::ImmediateParentMismatch {
112 pid,
113 expected: expected_parent,
114 found: other,
115 }
116 .into()),
117 }
118 }
119
120 pub fn assert_directory_consistent_with_registry(
121 registry: &RegistryPolicyInput,
122 entries: &[(CanisterRole, Principal)],
123 ) -> Result<(), TopologyPolicyError> {
124 let mut seen_roles = BTreeSet::new();
125
126 for (role, pid) in entries {
127 let record = Self::registry_record(registry, *pid)?;
128
129 if record.role != *role {
130 return Err(TopologyPolicyError::DirectoryRoleMismatch {
131 pid: *pid,
132 expected: record.role.clone(),
133 found: role.clone(),
134 });
135 }
136
137 if !seen_roles.insert(role.clone()) {
138 return Err(TopologyPolicyError::DuplicateDirectoryRole(role.clone()));
139 }
140 }
141
142 Ok(())
143 }
144}