canic_core/access/
env.rs

1use crate::{
2    access::{AccessError, AccessRuleError, AccessRuleResult},
3    cdk::{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
28///
29/// Env Checks
30///
31
32#[allow(clippy::unused_async)]
33pub async fn self_is_root() -> Result<(), AccessError> {
34    if EnvOps::is_root() {
35        Ok(())
36    } else {
37        Err(EnvAccessError::NotRoot.into())
38    }
39}
40
41#[allow(clippy::unused_async)]
42pub async fn self_is_not_root() -> Result<(), AccessError> {
43    if EnvOps::is_root() {
44        Err(EnvAccessError::IsRoot.into())
45    } else {
46        Ok(())
47    }
48}
49
50#[allow(clippy::unused_async)]
51pub async fn is_prime_root() -> Result<(), AccessError> {
52    if EnvOps::is_prime_root() {
53        Ok(())
54    } else {
55        Err(EnvAccessError::NotPrimeRoot.into())
56    }
57}
58
59#[allow(clippy::unused_async)]
60pub async fn is_prime_subnet() -> Result<(), AccessError> {
61    if EnvOps::is_prime_subnet() {
62        Ok(())
63    } else {
64        Err(EnvAccessError::NotPrimeSubnet.into())
65    }
66}
67
68// -----------------------------------------------------------------------------
69// Caller rules
70// -----------------------------------------------------------------------------
71
72/// Require that the caller controls the current canister.
73/// Allows controller-only maintenance calls.
74#[must_use]
75pub fn is_controller(caller: Principal) -> AccessRuleResult {
76    Box::pin(async move {
77        if caller_is_controller(&caller) {
78            Ok(())
79        } else {
80            Err(AccessRuleError::NotController(caller).into())
81        }
82    })
83}
84
85/// Require that the caller appears in the active whitelist (IC deployments).
86/// No-op on local builds; enforces whitelist on IC.
87#[must_use]
88pub fn is_whitelisted(caller: Principal) -> AccessRuleResult {
89    Box::pin(async move {
90        let cfg = Config::try_get().ok_or_else(|| {
91            AccessRuleError::DependencyUnavailable("config not initialized".to_string())
92        })?;
93
94        if !cfg.is_whitelisted(&caller) {
95            return Err(AccessRuleError::NotWhitelisted(caller).into());
96        }
97
98        Ok(())
99    })
100}