1use super::AuthApi;
8use crate::{
9 InternalError,
10 access::auth::validate_application_subject,
11 config::schema::LocalApplicationAuthorizationConfig,
12 diagnostics::codes,
13 domain::policy::pure::auth::application_authorization::{
14 ApplicationCapacityLimit, ApplicationProofEligibilityError,
15 ApplicationSessionAdmissionError, ApplicationSessionTtlError,
16 resolve_application_session_expiry,
17 },
18 dto::{
19 auth::{
20 ApplicationSessionAuditEntry, ApplicationSessionAuditResponse,
21 ApplicationSessionCommandResponse, ApplicationSessionPolicyView,
22 ApplicationSessionRequest, ApplicationSessionStatus,
23 ApplicationSessionVerifierPolicyView, ApplicationSessionView,
24 InactiveApplicationSession,
25 },
26 error::Error,
27 page::{Page, PageRequest},
28 },
29 model::auth::application_authorization::{
30 ApplicationScope, CanonicalApplicationScopes, LocalApplicationAuthoritySnapshot,
31 LocalApplicationSession, MAX_APPLICATION_PROOF_LIFETIME_NS,
32 },
33 ops::{
34 auth::{
35 AuthOps, VerifyDelegatedTokenRuntimeInput,
36 application_authorization::LocalApplicationAuthorizationAuthority,
37 },
38 ic::IcOps,
39 runtime::metrics::auth::{
40 AuthMetricReason, record_application_session_clear, record_application_session_created,
41 record_application_session_establishment_started,
42 record_application_session_expired_observation, record_application_session_idempotent,
43 record_application_session_rejected, record_application_session_replaced,
44 },
45 storage::auth::{
46 LocalApplicationAuthorizationStateOps,
47 application_sessions::{ApplicationReplayResolution, ApplicationSessionStateError},
48 },
49 },
50 workflow::{
51 auth::application_sessions::{
52 ApplicationSessionEstablishInput, ApplicationSessionEstablishResult,
53 ApplicationSessionWorkflow, ApplicationSessionWorkflowError,
54 },
55 runtime::intent::IntentCleanupWorkflow,
56 },
57};
58
59const NS_PER_SEC: u64 = 1_000_000_000;
60
61impl AuthApi {
62 pub fn establish_application_session(
64 request: ApplicationSessionRequest,
65 ) -> Result<ApplicationSessionCommandResponse, Error> {
66 record_application_session_establishment_started();
67 let result = Self::establish_application_session_inner(request);
68 if let Err(error) = &result {
69 record_application_session_rejected(establishment_rejection_reason(*error));
70 }
71 result
72 }
73
74 fn establish_application_session_inner(
75 request: ApplicationSessionRequest,
76 ) -> Result<ApplicationSessionCommandResponse, Error> {
77 let caller = IcOps::msg_caller();
78 require_application_caller(caller)?;
79 let authority = application_session_authority()?;
80 let now_ns = IcOps::now_nanos();
81 let requested_ttl_secs = request.requested_ttl_secs;
82 let requested_scopes = canonical_requested_scopes(request.requested_scopes)?;
83 require_configured_scopes(&authority.config, &requested_scopes)?;
84 let expires_at_ns = session_expiry(now_ns, requested_ttl_secs, &authority.config)?;
85 let proof_fingerprint =
86 AuthOps::delegated_token_claims_fingerprint(&request.delegated_token)
87 .map_err(Error::from)?;
88 let request_hash = ApplicationSessionWorkflow::establishment_request_hash(
89 &requested_scopes,
90 requested_ttl_secs,
91 );
92
93 match ApplicationSessionWorkflow::resolve_retry(
94 proof_fingerprint,
95 caller,
96 request_hash,
97 now_ns,
98 )
99 .map_err(map_state_error)?
100 {
101 ApplicationReplayResolution::ExactActive(session) => {
102 require_current_session(&session, &authority.snapshot, now_ns)?;
103 record_application_session_idempotent();
104 IntentCleanupWorkflow::reconcile_after_terminal();
105 return Ok(ApplicationSessionCommandResponse::Established(
106 session_view(&session),
107 ));
108 }
109 ApplicationReplayResolution::Conflict => {
110 return Err(Error::from_registered(codes::STATE_CONFLICT));
111 }
112 ApplicationReplayResolution::Absent => {}
113 }
114
115 let max_ttl_ns = AuthOps::delegated_token_max_ttl_ns().map_err(Error::from)?;
116 let verified = AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
117 token: &request.delegated_token,
118 caller,
119 max_cert_ttl_ns: max_ttl_ns,
120 max_token_ttl_ns: max_ttl_ns,
121 required_scopes: &[],
122 now_ns,
123 })
124 .map_err(Error::from)?;
125 if verified.fleet() != authority.snapshot.fleet()
126 || verified.role() != authority.snapshot.role()
127 {
128 return Err(Error::from_registered(codes::AUTHORITY_CONFLICT));
129 }
130
131 let established =
132 ApplicationSessionWorkflow::establish_verified(ApplicationSessionEstablishInput {
133 authority: verified,
134 requested_scopes,
135 authority_generation: authority.snapshot.generation(),
136 established_at_ns: now_ns,
137 expires_at_ns,
138 establishment_request_hash: request_hash,
139 })
140 .map_err(map_workflow_error)?;
141 let session = match established {
142 ApplicationSessionEstablishResult::Created(session) => {
143 record_application_session_created();
144 session
145 }
146 ApplicationSessionEstablishResult::ExactRetry(session) => {
147 record_application_session_idempotent();
148 session
149 }
150 ApplicationSessionEstablishResult::Replaced(session) => {
151 record_application_session_replaced();
152 session
153 }
154 };
155 IntentCleanupWorkflow::reconcile_after_terminal();
156 Ok(ApplicationSessionCommandResponse::Established(
157 session_view(&session),
158 ))
159 }
160
161 pub fn clear_application_session() -> Result<ApplicationSessionCommandResponse, Error> {
163 let caller = IcOps::msg_caller();
164 require_application_caller(caller)?;
165 application_session_authority()?;
166 let removed = LocalApplicationAuthorizationStateOps::clear_application_session(caller)
167 .map_err(map_state_error)?;
168 record_application_session_clear(removed);
169 IntentCleanupWorkflow::reconcile_after_terminal();
170 Ok(ApplicationSessionCommandResponse::Cleared)
171 }
172
173 pub fn application_session_status() -> Result<ApplicationSessionStatus, Error> {
175 let caller = IcOps::msg_caller();
176 require_application_caller(caller)?;
177 let authority = application_session_authority()?;
178 let Some(session) = LocalApplicationAuthorizationStateOps::application_session(caller)
179 .map_err(map_state_error)?
180 else {
181 return Ok(ApplicationSessionStatus::Inactive(
182 InactiveApplicationSession::Missing,
183 ));
184 };
185 let now_ns = IcOps::now_nanos();
186 let inactive = classify_inactive_session(&session, &authority.snapshot, now_ns);
187 if matches!(inactive, Some(InactiveApplicationSession::Expired { .. })) {
188 record_application_session_expired_observation();
189 }
190 Ok(match inactive {
191 Some(reason) => ApplicationSessionStatus::Inactive(reason),
192 None => ApplicationSessionStatus::Active(session_view(&session)),
193 })
194 }
195
196 pub fn application_session_audit(
198 page: PageRequest,
199 ) -> Result<ApplicationSessionAuditResponse, Error> {
200 let authority = application_session_authority()?;
201 let verifier = AuthOps::auth_proof_verifier_config().map_err(Error::from)?;
202 let sessions = LocalApplicationAuthorizationStateOps::application_session_page(
203 page.offset,
204 page.limit,
205 )
206 .map_err(map_state_error)?;
207 let now_ns = IcOps::now_nanos();
208 let entries = sessions
209 .entries
210 .into_iter()
211 .map(|session| ApplicationSessionAuditEntry {
212 transport_caller: session.transport_caller(),
213 status: match classify_inactive_session(&session, &authority.snapshot, now_ns) {
214 Some(reason) => ApplicationSessionStatus::Inactive(reason),
215 None => ApplicationSessionStatus::Active(session_view(&session)),
216 },
217 })
218 .collect();
219 let minimum_accepted_registry_epoch = verifier
220 .chain_key_root
221 .as_ref()
222 .map(|chain_key| chain_key.policy.min_accepted_registry_epoch);
223 Ok(ApplicationSessionAuditResponse {
224 policy: ApplicationSessionPolicyView {
225 fleet: authority.snapshot.fleet(),
226 role: authority.snapshot.role().clone(),
227 authority_generation: authority.snapshot.generation(),
228 allowed_scopes: authority.config.allowed_scopes,
229 default_session_ttl_secs: authority.config.default_session_ttl_secs,
230 maximum_session_ttl_secs: authority.config.maximum_session_ttl_secs,
231 proof_lifetime_ceiling_ns: MAX_APPLICATION_PROOF_LIFETIME_NS,
232 verifier: ApplicationSessionVerifierPolicyView {
233 root_canister_id: verifier.root_canister_id,
234 minimum_accepted_registry_epoch,
235 },
236 },
237 sessions: Page {
238 entries,
239 total: u64::try_from(sessions.total).unwrap_or(u64::MAX),
240 },
241 })
242 }
243}
244
245fn classify_inactive_session(
246 session: &LocalApplicationSession,
247 authority: &LocalApplicationAuthoritySnapshot,
248 now_ns: u64,
249) -> Option<InactiveApplicationSession> {
250 if now_ns >= session.expires_at_ns() {
251 Some(InactiveApplicationSession::Expired {
252 expired_at_ns: session.expires_at_ns(),
253 })
254 } else if session.fleet() != authority.fleet() {
255 Some(InactiveApplicationSession::StaleFleet)
256 } else if session.role() != authority.role() {
257 Some(InactiveApplicationSession::StaleRole)
258 } else if session.authority_generation() != authority.generation() {
259 Some(InactiveApplicationSession::StaleGeneration {
260 session_generation: session.authority_generation(),
261 current_generation: authority.generation(),
262 })
263 } else if validate_application_subject(session.authenticated_subject()).is_err() {
264 Some(InactiveApplicationSession::InadmissibleSubject)
265 } else {
266 None
267 }
268}
269
270fn require_application_caller(caller: crate::cdk::types::Principal) -> Result<(), Error> {
271 validate_application_subject(caller)
272 .map_err(|_| Error::from_registered(codes::AUTHORITY_UNAUTHORIZED))
273}
274
275fn application_session_authority() -> Result<LocalApplicationAuthorizationAuthority, Error> {
276 AuthOps::local_application_authorization_authority()
277 .map_err(Error::from)?
278 .ok_or_else(|| Error::from_registered(codes::SECURITY_INACTIVE))
279}
280
281fn canonical_requested_scopes(scopes: Vec<String>) -> Result<CanonicalApplicationScopes, Error> {
282 let scopes = scopes
283 .into_iter()
284 .map(ApplicationScope::parse)
285 .collect::<Result<Vec<_>, _>>()
286 .map_err(|_| Error::from_registered(codes::REQUEST_INVALID))?;
287 CanonicalApplicationScopes::for_session(scopes)
288 .map_err(|_| Error::from_registered(codes::REQUEST_INVALID))
289}
290
291fn require_configured_scopes(
292 config: &LocalApplicationAuthorizationConfig,
293 requested: &CanonicalApplicationScopes,
294) -> Result<(), Error> {
295 if requested.as_slice().iter().all(|scope| {
296 config
297 .allowed_scopes
298 .binary_search_by(|configured| configured.as_str().cmp(scope.as_str()))
299 .is_ok()
300 }) {
301 Ok(())
302 } else {
303 Err(Error::from_registered(codes::AUTHORITY_CONFLICT))
304 }
305}
306
307fn session_expiry(
308 now_ns: u64,
309 requested_ttl_secs: Option<u64>,
310 config: &LocalApplicationAuthorizationConfig,
311) -> Result<u64, Error> {
312 let default_ttl_ns = config
313 .default_session_ttl_secs
314 .checked_mul(NS_PER_SEC)
315 .ok_or_else(|| Error::from_registered(codes::TIME_CAPACITY))?;
316 let maximum_ttl_ns = config
317 .maximum_session_ttl_secs
318 .checked_mul(NS_PER_SEC)
319 .ok_or_else(|| Error::from_registered(codes::TIME_CAPACITY))?;
320 let requested_ttl_ns = match requested_ttl_secs {
321 Some(ttl) => Some(
322 ttl.checked_mul(NS_PER_SEC)
323 .ok_or_else(|| Error::from_registered(codes::TIME_CAPACITY))?,
324 ),
325 None => None,
326 };
327 resolve_application_session_expiry(now_ns, default_ttl_ns, maximum_ttl_ns, requested_ttl_ns)
328 .map_err(map_ttl_error)
329}
330
331fn require_current_session(
332 session: &LocalApplicationSession,
333 authority: &LocalApplicationAuthoritySnapshot,
334 now_ns: u64,
335) -> Result<(), Error> {
336 if classify_inactive_session(session, authority, now_ns).is_some() {
337 return Err(Error::from_registered(codes::STATE_CONFLICT));
338 }
339 Ok(())
340}
341
342fn session_view(session: &LocalApplicationSession) -> ApplicationSessionView {
343 ApplicationSessionView {
344 authenticated_subject: session.authenticated_subject(),
345 issuer: session.issuer(),
346 scopes: session
347 .scopes()
348 .as_slice()
349 .iter()
350 .map(ToString::to_string)
351 .collect(),
352 established_at_ns: session.established_at_ns(),
353 expires_at_ns: session.expires_at_ns(),
354 authority_generation: session.authority_generation(),
355 }
356}
357
358fn establishment_rejection_reason(error: Error) -> AuthMetricReason {
359 let code = error.code();
360 if code == codes::CAPACITY_LIMIT.raw_code() {
361 AuthMetricReason::Capacity
362 } else if code == codes::STATE_CONFLICT.raw_code() {
363 AuthMetricReason::ReplayConflict
364 } else if code == codes::AUTHORITY_CONFLICT.raw_code()
365 || code == codes::AUTHORITY_UNAUTHORIZED.raw_code()
366 || code == codes::SECURITY_INACTIVE.raw_code()
367 {
368 AuthMetricReason::AuthorityConflict
369 } else if code == codes::REQUEST_INVALID.raw_code() || code == codes::TIME_CAPACITY.raw_code() {
370 AuthMetricReason::InvalidRequest
371 } else if code == codes::SECURITY_INVALID.raw_code()
372 || code == codes::AUTH_CERT_EXPIRED.raw_code()
373 || code == codes::AUTH_TOKEN_EXPIRED.raw_code()
374 {
375 AuthMetricReason::ProofInvalid
376 } else {
377 AuthMetricReason::StateUnavailable
378 }
379}
380
381const fn map_ttl_error(error: ApplicationSessionTtlError) -> Error {
382 match error {
383 ApplicationSessionTtlError::RequestedZero
384 | ApplicationSessionTtlError::RequestedExceedsMaximum => {
385 Error::from_registered(codes::REQUEST_INVALID)
386 }
387 ApplicationSessionTtlError::DefaultZero
388 | ApplicationSessionTtlError::MaximumZero
389 | ApplicationSessionTtlError::DefaultExceedsMaximum
390 | ApplicationSessionTtlError::MaximumExceedsHardLimit { .. } => {
391 Error::from_registered(codes::CONFIGURATION_INVALID)
392 }
393 ApplicationSessionTtlError::ExpiryOverflow => Error::from_registered(codes::TIME_CAPACITY),
394 }
395}
396
397fn map_workflow_error(error: ApplicationSessionWorkflowError) -> Error {
398 match error {
399 ApplicationSessionWorkflowError::ProofIneligible(error) => match error {
400 ApplicationProofEligibilityError::LifetimeTooLong { .. } => {
401 Error::from_registered(codes::TIME_CAPACITY)
402 }
403 ApplicationProofEligibilityError::CallerMismatch
404 | ApplicationProofEligibilityError::InvalidWindow
405 | ApplicationProofEligibilityError::NotCurrentlyValid => {
406 Error::from_registered(codes::SECURITY_INVALID)
407 }
408 },
409 ApplicationSessionWorkflowError::AdmissionDenied(error) => match error {
410 ApplicationSessionAdmissionError::Capacity(limit) => map_capacity_error(limit),
411 ApplicationSessionAdmissionError::ReplayConflict => {
412 Error::from_registered(codes::STATE_CONFLICT)
413 }
414 ApplicationSessionAdmissionError::ScopeNotGranted { .. } => {
415 Error::from_registered(codes::AUTHORITY_CONFLICT)
416 }
417 },
418 ApplicationSessionWorkflowError::ModelInvalid(_) => {
419 Error::from_registered(codes::REQUEST_INVALID)
420 }
421 ApplicationSessionWorkflowError::State(error) => map_state_error(error),
422 }
423}
424
425const fn map_capacity_error(_limit: ApplicationCapacityLimit) -> Error {
426 Error::from_registered(codes::CAPACITY_LIMIT)
427}
428
429fn map_state_error(error: ApplicationSessionStateError) -> Error {
430 match error {
431 ApplicationSessionStateError::ActiveGlobalCapacity
432 | ApplicationSessionStateError::ActiveSubjectCapacity
433 | ApplicationSessionStateError::ReplayGlobalCapacity
434 | ApplicationSessionStateError::ReplaySubjectCapacity
435 | ApplicationSessionStateError::SessionRecordTooLarge
436 | ApplicationSessionStateError::StableStateTooLarge
437 | ApplicationSessionStateError::IndexStateTooLarge => {
438 Error::from_registered(codes::CAPACITY_LIMIT)
439 }
440 ApplicationSessionStateError::AuthorityGenerationMismatch
441 | ApplicationSessionStateError::ReplayAlreadyExists => {
442 Error::from_registered(codes::STATE_CONFLICT)
443 }
444 ApplicationSessionStateError::AuthorityGenerationExhausted => {
445 Error::from_registered(codes::VERSION_CAPACITY)
446 }
447 ApplicationSessionStateError::IndexesUnavailable
448 | ApplicationSessionStateError::DuplicateCaller
449 | ApplicationSessionStateError::DuplicateProofFingerprint
450 | ApplicationSessionStateError::InvalidAuthorityBinding
451 | ApplicationSessionStateError::InvalidSessionRecord
452 | ApplicationSessionStateError::InvalidReplayRecord
453 | ApplicationSessionStateError::SessionReplayMismatch
454 | ApplicationSessionStateError::FutureAuthorityGeneration
455 | ApplicationSessionStateError::EncodingFailed => Error::from(InternalError::unavailable()),
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::{
463 cdk::types::Principal,
464 ids::{CanisterRole, FleetKey},
465 model::auth::application_authorization::ApplicationScope,
466 test::support::fleet_key,
467 };
468
469 fn p(id: u8) -> Principal {
470 Principal::from_slice(&[id; 29])
471 }
472
473 fn session(
474 subject: Principal,
475 fleet: FleetKey,
476 role: CanisterRole,
477 generation: u64,
478 expires_at_ns: u64,
479 ) -> LocalApplicationSession {
480 LocalApplicationSession::new(
481 subject,
482 subject,
483 p(9),
484 fleet,
485 role,
486 CanonicalApplicationScopes::for_session(vec![
487 ApplicationScope::parse("app:read").unwrap(),
488 ])
489 .unwrap(),
490 generation,
491 10,
492 expires_at_ns,
493 [1; 32],
494 [2; 32],
495 )
496 .unwrap()
497 }
498
499 #[test]
500 fn inactive_status_uses_the_frozen_denial_precedence() {
501 let fleet = fleet_key(1);
502 let role = CanisterRole::new("component");
503 let authority = LocalApplicationAuthoritySnapshot::new(fleet, role.clone(), 7);
504
505 assert_eq!(
506 classify_inactive_session(
507 &session(
508 Principal::anonymous(),
509 fleet_key(2),
510 CanisterRole::new("other"),
511 6,
512 20,
513 ),
514 &authority,
515 20,
516 ),
517 Some(InactiveApplicationSession::Expired { expired_at_ns: 20 })
518 );
519 assert_eq!(
520 classify_inactive_session(
521 &session(p(1), fleet_key(2), CanisterRole::new("other"), 6, 100),
522 &authority,
523 20,
524 ),
525 Some(InactiveApplicationSession::StaleFleet)
526 );
527 assert_eq!(
528 classify_inactive_session(
529 &session(p(1), fleet, CanisterRole::new("other"), 6, 100),
530 &authority,
531 20,
532 ),
533 Some(InactiveApplicationSession::StaleRole)
534 );
535 assert_eq!(
536 classify_inactive_session(&session(p(1), fleet, role.clone(), 6, 100), &authority, 20),
537 Some(InactiveApplicationSession::StaleGeneration {
538 session_generation: 6,
539 current_generation: 7,
540 })
541 );
542 assert_eq!(
543 classify_inactive_session(
544 &session(Principal::anonymous(), fleet, role.clone(), 7, 100),
545 &authority,
546 20,
547 ),
548 Some(InactiveApplicationSession::InadmissibleSubject)
549 );
550 assert_eq!(
551 classify_inactive_session(&session(p(1), fleet, role, 7, 100), &authority, 20),
552 None
553 );
554 }
555
556 #[test]
557 fn configured_scope_and_ttl_policy_are_enforced_at_the_adapter_boundary() {
558 let config = LocalApplicationAuthorizationConfig {
559 allowed_scopes: vec!["app:read".to_string(), "app:write".to_string()],
560 default_session_ttl_secs: 10,
561 maximum_session_ttl_secs: 20,
562 };
563 let read = CanonicalApplicationScopes::for_session(vec![
564 ApplicationScope::parse("app:read").unwrap(),
565 ])
566 .unwrap();
567 let admin = CanonicalApplicationScopes::for_session(vec![
568 ApplicationScope::parse("app:admin").unwrap(),
569 ])
570 .unwrap();
571
572 assert!(require_configured_scopes(&config, &read).is_ok());
573 assert!(require_configured_scopes(&config, &admin).is_err());
574 assert_eq!(session_expiry(100, None, &config).unwrap(), 10_000_000_100);
575 assert_eq!(
576 session_expiry(100, Some(20), &config).unwrap(),
577 20_000_000_100
578 );
579 assert!(session_expiry(100, Some(0), &config).is_err());
580 assert!(session_expiry(100, Some(21), &config).is_err());
581 }
582}