canic_core/access/
auth.rs1use crate::{
9 access::AccessError,
10 cdk::{
11 api::{canister_self, msg_caller},
12 types::Principal,
13 },
14 config::Config,
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;
28use thiserror::Error as ThisError;
29
30#[derive(Debug, ThisError)]
38pub enum AuthAccessError {
39 #[error("invalid error state - this should never happen")]
41 InvalidState,
42
43 #[error("one or more rules must be defined")]
45 NoRulesDefined,
46
47 #[error("access dependency unavailable: {0}")]
48 DependencyUnavailable(String),
49
50 #[error("caller '{0}' does not match the app directory's canister role '{1}'")]
51 NotAppDirectoryType(Principal, CanisterRole),
52
53 #[error("caller '{0}' does not match the subnet directory's canister role '{1}'")]
54 NotSubnetDirectoryType(Principal, CanisterRole),
55
56 #[error("caller '{0}' is not a child of this canister")]
57 NotChild(Principal),
58
59 #[error("caller '{0}' is not a controller of this canister")]
60 NotController(Principal),
61
62 #[error("caller '{0}' is not the parent of this canister")]
63 NotParent(Principal),
64
65 #[error("expected caller principal '{1}' got '{0}'")]
66 NotPrincipal(Principal, Principal),
67
68 #[error("caller '{0}' is not root")]
69 NotRoot(Principal),
70
71 #[error("caller '{0}' is not the current canister")]
72 NotSameCanister(Principal),
73
74 #[error("caller '{0}' is not registered on the subnet registry")]
75 NotRegisteredToSubnet(Principal),
76
77 #[error("caller '{0}' is not on the whitelist")]
78 NotWhitelisted(Principal),
79}
80
81pub type AuthRuleFn = Box<
83 dyn Fn(Principal) -> Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>
84 + Send
85 + Sync,
86>;
87
88pub type AuthRuleResult = Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>;
90
91pub async fn require_all(rules: Vec<AuthRuleFn>) -> Result<(), AccessError> {
96 let caller = msg_caller();
97
98 if rules.is_empty() {
99 return Err(AuthAccessError::NoRulesDefined.into());
100 }
101
102 for rule in rules {
103 if let Err(err) = rule(caller).await {
104 log!(
105 Topic::Auth,
106 Warn,
107 "auth failed (all) caller={caller}: {err}",
108 );
109
110 return Err(err);
111 }
112 }
113
114 Ok(())
115}
116
117pub async fn require_any(rules: Vec<AuthRuleFn>) -> Result<(), AccessError> {
122 let caller = msg_caller();
123
124 if rules.is_empty() {
125 return Err(AuthAccessError::NoRulesDefined.into());
126 }
127
128 let mut last_error = None;
129 for rule in rules {
130 match rule(caller).await {
131 Ok(()) => return Ok(()),
132 Err(e) => last_error = Some(e),
133 }
134 }
135
136 let err = last_error.unwrap_or_else(|| AuthAccessError::InvalidState.into());
137 log!(
138 Topic::Auth,
139 Warn,
140 "auth failed (any) caller={caller}: {err}",
141 );
142
143 Err(err)
144}
145
146#[must_use]
153pub fn is_app_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
154 Box::pin(async move {
155 if AppDirectoryOps::matches(&role, caller) {
156 Ok(())
157 } else {
158 Err(AuthAccessError::NotAppDirectoryType(caller, role).into())
159 }
160 })
161}
162
163#[must_use]
166pub fn is_child(caller: Principal) -> AuthRuleResult {
167 Box::pin(async move {
168 if CanisterChildrenOps::contains_pid(&caller) {
169 Ok(())
170 } else {
171 Err(AuthAccessError::NotChild(caller).into())
172 }
173 })
174}
175
176#[must_use]
179pub fn is_controller(caller: Principal) -> AuthRuleResult {
180 Box::pin(async move {
181 if crate::cdk::api::is_controller(&caller) {
182 Ok(())
183 } else {
184 Err(AuthAccessError::NotController(caller).into())
185 }
186 })
187}
188
189#[must_use]
192pub fn is_parent(caller: Principal) -> AuthRuleResult {
193 Box::pin(async move {
194 let snapshot = EnvOps::snapshot();
195 let parent_pid = snapshot.parent_pid.ok_or_else(|| {
196 AuthAccessError::DependencyUnavailable("parent pid unavailable".to_string())
197 })?;
198
199 if parent_pid == caller {
200 Ok(())
201 } else {
202 Err(AuthAccessError::NotParent(caller).into())
203 }
204 })
205}
206
207#[must_use]
210pub fn is_principal(caller: Principal, expected: Principal) -> AuthRuleResult {
211 Box::pin(async move {
212 if caller == expected {
213 Ok(())
214 } else {
215 Err(AuthAccessError::NotPrincipal(caller, expected).into())
216 }
217 })
218}
219
220#[must_use]
224pub fn is_registered_to_subnet(caller: Principal) -> AuthRuleResult {
225 Box::pin(async move {
226 if SubnetRegistryOps::is_registered(caller) {
227 Ok(())
228 } else {
229 Err(AuthAccessError::NotRegisteredToSubnet(caller).into())
230 }
231 })
232}
233
234#[must_use]
237pub fn is_root(caller: Principal) -> AuthRuleResult {
238 Box::pin(async move {
239 let snapshot = EnvOps::snapshot();
240 let root_pid = snapshot.root_pid.ok_or_else(|| {
241 AuthAccessError::DependencyUnavailable("root pid unavailable".to_string())
242 })?;
243
244 if caller == root_pid {
245 Ok(())
246 } else {
247 Err(AuthAccessError::NotRoot(caller).into())
248 }
249 })
250}
251
252#[must_use]
255pub fn is_same_canister(caller: Principal) -> AuthRuleResult {
256 Box::pin(async move {
257 if caller == canister_self() {
258 Ok(())
259 } else {
260 Err(AuthAccessError::NotSameCanister(caller).into())
261 }
262 })
263}
264
265#[must_use]
268pub fn is_subnet_directory_role(caller: Principal, role: CanisterRole) -> AuthRuleResult {
269 Box::pin(async move {
270 match SubnetDirectoryOps::get(&role) {
271 Some(pid) if pid == caller => Ok(()),
272 _ => Err(AuthAccessError::NotSubnetDirectoryType(caller, role).into()),
273 }
274 })
275}
276
277#[must_use]
280pub fn is_whitelisted(caller: Principal) -> AuthRuleResult {
281 Box::pin(async move {
282 let cfg = Config::try_get().ok_or_else(|| {
283 AuthAccessError::DependencyUnavailable("config not initialized".to_string())
284 })?;
285
286 if !cfg.is_whitelisted(&caller) {
287 return Err(AuthAccessError::NotWhitelisted(caller).into());
288 }
289
290 Ok(())
291 })
292}