1use crate::{
9 Error,
10 cdk::api::{canister_self, msg_caller},
11 ids::CanisterRole,
12 log,
13 log::Topic,
14 ops::model::memory::{
15 EnvOps,
16 directory::{AppDirectoryOps, SubnetDirectoryOps},
17 topology::{SubnetCanisterChildrenOps, SubnetCanisterRegistryOps},
18 },
19};
20use candid::Principal;
21use std::pin::Pin;
22use thiserror::Error as ThisError;
23
24#[derive(Debug, ThisError)]
30pub enum AuthError {
31 #[error("invalid error state - this should never happen")]
33 InvalidState,
34
35 #[error("one or more rules must be defined")]
37 NoRulesDefined,
38
39 #[error("caller '{0}' does not match the app directory's canister type '{1}'")]
40 NotAppDirectoryType(Principal, CanisterRole),
41
42 #[error("caller '{0}' does not match the subnet directory's canister type '{1}'")]
43 NotSubnetDirectoryType(Principal, CanisterRole),
44
45 #[error("caller '{0}' is not a child of this canister")]
46 NotChild(Principal),
47
48 #[error("caller '{0}' is not a controller of this canister")]
49 NotController(Principal),
50
51 #[error("caller '{0}' is not the parent of this canister")]
52 NotParent(Principal),
53
54 #[error("expected caller principal '{1}' got '{0}'")]
55 NotPrincipal(Principal, Principal),
56
57 #[error("caller '{0}' is not root")]
58 NotRoot(Principal),
59
60 #[error("caller '{0}' is not the current canister")]
61 NotSameCanister(Principal),
62
63 #[error("caller '{0}' is not registered on the subnet registry")]
64 NotRegisteredToSubnet(Principal),
65
66 #[error("caller '{0}' is not on the whitelist")]
67 NotWhitelisted(Principal),
68}
69
70pub type AuthRuleFn =
72 Box<dyn Fn(Principal) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> + Send + Sync>;
73
74pub type AuthRuleResult = Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>;
76
77pub async fn require_all(rules: Vec<AuthRuleFn>) -> Result<(), Error> {
82 let caller = msg_caller();
83
84 if rules.is_empty() {
85 return Err(AuthError::NoRulesDefined.into());
86 }
87
88 for rule in rules {
89 if let Err(err) = rule(caller).await {
90 let err_msg = err.to_string();
91 log!(
92 Topic::Auth,
93 Warn,
94 "auth failed (all) caller={caller}: {err_msg}"
95 );
96
97 return Err(err);
98 }
99 }
100
101 Ok(())
102}
103
104pub async fn require_any(rules: Vec<AuthRuleFn>) -> Result<(), Error> {
109 let caller = msg_caller();
110
111 if rules.is_empty() {
112 return Err(AuthError::NoRulesDefined.into());
113 }
114
115 let mut last_error = None;
116 for rule in rules {
117 match rule(caller).await {
118 Ok(()) => return Ok(()),
119 Err(e) => last_error = Some(e),
120 }
121 }
122
123 let err = last_error.unwrap_or_else(|| AuthError::InvalidState.into());
124 let err_msg = err.to_string();
125 log!(
126 Topic::Auth,
127 Warn,
128 "auth failed (any) caller={caller}: {err_msg}"
129 );
130
131 Err(err)
132}
133
134#[macro_export]
139macro_rules! auth_require_all {
140 ($($f:expr),* $(,)?) => {{
141 $crate::auth::require_all(vec![
142 $( Box::new(move |caller| Box::pin($f(caller))) ),*
143 ]).await
144 }};
145}
146
147#[macro_export]
152macro_rules! auth_require_any {
153 ($($f:expr),* $(,)?) => {{
154 $crate::auth::require_any(vec![
155 $( Box::new(move |caller| Box::pin($f(caller))) ),*
156 ]).await
157 }};
158}
159
160#[must_use]
167pub fn is_app_directory_type(caller: Principal, ty: CanisterRole) -> AuthRuleResult {
168 Box::pin(async move {
169 let pids = AppDirectoryOps::try_get(&ty)?;
170
171 if pids.contains(&caller) {
172 Ok(())
173 } else {
174 Err(AuthError::NotAppDirectoryType(caller, ty.clone()))?
175 }
176 })
177}
178
179#[must_use]
182pub fn is_subnet_directory_type(caller: Principal, ty: CanisterRole) -> AuthRuleResult {
183 Box::pin(async move {
184 let pids = SubnetDirectoryOps::try_get(&ty)?;
185
186 if pids.contains(&caller) {
187 Ok(())
188 } else {
189 Err(AuthError::NotSubnetDirectoryType(caller, ty.clone()))?
190 }
191 })
192}
193
194#[must_use]
197pub fn is_child(caller: Principal) -> AuthRuleResult {
198 Box::pin(async move {
199 SubnetCanisterChildrenOps::find_by_pid(&caller).ok_or(AuthError::NotChild(caller))?;
200
201 Ok(())
202 })
203}
204
205#[must_use]
208pub fn is_controller(caller: Principal) -> AuthRuleResult {
209 Box::pin(async move {
210 if crate::cdk::api::is_controller(&caller) {
211 Ok(())
212 } else {
213 Err(AuthError::NotController(caller).into())
214 }
215 })
216}
217
218#[must_use]
221pub fn is_root(caller: Principal) -> AuthRuleResult {
222 Box::pin(async move {
223 let root_pid = EnvOps::try_get_root_pid()?;
224
225 if caller == root_pid {
226 Ok(())
227 } else {
228 Err(AuthError::NotRoot(caller))?
229 }
230 })
231}
232
233#[must_use]
236pub fn is_parent(caller: Principal) -> AuthRuleResult {
237 Box::pin(async move {
238 let parent_pid = EnvOps::try_get_parent_pid()?;
239
240 if parent_pid == caller {
241 Ok(())
242 } else {
243 Err(AuthError::NotParent(caller))?
244 }
245 })
246}
247
248#[must_use]
251pub fn is_principal(caller: Principal, expected: Principal) -> AuthRuleResult {
252 Box::pin(async move {
253 if caller == expected {
254 Ok(())
255 } else {
256 Err(AuthError::NotPrincipal(caller, expected))?
257 }
258 })
259}
260
261#[must_use]
264pub fn is_same_canister(caller: Principal) -> AuthRuleResult {
265 Box::pin(async move {
266 if caller == canister_self() {
267 Ok(())
268 } else {
269 Err(AuthError::NotSameCanister(caller))?
270 }
271 })
272}
273
274#[must_use]
279pub fn is_registered_to_subnet(caller: Principal) -> AuthRuleResult {
280 Box::pin(async move {
281 match SubnetCanisterRegistryOps::get(caller) {
282 Some(_) => Ok(()),
283 None => Err(AuthError::NotRegisteredToSubnet(caller))?,
284 }
285 })
286}
287
288#[must_use]
291#[allow(unused_variables)]
292pub fn is_whitelisted(caller: Principal) -> AuthRuleResult {
293 Box::pin(async move {
294 #[cfg(feature = "ic")]
295 {
296 use crate::config::Config;
297 let cfg = Config::get();
298
299 if !cfg.is_whitelisted(&caller) {
300 Err(AuthError::NotWhitelisted(caller))?;
301 }
302 }
303
304 Ok(())
305 })
306}