canic_core/access/
auth.rs1use crate::{
9 Error, ThisError,
10 access::AccessError,
11 cdk::{
12 api::{canister_self, msg_caller},
13 types::Principal,
14 },
15 ids::CanisterRole,
16 log,
17 log::Topic,
18 ops::{
19 runtime::env::EnvOps,
20 storage::{
21 children::CanisterChildrenOps,
22 directory::{app::AppDirectoryOps, subnet::SubnetDirectoryOps},
23 registry::subnet::SubnetRegistryOps,
24 },
25 },
26};
27use std::pin::Pin;
28
29#[derive(Debug, ThisError)]
37pub enum AuthAccessError {
38 #[error("invalid error state - this should never happen")]
40 InvalidState,
41
42 #[error("one or more rules must be defined")]
44 NoRulesDefined,
45
46 #[error("access dependency unavailable: {0}")]
47 DependencyUnavailable(String),
48
49 #[error("caller '{0}' does not match the app directory's canister role '{1}'")]
50 NotAppDirectoryType(Principal, CanisterRole),
51
52 #[error("caller '{0}' does not match the subnet directory's canister role '{1}'")]
53 NotSubnetDirectoryType(Principal, CanisterRole),
54
55 #[error("caller '{0}' is not a child of this canister")]
56 NotChild(Principal),
57
58 #[error("caller '{0}' is not a controller of this canister")]
59 NotController(Principal),
60
61 #[error("caller '{0}' is not the parent of this canister")]
62 NotParent(Principal),
63
64 #[error("expected caller principal '{1}' got '{0}'")]
65 NotPrincipal(Principal, Principal),
66
67 #[error("caller '{0}' is not root")]
68 NotRoot(Principal),
69
70 #[error("caller '{0}' is not the current canister")]
71 NotSameCanister(Principal),
72
73 #[error("caller '{0}' is not registered on the subnet registry")]
74 NotRegisteredToSubnet(Principal),
75
76 #[error("caller '{0}' is not on the whitelist")]
77 NotWhitelisted(Principal),
78}
79
80impl From<AuthAccessError> for Error {
81 fn from(err: AuthAccessError) -> Self {
82 AccessError::Auth(err).into()
83 }
84}
85
86pub type AuthRuleFn = Box<
88 dyn Fn(Principal) -> Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>
89 + Send
90 + Sync,
91>;
92
93pub type AuthRuleResult = Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>;
95
96pub async fn require_all(rules: Vec<AuthRuleFn>) -> Result<(), AccessError> {
101 let caller = msg_caller();
102
103 if rules.is_empty() {
104 return Err(AuthAccessError::NoRulesDefined.into());
105 }
106
107 for rule in rules {
108 if let Err(err) = rule(caller).await {
109 log!(
110 Topic::Auth,
111 Warn,
112 "auth failed (all) caller={caller}: {err}",
113 );
114
115 return Err(err);
116 }
117 }
118
119 Ok(())
120}
121
122pub async fn require_any(rules: Vec<AuthRuleFn>) -> Result<(), AccessError> {
127 let caller = msg_caller();
128
129 if rules.is_empty() {
130 return Err(AuthAccessError::NoRulesDefined.into());
131 }
132
133 let mut last_error = None;
134 for rule in rules {
135 match rule(caller).await {
136 Ok(()) => return Ok(()),
137 Err(e) => last_error = Some(e),
138 }
139 }
140
141 let err = last_error.unwrap_or_else(|| AuthAccessError::InvalidState.into());
142 log!(
143 Topic::Auth,
144 Warn,
145 "auth failed (any) caller={caller}: {err}",
146 );
147
148 Err(err)
149}
150
151#[macro_export]
156macro_rules! auth_require_all {
157 ($($f:expr),* $(,)?) => {{
158 $crate::access::auth::require_all(vec![
159 $( Box::new(move |caller| Box::pin($f(caller))) ),*
160 ]).await
161 }};
162}
163
164#[macro_export]
169macro_rules! auth_require_any {
170 ($($f:expr),* $(,)?) => {{
171 $crate::access::auth::require_any(vec![
172 $( Box::new(move |caller| Box::pin($f(caller))) ),*
173 ]).await
174 }};
175}
176
177#[must_use]
184pub fn is_app_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
185 Box::pin(async move {
186 if AppDirectoryOps::matches(&role, caller) {
187 Ok(())
188 } else {
189 Err(AuthAccessError::NotAppDirectoryType(caller, role).into())
190 }
191 })
192}
193
194#[must_use]
197pub fn is_child(caller: Principal) -> AuthRuleResult {
198 Box::pin(async move {
199 if CanisterChildrenOps::contains_pid(&caller) {
200 Ok(())
201 } else {
202 Err(AuthAccessError::NotChild(caller).into())
203 }
204 })
205}
206
207#[must_use]
210pub fn is_controller(caller: Principal) -> AuthRuleResult {
211 Box::pin(async move {
212 if crate::cdk::api::is_controller(&caller) {
213 Ok(())
214 } else {
215 Err(AuthAccessError::NotController(caller).into())
216 }
217 })
218}
219
220#[must_use]
223pub fn is_parent(caller: Principal) -> AuthRuleResult {
224 Box::pin(async move {
225 let parent_pid = EnvOps::parent_pid().map_err(to_access)?;
226
227 if parent_pid == caller {
228 Ok(())
229 } else {
230 Err(AuthAccessError::NotParent(caller).into())
231 }
232 })
233}
234
235#[must_use]
238pub fn is_principal(caller: Principal, expected: Principal) -> AuthRuleResult {
239 Box::pin(async move {
240 if caller == expected {
241 Ok(())
242 } else {
243 Err(AuthAccessError::NotPrincipal(caller, expected).into())
244 }
245 })
246}
247
248#[must_use]
252pub fn is_registered_to_subnet(caller: Principal) -> AuthRuleResult {
253 Box::pin(async move {
254 if SubnetRegistryOps::is_registered(caller) {
255 Ok(())
256 } else {
257 Err(AuthAccessError::NotRegisteredToSubnet(caller).into())
258 }
259 })
260}
261
262#[must_use]
265pub fn is_root(caller: Principal) -> AuthRuleResult {
266 Box::pin(async move {
267 let root_pid = EnvOps::root_pid().map_err(to_access)?;
268
269 if caller == root_pid {
270 Ok(())
271 } else {
272 Err(AuthAccessError::NotRoot(caller).into())
273 }
274 })
275}
276
277#[must_use]
280pub fn is_same_canister(caller: Principal) -> AuthRuleResult {
281 Box::pin(async move {
282 if caller == canister_self() {
283 Ok(())
284 } else {
285 Err(AuthAccessError::NotSameCanister(caller).into())
286 }
287 })
288}
289
290#[must_use]
293pub fn is_subnet_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
294 Box::pin(async move {
295 match SubnetDirectoryOps::get(&role) {
296 Some(pid) if pid == caller => Ok(()),
297 _ => Err(AuthAccessError::NotSubnetDirectoryType(caller, role).into()),
298 }
299 })
300}
301
302#[must_use]
305pub fn is_whitelisted(caller: Principal) -> AuthRuleResult {
306 Box::pin(async move {
307 use crate::config::Config;
308 let cfg = Config::get().map_err(to_access)?;
309
310 if !cfg.is_whitelisted(&caller) {
311 return Err(AuthAccessError::NotWhitelisted(caller).into());
312 }
313
314 Ok(())
315 })
316}
317
318fn to_access(err: Error) -> AccessError {
321 AuthAccessError::DependencyUnavailable(err.to_string()).into()
322}