1pub mod auth;
11pub mod env;
12pub mod guard;
13pub mod metrics;
14pub mod rule;
15pub mod topology;
16
17use crate::{
18 cdk::{api::msg_caller, types::Principal},
19 ids::{BuildNetwork, CanisterRole},
20 log,
21 log::Topic,
22};
23use std::pin::Pin;
24use thiserror::Error as ThisError;
25
26#[derive(Debug, ThisError)]
34pub enum AccessRuleError {
35 #[error("access dependency unavailable: {0}")]
36 DependencyUnavailable(String),
37
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("caller '{0}' does not match the app directory's canister role '{1}'")]
47 NotAppDirectoryType(Principal, CanisterRole),
48
49 #[error("caller '{0}' does not match the subnet directory's canister role '{1}'")]
50 NotSubnetDirectoryType(Principal, CanisterRole),
51
52 #[error("caller '{0}' is not a child of this canister")]
53 NotChild(Principal),
54
55 #[error("caller '{0}' is not a controller of this canister")]
56 NotController(Principal),
57
58 #[error("caller '{0}' is not the parent of this canister")]
59 NotParent(Principal),
60
61 #[error("expected caller principal '{1}' got '{0}'")]
62 NotPrincipal(Principal, Principal),
63
64 #[error("caller '{0}' is not root")]
65 NotRoot(Principal),
66
67 #[error("caller '{0}' is not the current canister")]
68 NotSameCanister(Principal),
69
70 #[error("caller '{0}' is not registered on the subnet registry")]
71 NotRegisteredToSubnet(Principal),
72
73 #[error("caller '{0}' is not on the whitelist")]
74 NotWhitelisted(Principal),
75}
76
77#[derive(Debug, ThisError)]
82pub enum RuleAccessError {
83 #[error("this endpoint requires a build-time network (DFX_NETWORK) of either 'ic' or 'local'")]
84 BuildNetworkUnknown,
85
86 #[error(
87 "this endpoint is only available when built for '{expected}' (DFX_NETWORK), but was built for '{actual}'"
88 )]
89 BuildNetworkMismatch {
90 expected: BuildNetwork,
91 actual: BuildNetwork,
92 },
93}
94
95#[derive(Debug, ThisError)]
100pub enum AccessError {
101 #[error(transparent)]
102 Auth(#[from] AccessRuleError),
103
104 #[error(transparent)]
105 Env(#[from] env::EnvAccessError),
106
107 #[error(transparent)]
108 Guard(#[from] guard::GuardAccessError),
109
110 #[error(transparent)]
111 Rule(#[from] RuleAccessError),
112
113 #[error("access denied: {0}")]
114 Denied(String),
115}
116
117pub type AccessRuleFn = Box<
119 dyn Fn(Principal) -> Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>
120 + Send
121 + Sync,
122>;
123
124pub type AccessRuleResult = Pin<Box<dyn Future<Output = Result<(), AccessError>> + Send>>;
126
127pub async fn require_all(rules: Vec<AccessRuleFn>) -> Result<(), AccessError> {
132 let caller = msg_caller();
133
134 if rules.is_empty() {
135 return Err(AccessRuleError::NoRulesDefined.into());
136 }
137
138 for rule in rules {
139 if let Err(err) = rule(caller).await {
140 log!(
141 Topic::Auth,
142 Warn,
143 "auth failed (all) caller={caller}: {err}",
144 );
145
146 return Err(err);
147 }
148 }
149
150 Ok(())
151}
152
153pub async fn require_any(rules: Vec<AccessRuleFn>) -> Result<(), AccessError> {
158 let caller = msg_caller();
159
160 if rules.is_empty() {
161 return Err(AccessRuleError::NoRulesDefined.into());
162 }
163
164 let mut last_error = None;
165 for rule in rules {
166 match rule(caller).await {
167 Ok(()) => return Ok(()),
168 Err(e) => last_error = Some(e),
169 }
170 }
171
172 let err = last_error.unwrap_or_else(|| AccessRuleError::InvalidState.into());
173 log!(
174 Topic::Auth,
175 Warn,
176 "auth failed (any) caller={caller}: {err}",
177 );
178
179 Err(err)
180}
181
182#[must_use]
184pub fn deny(reason: impl Into<String>) -> AccessError {
185 AccessError::Denied(reason.into())
186}