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 AuthStateOps,
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 = AuthStateOps::clear_application_session(caller).map_err(map_state_error)?;
167 record_application_session_clear(removed);
168 IntentCleanupWorkflow::reconcile_after_terminal();
169 Ok(ApplicationSessionCommandResponse::Cleared)
170 }
171
172 pub fn application_session_status() -> Result<ApplicationSessionStatus, Error> {
174 let caller = IcOps::msg_caller();
175 require_application_caller(caller)?;
176 let authority = application_session_authority()?;
177 let Some(session) = AuthStateOps::application_session(caller).map_err(map_state_error)?
178 else {
179 return Ok(ApplicationSessionStatus::Inactive(
180 InactiveApplicationSession::Missing,
181 ));
182 };
183 let now_ns = IcOps::now_nanos();
184 let inactive = classify_inactive_session(&session, &authority.snapshot, now_ns);
185 if matches!(inactive, Some(InactiveApplicationSession::Expired { .. })) {
186 record_application_session_expired_observation();
187 }
188 Ok(match inactive {
189 Some(reason) => ApplicationSessionStatus::Inactive(reason),
190 None => ApplicationSessionStatus::Active(session_view(&session)),
191 })
192 }
193
194 pub fn application_session_audit(
196 page: PageRequest,
197 ) -> Result<ApplicationSessionAuditResponse, Error> {
198 let authority = application_session_authority()?;
199 let verifier = AuthOps::auth_proof_verifier_config().map_err(Error::from)?;
200 let sessions = AuthStateOps::application_session_page(page.offset, page.limit)
201 .map_err(map_state_error)?;
202 let now_ns = IcOps::now_nanos();
203 let entries = sessions
204 .entries
205 .into_iter()
206 .map(|session| ApplicationSessionAuditEntry {
207 transport_caller: session.transport_caller(),
208 status: match classify_inactive_session(&session, &authority.snapshot, now_ns) {
209 Some(reason) => ApplicationSessionStatus::Inactive(reason),
210 None => ApplicationSessionStatus::Active(session_view(&session)),
211 },
212 })
213 .collect();
214 let minimum_accepted_registry_epoch = verifier
215 .chain_key_root
216 .as_ref()
217 .map(|chain_key| chain_key.policy.min_accepted_registry_epoch);
218 Ok(ApplicationSessionAuditResponse {
219 policy: ApplicationSessionPolicyView {
220 fleet: authority.snapshot.fleet(),
221 role: authority.snapshot.role().clone(),
222 authority_generation: authority.snapshot.generation(),
223 allowed_scopes: authority.config.allowed_scopes,
224 default_session_ttl_secs: authority.config.default_session_ttl_secs,
225 maximum_session_ttl_secs: authority.config.maximum_session_ttl_secs,
226 proof_lifetime_ceiling_ns: MAX_APPLICATION_PROOF_LIFETIME_NS,
227 verifier: ApplicationSessionVerifierPolicyView {
228 root_canister_id: verifier.root_canister_id,
229 minimum_accepted_registry_epoch,
230 },
231 },
232 sessions: Page {
233 entries,
234 total: u64::try_from(sessions.total).unwrap_or(u64::MAX),
235 },
236 })
237 }
238}
239
240fn classify_inactive_session(
241 session: &LocalApplicationSession,
242 authority: &LocalApplicationAuthoritySnapshot,
243 now_ns: u64,
244) -> Option<InactiveApplicationSession> {
245 if now_ns >= session.expires_at_ns() {
246 Some(InactiveApplicationSession::Expired {
247 expired_at_ns: session.expires_at_ns(),
248 })
249 } else if session.fleet() != authority.fleet() {
250 Some(InactiveApplicationSession::StaleFleet)
251 } else if session.role() != authority.role() {
252 Some(InactiveApplicationSession::StaleRole)
253 } else if session.authority_generation() != authority.generation() {
254 Some(InactiveApplicationSession::StaleGeneration {
255 session_generation: session.authority_generation(),
256 current_generation: authority.generation(),
257 })
258 } else if validate_application_subject(session.authenticated_subject()).is_err() {
259 Some(InactiveApplicationSession::InadmissibleSubject)
260 } else {
261 None
262 }
263}
264
265fn require_application_caller(caller: crate::cdk::types::Principal) -> Result<(), Error> {
266 validate_application_subject(caller)
267 .map_err(|_| Error::from_registered(codes::AUTHORITY_UNAUTHORIZED))
268}
269
270fn application_session_authority() -> Result<LocalApplicationAuthorizationAuthority, Error> {
271 AuthOps::local_application_authorization_authority()
272 .map_err(Error::from)?
273 .ok_or_else(|| Error::from_registered(codes::SECURITY_INACTIVE))
274}
275
276fn canonical_requested_scopes(scopes: Vec<String>) -> Result<CanonicalApplicationScopes, Error> {
277 let scopes = scopes
278 .into_iter()
279 .map(ApplicationScope::parse)
280 .collect::<Result<Vec<_>, _>>()
281 .map_err(|_| Error::from_registered(codes::REQUEST_INVALID))?;
282 CanonicalApplicationScopes::for_session(scopes)
283 .map_err(|_| Error::from_registered(codes::REQUEST_INVALID))
284}
285
286fn require_configured_scopes(
287 config: &LocalApplicationAuthorizationConfig,
288 requested: &CanonicalApplicationScopes,
289) -> Result<(), Error> {
290 if requested.as_slice().iter().all(|scope| {
291 config
292 .allowed_scopes
293 .binary_search_by(|configured| configured.as_str().cmp(scope.as_str()))
294 .is_ok()
295 }) {
296 Ok(())
297 } else {
298 Err(Error::from_registered(codes::AUTHORITY_CONFLICT))
299 }
300}
301
302fn session_expiry(
303 now_ns: u64,
304 requested_ttl_secs: Option<u64>,
305 config: &LocalApplicationAuthorizationConfig,
306) -> Result<u64, Error> {
307 let default_ttl_ns = config
308 .default_session_ttl_secs
309 .checked_mul(NS_PER_SEC)
310 .ok_or_else(|| Error::from_registered(codes::TIME_CAPACITY))?;
311 let maximum_ttl_ns = config
312 .maximum_session_ttl_secs
313 .checked_mul(NS_PER_SEC)
314 .ok_or_else(|| Error::from_registered(codes::TIME_CAPACITY))?;
315 let requested_ttl_ns = match requested_ttl_secs {
316 Some(ttl) => Some(
317 ttl.checked_mul(NS_PER_SEC)
318 .ok_or_else(|| Error::from_registered(codes::TIME_CAPACITY))?,
319 ),
320 None => None,
321 };
322 resolve_application_session_expiry(now_ns, default_ttl_ns, maximum_ttl_ns, requested_ttl_ns)
323 .map_err(map_ttl_error)
324}
325
326fn require_current_session(
327 session: &LocalApplicationSession,
328 authority: &LocalApplicationAuthoritySnapshot,
329 now_ns: u64,
330) -> Result<(), Error> {
331 if classify_inactive_session(session, authority, now_ns).is_some() {
332 return Err(Error::from_registered(codes::STATE_CONFLICT));
333 }
334 Ok(())
335}
336
337fn session_view(session: &LocalApplicationSession) -> ApplicationSessionView {
338 ApplicationSessionView {
339 authenticated_subject: session.authenticated_subject(),
340 issuer: session.issuer(),
341 scopes: session
342 .scopes()
343 .as_slice()
344 .iter()
345 .map(ToString::to_string)
346 .collect(),
347 established_at_ns: session.established_at_ns(),
348 expires_at_ns: session.expires_at_ns(),
349 authority_generation: session.authority_generation(),
350 }
351}
352
353fn establishment_rejection_reason(error: Error) -> AuthMetricReason {
354 let code = error.code();
355 if code == codes::CAPACITY_LIMIT.raw_code() {
356 AuthMetricReason::Capacity
357 } else if code == codes::STATE_CONFLICT.raw_code() {
358 AuthMetricReason::ReplayConflict
359 } else if code == codes::AUTHORITY_CONFLICT.raw_code()
360 || code == codes::AUTHORITY_UNAUTHORIZED.raw_code()
361 || code == codes::SECURITY_INACTIVE.raw_code()
362 {
363 AuthMetricReason::AuthorityConflict
364 } else if code == codes::REQUEST_INVALID.raw_code() || code == codes::TIME_CAPACITY.raw_code() {
365 AuthMetricReason::InvalidRequest
366 } else if code == codes::SECURITY_INVALID.raw_code()
367 || code == codes::AUTH_CERT_EXPIRED.raw_code()
368 || code == codes::AUTH_TOKEN_EXPIRED.raw_code()
369 {
370 AuthMetricReason::ProofInvalid
371 } else {
372 AuthMetricReason::StateUnavailable
373 }
374}
375
376const fn map_ttl_error(error: ApplicationSessionTtlError) -> Error {
377 match error {
378 ApplicationSessionTtlError::RequestedZero
379 | ApplicationSessionTtlError::RequestedExceedsMaximum => {
380 Error::from_registered(codes::REQUEST_INVALID)
381 }
382 ApplicationSessionTtlError::DefaultZero
383 | ApplicationSessionTtlError::MaximumZero
384 | ApplicationSessionTtlError::DefaultExceedsMaximum
385 | ApplicationSessionTtlError::MaximumExceedsHardLimit { .. } => {
386 Error::from_registered(codes::CONFIGURATION_INVALID)
387 }
388 ApplicationSessionTtlError::ExpiryOverflow => Error::from_registered(codes::TIME_CAPACITY),
389 }
390}
391
392fn map_workflow_error(error: ApplicationSessionWorkflowError) -> Error {
393 match error {
394 ApplicationSessionWorkflowError::ProofIneligible(error) => match error {
395 ApplicationProofEligibilityError::LifetimeTooLong { .. } => {
396 Error::from_registered(codes::TIME_CAPACITY)
397 }
398 ApplicationProofEligibilityError::CallerMismatch
399 | ApplicationProofEligibilityError::InvalidWindow
400 | ApplicationProofEligibilityError::NotCurrentlyValid => {
401 Error::from_registered(codes::SECURITY_INVALID)
402 }
403 },
404 ApplicationSessionWorkflowError::AdmissionDenied(error) => match error {
405 ApplicationSessionAdmissionError::Capacity(limit) => map_capacity_error(limit),
406 ApplicationSessionAdmissionError::ReplayConflict => {
407 Error::from_registered(codes::STATE_CONFLICT)
408 }
409 ApplicationSessionAdmissionError::ScopeNotGranted { .. } => {
410 Error::from_registered(codes::AUTHORITY_CONFLICT)
411 }
412 },
413 ApplicationSessionWorkflowError::ModelInvalid(_) => {
414 Error::from_registered(codes::REQUEST_INVALID)
415 }
416 ApplicationSessionWorkflowError::State(error) => map_state_error(error),
417 }
418}
419
420const fn map_capacity_error(_limit: ApplicationCapacityLimit) -> Error {
421 Error::from_registered(codes::CAPACITY_LIMIT)
422}
423
424fn map_state_error(error: ApplicationSessionStateError) -> Error {
425 match error {
426 ApplicationSessionStateError::ActiveGlobalCapacity
427 | ApplicationSessionStateError::ActiveSubjectCapacity
428 | ApplicationSessionStateError::ReplayGlobalCapacity
429 | ApplicationSessionStateError::ReplaySubjectCapacity
430 | ApplicationSessionStateError::SessionRecordTooLarge
431 | ApplicationSessionStateError::StableStateTooLarge
432 | ApplicationSessionStateError::IndexStateTooLarge => {
433 Error::from_registered(codes::CAPACITY_LIMIT)
434 }
435 ApplicationSessionStateError::AuthorityGenerationMismatch
436 | ApplicationSessionStateError::ReplayAlreadyExists => {
437 Error::from_registered(codes::STATE_CONFLICT)
438 }
439 ApplicationSessionStateError::AuthorityGenerationExhausted => {
440 Error::from_registered(codes::VERSION_CAPACITY)
441 }
442 ApplicationSessionStateError::IndexesUnavailable
443 | ApplicationSessionStateError::DuplicateCaller
444 | ApplicationSessionStateError::DuplicateProofFingerprint
445 | ApplicationSessionStateError::InvalidAuthorityBinding
446 | ApplicationSessionStateError::InvalidSessionRecord
447 | ApplicationSessionStateError::InvalidReplayRecord
448 | ApplicationSessionStateError::SessionReplayMismatch
449 | ApplicationSessionStateError::FutureAuthorityGeneration
450 | ApplicationSessionStateError::EncodingFailed => Error::from(InternalError::unavailable()),
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::{
458 cdk::types::Principal,
459 ids::{CanisterRole, FleetKey},
460 model::auth::application_authorization::ApplicationScope,
461 test::support::fleet_key,
462 };
463
464 fn p(id: u8) -> Principal {
465 Principal::from_slice(&[id; 29])
466 }
467
468 fn session(
469 subject: Principal,
470 fleet: FleetKey,
471 role: CanisterRole,
472 generation: u64,
473 expires_at_ns: u64,
474 ) -> LocalApplicationSession {
475 LocalApplicationSession::new(
476 subject,
477 subject,
478 p(9),
479 fleet,
480 role,
481 CanonicalApplicationScopes::for_session(vec![
482 ApplicationScope::parse("app:read").unwrap(),
483 ])
484 .unwrap(),
485 generation,
486 10,
487 expires_at_ns,
488 [1; 32],
489 [2; 32],
490 )
491 .unwrap()
492 }
493
494 #[test]
495 fn inactive_status_uses_the_frozen_denial_precedence() {
496 let fleet = fleet_key(1);
497 let role = CanisterRole::new("component");
498 let authority = LocalApplicationAuthoritySnapshot::new(fleet, role.clone(), 7);
499
500 assert_eq!(
501 classify_inactive_session(
502 &session(
503 Principal::anonymous(),
504 fleet_key(2),
505 CanisterRole::new("other"),
506 6,
507 20,
508 ),
509 &authority,
510 20,
511 ),
512 Some(InactiveApplicationSession::Expired { expired_at_ns: 20 })
513 );
514 assert_eq!(
515 classify_inactive_session(
516 &session(p(1), fleet_key(2), CanisterRole::new("other"), 6, 100),
517 &authority,
518 20,
519 ),
520 Some(InactiveApplicationSession::StaleFleet)
521 );
522 assert_eq!(
523 classify_inactive_session(
524 &session(p(1), fleet, CanisterRole::new("other"), 6, 100),
525 &authority,
526 20,
527 ),
528 Some(InactiveApplicationSession::StaleRole)
529 );
530 assert_eq!(
531 classify_inactive_session(&session(p(1), fleet, role.clone(), 6, 100), &authority, 20),
532 Some(InactiveApplicationSession::StaleGeneration {
533 session_generation: 6,
534 current_generation: 7,
535 })
536 );
537 assert_eq!(
538 classify_inactive_session(
539 &session(Principal::anonymous(), fleet, role.clone(), 7, 100),
540 &authority,
541 20,
542 ),
543 Some(InactiveApplicationSession::InadmissibleSubject)
544 );
545 assert_eq!(
546 classify_inactive_session(&session(p(1), fleet, role, 7, 100), &authority, 20),
547 None
548 );
549 }
550
551 #[test]
552 fn configured_scope_and_ttl_policy_are_enforced_at_the_adapter_boundary() {
553 let config = LocalApplicationAuthorizationConfig {
554 allowed_scopes: vec!["app:read".to_string(), "app:write".to_string()],
555 default_session_ttl_secs: 10,
556 maximum_session_ttl_secs: 20,
557 };
558 let read = CanonicalApplicationScopes::for_session(vec![
559 ApplicationScope::parse("app:read").unwrap(),
560 ])
561 .unwrap();
562 let admin = CanonicalApplicationScopes::for_session(vec![
563 ApplicationScope::parse("app:admin").unwrap(),
564 ])
565 .unwrap();
566
567 assert!(require_configured_scopes(&config, &read).is_ok());
568 assert!(require_configured_scopes(&config, &admin).is_err());
569 assert_eq!(session_expiry(100, None, &config).unwrap(), 10_000_000_100);
570 assert_eq!(
571 session_expiry(100, Some(20), &config).unwrap(),
572 20_000_000_100
573 );
574 assert!(session_expiry(100, Some(0), &config).is_err());
575 assert!(session_expiry(100, Some(21), &config).is_err());
576 }
577}