1mod identity;
16mod predicates;
17mod token;
18
19use crate::{
20 access::AccessError,
21 cdk::types::Principal,
22 ids::CanisterRole,
23 ops::{
24 auth::VerifiedDelegatedToken, runtime::env::EnvOps,
25 storage::registry::subnet::SubnetRegistryOps,
26 },
27};
28use std::fmt;
29
30pub type Role = CanisterRole;
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum AuthenticatedIdentitySource {
38 RawCaller,
39 DelegatedSession,
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct ResolvedAuthenticatedIdentity {
48 pub transport_caller: Principal,
49 pub authenticated_subject: Principal,
50 pub identity_source: AuthenticatedIdentitySource,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum DelegatedSessionSubjectRejection {
59 Anonymous,
60 ManagementCanister,
61 LocalCanister,
62 RootCanister,
63 ParentCanister,
64 SubnetCanister,
65 PrimeRootCanister,
66 RegisteredCanister,
67}
68
69impl fmt::Display for DelegatedSessionSubjectRejection {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 let reason = match self {
72 Self::Anonymous => "anonymous principals are not allowed",
73 Self::ManagementCanister => "management canister principal is not allowed",
74 Self::LocalCanister => "current canister principal is not allowed",
75 Self::RootCanister => "root canister principal is not allowed",
76 Self::ParentCanister => "parent canister principal is not allowed",
77 Self::SubnetCanister => "subnet principal is not allowed",
78 Self::PrimeRootCanister => "prime root principal is not allowed",
79 Self::RegisteredCanister => "subnet-registered canister principal is not allowed",
80 };
81 f.write_str(reason)
82 }
83}
84
85#[must_use]
89pub fn resolve_authenticated_identity(
90 transport_caller: Principal,
91) -> ResolvedAuthenticatedIdentity {
92 identity::resolve_authenticated_identity(transport_caller)
93}
94
95#[cfg(test)]
96pub(crate) fn resolve_authenticated_identity_at(
97 transport_caller: Principal,
98 now_secs: u64,
99) -> ResolvedAuthenticatedIdentity {
100 identity::resolve_authenticated_identity_at(transport_caller, now_secs)
101}
102
103pub fn validate_delegated_session_subject(
107 subject: Principal,
108) -> Result<(), DelegatedSessionSubjectRejection> {
109 identity::validate_delegated_session_subject(subject)
110}
111
112pub(crate) async fn delegated_token_verified(
113 authenticated_subject: Principal,
114 required_scope: Option<&str>,
115) -> Result<VerifiedDelegatedToken, AccessError> {
116 token::delegated_token_verified(authenticated_subject, required_scope).await
117}
118
119#[cfg(test)]
120fn enforce_subject_binding(sub: Principal, caller: Principal) -> Result<(), AccessError> {
121 token::enforce_subject_binding(sub, caller)
122}
123
124#[cfg(test)]
125fn enforce_required_scope(
126 required_scope: Option<&str>,
127 token_scopes: &[String],
128) -> Result<(), AccessError> {
129 token::enforce_required_scope(required_scope, token_scopes)
130}
131
132pub async fn is_controller(caller: Principal) -> Result<(), AccessError> {
139 predicates::is_controller(caller).await
140}
141
142pub async fn is_whitelisted(caller: Principal) -> Result<(), AccessError> {
145 predicates::is_whitelisted(caller).await
146}
147
148pub async fn is_child(caller: Principal) -> Result<(), AccessError> {
150 predicates::is_child(caller).await
151}
152
153pub async fn is_parent(caller: Principal) -> Result<(), AccessError> {
155 predicates::is_parent(caller).await
156}
157
158pub async fn is_root(caller: Principal) -> Result<(), AccessError> {
160 predicates::is_root(caller).await
161}
162
163pub async fn is_same_canister(caller: Principal) -> Result<(), AccessError> {
165 predicates::is_same_canister(caller).await
166}
167
168pub async fn has_role(caller: Principal, role: Role) -> Result<(), AccessError> {
174 predicates::has_role(caller, role).await
175}
176
177pub async fn is_registered_to_subnet(caller: Principal) -> Result<(), AccessError> {
180 predicates::is_registered_to_subnet(caller).await
181}
182
183fn dependency_unavailable(detail: &str) -> AccessError {
184 AccessError::Denied(format!("access dependency unavailable: {detail}"))
185}
186
187fn non_root_subnet_registry_predicate_denial() -> AccessError {
188 AccessError::Denied(
189 "authentication error: illegal access to subnet registry predicate from non-root canister"
190 .to_string(),
191 )
192}
193
194fn caller_not_registered_denial(caller: Principal) -> AccessError {
195 let root = EnvOps::root_pid().map_or_else(|_| "unavailable".to_string(), |pid| pid.to_string());
196 let registry_count = SubnetRegistryOps::data().entries.len();
197 AccessError::Denied(format!(
198 "authentication error: caller '{caller}' is not registered on the subnet registry \
199 (root='{root}', registry_entries={registry_count}); verify caller root routing and \
200 canic_subnet_registry state"
201 ))
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::{
208 ids::{AccessMetricKind, cap},
209 ops::runtime::metrics::access::AccessMetrics,
210 test::seams,
211 };
212
213 fn p(id: u8) -> Principal {
214 Principal::from_slice(&[id; 29])
215 }
216
217 fn auth_session_metric_count(predicate: &str) -> u64 {
218 AccessMetrics::snapshot()
219 .entries
220 .into_iter()
221 .find_map(|(key, count)| {
222 if key.endpoint == "auth_session"
223 && key.kind == AccessMetricKind::Auth
224 && key.predicate == predicate
225 {
226 Some(count)
227 } else {
228 None
229 }
230 })
231 .unwrap_or(0)
232 }
233
234 #[test]
235 fn subject_binding_allows_matching_subject_and_caller() {
236 let sub = p(1);
237 let caller = p(1);
238 assert!(enforce_subject_binding(sub, caller).is_ok());
239 }
240
241 #[test]
242 fn subject_binding_rejects_mismatched_subject_and_caller() {
243 let sub = p(1);
244 let caller = p(2);
245 let err = enforce_subject_binding(sub, caller).expect_err("expected subject mismatch");
246 assert!(err.to_string().contains("does not match caller"));
247 }
248
249 #[test]
250 fn required_scope_allows_when_scope_present() {
251 let scopes = vec![cap::READ.to_string(), cap::VERIFY.to_string()];
252 assert!(enforce_required_scope(Some(cap::VERIFY), &scopes).is_ok());
253 }
254
255 #[test]
256 fn required_scope_rejects_when_scope_missing() {
257 let scopes = vec![cap::READ.to_string()];
258 let err = enforce_required_scope(Some(cap::VERIFY), &scopes).expect_err("expected denial");
259 assert!(err.to_string().contains("missing required scope"));
260 }
261
262 #[test]
263 fn required_scope_none_is_allowed() {
264 let scopes = vec![cap::READ.to_string()];
265 assert!(enforce_required_scope(None, &scopes).is_ok());
266 }
267
268 #[test]
269 fn resolve_authenticated_identity_defaults_to_wallet_when_no_override_exists() {
270 let _guard = seams::lock();
271 AccessMetrics::reset();
272 let wallet = p(9);
273 crate::ops::storage::auth::DelegationStateOps::clear_delegated_session(wallet);
274 let resolved = resolve_authenticated_identity(wallet);
275 assert_eq!(resolved.authenticated_subject, wallet);
276 assert_eq!(
277 auth_session_metric_count("session_fallback_raw_caller"),
278 1,
279 "missing delegated session should record raw-caller fallback"
280 );
281 }
282
283 #[test]
284 fn resolve_authenticated_identity_prefers_active_delegated_session() {
285 let _guard = seams::lock();
286 AccessMetrics::reset();
287 let wallet = p(8);
288 let delegated = p(7);
289 crate::ops::storage::auth::DelegationStateOps::upsert_delegated_session(
290 crate::ops::storage::auth::DelegatedSession {
291 wallet_pid: wallet,
292 delegated_pid: delegated,
293 issued_at: 100,
294 expires_at: 200,
295 bootstrap_token_fingerprint: None,
296 },
297 100,
298 );
299
300 let resolved = resolve_authenticated_identity_at(wallet, 150);
301 assert_eq!(resolved.transport_caller, wallet);
302 assert_eq!(resolved.authenticated_subject, delegated);
303 assert_eq!(
304 resolved.identity_source,
305 AuthenticatedIdentitySource::DelegatedSession
306 );
307 assert_eq!(
308 auth_session_metric_count("session_fallback_raw_caller"),
309 0,
310 "active delegated session should not fallback to raw caller"
311 );
312
313 crate::ops::storage::auth::DelegationStateOps::clear_delegated_session(wallet);
314 }
315
316 #[test]
317 fn resolve_authenticated_identity_falls_back_when_session_expired() {
318 let _guard = seams::lock();
319 AccessMetrics::reset();
320 let wallet = p(6);
321 let delegated = p(5);
322 crate::ops::storage::auth::DelegationStateOps::upsert_delegated_session(
323 crate::ops::storage::auth::DelegatedSession {
324 wallet_pid: wallet,
325 delegated_pid: delegated,
326 issued_at: 100,
327 expires_at: 120,
328 bootstrap_token_fingerprint: None,
329 },
330 100,
331 );
332
333 let resolved = resolve_authenticated_identity_at(wallet, 121);
334 assert_eq!(resolved.authenticated_subject, wallet);
335 assert_eq!(
336 resolved.identity_source,
337 AuthenticatedIdentitySource::RawCaller
338 );
339 assert_eq!(
340 auth_session_metric_count("session_fallback_raw_caller"),
341 1,
342 "expired delegated session should fallback to raw caller"
343 );
344
345 crate::ops::storage::auth::DelegationStateOps::clear_delegated_session(wallet);
346 }
347
348 #[test]
349 fn resolve_authenticated_identity_falls_back_after_clear() {
350 let _guard = seams::lock();
351 AccessMetrics::reset();
352 let wallet = p(4);
353 let delegated = p(3);
354 crate::ops::storage::auth::DelegationStateOps::upsert_delegated_session(
355 crate::ops::storage::auth::DelegatedSession {
356 wallet_pid: wallet,
357 delegated_pid: delegated,
358 issued_at: 50,
359 expires_at: 500,
360 bootstrap_token_fingerprint: None,
361 },
362 50,
363 );
364 crate::ops::storage::auth::DelegationStateOps::clear_delegated_session(wallet);
365
366 let resolved = resolve_authenticated_identity_at(wallet, 100);
367 assert_eq!(resolved.authenticated_subject, wallet);
368 assert_eq!(
369 resolved.identity_source,
370 AuthenticatedIdentitySource::RawCaller
371 );
372 assert_eq!(auth_session_metric_count("session_fallback_raw_caller"), 1);
373 }
374
375 #[test]
376 fn resolve_authenticated_identity_records_invalid_subject_fallback() {
377 let _guard = seams::lock();
378 AccessMetrics::reset();
379 let wallet = p(23);
380 crate::ops::storage::auth::DelegationStateOps::upsert_delegated_session(
381 crate::ops::storage::auth::DelegatedSession {
382 wallet_pid: wallet,
383 delegated_pid: Principal::management_canister(),
384 issued_at: 10,
385 expires_at: 100,
386 bootstrap_token_fingerprint: None,
387 },
388 10,
389 );
390
391 let resolved = resolve_authenticated_identity_at(wallet, 20);
392 assert_eq!(resolved.authenticated_subject, wallet);
393 assert_eq!(
394 resolved.identity_source,
395 AuthenticatedIdentitySource::RawCaller
396 );
397 assert_eq!(
398 auth_session_metric_count("session_fallback_invalid_subject"),
399 1
400 );
401 assert_eq!(auth_session_metric_count("session_fallback_raw_caller"), 1);
402 assert!(
403 crate::ops::storage::auth::DelegationStateOps::delegated_session(wallet, 20).is_none(),
404 "invalid delegated session should be cleared"
405 );
406 }
407
408 #[test]
409 fn validate_delegated_session_subject_rejects_anonymous() {
410 let _guard = seams::lock();
411 let err = validate_delegated_session_subject(Principal::anonymous())
412 .expect_err("anonymous must be rejected");
413 assert_eq!(err, DelegatedSessionSubjectRejection::Anonymous);
414 }
415
416 #[test]
417 fn validate_delegated_session_subject_rejects_management_canister() {
418 let _guard = seams::lock();
419 let err = validate_delegated_session_subject(Principal::management_canister())
420 .expect_err("management canister must be rejected");
421 assert_eq!(err, DelegatedSessionSubjectRejection::ManagementCanister);
422 }
423}