canic_core/access/
auth.rs1use crate::{
9 Error, ThisError,
10 access::AccessError,
11 cdk::api::{canister_self, msg_caller},
12 ids::CanisterRole,
13 log,
14 log::Topic,
15 ops::storage::{
16 directory::{AppDirectoryOps, SubnetDirectoryOps},
17 env::EnvOps,
18 topology::{SubnetCanisterChildrenOps, SubnetCanisterRegistryOps},
19 },
20};
21use candid::Principal;
22use std::pin::Pin;
23
24#[derive(Debug, ThisError)]
32pub enum AuthError {
33 #[error("invalid error state - this should never happen")]
35 InvalidState,
36
37 #[error("one or more rules must be defined")]
39 NoRulesDefined,
40
41 #[error("caller '{0}' does not match the app directory's canister role '{1}'")]
42 NotAppDirectoryType(Principal, CanisterRole),
43
44 #[error("caller '{0}' does not match the subnet directory's canister role '{1}'")]
45 NotSubnetDirectoryType(Principal, CanisterRole),
46
47 #[error("caller '{0}' is not a child of this canister")]
48 NotChild(Principal),
49
50 #[error("caller '{0}' is not a controller of this canister")]
51 NotController(Principal),
52
53 #[error("caller '{0}' is not the parent of this canister")]
54 NotParent(Principal),
55
56 #[error("expected caller principal '{1}' got '{0}'")]
57 NotPrincipal(Principal, Principal),
58
59 #[error("caller '{0}' is not root")]
60 NotRoot(Principal),
61
62 #[error("caller '{0}' is not the current canister")]
63 NotSameCanister(Principal),
64
65 #[error("caller '{0}' is not registered on the subnet registry")]
66 NotRegisteredToSubnet(Principal),
67
68 #[error("caller '{0}' is not on the whitelist")]
69 NotWhitelisted(Principal),
70}
71
72impl From<AuthError> for Error {
73 fn from(err: AuthError) -> Self {
74 AccessError::AuthError(err).into()
75 }
76}
77
78pub type AuthRuleFn =
80 Box<dyn Fn(Principal) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> + Send + Sync>;
81
82pub type AuthRuleResult = Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>;
84
85pub async fn require_all(rules: Vec<AuthRuleFn>) -> Result<(), Error> {
90 let caller = msg_caller();
91
92 if rules.is_empty() {
93 return Err(AuthError::NoRulesDefined.into());
94 }
95
96 for rule in rules {
97 if let Err(err) = rule(caller).await {
98 let err_msg = err.to_string();
99 log!(
100 Topic::Auth,
101 Warn,
102 "auth failed (all) caller={caller}: {err_msg}"
103 );
104
105 return Err(err);
106 }
107 }
108
109 Ok(())
110}
111
112pub async fn require_any(rules: Vec<AuthRuleFn>) -> Result<(), Error> {
117 let caller = msg_caller();
118
119 if rules.is_empty() {
120 return Err(AuthError::NoRulesDefined.into());
121 }
122
123 let mut last_error = None;
124 for rule in rules {
125 match rule(caller).await {
126 Ok(()) => return Ok(()),
127 Err(e) => last_error = Some(e),
128 }
129 }
130
131 let err = last_error.unwrap_or_else(|| AuthError::InvalidState.into());
132 let err_msg = err.to_string();
133 log!(
134 Topic::Auth,
135 Warn,
136 "auth failed (any) caller={caller}: {err_msg}"
137 );
138
139 Err(err)
140}
141
142#[macro_export]
147macro_rules! auth_require_all {
148 ($($f:expr),* $(,)?) => {{
149 $crate::auth::require_all(vec![
150 $( Box::new(move |caller| Box::pin($f(caller))) ),*
151 ]).await
152 }};
153}
154
155#[macro_export]
160macro_rules! auth_require_any {
161 ($($f:expr),* $(,)?) => {{
162 $crate::auth::require_any(vec![
163 $( Box::new(move |caller| Box::pin($f(caller))) ),*
164 ]).await
165 }};
166}
167
168#[must_use]
175pub fn is_app_directory_type(caller: Principal, role: CanisterRole) -> AuthRuleResult {
176 Box::pin(async move {
177 let pids = AppDirectoryOps::get(&role);
178
179 if pids.contains(&caller) {
180 Ok(())
181 } else {
182 Err(AuthError::NotAppDirectoryType(caller, role.clone()))?
183 }
184 })
185}
186
187#[must_use]
190pub fn is_subnet_directory_type(caller: Principal, role: CanisterRole) -> AuthRuleResult {
191 Box::pin(async move {
192 let pids = SubnetDirectoryOps::get(&role);
193
194 if pids.contains(&caller) {
195 Ok(())
196 } else {
197 Err(AuthError::NotSubnetDirectoryType(caller, role.clone()))?
198 }
199 })
200}
201
202#[must_use]
205pub fn is_child(caller: Principal) -> AuthRuleResult {
206 Box::pin(async move {
207 SubnetCanisterChildrenOps::find_by_pid(&caller).ok_or(AuthError::NotChild(caller))?;
208
209 Ok(())
210 })
211}
212
213#[must_use]
216pub fn is_controller(caller: Principal) -> AuthRuleResult {
217 Box::pin(async move {
218 if crate::cdk::api::is_controller(&caller) {
219 Ok(())
220 } else {
221 Err(AuthError::NotController(caller).into())
222 }
223 })
224}
225
226#[must_use]
229pub fn is_root(caller: Principal) -> AuthRuleResult {
230 Box::pin(async move {
231 let root_pid = EnvOps::root_pid();
232
233 if caller == root_pid {
234 Ok(())
235 } else {
236 Err(AuthError::NotRoot(caller))?
237 }
238 })
239}
240
241#[must_use]
244pub fn is_parent(caller: Principal) -> AuthRuleResult {
245 Box::pin(async move {
246 let parent_pid = EnvOps::parent_pid();
247
248 if parent_pid == caller {
249 Ok(())
250 } else {
251 Err(AuthError::NotParent(caller))?
252 }
253 })
254}
255
256#[must_use]
259pub fn is_principal(caller: Principal, expected: Principal) -> AuthRuleResult {
260 Box::pin(async move {
261 if caller == expected {
262 Ok(())
263 } else {
264 Err(AuthError::NotPrincipal(caller, expected))?
265 }
266 })
267}
268
269#[must_use]
272pub fn is_same_canister(caller: Principal) -> AuthRuleResult {
273 Box::pin(async move {
274 if caller == canister_self() {
275 Ok(())
276 } else {
277 Err(AuthError::NotSameCanister(caller))?
278 }
279 })
280}
281
282#[must_use]
287pub fn is_registered_to_subnet(caller: Principal) -> AuthRuleResult {
288 Box::pin(async move {
289 match SubnetCanisterRegistryOps::get(caller) {
290 Some(_) => Ok(()),
291 None => Err(AuthError::NotRegisteredToSubnet(caller))?,
292 }
293 })
294}
295
296#[must_use]
299pub fn is_whitelisted(caller: Principal) -> AuthRuleResult {
300 Box::pin(async move {
301 use crate::config::Config;
302 let cfg = Config::get();
303
304 if !cfg.is_whitelisted(&caller) {
305 Err(AuthError::NotWhitelisted(caller))?;
306 }
307
308 Ok(())
309 })
310}