canic_core/access/
env.rs

1use crate::{
2    access::{AccessError, AccessRuleError, AccessRuleResult},
3    cdk::{api::canister_self, api::is_controller as caller_is_controller, types::Principal},
4    config::Config,
5    ops::runtime::env::EnvOps,
6};
7use thiserror::Error as ThisError;
8
9///
10/// EnvAccessError
11///
12
13#[derive(Debug, ThisError)]
14pub enum EnvAccessError {
15    #[error("this endpoint is only available on the prime subnet")]
16    NotPrimeSubnet,
17
18    #[error("this endpoint is only available on prime root")]
19    NotPrimeRoot,
20
21    #[error("operation must be called from the root canister")]
22    NotRoot,
23
24    #[error("operation cannot be called from the root canister")]
25    IsRoot,
26
27    #[error("access dependency unavailable: {0}")]
28    DependencyUnavailable(String),
29}
30
31///
32/// Env Checks
33///
34
35#[allow(clippy::unused_async)]
36pub async fn is_root() -> Result<(), AccessError> {
37    let root_pid = EnvOps::root_pid()
38        .map_err(|_| EnvAccessError::DependencyUnavailable("root pid unavailable".to_string()))?;
39
40    if root_pid == canister_self() {
41        Ok(())
42    } else {
43        Err(EnvAccessError::NotRoot.into())
44    }
45}
46
47#[allow(clippy::unused_async)]
48pub async fn is_not_root() -> Result<(), AccessError> {
49    let root_pid = EnvOps::root_pid()
50        .map_err(|_| EnvAccessError::DependencyUnavailable("root pid unavailable".to_string()))?;
51
52    if root_pid == canister_self() {
53        Err(EnvAccessError::IsRoot.into())
54    } else {
55        Ok(())
56    }
57}
58
59#[allow(clippy::unused_async)]
60pub async fn is_prime_root() -> Result<(), AccessError> {
61    if EnvOps::is_prime_root() {
62        Ok(())
63    } else {
64        Err(EnvAccessError::NotPrimeRoot.into())
65    }
66}
67
68#[allow(clippy::unused_async)]
69pub async fn is_prime_subnet() -> Result<(), AccessError> {
70    if EnvOps::is_prime_subnet() {
71        Ok(())
72    } else {
73        Err(EnvAccessError::NotPrimeSubnet.into())
74    }
75}
76
77/// Ensure the caller is the root canister.
78pub(crate) fn require_root() -> Result<(), AccessError> {
79    let root_pid = EnvOps::snapshot()
80        .root_pid
81        .ok_or_else(|| EnvAccessError::DependencyUnavailable("root pid unavailable".to_string()))?;
82
83    if root_pid == canister_self() {
84        Ok(())
85    } else {
86        Err(EnvAccessError::NotRoot.into())
87    }
88}
89
90/// Ensure the caller is not the root canister.
91pub(crate) fn deny_root() -> Result<(), AccessError> {
92    let root_pid = EnvOps::snapshot()
93        .root_pid
94        .ok_or_else(|| EnvAccessError::DependencyUnavailable("root pid unavailable".to_string()))?;
95
96    if root_pid == canister_self() {
97        Err(EnvAccessError::IsRoot.into())
98    } else {
99        Ok(())
100    }
101}
102
103// -----------------------------------------------------------------------------
104// Caller rules
105// -----------------------------------------------------------------------------
106
107/// Require that the caller controls the current canister.
108/// Allows controller-only maintenance calls.
109#[must_use]
110pub fn is_controller(caller: Principal) -> AccessRuleResult {
111    Box::pin(async move {
112        if caller_is_controller(&caller) {
113            Ok(())
114        } else {
115            Err(AccessRuleError::NotController(caller).into())
116        }
117    })
118}
119
120/// Require that the caller appears in the active whitelist (IC deployments).
121/// No-op on local builds; enforces whitelist on IC.
122#[must_use]
123pub fn is_whitelisted(caller: Principal) -> AccessRuleResult {
124    Box::pin(async move {
125        let cfg = Config::try_get().ok_or_else(|| {
126            AccessRuleError::DependencyUnavailable("config not initialized".to_string())
127        })?;
128
129        if !cfg.is_whitelisted(&caller) {
130            return Err(AccessRuleError::NotWhitelisted(caller).into());
131        }
132
133        Ok(())
134    })
135}