1mod attestation;
8mod identity;
9mod predicates;
10mod token;
11
12use crate::{access::AccessError, cdk::types::Principal};
13use std::fmt;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum AuthenticatedIdentitySource {
24 RawCaller,
25 DelegatedSession,
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct ResolvedAuthenticatedIdentity {
37 pub transport_caller: Principal,
38 pub authenticated_subject: Principal,
39 pub identity_source: AuthenticatedIdentitySource,
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum DelegatedSessionSubjectRejection {
51 Anonymous,
52 ManagementCanister,
53 LocalCanister,
54 RootCanister,
55 ParentCanister,
56 SubnetCanister,
57 FleetSubnetRootCanister,
58 DirectChildCanister,
59}
60
61impl fmt::Display for DelegatedSessionSubjectRejection {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 let reason = match self {
64 Self::Anonymous => "anonymous principals are not allowed",
65 Self::ManagementCanister => "management canister principal is not allowed",
66 Self::LocalCanister => "current canister principal is not allowed",
67 Self::RootCanister => "root canister principal is not allowed",
68 Self::ParentCanister => "parent canister principal is not allowed",
69 Self::SubnetCanister => "subnet principal is not allowed",
70 Self::FleetSubnetRootCanister => "Fleet Subnet Root principal is not allowed",
71 Self::DirectChildCanister => "direct child canister principal is not allowed",
72 };
73 f.write_str(reason)
74 }
75}
76
77#[must_use]
81pub fn resolve_authenticated_identity(
82 transport_caller: Principal,
83) -> ResolvedAuthenticatedIdentity {
84 identity::resolve_authenticated_identity(transport_caller)
85}
86
87pub fn validate_delegated_session_subject(
91 subject: Principal,
92) -> Result<(), DelegatedSessionSubjectRejection> {
93 identity::validate_delegated_session_subject(subject)
94}
95
96pub(crate) fn delegated_token_verified(
97 authenticated_subject: Principal,
98 required_scope: Option<&str>,
99) -> Result<Principal, AccessError> {
100 token::delegated_token_verified(authenticated_subject, required_scope)
101}
102
103pub async fn is_controller(caller: Principal) -> Result<(), AccessError> {
110 predicates::is_controller(caller).await
111}
112
113pub async fn is_whitelisted(caller: Principal) -> Result<(), AccessError> {
116 predicates::is_whitelisted(caller).await
117}
118
119pub async fn is_child(caller: Principal) -> Result<(), AccessError> {
121 predicates::is_child(caller).await
122}
123
124pub async fn is_parent(caller: Principal) -> Result<(), AccessError> {
126 predicates::is_parent(caller).await
127}
128
129pub async fn is_root(caller: Principal) -> Result<(), AccessError> {
131 predicates::is_root(caller).await
132}
133
134pub async fn is_same_canister(caller: Principal) -> Result<(), AccessError> {
136 predicates::is_same_canister(caller).await
137}
138
139pub async fn is_attested_local_subnet(caller: Principal) -> Result<(), AccessError> {
141 attestation::is_attested_local_subnet(caller).await
142}
143
144const fn dependency_unavailable(error: crate::InternalError) -> AccessError {
145 AccessError::Internal(error)
146}
147
148#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::{
156 ids::CanisterRole,
157 ops::runtime::metrics::auth::{
158 AuthMetricOperation, AuthMetricOutcome, AuthMetricReason, AuthMetricSurface,
159 AuthMetrics,
160 },
161 test::seams,
162 };
163
164 fn p(id: u8) -> Principal {
165 Principal::from_slice(&[id; 29])
166 }
167
168 fn auth_identity_fallback_metric_count(reason: AuthMetricReason) -> u64 {
169 AuthMetrics::snapshot()
170 .into_iter()
171 .find_map(|(key, count)| {
172 if key.surface == AuthMetricSurface::Session
173 && key.operation == AuthMetricOperation::IdentityFallback
174 && key.outcome == AuthMetricOutcome::Completed
175 && key.reason == reason
176 {
177 Some(count)
178 } else {
179 None
180 }
181 })
182 .unwrap_or(0)
183 }
184
185 #[test]
186 fn resolve_authenticated_identity_defaults_to_wallet_when_no_override_exists() {
187 let _guard = seams::lock();
188 AuthMetrics::reset();
189 let wallet = p(9);
190 crate::ops::storage::auth::AuthStateOps::clear_delegated_session(wallet);
191 let resolved = resolve_authenticated_identity(wallet);
192 assert_eq!(resolved.authenticated_subject, wallet);
193 assert_eq!(
194 auth_identity_fallback_metric_count(AuthMetricReason::RawCaller),
195 1,
196 "missing delegated session should record raw-caller fallback"
197 );
198 }
199
200 #[test]
201 fn resolve_authenticated_identity_prefers_active_delegated_session() {
202 let _guard = seams::lock();
203 AuthMetrics::reset();
204 let wallet = p(8);
205 let delegated = p(7);
206 crate::ops::storage::auth::AuthStateOps::upsert_delegated_session(
207 crate::ops::storage::auth::DelegatedSession {
208 wallet_pid: wallet,
209 delegated_pid: delegated,
210 issued_at: 100,
211 expires_at: 200,
212 bootstrap_token_fingerprint: None,
213 },
214 100,
215 );
216
217 let resolved = identity::resolve_authenticated_identity_at(wallet, 150);
218 assert_eq!(resolved.transport_caller, wallet);
219 assert_eq!(resolved.authenticated_subject, delegated);
220 assert_eq!(
221 resolved.identity_source,
222 AuthenticatedIdentitySource::DelegatedSession
223 );
224 assert_eq!(
225 auth_identity_fallback_metric_count(AuthMetricReason::RawCaller),
226 0,
227 "active delegated session should not fallback to raw caller"
228 );
229
230 crate::ops::storage::auth::AuthStateOps::clear_delegated_session(wallet);
231 }
232
233 #[test]
234 fn resolve_authenticated_identity_falls_back_when_session_expired() {
235 let _guard = seams::lock();
236 AuthMetrics::reset();
237 let wallet = p(6);
238 let delegated = p(5);
239 crate::ops::storage::auth::AuthStateOps::upsert_delegated_session(
240 crate::ops::storage::auth::DelegatedSession {
241 wallet_pid: wallet,
242 delegated_pid: delegated,
243 issued_at: 100,
244 expires_at: 120,
245 bootstrap_token_fingerprint: None,
246 },
247 100,
248 );
249
250 let resolved = identity::resolve_authenticated_identity_at(wallet, 121);
251 assert_eq!(resolved.authenticated_subject, wallet);
252 assert_eq!(
253 resolved.identity_source,
254 AuthenticatedIdentitySource::RawCaller
255 );
256 assert_eq!(
257 auth_identity_fallback_metric_count(AuthMetricReason::RawCaller),
258 1,
259 "expired delegated session should fallback to raw caller"
260 );
261
262 crate::ops::storage::auth::AuthStateOps::clear_delegated_session(wallet);
263 }
264
265 #[test]
266 fn resolve_authenticated_identity_falls_back_at_session_expiry_boundary() {
267 let _guard = seams::lock();
268 AuthMetrics::reset();
269 let wallet = p(16);
270 let delegated = p(15);
271 crate::ops::storage::auth::AuthStateOps::upsert_delegated_session(
272 crate::ops::storage::auth::DelegatedSession {
273 wallet_pid: wallet,
274 delegated_pid: delegated,
275 issued_at: 100,
276 expires_at: 120,
277 bootstrap_token_fingerprint: None,
278 },
279 100,
280 );
281
282 let resolved = identity::resolve_authenticated_identity_at(wallet, 120);
283 assert_eq!(resolved.authenticated_subject, wallet);
284 assert_eq!(
285 resolved.identity_source,
286 AuthenticatedIdentitySource::RawCaller
287 );
288 assert_eq!(
289 auth_identity_fallback_metric_count(AuthMetricReason::RawCaller),
290 1,
291 "delegated session expiry must match token expiry boundary"
292 );
293
294 crate::ops::storage::auth::AuthStateOps::clear_delegated_session(wallet);
295 }
296
297 #[test]
298 fn resolve_authenticated_identity_falls_back_after_clear() {
299 let _guard = seams::lock();
300 AuthMetrics::reset();
301 let wallet = p(4);
302 let delegated = p(3);
303 crate::ops::storage::auth::AuthStateOps::upsert_delegated_session(
304 crate::ops::storage::auth::DelegatedSession {
305 wallet_pid: wallet,
306 delegated_pid: delegated,
307 issued_at: 50,
308 expires_at: 500,
309 bootstrap_token_fingerprint: None,
310 },
311 50,
312 );
313 crate::ops::storage::auth::AuthStateOps::clear_delegated_session(wallet);
314
315 let resolved = identity::resolve_authenticated_identity_at(wallet, 100);
316 assert_eq!(resolved.authenticated_subject, wallet);
317 assert_eq!(
318 resolved.identity_source,
319 AuthenticatedIdentitySource::RawCaller
320 );
321 assert_eq!(
322 auth_identity_fallback_metric_count(AuthMetricReason::RawCaller),
323 1
324 );
325 }
326
327 #[test]
328 fn resolve_authenticated_identity_records_invalid_subject_fallback() {
329 let _guard = seams::lock();
330 AuthMetrics::reset();
331 let wallet = p(23);
332 crate::ops::storage::auth::AuthStateOps::upsert_delegated_session(
333 crate::ops::storage::auth::DelegatedSession {
334 wallet_pid: wallet,
335 delegated_pid: Principal::management_canister(),
336 issued_at: 10,
337 expires_at: 100,
338 bootstrap_token_fingerprint: None,
339 },
340 10,
341 );
342
343 let resolved = identity::resolve_authenticated_identity_at(wallet, 20);
344 assert_eq!(resolved.authenticated_subject, wallet);
345 assert_eq!(
346 resolved.identity_source,
347 AuthenticatedIdentitySource::RawCaller
348 );
349 assert_eq!(
350 auth_identity_fallback_metric_count(AuthMetricReason::InvalidSubject),
351 1
352 );
353 assert_eq!(
354 auth_identity_fallback_metric_count(AuthMetricReason::RawCaller),
355 1
356 );
357 assert!(
358 crate::ops::storage::auth::AuthStateOps::delegated_session(wallet, 20).is_none(),
359 "invalid delegated session should be cleared"
360 );
361 }
362
363 #[test]
364 fn validate_delegated_session_subject_rejects_anonymous() {
365 let _guard = seams::lock();
366 let err = validate_delegated_session_subject(Principal::anonymous())
367 .expect_err("anonymous must be rejected");
368 assert_eq!(err, DelegatedSessionSubjectRejection::Anonymous);
369 }
370
371 #[test]
372 fn validate_delegated_session_subject_rejects_management_canister() {
373 let _guard = seams::lock();
374 let err = validate_delegated_session_subject(Principal::management_canister())
375 .expect_err("management canister must be rejected");
376 assert_eq!(err, DelegatedSessionSubjectRejection::ManagementCanister);
377 }
378
379 #[test]
380 fn validate_delegated_session_subject_rejects_direct_child() {
381 let _guard = seams::lock();
382 let child = p(31);
383 crate::ops::storage::children::CanisterChildrenOps::import_direct_children(
384 p(30),
385 vec![(child, CanisterRole::new("session_subject_child"))],
386 );
387
388 let err = validate_delegated_session_subject(child)
389 .expect_err("direct child canister must be rejected");
390 assert_eq!(err, DelegatedSessionSubjectRejection::DirectChildCanister);
391
392 crate::ops::storage::children::CanisterChildrenOps::import_direct_children(p(30), vec![]);
393 }
394}