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::{app::AppDirectoryOps, subnet::SubnetDirectoryOps},
20 registry::subnet::SubnetRegistryOps,
21 },
22 },
23};
24use candid::Principal;
25use std::pin::Pin;
26
27#[derive(Debug, ThisError)]
35pub enum AuthAccessError {
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("access dependency unavailable: {0}")]
45 DependencyUnavailable(String),
46
47 #[error("caller '{0}' does not match the app directory's canister role '{1}'")]
48 NotAppDirectoryType(Principal, CanisterRole),
49
50 #[error("caller '{0}' does not match the subnet directory's canister role '{1}'")]
51 NotSubnetDirectoryType(Principal, CanisterRole),
52
53 #[error("caller '{0}' is not a child of this canister")]
54 NotChild(Principal),
55
56 #[error("caller '{0}' is not a controller of this canister")]
57 NotController(Principal),
58
59 #[error("caller '{0}' is not the parent of this canister")]
60 NotParent(Principal),
61
62 #[error("expected caller principal '{1}' got '{0}'")]
63 NotPrincipal(Principal, Principal),
64
65 #[error("caller '{0}' is not root")]
66 NotRoot(Principal),
67
68 #[error("caller '{0}' is not the current canister")]
69 NotSameCanister(Principal),
70
71 #[error("caller '{0}' is not registered on the subnet registry")]
72 NotRegisteredToSubnet(Principal),
73
74 #[error("caller '{0}' is not on the whitelist")]
75 NotWhitelisted(Principal),
76}
77
78impl From<AuthAccessError> for Error {
79 fn from(err: AuthAccessError) -> Self {
80 AccessError::Auth(err).into()
81 }
82}
83
84pub type AuthRuleFn = Box<
86 dyn Fn(Principal) -> Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>
87 + Send
88 + Sync,
89>;
90
91pub type AuthRuleResult = Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>;
93
94pub async fn require_all(rules: Vec<AuthRuleFn>) -> Result<(), AccessError> {
99 let caller = msg_caller();
100
101 if rules.is_empty() {
102 return Err(AuthAccessError::NoRulesDefined.into());
103 }
104
105 for rule in rules {
106 if let Err(err) = rule(caller).await {
107 log!(
108 Topic::Auth,
109 Warn,
110 "auth failed (all) caller={caller}: {err}",
111 );
112
113 return Err(err);
114 }
115 }
116
117 Ok(())
118}
119
120pub async fn require_any(rules: Vec<AuthRuleFn>) -> Result<(), AccessError> {
125 let caller = msg_caller();
126
127 if rules.is_empty() {
128 return Err(AuthAccessError::NoRulesDefined.into());
129 }
130
131 let mut last_error = None;
132 for rule in rules {
133 match rule(caller).await {
134 Ok(()) => return Ok(()),
135 Err(e) => last_error = Some(e),
136 }
137 }
138
139 let err = last_error.unwrap_or_else(|| AuthAccessError::InvalidState.into());
140 log!(
141 Topic::Auth,
142 Warn,
143 "auth failed (any) caller={caller}: {err}",
144 );
145
146 Err(err)
147}
148
149#[macro_export]
154macro_rules! auth_require_all {
155 ($($f:expr),* $(,)?) => {{
156 $crate::access::auth::require_all(vec![
157 $( Box::new(move |caller| Box::pin($f(caller))) ),*
158 ]).await
159 }};
160}
161
162#[macro_export]
167macro_rules! auth_require_any {
168 ($($f:expr),* $(,)?) => {{
169 $crate::access::auth::require_any(vec![
170 $( Box::new(move |caller| Box::pin($f(caller))) ),*
171 ]).await
172 }};
173}
174
175#[must_use]
182pub fn is_app_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
183 Box::pin(async move {
184 if AppDirectoryOps::matches(&role, caller) {
185 Ok(())
186 } else {
187 Err(AuthAccessError::NotAppDirectoryType(caller, role).into())
188 }
189 })
190}
191
192#[must_use]
195pub fn is_child(caller: Principal) -> AuthRuleResult {
196 Box::pin(async move {
197 if CanisterChildrenOps::contains_pid(&caller) {
198 Ok(())
199 } else {
200 Err(AuthAccessError::NotChild(caller).into())
201 }
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(AuthAccessError::NotController(caller).into())
214 }
215 })
216}
217
218#[must_use]
221pub fn is_parent(caller: Principal) -> AuthRuleResult {
222 Box::pin(async move {
223 let parent_pid = EnvOps::parent_pid().map_err(to_access)?;
224
225 if parent_pid == caller {
226 Ok(())
227 } else {
228 Err(AuthAccessError::NotParent(caller).into())
229 }
230 })
231}
232
233#[must_use]
236pub fn is_principal(caller: Principal, expected: Principal) -> AuthRuleResult {
237 Box::pin(async move {
238 if caller == expected {
239 Ok(())
240 } else {
241 Err(AuthAccessError::NotPrincipal(caller, expected).into())
242 }
243 })
244}
245
246#[must_use]
250pub fn is_registered_to_subnet(caller: Principal) -> AuthRuleResult {
251 Box::pin(async move {
252 if SubnetRegistryOps::is_registered(caller) {
253 Ok(())
254 } else {
255 Err(AuthAccessError::NotRegisteredToSubnet(caller).into())
256 }
257 })
258}
259
260#[must_use]
263pub fn is_root(caller: Principal) -> AuthRuleResult {
264 Box::pin(async move {
265 let root_pid = EnvOps::root_pid().map_err(to_access)?;
266
267 if caller == root_pid {
268 Ok(())
269 } else {
270 Err(AuthAccessError::NotRoot(caller).into())
271 }
272 })
273}
274
275#[must_use]
278pub fn is_same_canister(caller: Principal) -> AuthRuleResult {
279 Box::pin(async move {
280 if caller == canister_self() {
281 Ok(())
282 } else {
283 Err(AuthAccessError::NotSameCanister(caller).into())
284 }
285 })
286}
287
288#[must_use]
291pub fn is_subnet_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
292 Box::pin(async move {
293 match SubnetDirectoryOps::get(&role) {
294 Some(pid) if pid == caller => Ok(()),
295 _ => Err(AuthAccessError::NotSubnetDirectoryType(caller, role).into()),
296 }
297 })
298}
299
300#[must_use]
303pub fn is_whitelisted(caller: Principal) -> AuthRuleResult {
304 Box::pin(async move {
305 use crate::config::Config;
306 let cfg = Config::get().map_err(to_access)?;
307
308 if !cfg.is_whitelisted(&caller) {
309 return Err(AuthAccessError::NotWhitelisted(caller).into());
310 }
311
312 Ok(())
313 })
314}
315
316fn to_access(err: Error) -> AccessError {
319 AuthAccessError::DependencyUnavailable(err.to_string()).into()
320}