ic_auth/
session_request.rs1use crate::canonical::{CanonicalAuthError, validate_scope_label};
7use ic_auth_protocol_types::{AuthRole, DelegatedRoleGrant};
8use sha2::{Digest, Sha256};
9use thiserror::Error;
10
11#[derive(Debug, Error, Eq, PartialEq)]
13pub enum SessionRequestError {
14 #[error("session request has invalid scope bounds, duplicate scopes or zero TTL")]
15 InvalidRequest,
16 #[error("invalid session TTL policy, requested lifetime or expiry overflow")]
17 InvalidLifetime,
18 #[error("requested scopes are not granted for the protected receiver role")]
19 ScopeRejected,
20 #[error(transparent)]
21 Canonical(#[from] CanonicalAuthError),
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub struct SessionTtlLimit(u64);
33impl SessionTtlLimit {
34 pub fn new(seconds: u64) -> Result<Self, SessionRequestError> {
35 if seconds == 0 || seconds > crate::limits::MAX_SESSION_SECS {
36 return Err(SessionRequestError::InvalidLifetime);
37 }
38 Ok(Self(seconds))
39 }
40 pub const fn seconds(self) -> u64 {
41 self.0
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub struct SessionTtlPolicy {
49 default_secs: u64,
50 maximum: SessionTtlLimit,
51}
52impl SessionTtlPolicy {
53 pub fn new(default_secs: u64, maximum_secs: u64) -> Result<Self, SessionRequestError> {
54 let maximum = SessionTtlLimit::new(maximum_secs)?;
55 if default_secs == 0 || default_secs > maximum_secs {
56 return Err(SessionRequestError::InvalidLifetime);
57 }
58 Ok(Self {
59 default_secs,
60 maximum,
61 })
62 }
63 pub const fn default_secs(self) -> u64 {
64 self.default_secs
65 }
66 pub const fn maximum(self) -> SessionTtlLimit {
67 self.maximum
68 }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct SessionRequest {
75 scopes: Vec<String>,
76 ttl_secs: Option<u64>,
77}
78impl SessionRequest {
79 pub fn new(
80 mut scopes: Vec<String>,
81 ttl_secs: Option<u64>,
82 ) -> Result<Self, SessionRequestError> {
83 check_scopes(&scopes)?;
84 scopes.sort_unstable();
85 if scopes.windows(2).any(|pair| pair[0] == pair[1]) || ttl_secs == Some(0) {
86 return Err(SessionRequestError::InvalidRequest);
87 }
88 Ok(Self { scopes, ttl_secs })
89 }
90 pub const fn ttl_secs(&self) -> Option<u64> {
92 self.ttl_secs
93 }
94 pub fn scopes(&self) -> &[String] {
95 &self.scopes
96 }
97 pub fn expires_at_ns(
101 &self,
102 now_ns: u64,
103 policy: SessionTtlPolicy,
104 ) -> Result<u64, SessionRequestError> {
105 let ttl = self.ttl_secs.unwrap_or(policy.default_secs());
106 if ttl > policy.maximum().seconds() {
107 return Err(SessionRequestError::InvalidLifetime);
108 }
109 now_ns
110 .checked_add(ttl * crate::limits::NS_PER_SECOND)
111 .ok_or(SessionRequestError::InvalidLifetime)
112 }
113 fn check_granted_scopes(&self, granted: &[String]) -> Result<(), SessionRequestError> {
114 if !crate::certificate_issuance::scopes_are_subset(&self.scopes, granted) {
115 return Err(SessionRequestError::ScopeRejected);
116 }
117 Ok(())
118 }
119 pub(crate) fn evaluate(
123 &self,
124 grants: &[DelegatedRoleGrant],
125 role: &AuthRole,
126 now_ns: u64,
127 policy: SessionTtlPolicy,
128 ) -> Result<u64, SessionRequestError> {
129 let grant = grants
130 .iter()
131 .find(|grant| &grant.target == role)
132 .ok_or(SessionRequestError::ScopeRejected)?;
133 self.check_granted_scopes(&grant.scopes)?;
134 self.expires_at_ns(now_ns, policy)
135 }
136 pub fn hash(&self) -> [u8; 32] {
138 let mut hash = Sha256::new();
139 hash.update(b"canic-application-session-request-v1");
140 hash.update((self.scopes.len() as u64).to_be_bytes());
141 for scope in &self.scopes {
142 hash.update((scope.len() as u64).to_be_bytes());
143 hash.update(scope.as_bytes());
144 }
145 match self.ttl_secs {
146 None => hash.update([0]),
147 Some(ttl) => {
148 hash.update([1]);
149 hash.update(ttl.to_be_bytes());
150 }
151 }
152 hash.finalize().into()
153 }
154}
155
156pub(crate) fn check_scopes(scopes: &[String]) -> Result<(), SessionRequestError> {
157 if scopes.is_empty() || scopes.len() > crate::limits::MAX_SESSION_SCOPES {
158 return Err(SessionRequestError::InvalidRequest);
159 }
160 for scope in scopes {
162 validate_scope_label(scope)?;
163 }
164 Ok(())
165}