Skip to main content

canic_core/domain/policy/topology/
mod.rs

1pub 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///
10/// TopologyPolicyError
11///
12
13#[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
51///
52/// TopologyPolicy
53///
54
55pub struct TopologyPolicy;
56
57impl TopologyPolicy {
58    // -------------------------------------------------------------
59    // Internal helpers
60    // -------------------------------------------------------------
61
62    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    // -------------------------------------------------------------
74    // Assertions
75    // -------------------------------------------------------------
76
77    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}