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::{
16 runtime::env::EnvOps,
17 storage::{
18 children::CanisterChildrenOps,
19 directory::{AppDirectoryOps, SubnetDirectoryOps},
20 registry::SubnetRegistryOps,
21 },
22 },
23};
24use candid::Principal;
25use std::pin::Pin;
26
27#[derive(Debug, ThisError)]
35pub enum AuthError {
36 #[error("invalid error state - this should never happen")]
38 InvalidState,
39
40 #[error("one or more rules must be defined")]
42 NoRulesDefined,
43
44 #[error("caller '{0}' does not match the app directory's canister role '{1}'")]
45 NotAppDirectoryType(Principal, CanisterRole),
46
47 #[error("caller '{0}' does not match the subnet directory's canister role '{1}'")]
48 NotSubnetDirectoryType(Principal, CanisterRole),
49
50 #[error("caller '{0}' is not a child of this canister")]
51 NotChild(Principal),
52
53 #[error("caller '{0}' is not a controller of this canister")]
54 NotController(Principal),
55
56 #[error("caller '{0}' is not the parent of this canister")]
57 NotParent(Principal),
58
59 #[error("expected caller principal '{1}' got '{0}'")]
60 NotPrincipal(Principal, Principal),
61
62 #[error("caller '{0}' is not root")]
63 NotRoot(Principal),
64
65 #[error("caller '{0}' is not the current canister")]
66 NotSameCanister(Principal),
67
68 #[error("caller '{0}' is not registered on the subnet registry")]
69 NotRegisteredToSubnet(Principal),
70
71 #[error("caller '{0}' is not on the whitelist")]
72 NotWhitelisted(Principal),
73}
74
75impl From<AuthError> for Error {
76 fn from(err: AuthError) -> Self {
77 AccessError::AuthError(err).into()
78 }
79}
80
81pub type AuthRuleFn =
83 Box<dyn Fn(Principal) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> + Send + Sync>;
84
85pub type AuthRuleResult = Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>;
87
88pub async fn require_all(rules: Vec<AuthRuleFn>) -> Result<(), Error> {
93 let caller = msg_caller();
94
95 if rules.is_empty() {
96 return Err(AuthError::NoRulesDefined.into());
97 }
98
99 for rule in rules {
100 if let Err(err) = rule(caller).await {
101 let err_msg = err.to_string();
102 log!(
103 Topic::Auth,
104 Warn,
105 "auth failed (all) caller={caller}: {err_msg}"
106 );
107
108 return Err(err);
109 }
110 }
111
112 Ok(())
113}
114
115pub async fn require_any(rules: Vec<AuthRuleFn>) -> Result<(), Error> {
120 let caller = msg_caller();
121
122 if rules.is_empty() {
123 return Err(AuthError::NoRulesDefined.into());
124 }
125
126 let mut last_error = None;
127 for rule in rules {
128 match rule(caller).await {
129 Ok(()) => return Ok(()),
130 Err(e) => last_error = Some(e),
131 }
132 }
133
134 let err = last_error.unwrap_or_else(|| AuthError::InvalidState.into());
135 let err_msg = err.to_string();
136 log!(
137 Topic::Auth,
138 Warn,
139 "auth failed (any) caller={caller}: {err_msg}"
140 );
141
142 Err(err)
143}
144
145#[macro_export]
150macro_rules! auth_require_all {
151 ($($f:expr),* $(,)?) => {{
152 $crate::access::auth::require_all(vec![
153 $( Box::new(move |caller| Box::pin($f(caller))) ),*
154 ]).await
155 }};
156}
157
158#[macro_export]
163macro_rules! auth_require_any {
164 ($($f:expr),* $(,)?) => {{
165 $crate::access::auth::require_any(vec![
166 $( Box::new(move |caller| Box::pin($f(caller))) ),*
167 ]).await
168 }};
169}
170
171#[must_use]
178pub fn is_app_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
179 Box::pin(async move {
180 match AppDirectoryOps::get(&role) {
181 Some(pid) if pid == caller => Ok(()),
182 _ => Err(AuthError::NotAppDirectoryType(caller, role).into()),
183 }
184 })
185}
186
187#[must_use]
190pub fn is_subnet_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
191 Box::pin(async move {
192 match SubnetDirectoryOps::get(&role) {
193 Some(pid) if pid == caller => Ok(()),
194 _ => Err(AuthError::NotSubnetDirectoryType(caller, role).into()),
195 }
196 })
197}
198
199#[must_use]
202pub fn is_child(caller: Principal) -> AuthRuleResult {
203 Box::pin(async move {
204 CanisterChildrenOps::find_by_pid(&caller).ok_or(AuthError::NotChild(caller))?;
205
206 Ok(())
207 })
208}
209
210#[must_use]
213pub fn is_controller(caller: Principal) -> AuthRuleResult {
214 Box::pin(async move {
215 if crate::cdk::api::is_controller(&caller) {
216 Ok(())
217 } else {
218 Err(AuthError::NotController(caller).into())
219 }
220 })
221}
222
223#[must_use]
226pub fn is_root(caller: Principal) -> AuthRuleResult {
227 Box::pin(async move {
228 let root_pid = EnvOps::root_pid()?;
229
230 if caller == root_pid {
231 Ok(())
232 } else {
233 Err(AuthError::NotRoot(caller))?
234 }
235 })
236}
237
238#[must_use]
241pub fn is_parent(caller: Principal) -> AuthRuleResult {
242 Box::pin(async move {
243 let parent_pid = EnvOps::parent_pid()?;
244
245 if parent_pid == caller {
246 Ok(())
247 } else {
248 Err(AuthError::NotParent(caller))?
249 }
250 })
251}
252
253#[must_use]
256pub fn is_principal(caller: Principal, expected: Principal) -> AuthRuleResult {
257 Box::pin(async move {
258 if caller == expected {
259 Ok(())
260 } else {
261 Err(AuthError::NotPrincipal(caller, expected))?
262 }
263 })
264}
265
266#[must_use]
269pub fn is_same_canister(caller: Principal) -> AuthRuleResult {
270 Box::pin(async move {
271 if caller == canister_self() {
272 Ok(())
273 } else {
274 Err(AuthError::NotSameCanister(caller))?
275 }
276 })
277}
278
279#[must_use]
284pub fn is_registered_to_subnet(caller: Principal) -> AuthRuleResult {
285 Box::pin(async move {
286 if SubnetRegistryOps::is_registered(caller) {
287 Ok(())
288 } else {
289 Err(AuthError::NotRegisteredToSubnet(caller))?
290 }
291 })
292}
293
294#[must_use]
297pub fn is_whitelisted(caller: Principal) -> AuthRuleResult {
298 Box::pin(async move {
299 use crate::config::Config;
300 let cfg = Config::get()?;
301
302 if !cfg.is_whitelisted(&caller) {
303 Err(AuthError::NotWhitelisted(caller))?;
304 }
305
306 Ok(())
307 })
308}